diff options
| author | Adam Soltys <[email protected]> | 2026-06-24 09:45:24 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-24 09:45:24 -0700 |
| commit | ffb5c30b54f913746cd6f9f79166cc4d0842dc78 (patch) | |
| tree | 8b14f0e828cd38b0343a4cdb598a6bc111c0c991 /src/wallet.js | |
| parent | b72534bc4284992f7e28597e6630bdf6db932e1c (diff) | |
| download | coinosv3-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.js | 25 |
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; |
