aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-28 22:32:49 -0700
committerAdam Soltys <[email protected]>2026-09-28 22:33:37 -0700
commit615e8fa6ea643660ae6b6d35e669d6ad4816728a (patch)
tree65392b6f49fb40cb035fffe2eb44f14a2742e634 /src
parenta31d97220c8acb858c71301972f38597c1eb2270 (diff)
downloadcoinosv3-615e8fa6ea643660ae6b6d35e669d6ad4816728a.tar.gz
coinosv3-615e8fa6ea643660ae6b6d35e669d6ad4816728a.zip
A zap refused for an old expired coin goes again on the other coins instead of showing the server error.
Sub-dust coins skip the local expiry check, so the ASP can refuse one at cosign ("vtxo … expired at height …"). The coin was already marked excluded, but the error still surfaced and the user had to tap again. send() and payLnInvoice() now retry (up to 5 attempts) when that exact refusal marks an input, without a failed history row for the retried try. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Du1qT1wbkYwYK6zkGH79Rp
Diffstat (limited to 'src')
-rw-r--r--src/ark/manager.js31
1 files changed, 25 insertions, 6 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index 311c793..b0985de 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -853,7 +853,23 @@ export class ArkManager {
: 'insufficient ark balance';
}
+ // A server refusal naming an expired input marks that coin; the payment
+ // then goes again on the remaining coins instead of surfacing an error the
+ // user would only answer by tapping again. Each refusal names one coin.
+ async _retryExpired(attempt) {
+ for (let i = 0; ; i++) {
+ const action = await attempt();
+ if (!(action.step === 'failed' && action.expiryRetry) || i >= 4) return action;
+ }
+ }
+
async send(addrString, amountSat) {
+ const action = await this._retryExpired(() => this._sendOnce(addrString, amountSat));
+ if (action.step !== 'done') throw new Error(action.error || 'Ark payment did not complete');
+ return action.id;
+ }
+
+ async _sendOnce(addrString, amountSat) {
const dest = decodeAddress(addrString);
if (dest.arkId !== hex.encode(arkIdFromServerPubkey(this.serverPub))) {
throw new Error('address belongs to a different ark server');
@@ -887,8 +903,7 @@ export class ArkManager {
this.state.actions.push(action);
this._save();
await this._driveSend(action);
- if (action.step !== 'done') throw new Error(action.error || 'Ark payment did not complete');
- return action.id;
+ return action;
}
// pre-multi-input actions carried the single part in flat fields
@@ -931,7 +946,7 @@ export class ArkManager {
if (spent) this.reconcile().catch(() => {});
action.step = 'failed';
action.error = e.message;
- this._recordExpiryRejection(action);
+ if (this._recordExpiryRejection(action)) { action.expiryRetry = true; this._save(); return; }
this._movement({ type: 'send', amountSat: action.amountSat, status: 'failed', detail: e.message, to: action.destAddress });
this._save();
return;
@@ -1089,7 +1104,11 @@ export class ArkManager {
// Pay a bolt11 or bolt12 invoice with ark funds. Returns the action id;
// drive to a terminal step ('done' | 'failed') via driveLn()/sync.
- async payLnInvoice(invoice, { amountSat: userAmountSat, routingFeeSat } = {}) {
+ async payLnInvoice(invoice, opts = {}) {
+ return (await this._retryExpired(() => this._payLnInvoiceOnce(invoice, opts))).id;
+ }
+
+ async _payLnInvoiceOnce(invoice, { amountSat: userAmountSat, routingFeeSat } = {}) {
const dec = decodeLnInvoice(invoice);
const expectNet = { bitcoin: 'mainnet', regtest: 'regtest', signet: 'signet', testnet: 'testnet' }[this.info.network];
// a null network is a bolt12 on a chain we don't recognize (custom
@@ -1143,7 +1162,7 @@ export class ArkManager {
this.state.actions.push(action);
this._save();
await this._driveLnPay(action);
- return action.id;
+ return action;
}
// pre-multi-input ln-pay actions carried the single part in flat fields
@@ -1215,7 +1234,7 @@ export class ArkManager {
if (spent) this.reconcile().catch(() => {});
action.step = 'failed';
action.error = e.message;
- this._recordExpiryRejection(action);
+ if (this._recordExpiryRejection(action)) { action.expiryRetry = true; this._save(); return; }
// "already in progress" means another device of this wallet is
// paying this very invoice — that payment's row is the history,
// a failed row beside it would read as a lost zap.