Venue adapters
The one interface every venue is reduced to, the pool state it produces, and the adapters that ship.
Every venue is reduced to one interface so the quote engine never branches on a venue name. The execution class is part of that interface, because it decides which transaction path is used and what the interface is allowed to promise.
The interface
interface VenueAdapter {
readonly id: string;
readonly displayName: string;
readonly executionClass: ExecutionClass;
readonly networks: readonly Network[];
isAvailable(network: Network): boolean;
discoverPools(provider: ChainProvider, network: Network): Promise<PoolState[]>;
decodePool(utxo: Utxo, network: Network, readAt: Tip): PoolState;
quote(input: SwapQuoteInput): SwapQuoteResult;
}discoverPools finds every pool of the venue on the network, decoded, all read at one tip.
Discovery is by payment credential, because a venue's pools sit at many addresses that share
one payment credential and differ in their staking part.
quote returns the exact output for an input, in the venue's own integer arithmetic and with
the venue's own rounding. Never floating point: an off-by-one in rounding produces an order
the validator rejects, not a slightly worse price.
The adapters that ship
| Adapter | Venue id | Class | Curve |
|---|---|---|---|
| Dano Finance CLMM | danogo-clmm | Kernel | Concentrated liquidity |
| Minswap V2 | minswap-v2 | Commitment | Constant product |
| Minswap Stableswap | minswap-stable | Commitment | Stableswap |
| SundaeSwap V3 | sundaeswap-v3 | Commitment | Constant product |
| SundaeSwap Stableswaps | sundaeswap-stable | Commitment | Stableswap |
Which of these run on a given network comes from config/<network>.json. A venue that is
disabled there, or whose poolScriptHash is null, is skipped with its reason recorded.
PoolState
interface PoolState {
identity: PoolIdentity;
executionClass: ExecutionClass;
assetA: AssetRef;
assetB: AssetRef;
reserveA: bigint;
reserveB: bigint;
rawValueA: bigint;
rawValueB: bigint;
curve: CurveParameters;
cost: VenueExecutionCost;
risk: PoolRiskFlags;
supportsAtomicMultiHop: boolean;
}Tradeable reserves are not the UTxO value
reserveA and reserveB are the tradeable reserves after every venue specific exclusion has
been applied. rawValueA and rawValueB are what the UTxO literally holds.
The two differ, and the difference is the point. The validator reads the datum, so quoting from the UTxO value produces orders the validator rejects.
There is deliberately no decimal count
Decimals are a property of an asset, not of a pool, and their single source of truth is the network's asset registry. A consumer that needs them resolves them there by asset reference.
Carrying a number the adapter never read would invite a consumer to trust it, and a wrong decimal count is a pricing error by orders of magnitude rather than a cosmetic one.
Identity carries the chain state
interface PoolIdentity {
venueId: string;
/** `txHash#index` of the pool UTxO this state was decoded from. */
outRef: string;
poolId: string;
address: string;
readAt: Tip;
}Curve parameters
CurveParameters is a discriminated union so the maths cannot be mixed up.
{
type: "constant-product";
/** Fee numerator per direction; the two can differ on some venues. */
feeNumeratorAToB: bigint;
feeNumeratorBToA: bigint;
feeDenominator: bigint;
}Risk flags change what the product may promise
PoolRiskFlags are not diagnostics. Each one changes what can be said before a user signs,
and each one travels to the quote's warnings array.
| Flag | What it means |
|---|---|
dynamicFee | The pool's fee can change after the quote. Such a route cannot honour a fill-at-the-quoted-rate promise. |
noOrderExpiry | The venue's order carries no expiry, so a stale order needs a paid cancel. |
permissionedBatching | Batching is permissioned, so a fill depends on a whitelisted third party. |
notes | The plain language reasons behind the flags above, for display and for logs. |
routing.dynamicFeePolicy decides whether a dynamicFee pool is excluded from routing
outright or kept and labelled.
Execution cost
interface VenueExecutionCost {
/** Batcher or execution fee in lovelace. Null when the venue has none. */
executionFeeLovelace: bigint | null;
/** Refundable minimum ada that must ride along with the order. */
depositLovelace: bigint | null;
/** True when the fee above is a worst case rather than a fixed amount. */
feeIsWorstCase: boolean;
}feeIsWorstCase covers venues whose fee is amortised across a scooped batch. The quote shows
the worst case and says so, and a total containing one upper bound is itself only an upper
bound.
The venue registry
VenueRegistry.forNetwork(provider, network) builds every adapter the configuration enables.
It refuses a provider for a different network outright, because that provider's reads would come from the wrong chain.
For each adapter it also asserts four things against the configuration, and each mismatch throws naming both sides:
- The adapter's
idmatches the venue it was built for. - The adapter's
executionClassmatches the configured one. The two decide different transaction paths, so a silent disagreement would be a wrong promise. - The adapter declares support for this network.
- The adapter reports itself available on this network.
Venues that produce no adapter are collected in registry.skipped with the reason, because
"the router saw one venue" and "the router saw one venue and silently lost three" look
identical without it.