Mavna

Track and resume a transfer

Read a transfer's state from the chain with a lookup key, keep it moving forward only, and tell a slow transfer from a stalled one.

@mavna/status holds the functions for tracking a transfer and finding it again. Everything is read from the chains: local storage is a convenience, never the record. Reading a transfer's state needs no wallet; only acting on it, such as finishing a delivery, needs a signature.

Ask a rail where a transfer is

Every adapter has status(handle), which returns the transfer's current StepPhase. A TransferHandle carries what you know about the transfer:

const phase = await cctp.status({
  sourceTxHash: "0x...",
  depositAddress: undefined,
  destinationAddress: undefined,
  sourceChainId: undefined,
});

Which field a rail can find a transfer by:

RailFinds a transfer by
Circle CCTPsourceTxHash, searching every source chain when sourceChainId is absent
AxelarsourceTxHash
LayerZerosourceTxHash
NEAR IntentsdepositAddress, or sourceTxHash, from which it reads the deposit address

A handle with only destinationAddress answers idle on every rail: a Stellar address alone does not identify one transfer.

Circle CCTP, Axelar and LayerZero also answer idle for a sourceTxHash their index has no record of. Each index answers the same for a hash it has never seen and for one it has not listed yet, so idle claims neither failure nor progress. When you watch a transfer you sent, keep asking; when you look up a pasted hash, idle means no rail lists it. A Circle burn that Circle has observed and is still confirming answers pending.

Turn what your user pasted into a handle

parseLookupKey classifies a pasted string, and toHandle turns the result into a handle:

import { parseLookupKey, toHandle } from "@mavna/status";

const handle = toHandle(parseLookupKey(pasted));
InputRead as
64 hex digits, with or without 0xsource transaction
a Stellar address starting with G, M or Cdestination address
0x and 40 hex digitsdeposit address
any other 0x hex value of up to 64 digitssource transaction (a Starknet hash without leading zeros)
64 to 88 base58 characterssource transaction (a Solana signature)

Anything else throws a MavnaError with a sentence to show your user. toHandle leaves sourceChainId empty unless you pass it, so a rail that serves many chains searches them all.

Phases

StepPhase is one of these, by its phase field:

PhaseMeaning
idlenothing found, or not started
blockedwaiting on your user, with a Blocked reason
pendingmoving, with an on-chain reference and when it is expected
stalledoverdue, with how long and a Remedy that says what to do
donearrived, with the amount received
refundedreturned, with the address and the amount
partialpart of it arrived, with what was received and what was expected
failedended without arriving, with the cause and a remedy
delivered-conversion-pendingdelivered to the Arrival Contract, with the conversion still to run
delivered-and-converteddelivered and converted on arrival
delivered-as-camedelivered without conversion, because the conversion could not meet the minimum
delivered-unverifiedthe rail records it as executed on Stellar, and the amount can no longer be read back

isTerminal in @mavna/core is true for done, refunded, partial, failed, delivered-and-converted, delivered-as-came and delivered-unverified. isSuccess is true only for done and delivered-and-converted. The full declarations are in Core types.

Keep the state moving forward

A TransferState holds a transfer's steps, each a TrackedStep with its phase, its expected duration and when it started.

  • applyUpdate(state, stepId, next, nowUnix) applies a new phase only when canAdvance allows it: a finished step never changes, and an unfinished one never moves back. The one exception is a stalled step that a new transaction set moving again, which can go back to pending. A late answer therefore cannot turn a delivered transfer back into a pending one.
  • escalateIfOverdue(step, nowUnix, remedyFor) turns a pending step into stalled once it is past its start plus its expected duration, with the seconds it is overdue and the remedy your remedyFor returns.
  • progressOf(state) says how many steps are done, which step is current, whether the transfer needs your user, and whether it has finished.
  • outcomeOf(state) sums up the transfer as failed, refunded, partial, delivered-as-came, delivered-unverified, succeeded or in-flight, checked in that order. succeeded needs every step to be done or delivered-and-converted.

To keep handles across page loads, implement TransferStore (remember, forget, all) over whatever storage you use. InMemoryTransferStore is the in-memory version.

On this page