feat(ecash): import a backup phrase from another NUT-13 wallet
Demo images / Build & push demo images (push) Failing after 2m2s
Demo images / Build & push demo images (push) Failing after 2m2s
Bring-your-own, the open question the migration plan left. Point this wallet at a phrase you already hold — Minibits, Nutstash, cdk-cli — and its coins become restorable here, which is the other half of "these words are portable". Replacing an established phrase is the one genuinely lossy thing this module can do, so it is treated that way. The coins already held stay spendable: they are proofs, not derivations, and nothing here touches `ecash.json`. But they were minted under the *old* phrase, so a restore will no longer find them. Hence an explicit confirm, a prompt to reveal and write down the current phrase first, and — most importantly — the replaced phrase is archived beside the wallet, never overwritten. It may be the last copy of the words a balance was minted under, and quietly destroying that is precisely what this module exists to prevent. Re-importing the phrase already in use is a no-op rather than a replacement, so it archives nothing. Counters are deliberately left alone. They are per-keyset and seed-relative, so under a new seed they merely start high, which costs nothing because a restore scans from zero regardless. Resetting them would be the dangerous choice on the day someone imports the phrase they were already using. `imported` is its own provenance rather than reusing `independent`: both mean the node's recovery phrase does not cover the wallet, but only one of them means the operator already knows where else the words live. 15 NUT-13 tests green, 1000 frontend tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c1a79fdd69
commit
ee40880ce5
@@ -271,6 +271,7 @@ impl RpcHandler {
|
||||
"wallet.ecash-seed-status" => self.handle_wallet_ecash_seed_status().await,
|
||||
"wallet.ecash-seed-reveal" => self.handle_wallet_ecash_seed_reveal(params).await,
|
||||
"wallet.ecash-restore" => self.handle_wallet_ecash_restore(params).await,
|
||||
"wallet.ecash-seed-import" => self.handle_wallet_ecash_seed_import(params).await,
|
||||
"wallet.networking-profits" => self.handle_wallet_networking_profits().await,
|
||||
// Fedimint ecash (via fedimint-clientd sidecar)
|
||||
"wallet.fedimint-list" => self.handle_wallet_fedimint_list().await,
|
||||
|
||||
@@ -354,6 +354,45 @@ impl RpcHandler {
|
||||
}))
|
||||
}
|
||||
|
||||
/// `wallet.ecash-seed-import` — adopt a phrase from another NUT-13 wallet.
|
||||
///
|
||||
/// Gated like every other route that touches key material. Replacing an
|
||||
/// established phrase additionally needs `confirm: true`, because coins
|
||||
/// minted under the old one stop being restorable from words — they stay
|
||||
/// spendable, but a restore will not find them. The old phrase is archived
|
||||
/// beside the wallet rather than overwritten.
|
||||
pub(super) async fn handle_wallet_ecash_seed_import(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
use zeroize::Zeroize;
|
||||
|
||||
let params = params.unwrap_or_default();
|
||||
let words = params
|
||||
.get("words")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| anyhow::anyhow!("A recovery phrase is required"))?
|
||||
.to_string();
|
||||
let confirm = params
|
||||
.get("confirm")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut password = self
|
||||
.verify_reveal_auth(¶ms, "the ecash seed")
|
||||
.await?;
|
||||
password.zeroize();
|
||||
|
||||
let seed =
|
||||
crate::wallet::nut13::import_mnemonic(&self.config.data_dir, &words, confirm).await?;
|
||||
Ok(serde_json::json!({
|
||||
"source": seed.source(),
|
||||
"word_count": seed.words().len(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// `wallet.ecash-restore` — rebuild the wallet's coins from its NUT-13
|
||||
/// phrase by asking a mint which re-derived secrets it has signed.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user