feat(defi): add Soroswap and SDEX quote aggregation with split-route support#204
Conversation
…ocation-utilities feat: Add Soroban Contract Invocation Utilities [Issue Galaxy-KJ#82]
📝 WalkthroughWalkthroughImplements a DEX aggregator service that compares swap routes across Soroswap and SDEX, optionally splitting trades to optimize execution price. Adds Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant REST as REST API
participant Agg as DexAggregatorService
participant Soroswap as SoroswapProtocol
participant SDEX as Stellar DEX<br/>(Horizon)
Client->>REST: GET /api/v1/defi/aggregator/quote<br/>(assetIn, assetOut, amountIn)
activate REST
REST->>REST: Validate query params
REST->>Agg: new DexAggregatorService(config)
activate Agg
Agg->>Agg: Validate assets & amounts
Agg->>Agg: Parse split weights (if provided)
Agg->>Soroswap: getSwapQuote(assetIn, assetOut, amountIn)
activate Soroswap
Soroswap->>Soroswap: Query liquidity pools
Soroswap->>Soroswap: Calculate amountOut & priceImpact
Soroswap-->>Agg: Return quote
deactivate Soroswap
Agg->>SDEX: strictSendPaths(assetIn, amountIn, [assetOut])
activate SDEX
SDEX-->>Agg: Return path records
deactivate SDEX
Agg->>Agg: Compare venues & evaluate splits
Agg->>Agg: Build AggregatorQuote with<br/>totalAmountOut & savings
Agg-->>REST: Return aggregated quote
deactivate Agg
REST-->>Client: 200 JSON (routes, totalAmountOut, etc.)
deactivate REST
sequenceDiagram
participant User
participant Agg as DexAggregatorService
User->>Agg: getBestQuote(assetIn, assetOut, amountIn)
activate Agg
Agg->>Agg: Fetch best single-venue route
Agg->>Agg: Evaluate predefined splits<br/>(e.g. 60/40, 40/60)
Agg->>Agg: Compare split vs single-venue output
alt Split wins
Agg->>Agg: Return 2-leg split with savings
else Single venue wins
Agg->>Agg: Return single best route
end
Agg-->>User: AggregatorQuote
deactivate Agg
User->>Agg: getSplitQuote(assetIn, assetOut,<br/>amountIn, splits: [60, 40])
activate Agg
Agg->>Agg: Validate & normalize splits to 100%
Agg->>Agg: Allocate amounts per venue
Agg->>Agg: Fetch routes concurrently
Agg->>Agg: Combine routes & compute metrics
Agg-->>User: AggregatorQuote with 2 routes
deactivate Agg
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
packages/core/defi-protocols/src/aggregator/DexAggregatorService.ts (2)
149-170: Consider caching or reusing protocol instances.A new
SoroswapProtocolinstance is created and initialized for every quote request. For high-frequency usage, this overhead could be significant. Consider caching the initialized protocol instance or making initialization idempotent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/defi-protocols/src/aggregator/DexAggregatorService.ts` around lines 149 - 170, fetchSoroswapRoute currently calls this.protocolFactory.createProtocol(this.soroswapConfig) and await protocol.initialize() on every request which is expensive; change this to reuse a cached initialized instance (e.g., store it on the DexAggregatorService as a private soroswapProtocol field) or ensure protocol.initialize() is idempotent by checking an initialized flag on the protocol before calling initialize() again; update fetchSoroswapRoute to get the protocol from the cache (or call a helper getOrInitSoroswapProtocol) instead of creating/initializing a new instance each time.
109-112: Minor inefficiency:getSplitQuotere-fetches single-venue routes.When
getSplitQuoteis called fromgetBestQuote, the single-venue routes have already been fetched at line 53. Passing the pre-fetchedbestSingleAmountOutas a parameter could avoid this redundant network call.♻️ Potential optimization
async getSplitQuote( assetIn: Asset, assetOut: Asset, amountIn: string, - splits: number[] + splits: number[], + precomputedBestSingleOut?: string ): Promise<AggregatorQuote> { // ... - const bestSingleRoutes = await this.fetchSingleVenueRoutes(assetIn, assetOut, amountIn); - const bestSingleRoute = this.getBestRoute(bestSingleRoutes); - return this.buildQuote(assetIn, assetOut, amountIn, routes, bestSingleRoute.amountOut); + const bestSingleOut = precomputedBestSingleOut ?? ( + await this.fetchSingleVenueRoutes(assetIn, assetOut, amountIn) + ).reduce((best, r) => this.compareAmount(r.amountOut, best) > 0 ? r.amountOut : best, '0'); + return this.buildQuote(assetIn, assetOut, amountIn, routes, bestSingleOut); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/defi-protocols/src/aggregator/DexAggregatorService.ts` around lines 109 - 112, getSplitQuote currently re-fetches single-venue routes via fetchSingleVenueRoutes/getBestRoute causing redundant network calls; change getSplitQuote to accept an optional bestSingleAmountOut parameter (or bestSingleRouteAmount) and, if provided, skip calling fetchSingleVenueRoutes/getBestRoute inside getSplitQuote, otherwise keep the existing behavior; update the caller getBestQuote to pass the pre-fetched bestSingleRoute.amountOut to getSplitQuote when available and remove the duplicate fetch in that path so buildQuote still receives the same bestSingleAmountOut value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/api/rest/src/routes/defi.routes.ts`:
- Around line 125-134: The handler currently only checks presence of assetIn,
assetOut, and amountIn; validate amountIn's format and value before calling the
aggregator by parsing it (e.g., Number/parseFloat or BigNumber), ensuring it's a
finite number greater than zero and rejecting non-numeric or non-positive values
with a 400 VALIDATION_ERROR and clear message; update the validation blocks
around the existing checks (variables assetIn, assetOut, amountIn in the route
handler) and apply the same numeric validation to the other occurrence mentioned
(the later check at ~140-142) so invalid amountIn inputs like "abc" or negative
numbers return a 400 instead of propagating to protocol calls.
- Around line 56-73: parseSplits currently treats an empty query param
(?splits=) as "not provided", accepts [0,0], and relies on brittle
message.includes('splits') elsewhere to classify the error; fix by (1) changing
the presence check to value === undefined so empty-string inputs are parsed as
provided, (2) validate that the parsed array contains exactly two finite
non-negative numbers and reject the all-zero case (e.g., ensure !(splits[0] ===
0 && splits[1] === 0)), (3) throw a specific ValidationError (create class
ValidationError extends Error or attach a stable code/name like
'SPLITS_VALIDATION_ERROR') with a clear constant message from parseSplits, and
(4) replace any downstream message.includes('splits') checks with a reliable
instance/type or code check against ValidationError/SPLITS_VALIDATION_ERROR;
reference parseSplits and the error-handling logic that currently inspects
message.includes('splits').
In `@packages/core/defi-protocols/src/aggregator/DexAggregatorService.ts`:
- Around line 300-313: The toHorizonAssetParams function always sets non-native
assets to 'credit_alphanum4', which breaks assets with 5-12 character codes;
update toHorizonAssetParams to inspect the asset code length via asset.getCode()
and set [`${prefix}_asset_type`] to 'credit_alphanum4' when length <= 4 and to
'credit_alphanum12' when length is between 5 and 12, preserving the existing
[`${prefix}_asset_code`] and [`${prefix}_asset_issuer`] entries and the native
branch that returns 'native'.
In `@packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.ts`:
- Around line 543-557: getSwapQuote currently calls getLiquidityPool which
triggers two extra network simulations (get_pair and get_reserves) per quote; to
avoid doubling latency, modify SoroswapProtocol.getSwapQuote to accept an
optional reserves parameter (or a pre-fetched liquidityPool) and use that if
provided, and implement a short-TTL in-memory cache inside SoroswapProtocol
keyed by pair (or tokenIn+tokenOut) that stores liquidityPool.reserveA/reserveB
and expires after e.g. N seconds; update references to getLiquidityPool,
get_pair and get_reserves so callers can either supply reserves or rely on the
cache-before-falling-back-to-getLiquidityPool behavior, ensuring existing code
paths still call getLiquidityPool when no cached/passed reserves exist.
---
Nitpick comments:
In `@packages/core/defi-protocols/src/aggregator/DexAggregatorService.ts`:
- Around line 149-170: fetchSoroswapRoute currently calls
this.protocolFactory.createProtocol(this.soroswapConfig) and await
protocol.initialize() on every request which is expensive; change this to reuse
a cached initialized instance (e.g., store it on the DexAggregatorService as a
private soroswapProtocol field) or ensure protocol.initialize() is idempotent by
checking an initialized flag on the protocol before calling initialize() again;
update fetchSoroswapRoute to get the protocol from the cache (or call a helper
getOrInitSoroswapProtocol) instead of creating/initializing a new instance each
time.
- Around line 109-112: getSplitQuote currently re-fetches single-venue routes
via fetchSingleVenueRoutes/getBestRoute causing redundant network calls; change
getSplitQuote to accept an optional bestSingleAmountOut parameter (or
bestSingleRouteAmount) and, if provided, skip calling
fetchSingleVenueRoutes/getBestRoute inside getSplitQuote, otherwise keep the
existing behavior; update the caller getBestQuote to pass the pre-fetched
bestSingleRoute.amountOut to getSplitQuote when available and remove the
duplicate fetch in that path so buildQuote still receives the same
bestSingleAmountOut value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d180d738-0fd3-4aa3-8541-556482d24b09
📒 Files selected for processing (10)
docs/defi/aggregator-guide.mdpackages/api/rest/src/routes/defi.routes.test.tspackages/api/rest/src/routes/defi.routes.tspackages/core/defi-protocols/README.mdpackages/core/defi-protocols/__tests__/aggregator/DexAggregator.test.tspackages/core/defi-protocols/__tests__/protocols/soroswap-protocol.test.tspackages/core/defi-protocols/src/aggregator/DexAggregatorService.tspackages/core/defi-protocols/src/aggregator/types.tspackages/core/defi-protocols/src/index.tspackages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.ts
| function parseSplits(value: Request['query']['splits']): number[] | null { | ||
| if (!value) { | ||
| return null; | ||
| } | ||
|
|
||
| const raw = Array.isArray(value) ? value.join(',') : String(value); | ||
| const splits = raw | ||
| .split(',') | ||
| .map((item) => item.trim()) | ||
| .filter(Boolean) | ||
| .map((item) => Number(item)); | ||
|
|
||
| if (splits.length !== 2 || splits.some((split) => !Number.isFinite(split) || split < 0)) { | ||
| throw new Error('splits must contain exactly two non-negative numeric weights'); | ||
| } | ||
|
|
||
| return splits; | ||
| } |
There was a problem hiding this comment.
splits validation can be bypassed and error classification is brittle
At Line 57, if (!value) treats ?splits= as “not provided”, so malformed input can silently fall back to best-route mode. At Line 68, [0,0] is also accepted (invalid zero-allocation). At Line 146, message.includes('splits') is too fragile for classifying validation failures.
Proposed fix
+class SplitsValidationError extends Error {}
+
function parseSplits(value: Request['query']['splits']): number[] | null {
- if (!value) {
+ if (value === undefined) {
return null;
}
const raw = Array.isArray(value) ? value.join(',') : String(value);
+ if (!raw.trim()) {
+ throw new SplitsValidationError('splits must contain exactly two non-negative numeric weights');
+ }
+
const splits = raw
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.map((item) => Number(item));
- if (splits.length !== 2 || splits.some((split) => !Number.isFinite(split) || split < 0)) {
- throw new Error('splits must contain exactly two non-negative numeric weights');
+ const totalWeight = splits.reduce((sum, split) => sum + split, 0);
+ if (
+ splits.length !== 2 ||
+ splits.some((split) => !Number.isFinite(split) || split < 0) ||
+ totalWeight <= 0
+ ) {
+ throw new SplitsValidationError('splits must contain exactly two non-negative numeric weights');
}
return splits;
}
...
- if (error instanceof Error && error.message.includes('splits')) {
+ if (error instanceof SplitsValidationError) {
res.status(400).json({Also applies to: 146-147
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/rest/src/routes/defi.routes.ts` around lines 56 - 73,
parseSplits currently treats an empty query param (?splits=) as "not provided",
accepts [0,0], and relies on brittle message.includes('splits') elsewhere to
classify the error; fix by (1) changing the presence check to value ===
undefined so empty-string inputs are parsed as provided, (2) validate that the
parsed array contains exactly two finite non-negative numbers and reject the
all-zero case (e.g., ensure !(splits[0] === 0 && splits[1] === 0)), (3) throw a
specific ValidationError (create class ValidationError extends Error or attach a
stable code/name like 'SPLITS_VALIDATION_ERROR') with a clear constant message
from parseSplits, and (4) replace any downstream message.includes('splits')
checks with a reliable instance/type or code check against
ValidationError/SPLITS_VALIDATION_ERROR; reference parseSplits and the
error-handling logic that currently inspects message.includes('splits').
| if (!assetIn || !assetOut || !amountIn) { | ||
| res.status(400).json({ | ||
| error: { | ||
| code: 'VALIDATION_ERROR', | ||
| message: 'assetIn, assetOut, and amountIn are required query parameters', | ||
| details: {}, | ||
| }, | ||
| }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Validate amountIn format/value before calling the aggregator
At Line 125, only presence is checked. Invalid inputs like amountIn=abc or negative values can propagate to protocol calls and return avoidable 5xx errors instead of a clean 400.
Proposed fix
if (!assetIn || !assetOut || !amountIn) {
res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'assetIn, assetOut, and amountIn are required query parameters',
details: {},
},
});
return;
}
+
+ const amountInStr = String(amountIn).trim();
+ if (!/^\d+(\.\d+)?$/.test(amountInStr) || Number(amountInStr) <= 0) {
+ res.status(400).json({
+ error: {
+ code: 'VALIDATION_ERROR',
+ message: 'amountIn must be a positive numeric string',
+ details: {},
+ },
+ });
+ return;
+ }
const tokenIn = parseAsset(assetIn as string);
const tokenOut = parseAsset(assetOut as string);
const aggregator = new DexAggregatorService({ ...defaultConfig, protocolId: 'soroswap' });
const parsedSplits = parseSplits(splits);
const quote = parsedSplits
- ? await aggregator.getSplitQuote(tokenIn, tokenOut, amountIn as string, parsedSplits)
- : await aggregator.getBestQuote(tokenIn, tokenOut, amountIn as string);
+ ? await aggregator.getSplitQuote(tokenIn, tokenOut, amountInStr, parsedSplits)
+ : await aggregator.getBestQuote(tokenIn, tokenOut, amountInStr);Also applies to: 140-142
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/rest/src/routes/defi.routes.ts` around lines 125 - 134, The
handler currently only checks presence of assetIn, assetOut, and amountIn;
validate amountIn's format and value before calling the aggregator by parsing it
(e.g., Number/parseFloat or BigNumber), ensuring it's a finite number greater
than zero and rejecting non-numeric or non-positive values with a 400
VALIDATION_ERROR and clear message; update the validation blocks around the
existing checks (variables assetIn, assetOut, amountIn in the route handler) and
apply the same numeric validation to the other occurrence mentioned (the later
check at ~140-142) so invalid amountIn inputs like "abc" or negative numbers
return a 400 instead of propagating to protocol calls.
| private toHorizonAssetParams( | ||
| asset: StellarAsset, | ||
| prefix: 'source' | 'destination' | ||
| ): Record<string, string> { | ||
| if (asset.isNative()) { | ||
| return { [`${prefix}_asset_type`]: 'native' }; | ||
| } | ||
|
|
||
| return { | ||
| [`${prefix}_asset_type`]: 'credit_alphanum4', | ||
| [`${prefix}_asset_code`]: asset.getCode(), | ||
| [`${prefix}_asset_issuer`]: asset.getIssuer(), | ||
| }; | ||
| } |
There was a problem hiding this comment.
Hardcoded credit_alphanum4 may fail for 5-12 character asset codes.
The toHorizonAssetParams method always returns credit_alphanum4 for non-native assets. Stellar supports credit_alphanum12 for asset codes with 5-12 characters. This could cause SDEX path queries to fail or return incorrect results for such assets.
🐛 Proposed fix
private toHorizonAssetParams(
asset: StellarAsset,
prefix: 'source' | 'destination'
): Record<string, string> {
if (asset.isNative()) {
return { [`${prefix}_asset_type`]: 'native' };
}
+ const assetType = asset.getCode().length <= 4 ? 'credit_alphanum4' : 'credit_alphanum12';
return {
- [`${prefix}_asset_type`]: 'credit_alphanum4',
+ [`${prefix}_asset_type`]: assetType,
[`${prefix}_asset_code`]: asset.getCode(),
[`${prefix}_asset_issuer`]: asset.getIssuer(),
};
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/defi-protocols/src/aggregator/DexAggregatorService.ts` around
lines 300 - 313, The toHorizonAssetParams function always sets non-native assets
to 'credit_alphanum4', which breaks assets with 5-12 character codes; update
toHorizonAssetParams to inspect the asset code length via asset.getCode() and
set [`${prefix}_asset_type`] to 'credit_alphanum4' when length <= 4 and to
'credit_alphanum12' when length is between 5 and 12, preserving the existing
[`${prefix}_asset_code`] and [`${prefix}_asset_issuer`] entries and the native
branch that returns 'native'.
|
|
||
| const amountOutRaw = amounts[1]; | ||
| const amountOut = (Number(amountOutRaw) / 1e7).toString(); | ||
| const minimumReceived = (parseFloat(amountOut) * (1 - SoroswapProtocol.SLIPPAGE_TOLERANCE)).toFixed(7); | ||
| const amountOut = new BigNumber(amountOutRaw.toString()).dividedBy(1e7).toFixed(7); | ||
| const minimumReceived = new BigNumber(amountOut) | ||
| .multipliedBy(1 - SoroswapProtocol.SLIPPAGE_TOLERANCE) | ||
| .toFixed(7); | ||
| const liquidityPool = await this.getLiquidityPool(tokenIn, tokenOut); | ||
| const reserveIn = new BigNumber(liquidityPool.reserveA).dividedBy(1e7); | ||
| const priceImpact = reserveIn.isZero() | ||
| ? '0' | ||
| : new BigNumber(amountIn) | ||
| .dividedBy(reserveIn.plus(amountIn)) | ||
| .multipliedBy(100) | ||
| .decimalPlaces(4) | ||
| .toFixed(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify getLiquidityPool returns reserveA in stroops (raw integer string)
rg -n -A 10 'reserveA =' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 838
🏁 Script executed:
#!/bin/bash
# Get the full getLiquidityPool function to see return type and units
rg -n -B 5 -A 40 'getLiquidityPool' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.ts | head -100Repository: Galaxy-KJ/Galaxy-DevKit
Length of output: 3993
🏁 Script executed:
#!/bin/bash
# Get the getSwapQuote function around lines 543-557 to see context of amountIn
sed -n '500,580p' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 2964
🏁 Script executed:
#!/bin/bash
# Check how amountIn is calculated/prepared before line 543
sed -n '520,545p' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 1067
🏁 Script executed:
#!/bin/bash
# Find the getSwapQuote function signature
rg -n -B 5 'getSwapQuote' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.ts | grep -A 15 'public async getSwapQuote'Repository: Galaxy-KJ/Galaxy-DevKit
Length of output: 348
🏁 Script executed:
#!/bin/bash
# Find the full getLiquidityPool implementation to confirm it's an async network call
sed -n '776,850p' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 2579
🏁 Script executed:
#!/bin/bash
# Check if there's any caching mechanism in the class
rg -n 'cache\|Cache' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Get the getSwapQuote full signature and documentation
sed -n '470,510p' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 1248
🏁 Script executed:
#!/bin/bash
# Check the amountToI128ScVal function to understand how amountIn is converted
rg -n -A 5 'amountToI128ScVal' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 1641
🏁 Script executed:
#!/bin/bash
# Double-check the reserveA return value by looking at the complete getLiquidityPool return statement
sed -n '850,880p' packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.tsRepository: Galaxy-KJ/Galaxy-DevKit
Length of output: 1034
Performance: getLiquidityPool adds extra network round-trips per quote.
Each getSwapQuote call incurs two additional Soroban simulations via getLiquidityPool: one for get_pair (line ~828) and another for get_reserves (line ~854). For high-frequency quote requests, this doubles latency. Consider caching pool reserves with a short TTL or accepting reserves as an optional parameter when the caller already has them.
Unit consistency in priceImpact calculation is correct.
amountIn is documented as a decimal string (e.g., "100"), and reserveA from getLiquidityPool is returned as stroops (raw bigint), then divided by 1e7 to convert to decimal units (line 550). Both values are in the same decimal unit space when used in the priceImpact calculation on line 553–557, so the logic is valid.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/defi-protocols/src/protocols/soroswap/soroswap-protocol.ts`
around lines 543 - 557, getSwapQuote currently calls getLiquidityPool which
triggers two extra network simulations (get_pair and get_reserves) per quote; to
avoid doubling latency, modify SoroswapProtocol.getSwapQuote to accept an
optional reserves parameter (or a pre-fetched liquidityPool) and use that if
provided, and implement a short-TTL in-memory cache inside SoroswapProtocol
keyed by pair (or tokenIn+tokenOut) that stores liquidityPool.reserveA/reserveB
and expires after e.g. N seconds; update references to getLiquidityPool,
get_pair and get_reserves so callers can either supply reserves or rely on the
cache-before-falling-back-to-getLiquidityPool behavior, ensuring existing code
paths still call getLiquidityPool when no cached/passed reserves exist.
Summary
This pull request introduces cross-venue quote aggregation for Stellar DeFi swaps by adding a new
DexAggregatorServiceto@galaxy-kj/core-defi-protocolsand exposing it through the REST API.The implementation compares quotes from Soroswap and the Stellar DEX (SDEX), evaluates split execution across both venues, and returns the route with the best expected output. It also improves Soroswap quote quality by calculating price impact from pool reserves instead of returning a placeholder value.
Closes #154
What Changed
Core aggregator service
DexAggregatorServiceunderpackages/core/defi-protocols/src/aggregator/getBestQuote()to compare Soroswap and SDEX quotes and evaluate default split candidatesgetSplitQuote()to support explicit venue allocations such as60/40Soroswap quote quality
SoroswapProtocol.getSwapQuote()to calculate price impact using the constant-product reserve formulaREST API
GET /api/v1/defi/aggregator/quoteassetIn,assetOut, andamountInsplits=60,40Tests and documentation
Design Notes
bigintto remain consistent with the existingcore-defi-protocolsinterfaces and REST serialization modelcore-defi-protocolsto avoid duplicating routing logic in multiple packagesValidation
Validated locally for the package that is currently exercised by the repository CI workflow:
npm run type-checkinpackages/core/defi-protocolsnpm run lintinpackages/core/defi-protocolsnpm run buildinpackages/core/defi-protocolsI also reviewed the active GitHub Actions workflows to confirm alignment with the current CI expectations.
Known Limitations
friendbot.stellar.orgis unavailable herepackages/api/restworkspace also has pre-existing local dependency/configuration issues unrelated to this feature; the active repository CI workflows currently do not run that package directlySummary by CodeRabbit
Release Notes
New Features
Improvements
Documentation