Swap SDK
The SDK exposes stateless private EBSwap.atomicSwap primitives plus a small taker-side helper layer. It does not run the rate provider, maker service, registrar, DB, or rebalancer.
The primitive layer consists of current SwapIntent types, hashSwapIntent, hashEBSwapTransferParams, buildSwapIntentAuthTypedData (SwapIntentAuth EIP-712 primary type), prepareSwapTransferLeg, and maker amount-opening validation. Deterministic hashing uses explicit internal ABI tuple components.
The taker layer computes deterministic swap amounts from a decoded rate quote, requests an accepted maker quote, and turns that quote into a signed maker-order submission. The quote layer speaks rate-provider token symbols end to end, mirroring mm_quote; the SDK resolves symbols to on-chain addresses only when it builds the SwapIntent at submit. zkpSwapActions owns the rate-provider call; the stateless entry points remain usable directly with any objects satisfying the maker duck types (createMakerClient provides the standard JSON-RPC implementation). One object may implement both interfaces. Passing amountIn fixes the input (fee folds into amountOut); passing amountOut fixes the output (fee folds into amountIn). Same-side combinations are not representable - the MM only verifies opposite-side derivations.
calculateSwapAmounts is exported standalone and attached to the swap actions for discoverability; react-hooks adds useSwapRate (interval-refetched rate, default 15s, keeping the last rate through transient failures) so a UI derives amounts per input change with no network call. Besides the derived amount it returns feeAmount: the derived-side difference against the no-fee conversion - floor(amountIn * price) - amountOut when amountIn is fixed (denominated in tokenOut), amountIn - ceil(amountOut / price) when amountOut is fixed (denominated in tokenIn). It is informational and already folded into the derived amount; the MM defines no fee amount of its own, so this is the SDK's canonical definition.
submitTakerSwap uses the main account as the taker source. It only prepares/signs locally and does not need a send-capable wallet client. The helper prepares the signed taker order, submits the native order payload to the maker client, and returns only { intentHash, submitResult }.
const maker = appMakerClient;
const rateQuote = await rateProvider.getQuote(...);
const quote = await quoteTakerSwap(maker, {
tokenIn: 'zkUSD',
tokenOut: 'zkEUR',
amountIn,
rateQuote, // decoded to native SDK values
});
await submitTakerSwap(client, account, maker, { ebSwap, quote });
Maker amount validation takes the on-chain intent, plaintext amountIn and amountOut, the maker receive secret key for makerReceiveEpk, and amountOut randomness. It validates amountIn by decrypting amountInCiphertext with that secret key, and validates amountOut by re-encrypting it under takerReceiveEpk. The client does not send amountIn randomness to the maker. The amounts must be nonzero u64 values, matching the transfer circuit domain.
The private atomicSwap model has no on-chain pre-settlement lifecycle; an unfilled order expires off-chain, and that path escrows no funds.
Client surface
The app-facing surface extends the existing client, mirroring the other action bundles. createSwapClient bundles a decrypt client with the swap actions; maker, rates, and ebSwap are optional and default per chain from the deployment manifest (services.makerUrl, services.rateProviderUrl, swaps.ebSwap via DEFAULT_SWAP_CONFIG), resolving at extend time and throwing a field-naming error when the chain has no canonical value:
const client = createSwapClient({ chain, zkpAccount }); // manifest defaults
// or explicit, extending any compatible client:
const client = createDecryptClient({ chain, transport, zkpAccount }).extend(
zkpSwapActions({
zkpAccount: account,
maker: createMakerClient(url),
rates: createRateProviderClient(url),
ebSwap,
}),
);
const rate = await client.getSwapRate({ tokenIn: 'zkUSD', tokenOut: 'zkEUR' });
const quote = await client.quoteSwap({ tokenIn: 'zkUSD', tokenOut: 'zkEUR', amountIn });
const { intentHash } = await client.swap({ quote, onProgress });
await client.waitForSwap({ intentHash, onStatus });
createMakerClient speaks the market-maker JSON-RPC wire format and createRateProviderClient the rate-provider one (see those specs); wire encoding lives in swap/wire.ts and service errors surface as SwapRpcError (JSON-RPC code + method). quoteSwap owns the rate-provider call; the app no longer fetches rates itself. waitForSwap polls mm_swapIntentStatus (3s interval, 120s timeout by default), tolerates transient poll failures until the deadline, and throws on fail/expiry/timeout. The quote wire speaks token symbols end to end (the same language as the rate provider); the SDK resolves symbols to addresses only when building the SwapIntent, and the MM binds those addresses in the quote HMAC server-side.
The mixed public/encrypted counterpart is specified separately in Public Swaps. It uses EBSwap.atomicPublicSwap on the same contract and reuses the existing ebSwap configuration. Existing private SDK and MM primitives and APIs remain unchanged.