> ## Documentation Index
> Fetch the complete documentation index at: https://sdk-guide.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Preflight

> Simulate and verify a transaction before a signature is collected

Preflight answers one question before a signature exists: will this transaction succeed? It is not
fee estimation, and it does not replace the integrity checks that run inside signing.

> [!NOTE]
>
> Chains with a virtual machine (EVM, Solana, Tron) are executed against a node. Bitcoin and XRP
> have nothing to execute, so their transactions are checked by reproducing the ledger's own
> arithmetic, and so is a plain TRX send. The `check` field on every result says which one happened.

## Why it is separate from fee estimation

`estimateFee` answers "what will this cost" and pads the gas price so the quote survives a few
blocks. Preflight's affordability check uses the lower price the sender actually signs, because
borrowing the padded estimate would falsely block every "send all" transfer.

## The two entry points

```typescript theme={null}
sdk.preflightOrder(input: PreflightOrderInput): Promise<PreflightResult | null>
sdk.preflightSend(input: PreflightSendInput): Promise<PreflightResult | null>
```

```typescript theme={null}
interface PreflightOrderInput extends OrderPreflightOptions {
  order: Order;
  multisig: MultisigsItem;
  walletScope?: WalletScope;
}

interface OrderPreflightOptions {
  /**
   * The signing member. BTC and XRP orders are checked as the vault itself and
   * ignore it; Solana signs and pays from the member's own key. An EVM order is
   * simulated as the vault, and with `intent: 'execute'` this member's balance
   * is checked against the gas they pay.
   */
  signerAddress?: string;
  /** Which signature is being collected. Solana builds a different transaction for each. */
  intent?: SolanaOrderIntent; // 'sign' | 'execute'
  /** Overrides the order's own index in the vault's queue. */
  transactionIndex?: number | string | bigint;
}

interface PreflightSendInput extends SendPreflightInput {
  walletScope?: WalletScope;
}

interface SendPreflightInput {
  chainType: EChainType;
  chainId: string;
  fromAddress: string;
  to: string;
  /** Human-readable amount as typed into the form. */
  amount: string;
  decimals: number;
  tokenAddress?: string;
  isNativeToken: boolean;
  /** The TRON memo the send will carry. It enlarges the transaction and its bandwidth cost. */
  memo?: string;
  /** A BTC send of everything: the amount is what the spendable coins leave after the fee. */
  isMax?: boolean;
}
```

> [!WARNING]
>
> **`null` means "not applicable", never "checked and safe".** Both methods return `null` when no
> checker covers this chain, method or state. Gating a signing button on truthiness silently treats
> "nothing was checked" as a pass. Gate on `preflightBlocksSigning(result, isRunning)` instead.

## `PreflightResult`

```typescript theme={null}
type PreflightOutcome = "pass" | "fail" | "inconclusive";
type PreflightCheck = "simulation" | "arithmetic";

type PreflightFailureCode =
  | "simulation_reverted"
  | "insufficient_funds"
  | "insufficient_reserve"
  | "utxo_mismatch"
  | "dust_output"
  | "utxo_unavailable"
  | "unfunded_destination"
  | "non_replaceable"
  | "decimals_mismatch";

interface PreflightDetail {
  label: string;
  value: string;
}

interface PreflightResult {
  outcome: PreflightOutcome;
  check: PreflightCheck;
  chainType: EChainType;
  code: PreflightFailureCode | null;
  title: string | null;
  /** Decoded, reader-facing explanation: a revert string, a panic description, or arithmetic that does not balance. */
  reason: string | null;
  inconclusiveReason: string | null;
  details: PreflightDetail[];
  /** VM logs (Solana, Tron), shown collapsed. */
  logs: string[] | null;
  feeDisplay: string | null;
  balanceDisplay: string | null;
}
```

`code`, `title` and `reason` are populated only on `fail`, and `inconclusiveReason` only on
`inconclusive`.

## Decision table

| Result | Blocks signing | What it means | What to do |
| - | :-: | - | - |
| `null` | No | No checker covers this chain / method / state. Nothing was verified. | Proceed, but never label it "verified". Show no verdict at all. |
| `outcome: 'pass'` | No | The transaction executed cleanly, or the arithmetic balances. | Enable signing. Surface `feeDisplay` / `balanceDisplay` if you have a place for them. |
| `outcome: 'inconclusive'` | No | The check could not run: RPC unreachable, timeout, unsupported payload shape. | Proceed, but show `inconclusiveReason`. Treating it as failure would make an outage look like a reverting transaction and strand every signer. |
| `outcome: 'fail'` | Yes | The transaction will not succeed as built. | Block signing. Show `title` + `reason`, plus `details` and collapsed `logs`. |
| check still running | Yes | A verdict is in flight. | Keep the button disabled. Enabling it early lets the user outrun the check. |

```typescript theme={null}
const preflightBlocksSigning = (
  result: PreflightResult | null,
  isRunning: boolean,
): boolean => isRunning || result?.outcome === "fail";
```

> [!WARNING]
>
> A check that is still running blocks: `preflightBlocksSigning(null, true)` is `true`. Enabling the
> signing control before the verdict lands would let a user outrun it. A `null` result on its own
> does not block, so `preflightBlocksSigning(null, false)` is `false`.

Every check is bounded at 12 seconds. A node that never answers resolves to `inconclusive` with
`"The network did not respond in time."` rather than holding the gate shut. Locking a user out of
their own funds for as long as a node stays silent would be worse than reporting that the check could
not be made.

## Failure codes

| Code | Raised by | Title(s) |
| - | - | - |
| `simulation_reverted` | EVM, Solana, Tron | `Transaction will revert` / `Transaction will fail` |
| `insufficient_funds` | EVM, Solana, Tron, XRP, Bitcoin | `Insufficient funds` / `Insufficient SOL for fees` / `Insufficient funds for this transaction` / `Insufficient balance` / `Source account not activated` / `Executor cannot pay the network fee` / `Insufficient SOL` / `Insufficient TRX` / `Not enough TRX for energy` / `Insufficient BTC` |
| `insufficient_reserve` | XRP | `Reserve requirement not met` |
| `unfunded_destination` | XRP, Solana | `Destination requires more XRP to activate` / `Amount too small for a new account` |
| `utxo_mismatch` | Bitcoin | `Outputs exceed inputs` / `Fee exceeds declared amount` / `Recipient does not match` / `Change pays a third party` |
| `dust_output` | Bitcoin | `Dust output` / `Amount below the dust limit` |
| `utxo_unavailable` | Bitcoin | `Input is not spendable` / `No spendable coins` |
| `non_replaceable` | Bitcoin | `Replace-by-Fee not signaled` |
| `decimals_mismatch` | Solana | `Token decimals mismatch` |

`decimals_mismatch` fails an SPL send whose `decimals` differ from the mint's. The send scales the
amount by the `decimals` you pass and the token transfer does not check them, so it would move a
different amount than the one typed.

Bitcoin's dust thresholds are Bitcoin Core's, per output type:

| Output type | Dust threshold |
| :- | :- |
| P2WPKH | 294 sats |
| P2WSH, P2TR | 330 sats |
| P2SH | 540 sats |
| P2PKH | 546 sats |

## Coverage

### `preflightOrder`

| Chain | Check | Runs when | Returns `null` when |
| - | - | - | - |
| BTC / BTC\_T3 | arithmetic (PSBT) | `order.data` holds a PSBT | `order.data` is absent |
| XRP | arithmetic (reserve) | the first instruction is `native` and both drops and recipient resolve | an issued-asset order, or a missing amount or recipient. `token.amount` is only denominated in drops for a native transfer |
| EVM | simulation | the vault is deployed and this order is the smallest nonce. With `intent: 'execute'` and `signerAddress`, the executor's balance is also checked against the gas | vault not deployed, `order.isSmallestNonce === false`, or the chain id has no configured RPC |
| Tron | simulation | always attempted | never `null`. A missing transaction source yields `inconclusive` |
| Solana | simulation | `intent` is supplied and the order has left `proposed` | no `intent`, or `order.status === 'proposed'` |
| anything else | none | never | always |

Two EVM guards are deliberate `null`s rather than failures:

* An undeployed vault has no code to execute the call, so simulating from its address would report a
  failure that says nothing about the order.
* An order behind others in the queue executes against state those earlier orders produce. Simulating
  it against state as it stands today can revert for a reason that will not exist by the time it
  runs, and a verdict that blocks signing must never rest on that.

An EVM order is simulated as the vault, and the vault is held only to the order's `value`: the gas is
paid by the member who executes it. A vault with no native balance therefore passes a zero-value
order. When the executor's balance cannot cover the gas, the result is `insufficient_funds` with the
title `Executor cannot pay the network fee`, reported ahead of the simulation's own verdict.

On Solana, "the account is not on chain yet" is reported as `inconclusive` rather than as a failure:
a proposal the node has not caught up to answers as though it were never written, and reading that
as proof of failure would lock a co-signer out of a healthy order until the lag clears. For the same
reason, a node that has not seen the transaction's recent blockhash, or has not yet seen the final
approval of a proposal being executed, gives `inconclusive`.

### `preflightSend`

| Chain | Check | Notes |
| - | - | - |
| EVM | simulation | Native sends simulate `to`/`value`; token sends simulate an encoded ERC-20 `transfer`. `null` when `tokenAddress` is missing, when the address is still mid-typing (encoding fails), or when the chain id has no configured RPC. |
| XRP | arithmetic | Native sends only. `null` for issued assets. |
| Solana | simulation | Checks the balance against the amount, the network fee and the rent for the recipient's token account when one has to be created. A native send that would leave a new recipient below the rent-exempt minimum fails with `unfunded_destination`. Then the transfer itself is simulated. `null` when `tokenAddress` is missing on a token send, or while an address is not yet valid. |
| Tron (TRX) | arithmetic | Checks TRX against the amount plus the least the network will charge: bandwidth, only when neither staked nor free bandwidth covers it on its own, and the fee for creating a new recipient's account. Pass `memo` when the send carries one. `null` while an address is not yet valid. |
| Tron (TRC-20) | simulation | Simulates the transfer, then checks the wallet's TRX against the energy and bandwidth it would burn. `null` while an address is not yet valid. |
| BTC | arithmetic | Selects coins the same way the send does. Fails with `insufficient_funds`, `dust_output` or `utxo_unavailable`. A shortfall found on the fallback fee rate, used when the live rate is unavailable, is `inconclusive`. Pass `isMax` for a send of everything. `null` while the recipient is not yet an address on this network. |
| ADA | none | Always `null`. |

`preflightSend` also returns `null` for an unparseable amount, or an amount that resolves to zero or
less.

## Worked example: preflight, inspect, sign or abort

```typescript theme={null}
import {
  preflightBlocksSigning,
  type PreflightResult,
} from "vaults-multichain-sdk";

async function signWithPreflight(order, multisig, signerAddress) {
  let result: PreflightResult | null = null;
  let running = true;

  try {
    result = await sdk.preflightOrder({
      order,
      multisig,
      signerAddress,
      intent: "sign",
      walletScope: "organization",
    });
  } finally {
    running = false;
  }

  if (preflightBlocksSigning(result, running)) {
    // Only reachable on outcome === 'fail' once running is false.
    throw new Error(`${result!.title}: ${result!.reason}`);
  }

  if (result === null) {
    // Nothing was checked. Do not tell the user the order was verified.
    console.log("No preflight check covers this order.");
  } else if (result.outcome === "inconclusive") {
    console.warn("Preflight could not run:", result.inconclusiveReason);
  } else {
    console.log("Preflight passed.", result.feeDisplay, result.balanceDisplay);
    for (const { label, value } of result.details) {
      console.log(`  ${label}: ${value}`);
    }
  }

  return sdk.signOrder({
    /* … */
  });
}
```

A personal send follows the same shape:

```typescript theme={null}
const verdict = await sdk.preflightSend({
  chainType: EChainType.EVM,
  chainId: "1",
  fromAddress: senderAddress,
  to: recipientAddress,
  amount: "0.25",
  decimals: 18,
  isNativeToken: true,
});

if (preflightBlocksSigning(verdict, false)) {
  throw new Error(verdict!.reason ?? "Preflight failed");
}

await sdk.sendTransaction({
  /* … */
});
```

## Signing-time integrity checks

Preflight asks *will it succeed*. A separate family of checks asks *is this the transaction I was
shown*. These throw rather than returning a verdict, and they run inside signing on every chain, so
you get them by signing through the SDK.

Three things are enforced. The recipient and amount decoded from the transaction are compared with
the recipient and amount the order put in front of the co-signer, and a disagreement throws
`SignatureIntentMismatchError` instead of producing a signature. On EVM vaults the digest presented
for signature is recomputed from the readable order fields, so an opaque hash cannot stand for a
different transaction than the one displayed. And the key rebuilt for the signature must derive the
wallet's own address.

Together that is what makes signing "what you see is what you sign": the user consents to what they
were shown, so a payload that pays someone else never reaches the signing key.

> [!NOTE]
>
> These checks cannot defend against a backend that lies *consistently* in both the order fields and
> the payload, and nothing on the client can. They defend against the payload and the display
> diverging.

### Things worth surfacing to a signer

Some properties of an order are not failures but do change what the signer is agreeing to. Show them
alongside the order:

| Condition | What to tell the signer |
| - | - |
| always | The network fee is paid by whoever executes this order, and is not fixed by your signature. |
| the order runs a delegatecall | The target contract executes with the vault's own permissions. |
| the order refunds its executor in a token | Name the token. |
| a token transfer | Name the token contract. |
| a token whose scale is unknown | The amount is shown in the token's smallest unit. |
| calldata that is not a standard transfer | This order calls a contract with data that is not a standard token transfer. |

> [!WARNING]
>
> When a token's decimals are not known, the amount is reported in base units and the amount
> comparison is skipped rather than run against a guess, so an honest order does not fail on a
> missing scale. The recipient is still checked either way, so an address swap is caught regardless.
> Supply the token's decimals to get the amount checked too.

## Next

> **[Orders](https://sdk-guide.mintlify.site/sdk-guide/orders.md)**
>
> Create, sign, and execute multisig orders

> **[Errors](https://sdk-guide.mintlify.site/sdk-guide/errors.md)**
>
> `SignatureIntentMismatchError` and the rest of the error surface

> **[Transactions](https://sdk-guide.mintlify.site/sdk-guide/transactions.md)**
>
> Personal sends, fees, memos, and spend authorization

> **[Multisig Fees](https://sdk-guide.mintlify.site/sdk-guide/multisig-fees.md)**
>
> Accurate per-chain vault fee estimation


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.