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

# Multisig Vaults

> Create, activate, and manage multi-signature vaults with the Trustodian SDK

A multisig vault is an on-chain account controlled by a list of owner addresses and a signature
threshold. Spending from it requires an [order](https://sdk-guide.mintlify.site/sdk-guide/orders.md) approved by at least `threshold`
owners.

Every chain implements this differently: Gnosis Safe on EVM, Squads v4 on Solana, native account
permissions on Tron, a `SignerList` on XRP, a P2WSH script on Bitcoin, a native script on Cardano.
The SDK normalizes creation and activation behind `createMultisig` / `activateMultisig`, but the
number of round trips and the client-side work differ substantially per chain. That is the main
subject of this page.

> [!NOTE]
>
> Personal Vaults can have owners on a hardware wallet. Organization Vaults cannot. A Trezor cannot sign
> TRON or XRP Vaults, and the Bitcoin and Cardano Vault keys of a hardware account come from
> `prepareHardwareVaultKeys`. See [Hardware Wallets](https://sdk-guide.mintlify.site/sdk-guide/hardware-wallets.md#method-map).

## Prerequisites

Vault creation and activation run on the same v2 wallet session as personal wallets
(see [Key Management](https://sdk-guide.mintlify.site/sdk-guide/key-management.md)):

```typescript theme={null}
import {
  TrustodianSDK,
  EChainType,
  TransactionEstimatingType,
} from "vaults-multichain-sdk";

const sdk = new TrustodianSDK({
  apiKey: process.env.TRUSTODIAN_API_KEY!,
  network: "mainnet",
  shareStore: myShareStore,
});

const userRef = await sdk.getSdkUserId();
await sdk.unlockWalletSession(userRef, walletPassword);
```

> [!WARNING]
>
> Creating a vault on SOL, TRON or XRP requires both `userRef` and
> `walletPassword` on the call. Either one missing raises `InvalidInputError`.
> EVM, BTC and ADA creation does not need them.

## Bitcoin and Cardano need a cosigner pool

On Bitcoin and Cardano a vault does not reuse an owner's personal wallet key. Each owner
contributes a key from a separate derivation subtree — BIP-48 for Bitcoin, CIP-1854 for Cardano —
and those are what the P2WSH descriptor and the native script are built from.

Only the owner can produce them, because they come from that account's recovery phrase. So the
backend keeps a pool of them per account, filled by the client, and every vault created claims one
row from the pool of **every** owner it names. The pool therefore drains over the account's
lifetime, not just once at creation.

> [!WARNING]
>
> With an empty pool, creating a Bitcoin or Cardano vault that names this
> account as an owner fails with `409` and the code `COSIGNER_KEYS_EXHAUSTED`.
> Nobody can refill it on the owner's behalf.

Refill it from any unlocked session:

```typescript theme={null}
await sdk.topUpCosignerKeys(userRef, { walletPassword });
```

Call it after unlocking, and again after creating a vault. It starts with a read and derives only
what the backend reports as short, so it is cheap when there is nothing to do. Only public material
leaves your process: an account-level `xpub` and a master fingerprint for Bitcoin, a payment and a
stake key hash for Cardano — never a private key.

```typescript theme={null}
const { inserted, pools } = await sdk.topUpCosignerKeys(userRef, {
  walletPassword,
});

// pools: [{ chain, unconsumed, missing, nextAccountIndex }, ...]
```

`unconsumed` is how many vaults this account can still join on that chain. Read it before offering
a vault-creation screen if you want to warn early rather than fail at submit. `isCosignerPoolChain`
answers whether a chain draws on a pool at all, so you can skip the read where it cannot matter.

For the Master of an organization with its own [recovery phrase](https://sdk-guide.mintlify.site/sdk-guide/recovery-phrase.md#organization-recovery-phrase),
the organization has a pool of its own. Without `ownerKind` the call fills the personal pool and,
when the organization's phrase is active and confirmed, the organization's pool too. Pass
`ownerKind: 'personal' | 'organization'` to fill one of them.

```typescript theme={null}
const { seeds } = await sdk.topUpCosignerKeys(userRef, { walletPassword });

// seeds: one entry per pool
//   { ownerKind, seedId?, ok: true, result: { inserted, pools } }
//   { ownerKind, seedId?, ok: false, error, code? }
```

The top-level `inserted` and `pools` are those of the first pool that succeeded, the personal one
first. One pool failing does not stop the other; the call throws only when every pool failed. If
the personal phrase was replaced in the web app after the session cached it, the top-up is refused
instead of filling the pool with keys of the old phrase.

> [!NOTE]
>
> Only wallets derived from the account recovery phrase draw on the pool. A
> wallet imported by private key has no derivation tree, so it contributes its
> single public key instead and needs no pool. Such a vault is still fully
> spendable, but it cannot be imported into an external wallet like Sparrow or
> Eternl.

## Method reference

| Method | Signature |
| - | - |
| `createMultisig` | `(input: CreateMultisigInput) => Promise<MultisigsItem \| MultisigsItem[]>` |
| `topUpCosignerKeys` | `(userRef, params?: { walletPassword?: string; ownerKind?: SeedOwnerKind }) => Promise<CosignerPoolsTopupResult>` |
| `getMultisigs` | `(input: GetMultisigsInput) => Promise<MultisigBaseDTO[]>` |
| `getPaginatedMultisigs` | `(input?: GetPaginatedMultisigsInput) => Promise<PaginatedMultisigsWithTokensResponse>` |
| `getMultisigById` | `(input: GetMultisigByIdInput) => Promise<MultisigBaseDTO>` |
| `activateMultisig` | `(input: ActivateMultisigInput) => Promise<ActivateMultisigOutput>` |
| `updateMultisigName` | `(input: UpdateMultisigNameInput) => Promise<MultisigBaseDTO>` |
| `setOwnerName` | `(input: SetOwnerNameInput) => Promise<MultisigBaseDTO>` |
| `resetOwnerName` | `(input: ResetOwnerNameInput) => Promise<MultisigBaseDTO>` |
| `deleteMultisig` | `(input: DeleteMultisigInput) => Promise<DeleteResponse>` |
| `getMultisigTxHistory` | `(input: GetMultisigTxHistoryInput) => Promise<TxHistoryCombinedResponse>` |
| `getAllMultisigsTxHistory` | `(input?: GetAllMultisigsTxHistoryInput) => Promise<TxHistoryCombinedResponse>` |

> [!NOTE]
>
> `feeQuote` is required by `ActivateMultisigInput`, so it has to be present. Price it with
> `estimateFee({ txType: TransactionEstimatingType.ActivateMultisig, payload: { from, multisig } })`
> See [Multisig Network Fees](https://sdk-guide.mintlify.site/sdk-guide/multisig-fees.md). On EVM the gas actually spent comes from
> the `gasLimit` / `gasPrice` overrides, not from `feeQuote`.

## Owners and threshold

`CreateMultisigInput`:

```typescript theme={null}
interface CreateMultisigInput {
  chain: ChainType;
  owners: string[];
  threshold: number;
  chainIds?: ChainIdValue[];
  /** 'personal' (default) or 'organization' */
  walletScope?: WalletScope;
  /** Required for SOL / TRON / XRP */
  userRef?: string;
  walletPassword?: string;
  /** Optional signer names, one per entry in `owners`, same order */
  ownerLabels?: string[];
}
```

* Every entry in `owners` is validated with `assertValidAddressForChain` before any request goes
  out; a bad address fails locally with the field name (`owners[1]`) in the message.
* `owners[0]` is used as the creator address for SOL, BTC, ADA and XRP creation.
* `ownerLabels` is optional on every chain. When present it must have exactly one entry per owner,
  otherwise `InvalidInputError` is raised before any request. Each label is trimmed; an empty one
  means "no name", and one longer than 100 characters is rejected with
  `Name must be 1–100 characters`. These are the creator's names: every member sees them as the
  default until they set their own (see [Owner names](#owner-names)). They also go into the vault's
  recovery file.
* Every owner must belong to the account's current recovery phrase. A wallet whose `accessState` is
  `archived` or `access_lost` is refused with `VAULT_OWNER_NOT_CURRENT_GENERATION`, and
  `details.walletAddresses` names which ones. See
  [Wallet generations](https://sdk-guide.mintlify.site/sdk-guide/wallets.md#wallet-generations).
* `chainIds` is optional. Omitted, the SDK picks the default chain id for each chain based on the
  `network` it was constructed with.
* For EVM only, `chainIds` may hold several chains. One vault record is created per chain and
  `createMultisig` returns a `MultisigsItem[]`. Every other chain uses `chainIds[0]` and returns a
  single `MultisigsItem`.
* XRP caps the signer list at 8, and the SDK raises `InvalidInputError` when an owner-update order
  would exceed it.
* On EVM, removing an owner is rejected client-side when the vault would drop below 2 owners
  (`Vault should have at least 2 owners`).

## Core types

```typescript theme={null}
interface MultisigsItem {
  id: string;
  address: string | null;
  chainId: ChainIdValue;
  status: "created" | "execution" | "deployed";
  threshold: string; // string, not number
  chainType: EChainType;
  isDeployed: boolean;
  factoryId: string;
  name: string;
  params: MultisigParams;
  updatedAt: string;
  createdAt: string;
  owners: MultisigOwner[];
  tokens: MultisignToken[];
  factoryAddress: string;
  /** Whether the caller created this vault. Absent on older backends. */
  isCreator?: boolean;
}

interface MultisigOwner {
  walletAddress: string;
  /** Sort key of the owner order (`Address N` numbering); not contiguous. */
  position?: number;
  /** The creator's name for this owner, the default for every member. */
  defaultLabel?: string | null;
  /** The caller's own name for this owner; always null for the creator. */
  customLabel?: string | null;
  /** The name the caller sees: customLabel, else defaultLabel. */
  label?: string | null;
}

interface MultisigParams {
  chainType: EChainType;
  to: string;
  data: string;
  paymentToken: string;
  payment: string;
  vaultAddress: string;
  paymentReceiverAddress: string;
  fallbackAddress: string;
  singleton: string;
  initializer: string;
  deployData: string;
  share?: string; // vault device share, set locally after create
}
```

> [!NOTE]
>
> `MultisigsItem.status` is the literal union `'created' | 'execution' |
>     'deployed'`. The exported `MultisigStatus` enum (`PENDING = 'pending'`,
> `ACTIVE = 'active'`, `INACTIVE = 'inactive'`) is a separate type that the
> vault record does not use, so do not compare `multisig.status` against it. Use
> `isDeployed` for a plain "is it live on chain" check.

Read shapes:

```typescript theme={null}
interface MultisigBaseDTO {
  address: string;
  chainType: EChainType;
  data: MultisigsItem[];
}

/** One grouped row from `getMultisigs` */
interface GroupedMultisigResponseDTO {
  address: string;
  chainType: EChainType;
  createdAt: string;
  data: MultisigsItem[];
}

interface PaginatedMultisigsWithTokensResponse {
  multisigs: GroupedMultisigResponseDTO[];
  total: number;
  page: number;
  limit: number;
}
```

Both read shapes are grouped: one row per vault address, with the per-chain records in `data`.
A single-chain vault still arrives as `data[0]`, which is where `factoryAddress`,
`params.deployData`, `owners`, and `threshold` live.

## Vault activation shares

Solana, Tron and XRP vaults are activated by a key that is not any owner's wallet key. Creating the
vault generates that secret in your process and splits it the same 2-of-3 way as a wallet key (see
[Key Management](https://sdk-guide.mintlify.site/sdk-guide/key-management.md#the-three-shares)). The backend alone can never rebuild it.

You do not manage any of this by hand:

* Creating a vault splits the secret and stores the device share under the vault id when you pass `userRef`.
* Activating from the same host rebuilds the secret from the device share.
* Activating from a different host needs `walletPassword` on `activateMultisig`, so the secret can be rebuilt from the two server-side shares. Without it you get `DEVICE_SHARE_REQUIRES_PASSWORD`.
* The reconstructed secret is wiped as soon as activation finishes, on every path.

> [!WARNING]
>
> The recovery share is stored under the creating user's account. A co-owner who
> did not create the vault cannot rebuild the activation secret and cannot
> activate it.

### Password rotation

`changeWalletPassword` rotates vault activation shares alongside wallet shares. See
[Key Management](https://sdk-guide.mintlify.site/sdk-guide/key-management.md#changing-the-wallet-password) for the full result shape.

```typescript theme={null}
const result = await sdk.changeWalletPassword(userRef, {
  currentPassword,
  newPassword,
});

result.vaultActivationsRotated; // number of vaults re-encrypted
```

## Creating and activating, per chain

Vaults fall into two groups. On EVM, Solana, Tron and XRP the vault must be activated on chain before it can hold or move funds. On Bitcoin and Cardano the address is spendable as soon as it is funded, and there is nothing to activate.

| Chain | Activation | Paid by | Notes |
| - | - | - | - |
| EVM | `activateMultisig` deploys the vault contract | The activating owner's wallet | Pass the full `multisig` object and a `feeQuote`; `gasLimit` / `gasPrice` are overridable |
| Solana | `activateMultisig` | The activating owner's wallet | Also funds the vault's on-chain rent |
| Tron | `activateMultisig` | The activating owner's wallet | The vault address must already hold at least 100 TRX |
| XRP | `activateMultisig` | The activating owner's wallet | The vault account must already be funded, about 1.2 XRP. Returns an empty `txHash`; the ledger hashes are recorded on the vault record |
| Bitcoin | none | | Spendable once funded. Owners and threshold are fixed at creation. Needs a filled [cosigner pool](#bitcoin-and-cardano-need-a-cosigner-pool) |
| Cardano | none | | Spendable once funded. Owners and threshold are fixed at creation. Needs a filled [cosigner pool](#bitcoin-and-cardano-need-a-cosigner-pool) |

> [!WARNING]
>
> On XRP, activation permanently disables the account's master key. After it,
> the account can only be moved by the signer list. This cannot be undone.

> [!NOTE]
>
> Calling `activateMultisig` on Bitcoin or Cardano throws, because there is
> nothing to activate. For those two chains, changing the owner set or the
> threshold means creating a new vault and moving the funds.

A full EVM create-and-activate:

```typescript theme={null}
const created = await sdk.createMultisig({
  chain: EChainType.EVM,
  owners: ["0x1111...", "0x2222...", "0x3333..."],
  threshold: 2,
  chainIds: ["1"],
});
const multisig = Array.isArray(created) ? created[0] : created;

const { txHash } = await sdk.activateMultisig({
  chain: EChainType.EVM,
  multisigId: multisig.id,
  multisigAddress: multisig.address ?? "",
  walletAddress: myWalletAddress,
  multisig,
  feeQuote: await sdk.estimateFee({
    chain: EChainType.EVM,
    txType: TransactionEstimatingType.ActivateMultisig,
    payload: { from: myWalletAddress, multisig },
    chainId: multisig.chainId,
  }),
  userRef,
});
```

The other activating chains take the same call with their own `chain` value. Pass `walletPassword` as well when the host has no device share for the vault.

## Reading vaults

`getMultisigs` lists a chain's vaults, narrowed to one owner wallet when you pass `walletAddress`.
`getMultisigById` returns a single vault.

```typescript theme={null}
const vaults = await sdk.getMultisigs({
  chain: EChainType.EVM,
  walletAddress: myWalletAddress,
});

// One vault by its on-chain address
const vault = await sdk.getMultisigById({
  chain: EChainType.EVM,
  address: multisigAddress,
});
const item = vault.data[0];

// Paginated list across chains, with token balances
const page = await sdk.getPaginatedMultisigs({
  page: 1,
  limit: 10,
  chainTypes: [EChainType.EVM, EChainType.SOL],
});
```

`getPaginatedMultisigs` defaults to `page: 1, limit: 10` and serializes `chainTypes` as a
comma-separated query value (`?chainTypes=EVM,SOL`).

> [!NOTE]
>
> `getMultisigById` lower-cases EVM addresses before the lookup (the backend
> matches with `LOWER()`) and leaves base58 / mixed-case addresses on SOL, TRON,
> XRP, ADA, and BTC untouched, because lower-casing would corrupt them.

## Rename and delete

```typescript theme={null}
await sdk.updateMultisigName({
  chain: EChainType.EVM,
  multisigId: multisig.id,
  name: "Team Treasury",
  walletAddress: myWalletAddress,
});

await sdk.deleteMultisig({
  chain: EChainType.EVM,
  multisigId: multisig.id,
});
```

`createMultisig` does not accept a name. Call `updateMultisigName` right after creating if you want
one. `deleteMultisig` removes the vault from the account, not from the chain. Funds at the address remain reachable by anyone holding the owner keys.

## Owner names

Each owner address can carry a display name. The address itself never changes.

```typescript theme={null}
await sdk.setOwnerName({
  chain: EChainType.EVM,
  multisigId: multisig.id,
  walletAddress: ownerAddress,
  name: "CFO backup",
});

await sdk.resetOwnerName({
  chain: EChainType.EVM,
  multisigId: multisig.id,
  walletAddress: ownerAddress,
});
```

* **Who sees what.** Names set by the vault creator (at creation through `ownerLabels`, or later
  through `setOwnerName`) are the default every member sees. A name set by anyone else who can see
  the vault overrides the default for that caller only. `owners[].label` is already resolved for the
  caller; `defaultLabel` and `customLabel` show where it came from, and `isCreator` tells the caller
  which of the two its calls change.
* **Clearing.** `resetOwnerName` removes the caller's own name, so the default shows again. Called
  by the creator, it removes the default for everyone who has not set their own. With no name at
  all, show `Address N`, where N follows the order of `owners`.
* **Validation.** The name is trimmed and must be 1–100 characters; otherwise `InvalidInputError`
  (`Name must be 1–100 characters`) is thrown before any request. Duplicate names are allowed.
* **Scope.** On EVM a vault deployed on several chains has one record per chain; a name set through
  any of them applies to all. Vaults created before owner names existed have no recorded creator,
  so every name set on them is personal.
* **Recovery file.** It uses the names as the downloading caller sees them. A file downloaded
  earlier keeps the names it had.

## Changing owners and threshold

Owner and threshold changes are not a vault method. They are orders, created with
`createSettingsOrder` and subject to the same signing threshold as a transfer:

```typescript theme={null}
const currentOwners = multisig.owners.map((o) => o.walletAddress);

await sdk.createSettingsOrder({
  multisigId: multisig.id,
  multisig,
  walletAddress: myWalletAddress,
  newOwners: [...currentOwners, "0x4444..."], // full desired final list
  newThreshold: 3, // optional; keeps current if omitted
});
```

`newOwners` is the complete desired final list, not a delta. The SDK diffs it against
`multisig.owners`, diffs `newThreshold` against `multisig.threshold`, and picks the method with
`getChangeMethod`:

| Change | EVM | Solana | Tron |
| - | - | - | - |
| exactly one owner added | `addOwnerWithThreshold` | `addOwner` | `updateOwners` |
| exactly one owner removed | `removeOwner` | `removeOwner` | `updateOwners` |
| threshold only | `changeThreshold` | `changeThreshold` | `changeThreshold` |
| several owners, or owners + threshold together | `multiSend` | `MultiSend` | `updateOwnerThreshold` |

No detected change throws `InvalidInputError: No changes detected in multisig configuration`.

To name the owners an order adds, call [`setOwnerName`](#owner-names) for each of them once the order
is created. The address does not have to be an owner yet; the name shows once the order executes.

XRP settings changes go through `createOrder` with the XRP instruction types
(`xrp_changeThreshold`, `xrp_updateOwners`, `xrp_updateOwnerThreshold`). Bitcoin and Cardano have no
settings orders at all. See [Orders](https://sdk-guide.mintlify.site/sdk-guide/orders.md) for the full lifecycle.

## Transaction history

```typescript theme={null}
const history = await sdk.getMultisigTxHistory({
  multisigId: multisig.id,
  page: 1,
  limit: 20,
});

const all = await sdk.getAllMultisigsTxHistory({
  page: 1,
  limit: 20,
  filter: "all", // all | pending | executed | rejected
  hideDust: true, // hide transfers under $1
});

all.items.forEach((item) => console.log(item.method, item.status, item.hash));
```

Both return `TxHistoryCombinedResponse` (`{ items, total, page, limit }`).
`getMultisigTxHistory` defaults to `page: 1, limit: 10`; `getAllMultisigsTxHistory` defaults to
`limit: 20`.

## Organization vaults

Passing `walletScope: 'organization'` runs every vault lifecycle call against org-owned vaults
instead of personal ones, with identical request and response shapes. A vault that is not yours
surfaces as a 404 `Vault not found`; treat it as terminal and do not retry on the personal path.

Organization vaults also have their own read methods (`listOrgVaults`, `listMyOrgVaults`,
`getOrgVaultById`, `getOrgVaultHistory`). See
[Organization Vaults](https://sdk-guide.mintlify.site/sdk-guide/organization-vaults.md).

## Next

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

> [card] [Multisig Network Fees](https://sdk-guide.mintlify.site/sdk-guide/multisig-fees.md)
>
> Who pays what, at every step, on every chain.

> [card] [Preflight](https://sdk-guide.mintlify.site/sdk-guide/preflight.md)
>
> Simulate an order before a signature is collected.

> [card] [Organization Vaults](https://sdk-guide.mintlify.site/sdk-guide/organization-vaults.md)
>
> Vaults owned by an organization rather than by one user.


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