> ## 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.

# Transactions

> Send value from a personal wallet: fees, memos, address validation, and spend authorization

`sendTransaction` moves value out of a personal, single-signature wallet. Multisig transfers go
through the order lifecycle instead. See [Orders](https://sdk-guide.mintlify.site/sdk-guide/orders.md).

> [!NOTE]
>
> A wallet on a hardware root sends with `prepareHardwareSend` (or `prepareHardwareTrustLine` for XRP
> trust lines). `sendTransaction` throws `HardwareSignerRequiredError` for it. See
> [Hardware Wallets](https://sdk-guide.mintlify.site/sdk-guide/hardware-wallets.md#method-map).

## Prerequisites

> [!NOTE]
>
> If the account runs in [strict authentication](https://sdk-guide.mintlify.site/sdk-guide/strict-authentication.md), pass
> `walletPassword` on every signing call. Without it the SDK throws
> `StrictAuthPasswordRequiredError`.

Sending reconstructs a private key, so it needs the full session stack:

```typescript theme={null}
const userRef = await sdk.getSdkUserId();
await sdk.unlockWalletSession(userRef, password);

await sdk.runWithUserRef(userRef, async () => {
  const { txHash } = await sdk.sendTransaction({
    chain: EChainType.EVM,
    from: senderAddress,
    to: recipientAddress,
    amount: '0.1',
    chainId: 1,
    tokenAddress: '',
    decimals: 18,
    isNativeToken: true,
  });
});
```

`runWithUserRef` scopes the signing user for the whole callback, keeping concurrent requests
isolated. The alternative is passing `userRef` on each input.

## `SendTransactionInput`

```typescript theme={null}
interface SendTransactionInput {
  /** Opaque user id for session-scoped signing (or use runWithUserRef). */
  userRef?: string;
  chain: EChainType;
  from: string;
  to: string;
  amount: string;
  symbol?: string;
  chainId?: number;
  feeHint?: EstimatedFee;
  tokenAddress: string;
  decimals: number;
  isNativeToken?: boolean;
  memo?: string;
  /** Read shares from the personal or the organization partition. */
  walletScope?: WalletScope;
  /** Lets the SDK rebuild a missing or stale device share before signing. */
  walletPassword?: string;
}

interface SendTransactionOutput {
  txHash: string;
}
```

| Field | Notes |
| - | - |
| `amount` | Human-readable decimal string (`'0.1'`), never base units. Each chain converts it with `decimals`. |
| `tokenAddress` | Required by the type even for native sends, where you pass `''`. For ADA it is the token policy id; for XRP it is `"currency.issuer"`. |
| `decimals` | Required. Determines the base-unit conversion; Solana defaults to 9 internally if the value is undefined, Tron throws for TRC-20 without it. |
| `isNativeToken` | Omitted means "not explicitly native"; ADA and XRP treat `!== false` as native, EVM/Solana/Tron branch on the truthy value. |
| `chainId` | EVM only. Takes a `ChainIdValue` string (`'11155111'`) or a plain number. Defaults to `1` when omitted. Other chains derive their chain id from the SDK's `network`. |
| `symbol` | Carried into the spend context as the token symbol. On ADA it doubles as the hex asset name. |
| `feeHint` | An `EstimatedFee` from `estimateFee`, reused instead of re-quoting. |
| `walletPassword` | Only needed when the device share is missing or stale. It lets the SDK rebuild the share mid-send. |

> [!WARNING]
>
> A memo on a chain that supports it is validated before anything else happens: an invalid memo throws
> `InvalidInputError` and no transaction is built.
>
> Only XRP, Solana and Tron support one. On EVM, Bitcoin and Cardano a
> memo is neither validated nor rejected. It is silently dropped, so nothing reaches the chain.
> Check `isMemoSupported(chain)` before offering the field.

## RPC endpoints

Sends and fee quotes talk to a chain node. Defaults ship per network, and `SDKConfig.rpcOverrides`
replaces them:

```typescript theme={null}
type RpcOverrides = Partial<Record<ChainIdValue, string>>;
```

```typescript theme={null}
const sdk = new TrustodianSDK({
  apiKey,
  network: 'mainnet',
  shareStore,
  rpcOverrides: {
    '1': 'https://my-eth-node.example',
    'sol_mainnet': 'https://my-solana-node.example',
  },
});
```

> [!WARNING]
>
> `rpcOverrides` is a flat map keyed by `ChainIdValue`, not a nested `{ evm: { 1: '…' } }` object.
> EVM chain ids are the string form of the number (`'1'`, not `1`).

The shipped defaults are public endpoints: fine for trying things out, not for production
throughput. Point `rpcOverrides` at your own nodes before you go live.

> [!WARNING]
>
> An EVM chain id with no default and no override throws
> `No RPC endpoint configured for EVM chain ID {chainId}`. Only the chain ids listed in
> [Supported chains](https://sdk-guide.mintlify.site/sdk-guide/introduction.md#evm-chain-ids) resolve out of the box; add any other
> through `rpcOverrides`.

> [!NOTE]
>
> Cardano ships no default RPC and does not need one: Cardano sends and fee quotes source their
> UTXOs from the Trustodian API rather than from a node.

## Address validation

Validate before you send. These helpers check the address's embedded checksum rather than matching a
shape regex, so a single mistyped character is rejected instead of paid to an address nobody
controls.

```typescript theme={null}
import {
  validateEvmAddress,
  validateSolanaAddress,
  validateTronAddress,
  validateBitcoinAddress,
  validateCardanoAddress,
  validateXrpAddress,
  validateAddressForChain,
  assertValidAddressForChain,
} from 'vaults-multichain-sdk';
```

| Function | Signature |
| - | - |
| `validateEvmAddress` | `(address: string) => boolean` |
| `validateSolanaAddress` | `(address: string) => boolean` |
| `validateTronAddress` | `(address: string) => boolean` |
| `validateBitcoinAddress` | `(address: string, isMainnet?: boolean) => boolean`. Defaults to the SDK's network |
| `validateCardanoAddress` | `(address: string, isMainnet?: boolean) => boolean`. Defaults to the SDK's network |
| `validateXrpAddress` | `(address: string) => boolean` |
| `validateAddressForChain` | `(address, chainType, options?) => AddressValidationResult` |
| `assertValidAddressForChain` | `(address, chainType, options?) => void`. Throws `InvalidInputError` |

```typescript theme={null}
interface AddressValidationResult {
  isValid: boolean;
  error?: string;
}

interface AddressValidationOptions {
  /** Defaults to the SDK's network. Ignored for BTC/BTC_T3, whose chain type already names the network. */
  isMainnet?: boolean;
  field?: string;
  operation?: string;
}
```

```typescript theme={null}
const check = validateAddressForChain(recipient, EChainType.XRP);
if (!check.isValid) throw new Error(check.error);

// or, to throw directly
assertValidAddressForChain(recipient, EChainType.XRP, { operation: 'send' });
```

> [!NOTE]
>
> Two rejections are intentional: XRP X-addresses, since the destination tag is carried separately
> (see below), and Cardano Byron addresses (`Ae2...`, `DdzFF...`). Cardano addresses are also checked
> against the network, so a well-formed testnet address is rejected on mainnet.

## Memos and destination tags

```typescript theme={null}
import {
  isMemoSupported,
  validateMemo,
  getMemoPlaceholder,
} from 'vaults-multichain-sdk';
```

Only XRP, Solana and Tron carry a memo. EVM, Bitcoin and Cardano ignore `memo` entirely.

```typescript theme={null}
interface MemoValidationResult {
  isValid: boolean;
  error?: string;
}

validateMemo(memo: string, chainType: EChainType): MemoValidationResult;
getMemoPlaceholder(chainType: EChainType): string;
```

| Chain | Rule | Placeholder |
| - | - | - |
| XRP | Digits only, `0 … 4294967295`. Carried as the Payment's `DestinationTag`. | `Destination Tag (numeric)` |
| Solana | No whitespace, at most 200 characters. Added as an SPL Memo instruction. | `Memo (up to 200 characters, no spaces)` |
| Tron | No whitespace, at most 200 characters. Attached via `addUpdateData` as UTF-8. | `Memo (up to 200 characters, no spaces)` |
| Everything else | Always valid, and ignored | `""` (empty string) |

An empty memo is always valid.

> [!WARNING]
>
> `sendTransaction` calls `validateMemo` itself and throws `InvalidInputError` on failure, but only
> when `isMemoSupported(chain)` is true. A memo passed for an EVM or Bitcoin send is silently dropped,
> not rejected.

## Spend authorization

Before a key is rebuilt, every send hands the unsigned transaction to Trustodian and gets
back a single-use handle, which travels with the share request.

This exists because the share request would otherwise carry a recipient and an amount that the
*client* computed, and those are what the Address Book whitelist and the organization spending limit
are enforced against. A caller could simply understate them. By reading the payee and amount out of
the transaction that is actually about to be signed, the server enforces its policies against the
real spend rather than a claim about it.

The SDK does all of this inside `sendTransaction`; there is no method for you to call and nothing to
configure. What you may see is its effect: a send can be refused by an organization policy at this
step, before any signature exists. Nothing is reserved, so an abandoned send costs no allowance.

## Per-chain behaviour

What each chain needs from you, and what it will refuse.

> **EVM**
>
> * `chainId` selects both the RPC endpoint and the network; it defaults to `1`.
> * Token sends need `tokenAddress` and `decimals`; native sends leave `tokenAddress` empty and
>   set `isNativeToken: true`.
> * A chain id with no configured RPC throws `No RPC endpoint configured for EVM chain ID …`.

> **Solana**
>
> * The RPC URL must be `https://`.
> * A native send will not leave the recipient below rent-exemption, and refuses rather than
>   creating an unusable account.
> * Sending a token to a recipient who has no account for it creates that account in the same
>   transaction, and the sender pays rent for it. Budget for that.
> * Both SPL and Token-2022 tokens are supported; the right one is detected from the mint.

> **Tron**
>
> * Native sends keep about 0.3 TRX back for bandwidth, and refuse when the amount would eat
>   into it: *"Insufficient TRX for transaction. Please leave \~0.1 TRX for bandwidth costs."*
> * TRC-20 sends require both `tokenAddress` and `decimals`.
> * Sending to an account that is not yet activated costs roughly 1 TRX extra.

> **Bitcoin**
>
> * The recipient is checked against the active network before anything else.
> * Inputs are selected automatically and the fee comes from the current network rate.
> * Change below the dust limit is dropped into the fee rather than creating a dust output.
> * Chain type is `BTC` on mainnet and `BTC_T3` on testnet.

> **Cardano**
>
> * For a token transfer, `tokenAddress` carries the policy id and `symbol` the asset name;
>   both are required.
> * `amount` is the *token* amount. Every output must also carry a minimum of ADA, which the SDK
>   derives. You do not choose it.
> * A token send needs pure-ADA UTXOs to pay from. If all the ADA is locked in UTXOs that also hold
>   tokens, the send is refused and the tokens must be moved first.

> **XRP**
>
> * Sending to yourself is refused up front.
> * For an issued asset, `tokenAddress` must be `"currency.issuer"`, for example `"USD.rIssuer..."`.
>   A trust line for that pair must already exist; see [XRP trust lines](#xrp-trust-lines).
> * A numeric `memo` becomes the destination tag; a non-numeric one is dropped.
> * Every account locks 1 XRP as a base reserve plus 0.2 XRP per owned object, and that
>   part of the balance cannot be spent.

## Minimum balances and native currencies

These are the per-chain floors and native units the SDK works with, reproduced so you can apply the
same thresholds yourself.

| Chain | Minimum balance for a transaction | Native asset | Decimals |
| - | - | - | - |
| `EVM` | `0.001` | ETH | 18 |
| `SOL` | `0.01` | SOL | 9 |
| `TRON` | `1` | TRX | 6 |
| `BTC` | `0.0001` | BTC | 8 |
| `BTC_T3` | `0.0001` | BTC | 8 |
| `ADA` | `1` | ADA | 6 |
| `XRP` | `1` | XRP | 6 |

> [!WARNING]
>
> The native-asset column is one flat `ETH` for the whole `EVM` row. On Polygon, BNB Smart Chain or
> Avalanche the real native symbol is POL, BNB or AVAX, so label an EVM amount from
> `EstimatedFee.currency`, which is resolved per chain id, rather than from this table.

## Fee estimation

```typescript theme={null}
import { TransactionEstimatingType } from 'vaults-multichain-sdk';

const fee = await sdk.estimateFee({
  chain: EChainType.EVM,
  txType: TransactionEstimatingType.Send,
  payload: { from, to, amount: '0.1' },
  chainId: '1',
});

fee.estimatedFee; // human units of the native asset
fee.currency;     // e.g. "ETH"
```

`payload.from` is the wallet the quote is priced against. For a token transfer add `tokenAddress` and
`decimals`. When `chainId` is omitted it defaults to the SDK's network: `'1'` / `'11155111'` on EVM,
`sol_mainnet` / `sol_devnet`, and so on.

Pass the result back as `feeHint` on `sendTransaction` to reuse the quote instead of re-pricing it.

> [!NOTE]
>
> Always use the `TransactionEstimatingType` members rather than string literals, because an
> unrecognised value is priced as a plain transfer instead of throwing. The other operation types, the full
> `EstimatedFee` shape, and who pays what on each chain are covered in
> [Multisig Fees](https://sdk-guide.mintlify.site/sdk-guide/multisig-fees.md).

## New device or missing device share

If the device share for a wallet is absent or stale, pass `walletPassword` and the SDK performs a
recovery rotation as part of the send. Without it, the failed send throws an error you can test
with `isDeviceShareRecoveryError(error)`, then retry after recovering explicitly.

```typescript theme={null}
await sdk.sendTransaction({
  userRef,
  walletPassword: password,
  chain: EChainType.EVM,
  from,
  to,
  amount: '0.1',
  chainId: 1,
  tokenAddress: '',
  decimals: 18,
  isNativeToken: true,
});
```

To do the rotation explicitly instead, use
`ensureWalletReadyForCrypto` or `ensureWalletDeviceShare`. See
[Keeping a device share current](https://sdk-guide.mintlify.site/sdk-guide/key-management.md#keeping-a-device-share-current) for the
recovery pattern and what makes a share stale.

## XRP trust lines

An XRP wallet can only hold an issued asset once a trust line to that currency/issuer pair is open.
Opening one consumes 0.2 XRP of the account's available balance as a reserve object.

```typescript theme={null}
interface ActivateXrpTrustLineInput {
  /** XRP wallet address that will open the trust line */
  address: string;
  /** Currency code, e.g. "USD" or 40-char hex */
  currency: string;
  /** Issuer address, e.g. "rIssuer..." */
  issuer: string;
  /** Max token amount to hold (default: "1000000000") */
  limit?: string;
  walletScope?: WalletScope;
}

interface ActivateXrpTrustLineOutput {
  /** Transaction hash */
  hash: string;
  /** Signed tx blob (hex) */
  tx: string;
  /** Engine result, e.g. "tesSUCCESS" */
  result: string;
}
```

```typescript theme={null}
const { hash, result } = await sdk.activateXrpTrustLine({
  address: xrpWallet,
  currency: 'USD',
  issuer: 'rIssuerAddress...',
});
```

Both `address` and `issuer` are validated before any network call. Opening the line needs roughly
0.2 XRP of available balance for the new reserve object.

### Inspecting reserve and existing lines

```typescript theme={null}
interface XrpAccountReserveInfo {
  address: string;
  totalBalance: string;
  availableBalance: string;
  totalReserve: string;
  baseReserve: string;
  ownerCount: number;
  ownerReservePerObject: string;
  objects: {
    trustLines: number;
    signerList: boolean;
    offers: number;
    escrows: number;
    otherObjects: number;
  };
}

interface XrpTrustLineInfo {
  currency: string;
  issuer: string;
  balance: string;
  limit: string;
}
```

```typescript theme={null}
const reserve = await sdk.getXrpAccountReserve({ address: xrpWallet });
const lines = await sdk.getXrpTrustLines({ address: xrpWallet });
```

Both read straight from the XRP ledger, with no Trustodian API call, and both validate the address
first.

## Errors worth handling

| Error | When |
| - | - |
| `InvalidInputError` | Invalid memo, or a failed `assertValidAddressForChain` |
| `InsufficientBalanceError` | Balance is below what the transfer plus fees requires |
| `OrgPolicyRejectedError` | Refused at [spend authorization](#spend-authorization): whitelist, spending limit, or business hours |

A reconstructed key that fails to derive the wallet's address does not surface as a thrown
`KeyMismatchError` instance; catch it with `isDeviceShareRecoveryError(error)` instead. See
[Keeping a device share current](https://sdk-guide.mintlify.site/sdk-guide/key-management.md#keeping-a-device-share-current).

Use `isTrustodianError(error)` to narrow before reading these fields. For the full error taxonomy,
HTTP status mapping and retry guidance, see [Errors](https://sdk-guide.mintlify.site/sdk-guide/errors.md).

## Next

> **[Preflight](https://sdk-guide.mintlify.site/sdk-guide/preflight.md)**
>
> Simulate the send before a signature is collected

> **[Orders](https://sdk-guide.mintlify.site/sdk-guide/orders.md)**
>
> Multisig transfers through the order lifecycle

> **[Wallets](https://sdk-guide.mintlify.site/sdk-guide/wallets.md)**
>
> Creating, listing, and reading wallets

> **[Address Book](https://sdk-guide.mintlify.site/sdk-guide/address-book.md)**
>
> Whitelisting payees before they can be paid


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