diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/swap.js | 26 | ||||
| -rw-r--r-- | src/wallet.js | 13 |
2 files changed, 31 insertions, 8 deletions
diff --git a/src/swap.js b/src/swap.js index 2627068..129712d 100644 --- a/src/swap.js +++ b/src/swap.js @@ -276,13 +276,25 @@ export class SwapManager { // (bolt11) the user has someone pay; hal then claims the lockup on-chain. async startReverse(amountSat) { const w = this.wallet, net = this.network; - const idx = w.nextSwapIndex(); - const node = w.swapNode(idx); - const preimage = sha256(node.privateKey); - const res = await boltzReq(this.getApi(), '/v2/swap/reverse', { - invoiceAmount: amountSat, from: 'BTC', to: 'BTC', - claimPublicKey: bytesToHex(node.publicKey), preimageHash: bytesToHex(sha256(preimage)), - }); + let idx, node, preimage, res; + // Our preimage is deterministic per swap index. If a prior attempt burned this + // index at Boltz ("swap with this preimage exists"), nextSwapIndex has advanced, + // so just retry with the next one. + for (let attempt = 0; ; attempt++) { + idx = w.nextSwapIndex(); + node = w.swapNode(idx); + preimage = sha256(node.privateKey); + try { + res = await boltzReq(this.getApi(), '/v2/swap/reverse', { + invoiceAmount: amountSat, from: 'BTC', to: 'BTC', + claimPublicKey: bytesToHex(node.publicKey), preimageHash: bytesToHex(sha256(preimage)), + }); + break; + } catch (e) { + if (attempt < 5 && /preimage|already exist/i.test(String(e?.message || e))) continue; + throw e; + } + } const d = swapP2TR({ kind: 'reverse', preimageHash160: hash160(preimage), boltzPub33: hexToBytes(res.refundPublicKey), halPub33: node.publicKey, lockTime: res.timeoutBlockHeight, network: net }); if (d.address !== res.lockupAddress) throw new Error('lockup address mismatch — not paying'); if (bytesToHex(decodeBolt11(res.invoice).paymentHash) !== bytesToHex(sha256(preimage))) throw new Error('invoice hash mismatch'); diff --git a/src/wallet.js b/src/wallet.js index b3e3d40..c16bd66 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1537,8 +1537,19 @@ export class Wallet { saveSwaps(list) { try { localStorage.setItem(this._swapKey(), JSON.stringify(list)); } catch {} } + _swapIdxKey() { return this._cacheKey() + ':swapidx'; } + // Reserve the next swap index: monotonic + persisted on every call. A reverse/ + // submarine attempt that fails AFTER Boltz created the swap (a later validation + // throw, a dropped response) still burns its index, so a retry can't reuse the + // same deterministic preimage (Boltz rejects "swap with this preimage exists"). + // Seeded from saved swaps so existing wallets don't regress. nextSwapIndex() { - return this.loadSwaps().reduce((m, s) => Math.max(m, (s.swapIndex ?? -1) + 1), 0); + const fromSaved = this.loadSwaps().reduce((m, s) => Math.max(m, (s.swapIndex ?? -1) + 1), 0); + let stored = 0; + try { stored = parseInt(localStorage.getItem(this._swapIdxKey()) || '0', 10) || 0; } catch {} + const idx = Math.max(fromSaved, stored); + try { localStorage.setItem(this._swapIdxKey(), String(idx + 1)); } catch {} + return idx; } // Deterministic swap keypair on a dedicated chain (2) so swap keys never // collide with receive (0) / change (1). Returns the HDKey node (has |
