Adapter contract
The BridgeAdapter interface a rail implements, copied from @mavna/core when this site is built.
A rail is one package that implements BridgeAdapter. Nothing in the interface names a bridge, a
chain or another SDK, so a new rail needs no change elsewhere: pass it to raceAdapters with the
others. The declarations below are copied from packages/core/src/adapter.ts when this site is
built.
BridgeAdapter
quote returns a route or a refusal; a refusal is a normal answer, not an error. buildTransactions
builds and never signs or submits. status reads where a transfer is. recover and quoteCompose
are optional.
export interface BridgeAdapter {
readonly id: string;
readonly trustModel: TrustModel;
/** Chains this rail can move value from, resolved rather than hardcoded. */
supportedChains(): Promise<readonly string[]>;
/**
* Quote, or explain why not.
*
* Must resolve within the racing timeout or the routing engine drops it: one
* slow rail must never hold up the others. Returning a rejection is a normal
* outcome and is preferred to throwing, because the reason is shown to a user.
*/
quote(request: QuoteRequest): Promise<Route | RouteRejection>;
/** Build, never sign, never submit. */
buildTransactions(route: Route, context?: BuildContext): Promise<readonly UnsignedTransaction[]>;
/** Where this transfer stands now. */
status(handle: TransferHandle): Promise<StepPhase>;
/**
* Recover a stalled transfer from inside the product.
*
* A recoverable failure must be recoverable here, without the user needing a
* third-party explorer, documentation or support.
*/
recover?(handle: TransferHandle): Promise<readonly UnsignedTransaction[]>;
/**
* A compose route beside the plain one: the same send, delivered to a contract
* that converts on arrival, so the conversion is not a user prompt. Undefined
* when this rail has no such contract on this network, so a rail without one
* adds nothing to the rejected list; a rejection when it has one but cannot
* serve this request. Its status comes back through `status` like any other.
*/
quoteCompose?(request: ComposeQuoteRequest): Promise<Route | RouteRejection | undefined>;
}What a quote is asked
export interface QuoteRequest {
readonly from: Asset;
readonly to: Asset;
readonly sends: Amount;
/** Where the value ends up. Validated by the adapter for its own rules. */
readonly destination: string;
/**
* The user's tolerance, in basis points. Their exposure is bounded by a number
* they chose; unbounded slippage is not acceptable on any path.
*/
readonly maxSlippageBps: number;
}What a build returns
/**
* How a payload should be read. One per chain family, because they share nothing.
*
* `solana-instruction` rather than a whole message: a complete Solana message
* needs a fee payer and a recent blockhash, both of which belong to the wallet
* and go stale in about a minute. Handing over the instruction lets the wallet
* compose and price the transaction the way it already knows how.
*/
export type TransactionFormat =
| "stellar-xdr"
| "evm-tx"
| "solana-instruction"
| "starknet-invoke"
| "cosmos-msg";
/** A transaction the caller must sign. The SDK never signs and never submits. */
export interface UnsignedTransaction {
readonly chainId: string;
/** Opaque to the SDK. The caller's wallet knows how to read it. */
readonly payload: string;
readonly format: TransactionFormat;
/** Said in terms that mean something, so consent can be informed. */
readonly describes: string;
/**
* When this can be left unsent, and how the caller checks.
*
* Undefined means always send it. Anything else is a condition to evaluate
* against the source chain first.
*/
readonly unnecessaryWhen: UnnecessaryWhen | undefined;
/**
* Accounts the caller must generate a fresh keypair for and sign with.
*
* Solana's burn writes its message into a brand new account, so one exists that
* is neither the user nor anything we can derive. Carrying the requirement here
* means a caller learns about it from the type rather than from a failed
* signature, and it stays empty on every chain that has no such thing.
*
* The SDK does not create these. It does not hold keys, ever.
*/
readonly ephemeralSigners: readonly string[];
}unnecessaryWhen says when a transaction can be skipped and how the caller checks:
/**
* A check that decides whether a transaction is needed at all.
*
* Token approvals are the case this exists for. A rail that moves an ERC-20 out
* of a user's wallet needs a standing allowance first, but only when the wallet
* does not already have one, and asking a user to sign an approval they do not
* need costs them gas for nothing.
*
* The SDK cannot answer the question itself: reading an allowance needs a node on
* the source chain, and the caller is the one holding a wallet already connected
* to it. So the condition is stated in a form the caller can evaluate, rather
* than the transaction being emitted unconditionally and the waste being someone
* else's problem.
*/
export interface AllowanceCheck {
readonly kind: "erc20-allowance";
/** The token contract that records the allowance. */
readonly token: string;
/** Who the allowance must name. Not the recipient of the funds. */
readonly spender: string;
/** Skip the transaction when the existing allowance is at least this. */
readonly atLeastBaseUnits: string;
/**
* Also skip it when the existing allowance is exactly this.
*
* For clearing an allowance before setting a new one, which Tether's USDT
* on Ethereum demands: its `approve` reverts when both the current and the
* new value are non-zero. Clearing is pointless when the allowance is
* already zero, so that reset says "0" here. A caller that ignores the field
* only sends a harmless approve of zero over zero.
*/
readonly skipAlsoWhenExactly?: string;
}
export type UnnecessaryWhen = AllowanceCheck;What a transfer is found by
/**
* Where a transfer can be found again.
*
* All three are first-class lookup keys, which is the largest single
* differentiator available. The prior art can only resume from a transaction hash
* or an internal task id, and its own type refuses a deposit address as a key. On
* Stellar a deposit address plus memo is the natural shape, and a user who closed
* the tab has their destination address if they have nothing else.
*/
export interface TransferHandle {
readonly sourceTxHash: string | undefined;
readonly depositAddress: string | undefined;
readonly destinationAddress: string | undefined;
/**
* Which chain the source transaction is on.
*
* Optional, because a user who closed the tab may have nothing but a hash, and
* refusing to help them is the failure this whole type exists to avoid. An
* adapter that spans many chains should search when this is absent rather than
* decline: slower is a fine price for recovering someone's transfer.
*/
readonly sourceChainId: string | undefined;
}@mavna/core also exports ReferenceAdapter, a rail that touches no network. It shows that the
interface can be implemented without reaching outside the package.