Mavna
Reference

Core types

The money, route and execution types from @mavna/core, copied from the source with their comments when this site is built.

The comments in the declarations are the source's own.

Money

Assets, amounts and fees. Assets and amounts explains the rules behind them.

/** A chain this product can move value from or to. */
export interface ChainRef {
  /** Stable identifier we choose, not a third party's. */
  readonly id: string;
  readonly name: string;
  /** Where a transaction on this chain can be looked at. */
  readonly explorerTxUrl: (hash: string) => string;
}

export interface Asset {
  /** Unambiguous identity: chain plus the chain's own notion of the asset. */
  readonly chainId: string;
  /**
   * On Stellar this is `CODE:ISSUER` or `native`; on an EVM chain the contract
   * address. Never a bare symbol, which is not an identity.
   */
  readonly id: string;
  readonly symbol: string;
  readonly decimals: number;
  /** Who vouches for this asset appearing in a list a user picks from. */
  readonly curatedBy: string | undefined;
}

/** An amount of a specific asset. The pair travels together so they cannot drift apart. */
export interface Amount {
  readonly asset: Asset;
  /** Integer in the asset's base units. A string, never a number. */
  readonly baseUnits: string;
}

/**
 * Who receives a fee and how much.
 *
 * An array from the outset, deliberately. The most mature product in this space
 * shipped a two-party fee split, outgrew it, and had to bolt an array on beside
 * the original fields, which their own code comments now describe as legacy kept
 * for backward compatibility. Starting with the general shape costs nothing.
 */
export interface FeeRecipient {
  readonly name: string;
  readonly amount: Amount;
  /** Where it goes, when that is knowable and worth showing. */
  readonly address: string | undefined;
}

export type FeeKind =
  | "bridge"
  | "swap"
  | "gas"
  | "integrator"
  /**
   * The Stellar deposit. Not a fee taken from the user: it becomes XLM locked in
   * their own account as their reserve, returning to them if they ever close it.
   * Named separately so the interface can say so rather than overstating the cost.
   */
  | "account-reserve"
  /**
   * The most a held settlement may take of the arrival to repay a reserve the
   * sponsor fronted. Left aside from any conversion, in the arriving asset; what
   * the repayment does not use stays with the user.
   */
  | "reserve-repayment";

export interface Fee {
  readonly kind: FeeKind;
  readonly total: Amount;
  readonly recipients: readonly FeeRecipient[];
  /** True when this is already deducted from the quoted output. */
  readonly includedInQuote: boolean;
}

Routes

A quote and a transfer in flight have the same type: the execution state is added to the quoted route. Quote and rank routes shows how routes are produced.

/** How a rail is trusted, in terms a user can weigh without knowing the words. */
export type TrustModel =
  | "native-mint" // the issuer burns and mints; nobody holds your money in between
  | "validator-set" // a named set of validators attests to the transfer
  | "solver-network" // a competing market of solvers fronts the funds
  | "liquidity-pool" // a pool holds both sides and swaps between them
  // The application picks its own verifiers rather than inheriting a fixed set.
  // Kept apart from validator-set because the difference is the user's to weigh:
  // who is trusted is chosen by whoever deployed the token, not by the network,
  // so two routes on the same rail can have different answers.
  | "dvn-stack";

export interface StepQuote {
  readonly id: string;
  readonly kind:
    | "approve-source"
    | "bridge"
    | "swap"
    | "prepare-account"
    | "settle-account"
    // the conversion an on-chain contract performs on arrival; nobody signs it
    | "compose";
  /** Which route source produced this. Errors must be attributable. */
  readonly source: string;
  readonly from: Amount;
  readonly to: Amount;
  /** Seconds, honest rather than optimistic. */
  readonly estimatedSeconds: number;
}

export interface StepExecution {
  readonly status: StepPhase;
  readonly startedAtUnix: number | undefined;
}

/** The quoted step plus, once it starts, what actually happened. */
export interface Step extends StepQuote {
  readonly execution: StepExecution | undefined;
}

export interface Route {
  readonly id: string;
  readonly source: string;
  readonly trustModel: TrustModel;
  readonly fromChain: ChainRef;
  readonly toChain: ChainRef;
  /**
   * Where the value ends up.
   *
   * On the route rather than passed alongside it, because a route without a
   * destination is not a route: an adapter cannot build its transaction without
   * one. Carrying it here also means a quote can never be signed against an
   * address other than the one it was quoted for, which on CCTP is the exact
   * mistake Circle warns about twice, in red, as unrecoverable.
   */
  readonly destination: string;
  readonly sends: Amount;
  /**
   * What actually lands after every fee. This is the primary number in the
   * interface, never the amount sent.
   */
  readonly receives: Amount;
  /** Worst case the user has agreed to, so exposure is bounded by their choice. */
  readonly receivesAtLeast: Amount;
  /**
   * The floor of what the rail itself delivers, in the bridged asset, which
   * enrichment never changes and which builders enforce on-chain.
   */
  readonly deliveredAtLeast: Amount;
  readonly fees: readonly Fee[];
  readonly estimatedSeconds: number;
  readonly steps: readonly Step[];
  /** How many times the user will be asked to sign, told upfront. At most this many. */
  readonly signaturesRequired: number;
  /**
   * How many of those may be skipped, such as a reset of an allowance there may
   * be nothing to clear. Absent or zero means every one is asked for.
   */
  readonly optionalSignatures?: number;
  /** When this quote stops being honoured. */
  readonly expiresAtUnix: number;
  /**
   * Present on a compose route: the bridged token is delivered to a contract
   * that converts it on arrival in the same flow, so the conversion is not a
   * user prompt. Absent on every plain route.
   */
  readonly compose?: ComposeArrival | undefined;
}

/**
 * Why a route source produced nothing.
 *
 * Structural, not a silent omission. The functional requirements say a user must
 * be able to see WHY a route is unavailable rather than watching it quietly not
 * appear, and that is impossible if the reason was never carried.
 */
export 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";
  /** Shown to the user. Says what happened and what to do; never apologises. */
  readonly explanation: string;
  /**
   * Whether asking again can change the answer. A rail that is briefly down and
   * a chain that is genuinely unsupported both produce a rejection, and a caller
   * that cannot tell them apart either retries forever or gives up too soon.
   */
  readonly retryable: boolean;
  /**
   * For whoever runs the integration, never shown to the user: a venue's or a
   * node's own words (host errors, URLs), which can carry raw protocol text.
   */
  readonly detail?: string;
}

/**
 * The result of asking every source at once.
 *
 * Partial results are the NORMAL case, not a degraded one: sources are raced and
 * some answer before others. A caller must be able to render what arrived without
 * waiting for the slowest, which is why this carries both together.
 */
export interface RouteSet {
  readonly routes: readonly Route[];
  readonly rejected: readonly RouteRejection[];
  /** True once every source has answered or timed out. */
  readonly complete: boolean;
}

/** How routes are ordered for the user. Default is the best net outcome. */
export type RouteRanking = "best-received" | "fastest" | "fewest-signatures";

Execution

The phase of each step. Track and resume a transfer shows how to use them.

/** Why a transfer is waiting for the user rather than for the network. */
export type Blocked =
  | { readonly kind: "needs-signature"; readonly signaturesRemaining: number }
  | { readonly kind: "needs-wallet"; readonly requiredChainId: string }
  | {
      readonly kind: "needs-network-switch";
      readonly currentChainId: string;
      readonly requiredChainId: string;
    }
  | { readonly kind: "quote-expired"; readonly refreshable: true };

/** What a user can do about a failure. Never a message with no action beside it. */
export type Remedy =
  | { readonly kind: "wait"; readonly untilUnix: number; readonly because: string }
  | { readonly kind: "retry"; readonly because: string }
  | { readonly kind: "add-gas"; readonly chainId: string; readonly because: string }
  | { readonly kind: "claim"; readonly at: string; readonly because: string }
  | { readonly kind: "contact"; readonly reference: string; readonly because: string }
  /** Said plainly, so nobody retries forever hoping. */
  | { readonly kind: "unrecoverable"; readonly because: string };

export interface OnChainRef {
  readonly chainId: string;
  readonly txHash: string;
  readonly explorerUrl: string;
}

export type StepPhase =
  /** Nothing has happened yet. */
  | { readonly phase: "idle" }
  /** Waiting on the user. Carries exactly what the prompt needs to render itself. */
  | { readonly phase: "blocked"; readonly blocked: Blocked }
  /** Submitted and progressing normally. */
  | { readonly phase: "pending"; readonly ref: OnChainRef; readonly expectedByUnix: number }
  /**
   * Past its expected duration. Distinct from pending because the design rules
   * require a user to tell "slow but proceeding" from "stalled and needs you"
   * without reading, and a state that does not exist cannot be shown.
   */
  | {
      readonly phase: "stalled";
      readonly ref: OnChainRef;
      readonly overdueSeconds: number;
      readonly remedy: Remedy;
    }
  | { readonly phase: "done"; readonly ref: OnChainRef; readonly received: Amount }
  /**
   * Terminal states of their own, not sub-states of success. The prior art buries
   * these under "done" and has to scan nested actions to find them, which means a
   * refunded transfer reads as a plain success in its own history view.
   */
  | {
      readonly phase: "refunded";
      readonly ref: OnChainRef;
      readonly to: string;
      readonly returned: Amount;
    }
  | {
      readonly phase: "partial";
      readonly ref: OnChainRef;
      readonly received: Amount;
      readonly expected: Amount;
      readonly remedy: Remedy;
    }
  | {
      readonly phase: "failed";
      readonly cause: string;
      readonly remedy: Remedy;
      readonly ref: OnChainRef | undefined;
    }
  /**
   * Delivered to the contract that converts on arrival, and the conversion has
   * not run yet. Not a failure: the funds are on Stellar, held by a step anyone
   * may complete, and `finish` is what completing it takes.
   */
  | {
      readonly phase: "delivered-conversion-pending";
      readonly ref: OnChainRef;
      readonly deliveredAtUnix: number;
      readonly finish: ComposeFinish | undefined;
    }
  /** Converted on arrival and sent on, proven by the converting contract's own event. */
  | {
      readonly phase: "delivered-and-converted";
      readonly ref: OnChainRef;
      readonly received: Amount;
      readonly fee: Amount | undefined;
    }
  /**
   * A conversion on arrival that could not meet the user's minimum, so the
   * arriving token was delivered unchanged. The user has their money, but not
   * in the asset they asked for, so it is its own ending and never a success.
   */
  | {
      readonly phase: "delivered-as-came";
      readonly ref: OnChainRef;
      readonly received: Amount;
      readonly because: string;
    }
  /**
   * The rail's own record says the transfer executed on Stellar, in the
   * transaction `ref` names, and that transaction is now older than any
   * source this can read it back from (public RPC keeps days, and Horizon
   * shows no movement of a token that is its own Soroban contract). An ending,
   * shown as information with the link to check it: never a success, because
   * what arrived was not read from the chain, and never a failure, because
   * nothing says it failed.
   */
  | {
      readonly phase: "delivered-unverified";
      readonly ref: OnChainRef;
      readonly because: string;
    };

On this page