Skip to content

Spend from vaults and mint them - #1014

Open
Rigidity wants to merge 4 commits into
rigidity/vault-importfrom
rigidity/vault-spend
Open

Rigidity wants to merge 4 commits into
rigidity/vault-importfrom
rigidity/vault-spend

Conversation

@Rigidity

@Rigidity Rigidity commented Nov 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #952. Closes #951. Part of #945. Stacked on #969.

Vault wallets can now spend, and BLS wallets can mint vaults. This is the base of the rest of Phase 2: recovery (#954), assets (#953), offers (#955) and the external surface (#956) build on this pipeline.

Spending from a vault

  • Wallet::complete_spends has a vault path (wallet/vault.rs). Each coin at a vault address outputs the conditions the action system chose through a delegated spend, and the vault approves it with SEND_MESSAGE(0x17, delegated_puzzle_hash, [coin_id]). P2 singleton coins use P2Singleton::spend, and P2ConditionsOrSingleton coins their singleton path. The vault is recreated with the same custody and amount, with ASSERT_MY_AMOUNT. It's ported from the SDK's test-only TestVault::partial_custom_spend.
  • The messages tie the spends together, so vault transactions use Relation::None rather than the AssertConcurrent ring. Conditions no coin can output, such as the fee of a transaction that only spends the vault, are output by the vault (return_unplaced_conditions) instead of an intermediate coin.
  • The custody spend (custody_mips_spend) goes down the custody side of the top level 1 of 2, or the top level of a vault without recovery. For an M of N, members whose keys are attached are chosen first. Only non-fast-forward BLS members can be spent so far; others return VaultMemberNotSupported (Support secp256r1 secure element signers for vaults #957).
  • The vault singleton is its own asset, so it's never selected as XCH, and selecting its coin id fails. Change goes to the vault's first address, and split sends every output there, since vaults don't derive addresses. Fees are reserved from vault XCH.
  • The vault's lineage proof comes from its parent in vault_coins, or its launcher (coin_by_id) for the eve coin.

Signing

  • Vault members are keychain master keys, so sign_with_secret_keys signs each AGG_SIG_ME whose key is one of the vault's signers, with the same AGG_SIG_UNSAFE refusal as BLS wallets. Sage::sign_with picks this for vault wallets, using the signers in wallets.toml. A vault with no signers fails with NoVaultSigners.
  • Clear signing. Before a vault signs, parse_vault_spend_bundle finds the vault's spend and runs the SDK's parse_vault_transaction on it. A spend that can't be verified, changes the vault's custody, or melts it isn't signed. Nothing in this PR does either on purpose; rekeying and recovery (Recover, cancel, and rekey vaults #954) will say so explicitly.
  • TransactionSummary.vault (VaultSpendRecord) shows the result: verified, custody changed, melted, the reserved fee, and XCH that leaves without being accounted for. The confirmation dialog shows it and disables Submit when Sage won't sign.

Hardened by #1018. In this PR, sign_with signs any spend that doesn't include the vault coin. So a dApp's sign_coin_spends could ask the custody key for an AGG_SIG_ME on another coin, or include the vault coin twice. #1018 restricts vault keys to signing AGG_SIG_ME only for the single vault spend that clear signing parsed. The stack merges together, so this is never released on its own.

One spend at a time

Decision: the custody signs over the vault's coin id, so a second spend while one is pending would spend the same coin. spendable_vault refuses with VaultSpendPending until the pending one confirms, and it's checked before coin selection, so the error explains why. Chaining from the pending child is left for fast-forward members (#957). A vault with a pending recovery can't be spent here either (#954 adds cancel).

Minting (#951)

  • mint_vault { name, emoji, custody_fingerprint?, recovery_mnemonic | recovery_fingerprint, recovery_timelock, fee, auto_submit }, funded by the open BLS wallet. The vault uses ForceSingletonRecreation, nonce 0 and non-fast-forward BLS members, with the CHIP-0043 memo in the launcher solution, so it imports from its id alone.
  • Custody key decision: by default it's derived from the open wallet's key at m/12381'/8444'/12'/i' (purpose 12 doesn't overlap Chia's 0 to 6), using the first index no vault wallet uses, so the wallet's mnemonic backs it up and each vault gets its own key. An existing keychain key can be chosen instead.
  • Recovery key: a fresh mnemonic, shown with a warning to store it offline and a required confirmation, or an existing keychain key. It's stored in the keychain like other secrets and attached as the recovery signer, so Recover, cancel, and rekey vaults #954 can start or cancel recovery without re-entering it. It must differ from the custody key.
  • Timelock decision: 12 hours by default, as in Cloud Wallet, and at least an hour, so there's time to notice and cancel a recovery this wallet didn't start.
  • Pending mints. Without auto_submit nothing is stored. With it, the keys and the wallet entry are saved first, then the mint is sent to the peers right away (the vault wallet opens next, which stops the funding wallet from submitting later) and recorded as the funding wallet's pending transaction. If it's rejected, the wallet and the keys it added are removed. wallets.toml keeps vault_mint { funding_fingerprint, transaction_id, expires_at } until the vault is found. get_vault and login check it: found on chain clears it, and a mint whose transaction expired (plus 10 minutes) without the launcher appearing is removed. get_vault returns minting.

UI

  • A Mint Vault page for hot BLS wallets: custody key, recovery phrase, timelock and fee, review. Afterwards the vault wallet opens, shows that it's minting, and asks for a config backup once it confirms.
  • Vault wallets with an attached custody signer aren't read-only anymore, so send, bulk send, multi-send, combine, split and auto-combine work from the usual pages. Without one, the banner says to attach a custody key.
Recovery key Review
Recovery key Review
Config backup after the mint Sending from the vault
Backup Send confirmation

Testing

  • sage-wallet simulator tests (wallet/vault/tests.rs): minting with both versions (the memo decodes back to the config), minting with a fee, XCH sends with change to the vault for both versions, a fee-only spend, CAT sends, multi-send of XCH and CATs, split and combine, the pending-spend refusal, the singleton not being selectable, a wrong signer failing to sign, clear signing flagging a custody change, and coins received while the vault wallet was closed.
  • RPC tests (tests/vault_mint.rs): preview without storing anything, minting, the signers and a distinct second custody key, opening the vault, receiving and sending XCH with the clear-signing summary; rejected timelocks and key choices storing nothing; a pending mint kept and shown as minting, and an expired one removed.
  • Sandbox: minted a vault from alice's wallet through the wizard, exported the config backup, funded it from bob and sent 1.5 TXCH back from the vault through the send page.
  • Checks: cargo clippy --workspace --all-features --all-targets -D warnings, cargo fmt, cargo test --workspace, cargo machete, cargo sqlx prepare --workspace --check, pnpm prettier:check, pnpm lint, tsc -b and pnpm test:unit (169) pass. src/bindings.ts was regenerated with pnpm generate:bindings; two queries were added to .sqlx.

Notes

@Rigidity
Rigidity added this pull request to stack #970 November 15, 2026 03:52
@Rigidity Rigidity changed the title rigidity/vault spend Spend from vaults and mint them Nov 15, 2026
@Rigidity
Rigidity marked this pull request as ready for review November 15, 2026 03:52
A vault wallet's coins are spent by messages from the vault singleton, which is
recreated with the same custody. Change goes to the vault's first address, the
fee is reserved from its XCH, and a second spend is refused while one is
pending, since the custody signs over the vault's coin id. Clear signing parses
what a vault spend does before it's signed.
… summarize vault spends

The mint stores the custody and recovery keys and adds the vault wallet before
it's submitted, and removes them if submitting fails. A mint that's never found
on chain is removed once its transaction expires. Vault transactions are signed
with the keychain keys of the vault's signers, after clear signing checks that
they don't change its custody.
… signers

The vault wallet is opened right after a mint, which stops the funding wallet
from submitting it later.
…show clear signing in confirmations

BLS wallets get a Mint Vault page that walks through the custody key, a new
recovery phrase, the recovery timelock and the fee. Vault wallets with a custody
signer can use the send, multi-send and coin management flows, and the
confirmation dialog shows what clear signing found, refusing spends that change
the vault's custody.
@Rigidity
Rigidity force-pushed the rigidity/vault-spend branch from b820466 to 6304201 Compare November 15, 2026 06:00

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spend from a vault: spend pipeline, signing, and XCH/CAT transactions Mint a vault with BLS custody and recovery keys

1 participant