aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-24 09:45:24 -0700
committerAdam Soltys <[email protected]>2026-06-24 09:45:24 -0700
commitffb5c30b54f913746cd6f9f79166cc4d0842dc78 (patch)
tree8b14f0e828cd38b0343a4cdb598a6bc111c0c991 /src/wallet.js
parentb72534bc4284992f7e28597e6630bdf6db932e1c (diff)
downloadcoinosv3-ffb5c30b54f913746cd6f9f79166cc4d0842dc78.tar.gz
coinosv3-ffb5c30b54f913746cd6f9f79166cc4d0842dc78.zip
Add shareable gift links for fast onboarding
Create a #claim=<code> link from Settings → "Gift this wallet" (gated behind a reveal + warning, with a QR): the seed (and passphrase, if any) are encoded as BIP39 entropy in base64url in the URL fragment, which never reaches a server. Opening such a link loads that wallet and shows a welcome → balance → firm "back up your recovery phrase" flow, then drops into the wallet. The claim code is scrubbed from the address bar on load. Bearer by design — whoever opens the link controls the funds — so the creator UI warns to keep amounts small. New strings translated into all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/wallet.js')
-rw-r--r--src/wallet.js25
1 files changed, 23 insertions, 2 deletions
diff --git a/src/wallet.js b/src/wallet.js
index edacc05..08f5d9e 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -9,9 +9,9 @@
// transactions, only the (txid, vout, value, address) of each UTXO.
import { HDKey } from '@scure/bip32';
-import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
+import { generateMnemonic, mnemonicToSeedSync, validateMnemonic, mnemonicToEntropy, entropyToMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
-import { hex } from '@scure/base';
+import { hex, base64urlnopad } from '@scure/base';
import { sha256 } from '@noble/hashes/sha256';
import * as btc from '@scure/btc-signer';
import { p2wpkh } from '@scure/btc-signer/payment';
@@ -1185,6 +1185,27 @@ export function utxoId(u) {
return `${u.txid}:${u.vout}`;
}
+// Gift links: encode a wallet (12-word seed as BIP39 entropy, plus the optional
+// passphrase) compactly for the #claim= fragment as base64url, and decode it
+// back to { mnemonic, passphrase }. Returns null if the code isn't valid.
+export function encodeGift(mnemonic, passphrase = '') {
+ let code = base64urlnopad.encode(mnemonicToEntropy(mnemonic.trim().replace(/\s+/g, ' '), wordlist));
+ if (passphrase) code += '~' + base64urlnopad.encode(new TextEncoder().encode(passphrase));
+ return code;
+}
+
+export function decodeGift(code) {
+ try {
+ const [seedPart, passPart] = code.split('~');
+ const mnemonic = entropyToMnemonic(base64urlnopad.decode(seedPart), wordlist);
+ if (!validateMnemonic(mnemonic, wordlist)) return null;
+ const passphrase = passPart ? new TextDecoder().decode(base64urlnopad.decode(passPart)) : '';
+ return { mnemonic, passphrase };
+ } catch {
+ return null;
+ }
+}
+
function firstUnused(chain) {
const u = chain.find((a) => !a.used);
return u ? u.index : chain.length;