Quote and rank routes
Ask every rail at once with raceAdapters, show routes as they arrive, and treat a refusal as data with a reason.
raceAdapters from @mavna/routing asks every adapter you pass it at the same time and returns a
RouteSet: the routes, ranked, and the refusals.
import { raceAdapters } from "@mavna/routing";
const set = await raceAdapters([cctp, axelar, layerzero, nearIntents], request, {
onUpdate: (partial) => render(partial),
});request is a QuoteRequest: the asset sent (from), the asset wanted (to), the amount
(sends), the Stellar destination, and maxSlippageBps, the most your user accepts to lose to
price movement, in basis points. The quickstart builds one.
Show answers as they arrive
Each rail answers on its own time. onUpdate is called every time one answers, with the routes so
far, ranked, and the refusals so far. complete is true once every rail has answered or run out
of time, and the promise raceAdapters returns resolves with the complete set.
Each rail gets ADAPTER_QUOTE_TIMEOUT_MS from @mavna/core, 3000 milliseconds, unless you pass
timeoutMs. A rail that runs out of time is a refusal with the reason timed-out; the other rails
are not held back by it.
Refusals
A rail that cannot carry the transfer, that runs out of time, or that throws, appears in
set.rejected as a RouteRejection:
interface RouteRejection {
readonly source: string;
readonly reason:
| "no-liquidity"
| "amount-too-small"
| "amount-too-large"
| "asset-unsupported"
| "chain-unsupported"
| "destination-unsupported"
| "temporarily-unavailable"
| "timed-out"
| "errored";
readonly explanation: string;
readonly retryable: boolean;
readonly detail?: string;
}explanationis a sentence for your user: what happened and what to do.retryablesays whether asking again can change the answer. It istruefortemporarily-unavailable,timed-out,no-liquidityanderrored, andfalsefor the reasons that describe the request itself.rejectionIsRetryablein@mavna/coremakes that decision.detail, when present, carries a node's or a service's own words, which can include raw protocol text and URLs. Log it for yourself; show your userexplanation.
Every rail checks destination before it asks any network. An empty destination, a secret key, or
text that is not a Stellar address is refused as destination-unsupported with the same
explanation on every rail, the sentence recipientRefusal in @mavna/stellar returns. That
sentence never repeats the input, so nothing pasted is echoed back into your page.
Ranking
ranking orders the routes:
| Value | Order |
|---|---|
"best-received" (default) | the most arriving first, compared in integer base units |
"fastest" | the shortest estimatedSeconds first |
"fewest-signatures" | the fewest signaturesRequired first |
Turn a rail off
breaker takes a circuit breaker that removes rails from the race without a release. A rail it
turns off still appears in rejected, as temporarily-unavailable with your sentence as the
explanation.
import { createCircuitBreaker, parseDisabledSources } from "@mavna/routing";
const breaker = createCircuitBreaker(() => parseDisabledSources(process.env.MAVNA_DISABLED_SOURCES));parseDisabledSources reads comma-separated id:reason pairs, such as
cctp:Circle is not available right now. An entry without a reason uses
"This route is turned off right now." The breaker reads the list through the function on every
quote, so a change takes effect on the next quote.
Convert and take a fee
Two more options change the routes after a rail quotes them:
swapattaches a conversion on Stellar, so your user can receive another asset than the one the rail delivers. Conversion on Stellar explains it.integratorFeetakes your fee,{ bps, recipient, recipientName, collects }, from the routes thatcollectsaccepts, and lists it on each such route as anintegratorfee.bpsmust be an integer from 0 to 1000.