Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .changeset/multi-hop-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'lens': minor
---

Multi-hop routing through configurable intermediate assets bounded in depth.

- Evaluates multi-hop routes (up to `ROUTING_MAX_HOPS`, defaulting to 3) across configurable intermediate assets (`ROUTING_INTERMEDIATES`, defaulting to XLM and Circle USDC).
- Names each hop with its input/output assets, execution venue (`SDEX` | `AMM`), pool ID if applicable, price, spot price, and slippage percentage.
- Shows side-by-side comparison between direct routes and multi-hop paths, ensuring direct routes win when they produce higher output.
- Returns comprehensive execution provenance including sample timestamps, target network, queried venues, total sample count, and data freshness age in seconds.
- Adds test coverage verifying thin pairs where two hops beat a direct trade.
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ REFLECTOR_CONTRACT_ID_TESTNET=
# Comma-separated pairs to watch on testnet.
# Format: "CODE:ISSUER/CODE:ISSUER". Use "native" for XLM.
WATCHED_PAIRS_TESTNET=XLM:native/USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# Multi-hop routing configuration for testnet
ROUTING_MAX_HOPS_TESTNET=3
ROUTING_INTERMEDIATES_TESTNET=XLM:native,USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5

# --- Stellar Network — Mainnet ---
HORIZON_URL_MAINNET=https://horizon.stellar.org
Expand All @@ -99,6 +102,9 @@ AQUARIUS_ENABLED_MAINNET=true
REFLECTOR_CONTRACT_ID_MAINNET=
# Comma-separated pairs to watch on mainnet.
WATCHED_PAIRS_MAINNET=XLM:native/USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN
# Multi-hop routing configuration for mainnet
ROUTING_MAX_HOPS_MAINNET=3
ROUTING_INTERMEDIATES_MAINNET=XLM:native,USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN

# --- Venue endpoint overrides (optional, shared across networks unless a
# paired _TESTNET / _MAINNET variant is set) ---
Expand All @@ -115,6 +121,9 @@ RPC_URL=https://soroban-testnet.stellar.org
NETWORK_PASSPHRASE=Test SDF Network ; September 2015
SOROSWAP_FACTORY_ADDRESS=CDP3HMUH6SMS3S7NPGNDJLULCOXXEPSHY4JKUKMBNQMATHDHWXRRJTBY
WATCHED_PAIRS=XLM:native/USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# Routing configuration fallbacks
ROUTING_MAX_HOPS=3
ROUTING_INTERMEDIATES=XLM:native,USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# Reflector oracle contract ID on testnet (falls back from paired REFLECTOR_CONTRACT_ID_TESTNET)
REFLECTOR_CONTRACT_ID=

Expand Down
186 changes: 186 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2899,15 +2899,182 @@
}
}
},
"RouteHop": {
"type": "object",
"required": [
"assetIn",
"assetOut",
"venue",
"inputAmount",
"outputAmount",
"price",
"spotPrice",
"slippagePct"
],
"properties": {
"assetIn": {
"$ref": "#/components/schemas/Asset"
},
"assetOut": {
"$ref": "#/components/schemas/Asset"
},
"venue": {
"type": "string",
"enum": [
"SDEX",
"AMM"
]
},
"poolId": {
"type": "string"
},
"inputAmount": {
"type": "number"
},
"outputAmount": {
"type": "number"
},
"price": {
"type": "number"
},
"spotPrice": {
"type": "number"
},
"slippagePct": {
"type": "number"
}
}
},
"RouteOption": {
"type": "object",
"required": [
"hops",
"hopCount",
"estimatedOutput",
"effectivePrice",
"slippagePct"
],
"properties": {
"hops": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RouteHop"
}
},
"hopCount": {
"type": "integer"
},
"estimatedOutput": {
"type": "number"
},
"effectivePrice": {
"type": "number"
},
"slippagePct": {
"type": "number"
}
}
},
"RouteComparison": {
"type": "object",
"required": [
"direct",
"multiHop"
],
"properties": {
"direct": {
"type": "object",
"required": [
"route",
"sdexPrice",
"ammPrice",
"estimatedOutput",
"slippagePct"
],
"properties": {
"route": {
"type": "string",
"enum": [
"SDEX",
"AMM",
"SPLIT",
"UNKNOWN"
]
},
"sdexPrice": {
"type": "number"
},
"ammPrice": {
"type": "number"
},
"estimatedOutput": {
"type": "number"
},
"slippagePct": {
"type": "number"
}
}
},
"multiHop": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/RouteOption"
}
]
}
}
},
"RouteProvenance": {
"type": "object",
"required": [
"sampledAt",
"network",
"venuesQueried",
"sampleCount",
"freshnessSeconds"
],
"properties": {
"sampledAt": {
"type": "string",
"format": "date-time"
},
"network": {
"type": "string"
},
"venuesQueried": {
"type": "array",
"items": {
"type": "string"
}
},
"sampleCount": {
"type": "integer"
},
"freshnessSeconds": {
"type": "number",
"nullable": true
}
}
},
"RouteInfo": {
"type": "object",
"required": [
"route",
"sdexPrice",
"ammPrice",
"estimatedOutput",
"slippagePct",
"recommendation"
],
"properties": {
"route": {
"type": "string",
"enum": [
"SDEX",
"AMM",
"SPLIT",
"MULTI_HOP",
"UNKNOWN"
]
},
Expand All @@ -2925,6 +3092,25 @@
},
"recommendation": {
"type": "string"
},
"network": {
"type": "string",
"enum": [
"testnet",
"mainnet"
]
},
"hops": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RouteHop"
}
},
"comparison": {
"$ref": "#/components/schemas/RouteComparison"
},
"provenance": {
"$ref": "#/components/schemas/RouteProvenance"
}
}
},
Expand Down
98 changes: 97 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1931,12 +1931,97 @@ components:
type: integer
nullable: true

RouteHop:
type: object
required: [assetIn, assetOut, venue, inputAmount, outputAmount, price, spotPrice, slippagePct]
properties:
assetIn:
$ref: '#/components/schemas/Asset'
assetOut:
$ref: '#/components/schemas/Asset'
venue:
type: string
enum: [SDEX, AMM]
poolId:
type: string
inputAmount:
type: number
outputAmount:
type: number
price:
type: number
spotPrice:
type: number
slippagePct:
type: number

RouteOption:
type: object
required: [hops, hopCount, estimatedOutput, effectivePrice, slippagePct]
properties:
hops:
type: array
items:
$ref: '#/components/schemas/RouteHop'
hopCount:
type: integer
estimatedOutput:
type: number
effectivePrice:
type: number
slippagePct:
type: number

RouteComparison:
type: object
required: [direct, multiHop]
properties:
direct:
type: object
required: [route, sdexPrice, ammPrice, estimatedOutput, slippagePct]
properties:
route:
type: string
enum: [SDEX, AMM, SPLIT, UNKNOWN]
sdexPrice:
type: number
ammPrice:
type: number
estimatedOutput:
type: number
slippagePct:
type: number
multiHop:
nullable: true
allOf:
- $ref: '#/components/schemas/RouteOption'

RouteProvenance:
type: object
required: [sampledAt, network, venuesQueried, sampleCount, freshnessSeconds]
properties:
sampledAt:
type: string
format: date-time
network:
type: string
venuesQueried:
type: array
items:
type: string
sampleCount:
type: integer
freshnessSeconds:
type: number
nullable: true

RouteInfo:
type: object
required: [route, sdexPrice, ammPrice, estimatedOutput, slippagePct, recommendation]
properties:
route:
type: string
enum: [SDEX, AMM, SPLIT, UNKNOWN]
enum: [SDEX, AMM, SPLIT, MULTI_HOP, UNKNOWN]
sdexPrice:
type: number
ammPrice:
Expand All @@ -1947,6 +2032,17 @@ components:
type: number
recommendation:
type: string
network:
type: string
enum: [testnet, mainnet]
hops:
type: array
items:
$ref: '#/components/schemas/RouteHop'
comparison:
$ref: '#/components/schemas/RouteComparison'
provenance:
$ref: '#/components/schemas/RouteProvenance'

PriceBucket:
type: object
Expand Down
1 change: 1 addition & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type RouteInfo {
estimatedOutput: Float!
slippagePct: Float!
recommendation: String!
network: String
}

type PriceBucket {
Expand Down
Loading