Repository navigation
Conversation
This was referenced Oct 29, 2026
Rigidity
force-pushed
the
rigidity/vault-import
branch
from
October 30, 2026 01:00
8f3893f to
9b05b1e
Compare
Rigidity
force-pushed
the
rigidity/vault-import
branch
from
October 30, 2026 02:31
9b05b1e to
6368042
Compare
Rigidity
force-pushed
the
rigidity/vault-import
branch
from
November 9, 2026 01:07
6368042 to
c86ca44
Compare
Rigidity
force-pushed
the
rigidity/vault-import
branch
2 times, most recently
from
November 9, 2026 03:02
aa8c576 to
4c37823
Compare
Rigidity
force-pushed
the
rigidity/vault-import
branch
from
November 9, 2026 03:17
4c37823 to
d9dc2fb
Compare
Rigidity
marked this pull request as ready for review
November 9, 2026 07:04
Rigidity
force-pushed
the
rigidity/vault-import
branch
from
November 15, 2026 00:03
2db9a36 to
93046e3
Compare
Collaborator
Author
Screenshots and end-to-end testingTaken in Every import method was run end to end in the browser:
Attaching alice's key as a custody signer, and bob's as a recovery signer, Export Config (byte for byte the same as the minted file), deleting and re-importing a vault, the wallet switcher, and the wallet card's details all work. Fixes from this run (93046e3)
The sandbox also got a stand-in for the save dialog, so exports work in the browser (da38eec). Import
Vault page
Not fixed here
|
Rigidity
force-pushed
the
rigidity/vault-import
branch
from
November 15, 2026 01:10
93046e3 to
9bb3e7d
Compare
…s that can't be imported again
…earches on recovery states
…e file picker in its field
The sandbox mints a vault for each way one can be imported: with memos, without memos for its recovery keys to rebuild, with a 2 of 2 recovery, without recovery, and with a pending recovery. Alice's key is the custody of each and bob's the recovery, and instance.json lists their ids, addresses, recovery keys and config files.
A save dialog now picks its default name, and writing the file downloads it and records it in window.__SAGE_SANDBOX__.savedFiles, so exports such as a vault's config work in the browser.
Testing the vault pages in the sandbox turned up some rough edges: - Signers were only shown by fingerprint. They're named after their key wallet where there is one. - Member keys were cut off at a fixed width, even with room to spare. - A pending recovery looked like any other state, though it takes the vault away unless it's cancelled. - Recovery keys wrapped mid-key, so it was hard to tell where one ended. - Importing the vault found from a watched address dropped the name and emoji already entered, and the suggestion stayed after the address was changed.
- Skip recovery states when rebuilding a config, rather than letting each cancelled recovery use up one of the searches for older custodies, which left the original custody unsearched after two cancels. They're found from the spends through the recovery side that start them. - Refuse an import with recovery keys that only rebuild an older config, unless recovery is pending, as rebuilding later does, instead of importing with the config unknown. - Check an address's network in find_vault, as import does. - Treat the same singletons as vaults on import as in sync. - Show a timelock in days, hours and minutes, rather than seconds when it isn't whole hours. - Ignore a vault lookup for an address that was changed while it ran. - Test a vault with recovery pending, the keys a signer's vault keeps or deletes, and importing each of the sandbox's vaults.
Both go through the recovery side, so the custody a finished recovery moved to was taken for a recovery state and never rebuilt, and the vault looked like it was still in recovery. The lineage is now walked forward: a spend through the recovery side of a coin in the recovery state finishes it. Also test importing a vault without memos with its recovery keys while recovery is pending.
A config rebuilt from an older custody is only the one from before recovery started if that custody's coin is the one whose spend started it. Also start recovery before importing in the recovery-pending test, which made it flaky when recovery was started on a live wallet.
Rigidity
force-pushed
the
rigidity/vault-import
branch
from
November 15, 2026 06:00
9bb3e7d to
c89dd0e
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.








Closes #950. Part of #945. Stacked on #968.
Imports an existing vault, minted by Sage or Chia Cloud Wallet, as a read-only wallet, and lets users attach the keys that sign for it. Spending stays disabled until #952.
Vault wallets
wallets.tomldefines vault wallets withtype = "vault", thelauncher_id, andsigners: the fingerprints of the keychain keys that sign for it. The fingerprint is taken from the launcher ID, with collisions handled like watch wallets.WalletKind::Vaultinsage_wallet. Opening a vault wallet tracks its vault, so sync follows it from its launcher. It receives at, and sends change to, its nonce 0 P2 singleton address, and doesn't derive addresses. Building spends returnsVaultSpendNotSupported, and so does signing. Sage Apps bridge methods that sign are refused withvault_unsupported.delete_walletwithdelete_keysremoves a vault's signer keys unless another wallet still uses them.sage wallet statusshows the vault's launcher ID.Importing
import_vault { name, launcher_id | address | config, recovery?, login, emoji }takes exactly one source. Launcher IDs are returned asvault1…bech32m IDs everywhere in the API (the wallet kind,get_vault,find_vault, nested vault members, and errors), and accepted as either that or hex.wallets.tomlkeeps hex. Every source needs a connected peer:vault_config_fileinsage_wallet). It's only accepted if it rebuilds the custody hash of the vault's latest coin. A file without a puzzle version is tried with both. It's stored with theconfig_filesource.A vault with no memos still imports read-only, and shows its balances.
import_vault_configadds a config file to an imported vault, andexport_vault_configwrites the open vault's config in the same format, from whichever source it came from. While recovery is pending, that's the config from before it started. Otherwise, an older config is never used for the current coin, since it may no longer apply, for example after a rekey. The file format can't say whether members fast forward, so a config with members that don't follow Cloud Wallet's defaults isn't exported, since it wouldn't import again.Rebuilding configs without memos
Cloud Wallet is phasing out vault config files in favor of memos, and can't currently export new ones. So as well as a config file, the config of a vault without memos can be rebuilt from its chain history, and its recovery keys if it has recovery.
import_vaulttakes an optionalrecovery: { keys, threshold?, timelock? }with a launcher ID or address (not with a config file), andrebuild_vault_config { recovery? }does the same for an imported vault, returning the threshold and timelock it found.parse_vault_spendinsage_wallet::vault_history). The puzzle doesn't say whether the vault has recovery, so each spend is read both ways: as the custody side of a top level 1 of 2, and as the custody of a vault without recovery. A vault with recovery and a single custody member looks just like a 1 of N vault without recovery that only one member has signed, so both stay possible until one rebuilds the custody hash.import_vaultwithout recovery keys tries this on the vault's last 10 spends, and falls back to memos or an unknown config if it doesn't work. With recovery, the recovery side only appears as a hash, which depends on the recovery members and threshold, the timelock, the puzzle version, and the custody side.sha2isn't optimized there. Searches over 1.5 million hashes, or 4,096 candidates, are refused and ask for the threshold or timelock, which caps the worst case at about 15 seconds in release.vault1IDs of vaults. Hex launcher IDs aren't accepted, since they look like secret keys. Secrets aren't stored, so they still have to be attached as signers.reconstructedsource (3, afterunknown).rebuild_vault_configfails, unless recovery is pending and it's the config from before recovery started.When a single address is imported as a watch wallet and turns out to belong to a vault (
find_vault), the Import page offers to import the vault instead. This is the vault detection that moved here from #947.Signers
attach_vault_signer { fingerprint, key }takes a BLS mnemonic or secret key. As in Cloud Wallet, a BLS member's key is the mnemonic's master key. The key must be a custody member of the vault's current config, or a recovery member of its latest config outside the recovery state, so the config must be known. It's stored in the keychain and listed in the wallet'ssigners. Public keys are rejected, since they can't sign. Secure element signers come with Support secp256r1 secure element signers for vaults #957.get_vaultreturns the open vault's state, recovery start time and the time it can be finished (recovery_finishes_at, from the current coin), custody hash, config (with each member's kind, key and attached signer) and its signers with their roles.API
import_vault,get_vault,attach_vault_signer,export_vault_config,import_vault_config,rebuild_vault_configandfind_vault, under a new Vaults tag.src/bindings.tsis regenerated.WalletKindgains{ "vault": { launcher_id, signers } }.get_sync_statusandget_wallet_addressreturn the vault's nonce 0 address.Frontend
Everything is behind
VITE_DISABLE_VAULTS..pocatalogs aren't touched, so the next release needspnpm extract.Tests
RPC tests with the peer simulator import vaults by launcher ID, by an address the vault has spent from (with a real vault-approved spend), by config file, and by recovery keys. They check:
rebuild_vault_config, which can then be exported;Unit tests cover config file round trips (with and without recovery), parsing custody reveals (single members, M of N merged across spends, secp keys, vaults without recovery including the ambiguous 1 of N), the search with common and uncommon timelocks, both versions, several keys of mixed curves with the threshold found or given, the search limits, the fast hasher against the SDK's puzzle hashes for single and M of N recovery, and P2 singleton parsing. A simulator test rebuilds a vault whose recovery was cancelled, then started again to other custody, and recreated while pending, which finds the config from before recovery started.
bench_full_recovery_searchis ignored, and times searches that don't match.Open question: hints
Cloud Wallet's memos carry no hint, so a Sage seed can't rediscover its own vaults: keys only appear inside memos, which can't be searched for. A hint would have to be a puzzle hash derived from a member key, written as the first memo of each vault recreation. Cloud Wallet only reads the first memo as the CHIP-0043 memo list, so adding one ahead of it would break its decoding, and adding one after it is ignored by peers' hint indexes, which only index the first memo. So this relies on the launcher ID, address lookup and the recovery keys.
Not included
Review-loop changes
VaultCustodyUnrevealed, as rebuilding later does, unless recovery is pending and that config is the custody that started it.find_vaultchecks the address's network. Import treats the same singletons as vaults as sync does.Decisions for review
test-utilsfeature insage-walletnow.