Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 83 additions & 12 deletions agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
type RouteQuote,
type SwapBestRouteParams,
type SwapBestRouteResult,
type RouteMode,
} from "./lib/dex";
import {
RouteOptimizer,
type UnifiedSwapParams,
type RouteOptions
} from "./lib/route";
import { bridgeTokenTool } from "./tools/bridge";
import {
Horizon,
Expand Down Expand Up @@ -72,6 +78,7 @@ export class AgentClient {
private network: "testnet" | "mainnet";
private publicKey: string;
private rpcUrl: string;
private routeOptimizer: RouteOptimizer;

constructor(config: AgentConfig) {
// Mainnet safety check for general operations
Expand Down Expand Up @@ -103,27 +110,82 @@ export class AgentClient {
// In a real SDK, we might not throw here if only read-only methods are used,
// but for this implementation, we'll assume it's needed for most actions.
}

this.routeOptimizer = new RouteOptimizer(
{
network: this.network,
horizonUrl: this.rpcUrl,
publicKey: this.publicKey,
},
{
network: this.network,
rpcUrl: this.rpcUrl, // Note: Soroban might need a different URL than Horizon, but using rpcUrl from config
}
);
}

/**
* Perform a swap on the Stellar network.
* Supports both legacy Soroban single-pool swaps and new route-optimized swaps.
*
* @example
* // Best-route swap (Classic DEX + AMMs)
* await agent.swap({
* fromAsset: { type: "native" },
* toAsset: { code: "USDC", issuer: "..." },
* amount: "10",
* strategy: "best-route"
* });
*
* @param params Swap parameters
*/
async swap(params: {
to: string;
buyA: boolean;
out: string;
inMax: string;
// Legacy params
to?: string;
buyA?: boolean;
out?: string;
inMax?: string;
contractAddress?: string;

// New params
fromAsset?: StellarAssetInput;
toAsset?: StellarAssetInput;
amount?: string;
mode?: RouteMode;
strategy?: "best-route" | "soroban";
slippageBps?: number;
destination?: string;
}) {
return await contractSwap(
this.publicKey,
params.to,
params.buyA,
params.out,
params.inMax,
{ network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress }
);
if (params.strategy === "best-route") {
if (!params.fromAsset || !params.toAsset || !params.amount) {
throw new Error("fromAsset, toAsset, and amount are required for best-route strategy");
}
return await this.routeOptimizer.swap({
fromAsset: params.fromAsset,
toAsset: params.toAsset,
amount: params.amount,
mode: params.mode ?? "strict-send",
destination: params.destination ?? params.to ?? this.publicKey,
}, {
strategy: "best-route",
slippageBps: params.slippageBps,
});
}

// Default to legacy Soroban swap if strategy is not best-route or not provided
// Note: Older users expect the positional/object-based contractSwap logic
if (params.to && params.buyA !== undefined && params.out && params.inMax) {
return await contractSwap(
this.publicKey,
params.to,
params.buyA,
params.out,
params.inMax,
{ network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress }
);
}

throw new Error("Invalid swap parameters. Provide either best-route parameters or legacy Soroban parameters.");
}

/**
Expand Down Expand Up @@ -168,7 +230,16 @@ export class AgentClient {
desiredB: string;
minB: string;
contractAddress?: string;
strategy?: "best-route" | "soroban";
}) => {
if (params.strategy === "best-route") {
// For LP, best-route would involve finding the best protocol/pool for the pair.
// Currently, we default to the known Soroban contract if provided,
// or the Classic AMM if we can resolve the assets.
// This is a placeholder for the future unified LP routing engine.
console.log("Using best-route strategy for LP deposit...");
}

return await contractDeposit(
this.publicKey,
params.to,
Expand Down
131 changes: 131 additions & 0 deletions lib/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
quoteSwap as quoteDexSwap,
swapBestRoute,
type StellarAssetInput,
type RouteQuote,
type RouteMode,
type DexClientConfig,
type SwapBestRouteResult
} from "./dex";
import {
getReserves,
swap as contractSwap,
type SorobanContractConfig
} from "./contract";
import Big from "big.js";

export interface RouteOptions {
strategy: "best-route" | "classic" | "soroban";
slippageBps?: number;
}

export interface UnifiedSwapParams {
fromAsset: StellarAssetInput;
toAsset: StellarAssetInput;
amount: string;
mode: RouteMode;
destination?: string;
}

export interface BestRouteResult {
source: "classic" | "soroban";
quote: RouteQuote | SorobanQuote;
}

export interface BestLPResult {
source: "classic" | "soroban";
poolId: string;
expectedShares: string;
}

export interface SorobanQuote {
outAmount: string;
price: string;
contractAddress: string;
}

/**
* RouteOptimizer provides unified pathfinding across Stellar Classic (DEX/AMM)
* and Soroban-based Liquidity Pools.
*/
export class RouteOptimizer {
constructor(private dexConfig: DexClientConfig, private sorobanConfig: SorobanContractConfig) {}

/**
* Find the best route for a swap.
*/
async findBestRoute(params: UnifiedSwapParams): Promise<BestRouteResult> {
// 1. Get Classic DEX routes
let classicQuotes: RouteQuote[] = [];
try {
classicQuotes = await quoteDexSwap(this.dexConfig, {
mode: params.mode,
sendAsset: params.fromAsset,
destAsset: params.toAsset,
sendAmount: params.mode === "strict-send" ? params.amount : undefined,
destAmount: params.mode === "strict-receive" ? params.amount : undefined,
destination: params.destination,
});
} catch (e) {
console.warn("Failed to fetch classic DEX routes:", e);
}

// 2. TODO: Query Soroban AMM(s)
// For now, we only have one Soroban contract address in config.
// If we knew the assets for that contract, we could query it here.
// Since we don't have an asset-to-contract mapping yet, we'll favor Classic
// if available, but this structure allows adding more sources.

if (classicQuotes.length > 0) {
return {
source: "classic",
quote: classicQuotes[0],
};
}

throw new Error("No routes found for the requested swap");
}

/**
* Execute a swap using the best available route.
*/
async swap(params: UnifiedSwapParams, options: RouteOptions = { strategy: "best-route" }): Promise<any> {
if (options.strategy === "soroban") {
// Legacy Soroban swap (requires specific params which we might need to derive or ask for)
// This is a placeholder for the legacy flow integrated into the optimizer
throw new Error("Direct Soroban strategy requires specific contract parameters");
}

const { source, quote } = await this.findBestRoute(params);

if (source === "classic") {
return await swapBestRoute(this.dexConfig, {
mode: params.mode,
sendAsset: params.fromAsset,
destAsset: params.toAsset,
sendAmount: params.mode === "strict-send" ? params.amount : undefined,
destAmount: params.mode === "strict-receive" ? params.amount : undefined,
destination: params.destination,
slippageBps: options.slippageBps,
});
}

// Soroban execution would go here if we had Soroban quotes
throw new Error(`Execution for source ${source} not yet implemented in optimizer`);
}

/**
* Find the best pool for providing liquidity.
*/
async findBestLPPool(assetA: StellarAssetInput, assetB: StellarAssetInput): Promise<BestLPResult> {
// 1. Check Classic AMM
// 2. Check known Soroban AMMs

// Placeholder logic
return {
source: "classic",
poolId: "classic_amm",
expectedShares: "0"
};
}
}
51 changes: 40 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading