@orbitaldefi/sdk
v0.2.2
Published
Orbital AMM SDK: exact off-chain quotes, pool state reads and swap calldata.
Maintainers
Readme
@orbitaldefi/sdk
Integration SDK for Orbital pools: three stablecoins in one concentrated pool (Paradigm's Orbital design). It gives wallets, routers and aggregators:
- Exact off-chain quotes.
quoteExactInputis a dependency-freebigintport of the pool's quote path. It returns whatOrbitalPool.quote(assetIn, assetOut, amountIn, true)returns, to the wei, including every revert reason, on a pool of the R40 catalog (ticks 40 to 238). - State reads.
fetchPoolStatereads everything a quote needs at one block, in two multicall rounds, with the pool's swap access. - State tracking from events.
applyEventreplays aSwaporSwapExactOutputlog through the engine and gives the exact committed state, so no RPC call is needed between swaps;replayLogsreplays a run of logs and refetches aroundMintandBurn. - Transactions. Calldata for
swapExactInputandswapExactOutput(buildSwapExactOutputCall), approvals and slippage bounds.
npm install @orbitaldefi/sdk viemviem is an optional peer dependency: @orbitaldefi/sdk/math has no dependency at all and works from CommonJS or ESM,
with ethers or anything else.
Quote and swap
import {createPublicClient, http} from 'viem';
import {polygon} from 'viem/chains';
import {POLYGON, buildSwapCall, fetchPoolState, minAmountOut, quoteExactInput} from '@orbitaldefi/sdk';
const client = createPublicClient({chain: polygon, transport: http()});
const pool = await fetchPoolState(client, POLYGON.pool);
// 1,000 USDC (asset 0) for USDT0 (asset 1). Amounts are raw token units, fee included.
const quote = quoteExactInput(pool, 0, 1, 1_000_000_000n);
const call = buildSwapCall({
pool: POLYGON.pool,
assetIn: 0,
assetOut: 1,
amountIn: 1_000_000_000n,
minAmountOut: minAmountOut(quote.amountOut, 10), // 10 bp
recipient: account,
deadline: BigInt(Math.floor(Date.now() / 1000) + 600),
});
// Approve the pool for amountIn on the input token (approvalCalls), then send `call`.quoteExactInput throws OrbitalQuoteError when the pool would revert; error.reason is the on-chain string.
tryQuoteExactInput returns null instead. quoteExactOutput ports the exact-output quote: the output reserve
falls by exactly the request, the input reserve rises to the smallest certified value, and amountIn pays the net
input that adds, fee grossed up and rounded up to raw. nextState is that endpoint, the state swapExactOutput
commits.
Integration facts
| | |
|---|---|
| Assets | Exactly three per pool, addressed by index 0, 1, 2 in the pool's permanent order (assets(i)). |
| Execution | swapExactInput(assetIn, assetOut, amountIn, minAmountOut, recipient, deadline). The pool pulls amountIn with transferFrom(msg.sender): approve, then call. No callback, no native token, no fee-on-transfer tokens. |
| Exact output | swapExactOutput(assetIn, assetOut, amountOut, maxAmountIn, recipient, deadline), built by buildSwapExactOutputCall and quoted by quoteExactOutput; bound the input with maxAmountIn. On-chain, quote(assetIn, assetOut, amount, exactInput) returns (amountIn, amountOut) for both directions. |
| Access | pool.swapPublic must be true for an aggregator's executor to swap. The pool gates only mint (and the emergency shutdown and sweep) through its authority, canCall(caller, pool, selector), so anyone may swap until the pool is shut down: swapPublic is !isShutdown(). A shut-down pool still answers quote but refuses swaps. |
| Fee | fee() as a WAD fraction of the input, rounded up in favor of the pool. Polygon: 5e12 (0.0005 %). |
| Minimum | Normalized input of at least 1e12 (one raw unit of a 6-decimal token). |
| Discovery | Factory allPoolsLength() / allPools(i) (listPools), getAllPools(), or PoolCreated(asset0, asset1, asset2, fee, pool) logs. The same tokens in another order or with another fee form another pool. |
| Events | Swap(caller, recipient, assetIn, assetOut, amountIn, amountOut) for an exact input; SwapExactOutput with the same fields for an exact output; Mint/Burn(caller, receiver, tick, liquidity, amounts); Collect(caller, recipient, tick, amounts). |
| On-chain quote gas | Up to the whole tick walk: give eth_call a budget of QUOTE_GAS (2^24). |
| Swap gas | estimateSwapGas(quote.ticksCrossed): an upper bound of 260k plus 190k per crossed tick. |
Revert reasons of a quote: Input below minimum, Input exceeds radius, Reserve exceeds maximum (each real reserve
is stored on 96 bits), Output exceeds reserves, Output too small,
No output, Seed limit (the input would push the pool past its widest tick), Off-branch endpoint,
Invalid asset index, Same asset; an exact-output swap adds Excessive input above maxAmountIn.
Tracking state from events
import {fetchPoolState, replayLogs} from '@orbitaldefi/sdk';
// `logs`: the pool's logs after `blockNumber`, in chain order (getLogs).
let state = await fetchPoolState(client, pool, {blockNumber});
state = await replayLogs(state, logs, block => fetchPoolState(client, pool, {blockNumber: block}));A Swap is replayed as an exact input and checked against the logged output; a SwapExactOutput is replayed as an
exact output and checked against the logged input. Collect changes no quote input. Mint and
Burn certify their state with a repair this version does not port: replayLogs refetches the state at the end of
their block and skips that block's later logs, which the fresh read already holds. Replaying event by event with
applyEvent, do the same: a Swap after a Mint in the same block, applied again on the refetched state, fails its
replay check.
swapPublic is not replayed: decodePoolLog ignores Shutdown, so refetch after it.
poolStateToJson / poolStateFromJson serialize a state with decimal strings.
Exactness
The engine solves the swap endpoint with integer probes (bracketed Newton, then bisection) that may stop a few wei
above the exact optimum, so only the same sequence of integer operations matches the chain. src/math copies
OrbitalPool._quoteExactInput and _quoteExactOutput, OrbitalSwapEngine (without the fee attribution, which never
changes the endpoint), OrbitalInvariant, OrbitalState, the tick catalog and bitmap step for step, with Solidity's
checked arithmetic.
It is tested three ways:
Contract vectors.
test/vectors/*.jsonl(shipped in the package) is written by the realOrbitalPool(test/sdk/SdkVectors.t.solin the Orbital repository): about 9,700 quotes and revert reasons, exact inputs and exact outputs in equal numbers, over 27 states, sphere and torus, 6 to 18 decimals, deep depegs, the seed limit and the 96-bit reserve capacity, plus every committed state after a swap. Each line is one of:{"state": "<id>", ...PoolStateJson}["<id>", assetIn, assetOut, "amountIn", "amountOut" | "!reason"]["<id>", assetIn, assetOut, "amountOut", "amountIn" | "!reason", "exactOutput"]{"swap": "<id>", "next": "<id>", "assetIn", "assetOut", "amountIn", "amountOut"[, "exactOutput": true]}
The format is language-agnostic: ports to other languages can test against the same files.
Polygon fork.
npm run test:forkruns anvil on a Polygon fork: local quotes againsteth_callon the deployed pool, liquidity added on several ticks, executed swaps replayed from their logs, gas against receipts. Skipped withoutPOLYGON_RPC_URL.Unit tests of the tick catalog (all 199 R40 ticks, indices 40 to 238,
p = 1 - tickDeviationE8(tick) * 1e-8from 0.99999 to the seed at 0.10), bitmap, arithmetic, exact-output search, events and calldata.
Deployments
| Chain | Factory | Pool | Assets |
|---|---|---|---|
| Polygon PoS (137) | 0x143788B337452a466A154236F5128299B821670B | 0x6b229b018Cc4B5ba48ab3C7a5b4578E17Bc117DA | USDC, USDT0, DAI |
License
MIT. The Orbital core contracts are BUSL-1.1; IOrbitalPool and this SDK are MIT.
