@cogcoin/client 0.5.11 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/app-paths.d.ts +1 -0
- package/dist/app-paths.js +3 -0
- package/dist/bitcoind/bootstrap/controller.d.ts +3 -0
- package/dist/bitcoind/bootstrap/controller.js +7 -5
- package/dist/bitcoind/client/factory.d.ts +8 -0
- package/dist/bitcoind/client/factory.js +43 -6
- package/dist/bitcoind/client/managed-client.js +19 -10
- package/dist/bitcoind/client/sync-engine.js +35 -4
- package/dist/bitcoind/progress/formatting.js +1 -1
- package/dist/bitcoind/testing.d.ts +1 -0
- package/dist/bitcoind/testing.js +1 -0
- package/dist/cli/commands/follow.js +47 -14
- package/dist/cli/commands/sync.js +48 -15
- package/dist/cli/context.js +5 -1
- package/dist/cli/output.js +11 -0
- package/dist/cli/runner.js +2 -0
- package/dist/cli/signals.d.ts +1 -1
- package/dist/cli/signals.js +17 -4
- package/dist/cli/types.d.ts +4 -0
- package/dist/cli/update-notifier.d.ts +2 -0
- package/dist/cli/update-notifier.js +276 -0
- package/dist/client/default-client.d.ts +1 -1
- package/dist/client/default-client.js +7 -1
- package/dist/client/factory.js +6 -1
- package/dist/sqlite/store.js +3 -0
- package/dist/types.d.ts +2 -0
- package/dist/wallet/archive.js +10 -8
- package/dist/wallet/coin-control.d.ts +41 -0
- package/dist/wallet/coin-control.js +365 -0
- package/dist/wallet/lifecycle.js +39 -2
- package/dist/wallet/mining/runner.js +46 -44
- package/dist/wallet/read/context.js +15 -6
- package/dist/wallet/reset.js +2 -0
- package/dist/wallet/state/storage.js +5 -4
- package/dist/wallet/tx/anchor.js +36 -51
- package/dist/wallet/tx/cog.js +19 -12
- package/dist/wallet/tx/common.d.ts +41 -10
- package/dist/wallet/tx/common.js +112 -5
- package/dist/wallet/tx/domain-admin.js +13 -8
- package/dist/wallet/tx/domain-market.js +19 -12
- package/dist/wallet/tx/field.js +21 -18
- package/dist/wallet/tx/register.js +17 -12
- package/dist/wallet/tx/reputation.js +13 -8
- package/dist/wallet/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, } fro
|
|
|
7
7
|
import { resolveCogcoinProcessingStartHeight } from "../../bitcoind/processing-start-height.js";
|
|
8
8
|
import {} from "../../bitcoind/types.js";
|
|
9
9
|
import { loadOrAutoUnlockWalletState, verifyManagedCoreWalletReplica, } from "../lifecycle.js";
|
|
10
|
+
import { normalizeWalletStateRecord, persistWalletCoinControlStateIfNeeded } from "../coin-control.js";
|
|
10
11
|
import { persistNormalizedWalletDescriptorStateIfNeeded } from "../descriptor-normalization.js";
|
|
11
12
|
import { inspectMiningControlPlane } from "../mining/index.js";
|
|
12
13
|
import { normalizeMiningStateRecord } from "../mining/state.js";
|
|
@@ -76,9 +77,17 @@ async function normalizeLoadedWalletStateForRead(options) {
|
|
|
76
77
|
replacePrimary: options.loaded.source === "backup",
|
|
77
78
|
rpc: createRpcClient(node.rpc),
|
|
78
79
|
});
|
|
79
|
-
|
|
80
|
-
source: normalized.changed ? "primary" : options.loaded.source,
|
|
80
|
+
const coinControl = await persistWalletCoinControlStateIfNeeded({
|
|
81
81
|
state: normalized.state,
|
|
82
|
+
access,
|
|
83
|
+
paths: options.paths,
|
|
84
|
+
nowUnixMs: options.now,
|
|
85
|
+
replacePrimary: (normalized.changed ? "primary" : options.loaded.source) === "backup",
|
|
86
|
+
rpc: createRpcClient(node.rpc),
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
source: coinControl.changed ? "primary" : normalized.changed ? "primary" : options.loaded.source,
|
|
90
|
+
state: coinControl.state,
|
|
82
91
|
};
|
|
83
92
|
}
|
|
84
93
|
finally {
|
|
@@ -186,10 +195,10 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
186
195
|
return {
|
|
187
196
|
availability: "ready",
|
|
188
197
|
walletRootId: unlocked.state.walletRootId,
|
|
189
|
-
state: {
|
|
198
|
+
state: normalizeWalletStateRecord({
|
|
190
199
|
...unlocked.state,
|
|
191
200
|
miningState: normalizeMiningStateRecord(unlocked.state.miningState),
|
|
192
|
-
},
|
|
201
|
+
}),
|
|
193
202
|
source: unlocked.source,
|
|
194
203
|
unlockUntilUnixMs: unlocked.session.unlockUntilUnixMs,
|
|
195
204
|
hasPrimaryStateFile,
|
|
@@ -226,10 +235,10 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
226
235
|
return {
|
|
227
236
|
availability: "ready",
|
|
228
237
|
walletRootId: loaded.state.walletRootId,
|
|
229
|
-
state: {
|
|
238
|
+
state: normalizeWalletStateRecord({
|
|
230
239
|
...loaded.state,
|
|
231
240
|
miningState: normalizeMiningStateRecord(loaded.state.miningState),
|
|
232
|
-
},
|
|
241
|
+
}),
|
|
233
242
|
source: loaded.source,
|
|
234
243
|
unlockUntilUnixMs: null,
|
|
235
244
|
hasPrimaryStateFile,
|
package/dist/wallet/reset.js
CHANGED
|
@@ -187,6 +187,8 @@ function createEntropyRetainedWalletState(previousState, nowUnixMs) {
|
|
|
187
187
|
walletRootId,
|
|
188
188
|
network: previousState.network,
|
|
189
189
|
anchorValueSats: previousState.anchorValueSats,
|
|
190
|
+
proactiveReserveSats: previousState.proactiveReserveSats,
|
|
191
|
+
proactiveReserveOutpoints: previousState.proactiveReserveOutpoints,
|
|
190
192
|
nextDedicatedIndex: previousState.nextDedicatedIndex,
|
|
191
193
|
fundingIndex: previousState.fundingIndex,
|
|
192
194
|
mnemonic: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { writeJsonFileAtomic } from "../fs/atomic.js";
|
|
3
|
+
import { normalizeWalletStateRecord } from "../coin-control.js";
|
|
3
4
|
import { decryptJsonWithPassphrase, decryptJsonWithSecretProvider, encryptJsonWithPassphrase, encryptJsonWithSecretProvider, } from "./crypto.js";
|
|
4
5
|
async function readEnvelope(path) {
|
|
5
6
|
const raw = await readFile(path, "utf8");
|
|
@@ -73,18 +74,18 @@ export async function loadWalletState(paths, access) {
|
|
|
73
74
|
try {
|
|
74
75
|
return {
|
|
75
76
|
source: "primary",
|
|
76
|
-
state: typeof access === "string" || access instanceof Uint8Array
|
|
77
|
+
state: normalizeWalletStateRecord(typeof access === "string" || access instanceof Uint8Array
|
|
77
78
|
? await decryptJsonWithPassphrase(await readEnvelope(paths.primaryPath), access)
|
|
78
|
-
: await decryptJsonWithSecretProvider(await readEnvelope(paths.primaryPath), access.provider),
|
|
79
|
+
: await decryptJsonWithSecretProvider(await readEnvelope(paths.primaryPath), access.provider)),
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
catch (primaryError) {
|
|
82
83
|
try {
|
|
83
84
|
return {
|
|
84
85
|
source: "backup",
|
|
85
|
-
state: typeof access === "string" || access instanceof Uint8Array
|
|
86
|
+
state: normalizeWalletStateRecord(typeof access === "string" || access instanceof Uint8Array
|
|
86
87
|
? await decryptJsonWithPassphrase(await readEnvelope(paths.backupPath), access)
|
|
87
|
-
: await decryptJsonWithSecretProvider(await readEnvelope(paths.backupPath), access.provider),
|
|
88
|
+
: await decryptJsonWithSecretProvider(await readEnvelope(paths.backupPath), access.provider)),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
catch {
|
package/dist/wallet/tx/anchor.js
CHANGED
|
@@ -5,11 +5,12 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
|
5
5
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
6
6
|
import { acquireFileLock } from "../fs/lock.js";
|
|
7
7
|
import { deriveWalletIdentityMaterial, } from "../material.js";
|
|
8
|
+
import { computeDesignatedProactiveReserveOutpoints } from "../coin-control.js";
|
|
8
9
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
9
10
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
10
11
|
import { serializeDomainAnchor, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
|
|
11
12
|
import { openWalletReadContext } from "../read/index.js";
|
|
12
|
-
import { assertWalletMutationContextReady,
|
|
13
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, inputMatchesOutpoint, } from "./common.js";
|
|
13
14
|
import { confirmYesNo } from "./confirm.js";
|
|
14
15
|
const ACTIVE_FAMILY_STATUSES = new Set([
|
|
15
16
|
"draft",
|
|
@@ -463,9 +464,7 @@ function buildTx1Plan(options) {
|
|
|
463
464
|
return {
|
|
464
465
|
sender: options.operation.sourceSender,
|
|
465
466
|
changeAddress: options.state.funding.address,
|
|
466
|
-
|
|
467
|
-
? { txid: senderInput.txid, vout: senderInput.vout }
|
|
468
|
-
: { txid: entry.txid, vout: entry.vout }),
|
|
467
|
+
fixedInputs: [{ txid: senderInput.txid, vout: senderInput.vout }],
|
|
469
468
|
outputs,
|
|
470
469
|
changePosition: 2,
|
|
471
470
|
expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
|
|
@@ -474,6 +473,9 @@ function buildTx1Plan(options) {
|
|
|
474
473
|
expectedReplacementAnchorScriptHex: null,
|
|
475
474
|
expectedReplacementAnchorValueSats: null,
|
|
476
475
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
476
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos
|
|
477
|
+
.filter((entry) => !(entry.txid === senderInput.txid && entry.vout === senderInput.vout))
|
|
478
|
+
.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
477
479
|
requiredSenderOutpoint: null,
|
|
478
480
|
requiredProvisionalOutpoint: null,
|
|
479
481
|
errorPrefix: "wallet_anchor_tx1",
|
|
@@ -492,10 +494,7 @@ function buildTx1Plan(options) {
|
|
|
492
494
|
return {
|
|
493
495
|
sender: options.operation.sourceSender,
|
|
494
496
|
changeAddress: options.state.funding.address,
|
|
495
|
-
|
|
496
|
-
{ txid: sourceAnchor.txid, vout: sourceAnchor.vout },
|
|
497
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
498
|
-
],
|
|
497
|
+
fixedInputs: [{ txid: sourceAnchor.txid, vout: sourceAnchor.vout }],
|
|
499
498
|
outputs,
|
|
500
499
|
changePosition: 3,
|
|
501
500
|
expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
|
|
@@ -504,6 +503,7 @@ function buildTx1Plan(options) {
|
|
|
504
503
|
expectedReplacementAnchorScriptHex: options.operation.sourceSender.scriptPubKeyHex,
|
|
505
504
|
expectedReplacementAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
506
505
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
506
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
507
507
|
requiredSenderOutpoint: options.operation.sourceAnchorOutpoint,
|
|
508
508
|
requiredProvisionalOutpoint: null,
|
|
509
509
|
errorPrefix: "wallet_anchor_tx1",
|
|
@@ -535,10 +535,7 @@ function buildTx2Plan(options) {
|
|
|
535
535
|
address: options.operation.targetIdentity.address,
|
|
536
536
|
},
|
|
537
537
|
changeAddress: options.state.funding.address,
|
|
538
|
-
|
|
539
|
-
{ txid: provisional.txid, vout: provisional.vout },
|
|
540
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
541
|
-
],
|
|
538
|
+
fixedInputs: [{ txid: provisional.txid, vout: provisional.vout }],
|
|
542
539
|
outputs: [
|
|
543
540
|
{ data: Buffer.from(opReturnData).toString("hex") },
|
|
544
541
|
{ [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
|
|
@@ -550,6 +547,7 @@ function buildTx2Plan(options) {
|
|
|
550
547
|
expectedReplacementAnchorScriptHex: null,
|
|
551
548
|
expectedReplacementAnchorValueSats: null,
|
|
552
549
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
550
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
553
551
|
requiredSenderOutpoint: null,
|
|
554
552
|
requiredProvisionalOutpoint: {
|
|
555
553
|
txid: provisional.txid,
|
|
@@ -558,30 +556,13 @@ function buildTx2Plan(options) {
|
|
|
558
556
|
errorPrefix: "wallet_anchor_tx2",
|
|
559
557
|
};
|
|
560
558
|
}
|
|
561
|
-
function getDecodedInputScriptPubKeyHex(input) {
|
|
562
|
-
return input.prevout?.scriptPubKey?.hex ?? null;
|
|
563
|
-
}
|
|
564
|
-
function getDecodedInputVout(input) {
|
|
565
|
-
const vout = input.vout;
|
|
566
|
-
return typeof vout === "number" ? vout : null;
|
|
567
|
-
}
|
|
568
|
-
function inputMatchesOutpoint(input, outpoint) {
|
|
569
|
-
return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
|
|
570
|
-
}
|
|
571
|
-
function assertNoUnexpectedAnchorInputs(inputs, allowedScripts, unexpectedInputErrorCode) {
|
|
572
|
-
for (const input of inputs) {
|
|
573
|
-
const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
|
|
574
|
-
if (scriptPubKeyHex === null || !allowedScripts.has(scriptPubKeyHex)) {
|
|
575
|
-
throw new Error(unexpectedInputErrorCode);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
559
|
function validateTx1Draft(decoded, funded, plan) {
|
|
580
560
|
const inputs = decoded.tx.vin;
|
|
581
561
|
const outputs = decoded.tx.vout;
|
|
582
562
|
if (inputs.length === 0) {
|
|
583
563
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
584
564
|
}
|
|
565
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
585
566
|
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
586
567
|
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
|
|
587
568
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
@@ -590,14 +571,14 @@ function validateTx1Draft(decoded, funded, plan) {
|
|
|
590
571
|
if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
|
|
591
572
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
592
573
|
}
|
|
593
|
-
if (inputs.length < 2 || getDecodedInputScriptPubKeyHex(inputs[1]) !== plan.allowedFundingScriptPubKeyHex) {
|
|
594
|
-
throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
|
|
595
|
-
}
|
|
596
|
-
assertNoUnexpectedAnchorInputs(inputs.slice(2), new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
assertNoUnexpectedAnchorInputs(inputs, new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
|
|
600
574
|
}
|
|
575
|
+
assertFundingInputsAfterFixedPrefix({
|
|
576
|
+
inputs,
|
|
577
|
+
fixedInputs: plan.fixedInputs,
|
|
578
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
579
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
580
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
581
|
+
});
|
|
601
582
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
602
583
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
603
584
|
}
|
|
@@ -635,15 +616,19 @@ function validateTx2Draft(decoded, funded, plan) {
|
|
|
635
616
|
if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
|
|
636
617
|
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
637
618
|
}
|
|
619
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
|
|
638
620
|
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
639
621
|
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
|
|
640
622
|
|| !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
|
|
641
623
|
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
642
624
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
625
|
+
assertFundingInputsAfterFixedPrefix({
|
|
626
|
+
inputs,
|
|
627
|
+
fixedInputs: plan.fixedInputs,
|
|
628
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
629
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
630
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
631
|
+
});
|
|
647
632
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
648
633
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
649
634
|
}
|
|
@@ -668,31 +653,29 @@ function validateTx2Draft(decoded, funded, plan) {
|
|
|
668
653
|
}
|
|
669
654
|
}
|
|
670
655
|
async function buildTx1(options) {
|
|
671
|
-
|
|
656
|
+
const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
|
|
657
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
672
658
|
rpc: options.rpc,
|
|
673
659
|
walletName: options.walletName,
|
|
660
|
+
state: options.state,
|
|
674
661
|
plan: options.plan,
|
|
675
662
|
validateFundedDraft: validateTx1Draft,
|
|
676
663
|
finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
|
|
677
664
|
mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
|
|
678
|
-
|
|
679
|
-
addInputs: false,
|
|
680
|
-
},
|
|
665
|
+
reserveCandidates,
|
|
681
666
|
});
|
|
682
667
|
}
|
|
683
668
|
async function buildTx2(options) {
|
|
684
|
-
|
|
669
|
+
const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
|
|
670
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
685
671
|
rpc: options.rpc,
|
|
686
672
|
walletName: options.walletName,
|
|
673
|
+
state: options.state,
|
|
687
674
|
plan: options.plan,
|
|
688
675
|
validateFundedDraft: validateTx2Draft,
|
|
689
676
|
finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
|
|
690
677
|
mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
|
|
691
|
-
|
|
692
|
-
addInputs: false,
|
|
693
|
-
includeUnsafe: true,
|
|
694
|
-
minConf: 0,
|
|
695
|
-
},
|
|
678
|
+
reserveCandidates,
|
|
696
679
|
});
|
|
697
680
|
}
|
|
698
681
|
async function relockAnchorOutpoint(rpc, walletName, outpoint) {
|
|
@@ -998,6 +981,7 @@ async function submitTx2(options) {
|
|
|
998
981
|
const builtTx2 = await buildTx2({
|
|
999
982
|
rpc: options.rpc,
|
|
1000
983
|
walletName: options.walletName,
|
|
984
|
+
state: nextState,
|
|
1001
985
|
plan: tx2Plan,
|
|
1002
986
|
});
|
|
1003
987
|
const broadcastingTx2 = createBroadcastingTxRecord(builtTx2);
|
|
@@ -1232,6 +1216,7 @@ export async function anchorDomain(options) {
|
|
|
1232
1216
|
const builtTx1 = await buildTx1({
|
|
1233
1217
|
rpc,
|
|
1234
1218
|
walletName,
|
|
1219
|
+
state: nextState,
|
|
1235
1220
|
plan: tx1Plan,
|
|
1236
1221
|
});
|
|
1237
1222
|
const broadcastingTx1 = createBroadcastingTxRecord(builtTx1);
|
package/dist/wallet/tx/cog.js
CHANGED
|
@@ -7,7 +7,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
|
7
7
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
8
8
|
import { serializeCogClaim, serializeCogLock, serializeCogTransfer, } from "../cogop/index.js";
|
|
9
9
|
import { openWalletReadContext } from "../read/index.js";
|
|
10
|
-
import { assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
10
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
11
11
|
import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
|
|
12
12
|
import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
|
|
13
13
|
import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
|
|
@@ -258,11 +258,8 @@ function buildPlanForCogOperation(options) {
|
|
|
258
258
|
return {
|
|
259
259
|
sender: options.sender,
|
|
260
260
|
changeAddress: options.state.funding.address,
|
|
261
|
-
|
|
261
|
+
fixedInputs: [
|
|
262
262
|
{ txid: senderUtxo.txid, vout: senderUtxo.vout },
|
|
263
|
-
...fundingUtxos
|
|
264
|
-
.filter((entry) => !(entry.txid === senderUtxo.txid && entry.vout === senderUtxo.vout))
|
|
265
|
-
.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
266
263
|
],
|
|
267
264
|
outputs,
|
|
268
265
|
changePosition: 1,
|
|
@@ -270,6 +267,9 @@ function buildPlanForCogOperation(options) {
|
|
|
270
267
|
expectedAnchorScriptHex: null,
|
|
271
268
|
expectedAnchorValueSats: null,
|
|
272
269
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
270
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos
|
|
271
|
+
.filter((entry) => !(entry.txid === senderUtxo.txid && entry.vout === senderUtxo.vout))
|
|
272
|
+
.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
273
273
|
errorPrefix: options.errorPrefix,
|
|
274
274
|
};
|
|
275
275
|
}
|
|
@@ -288,9 +288,8 @@ function buildPlanForCogOperation(options) {
|
|
|
288
288
|
return {
|
|
289
289
|
sender: options.sender,
|
|
290
290
|
changeAddress: options.state.funding.address,
|
|
291
|
-
|
|
291
|
+
fixedInputs: [
|
|
292
292
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
293
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
294
293
|
],
|
|
295
294
|
outputs,
|
|
296
295
|
changePosition: 2,
|
|
@@ -298,6 +297,7 @@ function buildPlanForCogOperation(options) {
|
|
|
298
297
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
299
298
|
expectedAnchorValueSats: options.anchorValueSats,
|
|
300
299
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
300
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
301
301
|
errorPrefix: options.errorPrefix,
|
|
302
302
|
};
|
|
303
303
|
}
|
|
@@ -307,14 +307,17 @@ function validateFundedDraft(decoded, funded, plan) {
|
|
|
307
307
|
if (inputs.length === 0) {
|
|
308
308
|
throw new Error(`${plan.errorPrefix}_missing_sender_input`);
|
|
309
309
|
}
|
|
310
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
310
311
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
311
312
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
assertFundingInputsAfterFixedPrefix({
|
|
315
|
+
inputs,
|
|
316
|
+
fixedInputs: plan.fixedInputs,
|
|
317
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
318
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
319
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
320
|
+
});
|
|
318
321
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
319
322
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
320
323
|
}
|
|
@@ -344,6 +347,7 @@ async function buildTransaction(options) {
|
|
|
344
347
|
return buildWalletMutationTransaction({
|
|
345
348
|
rpc: options.rpc,
|
|
346
349
|
walletName: options.walletName,
|
|
350
|
+
state: options.state,
|
|
347
351
|
plan: options.plan,
|
|
348
352
|
validateFundedDraft,
|
|
349
353
|
finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
|
|
@@ -699,6 +703,7 @@ export async function sendCog(options) {
|
|
|
699
703
|
const built = await buildTransaction({
|
|
700
704
|
rpc,
|
|
701
705
|
walletName,
|
|
706
|
+
state: nextState,
|
|
702
707
|
plan: buildPlanForCogOperation({
|
|
703
708
|
state: nextState,
|
|
704
709
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -860,6 +865,7 @@ export async function lockCogToDomain(options) {
|
|
|
860
865
|
const built = await buildTransaction({
|
|
861
866
|
rpc,
|
|
862
867
|
walletName,
|
|
868
|
+
state: nextState,
|
|
863
869
|
plan: buildPlanForCogOperation({
|
|
864
870
|
state: nextState,
|
|
865
871
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1004,6 +1010,7 @@ async function runClaimLikeMutation(options, reclaim) {
|
|
|
1004
1010
|
const built = await buildTransaction({
|
|
1005
1011
|
rpc,
|
|
1006
1012
|
walletName,
|
|
1013
|
+
state: nextState,
|
|
1007
1014
|
plan: buildPlanForCogOperation({
|
|
1008
1015
|
state: nextState,
|
|
1009
1016
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RpcDecodedPsbt, RpcFinalizePsbtResult, RpcListUnspentEntry, RpcLockedUnspent, RpcTestMempoolAcceptResult, RpcTransaction, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult } from "../../bitcoind/types.js";
|
|
1
|
+
import type { RpcDecodedPsbt, RpcFinalizePsbtResult, RpcListUnspentEntry, RpcLockedUnspent, RpcTestMempoolAcceptResult, RpcTransaction, RpcVin, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult } from "../../bitcoind/types.js";
|
|
2
2
|
import { type WalletSecretProvider } from "../state/provider.js";
|
|
3
3
|
import type { OutpointRecord, PendingMutationRecord, PendingMutationStatus, WalletStateV1 } from "../types.js";
|
|
4
4
|
import type { WalletReadContext } from "../read/index.js";
|
|
@@ -33,6 +33,8 @@ export interface BuiltWalletMutationTransaction {
|
|
|
33
33
|
wtxid: string | null;
|
|
34
34
|
temporaryBuilderLockedOutpoints: OutpointRecord[];
|
|
35
35
|
}
|
|
36
|
+
export interface FixedWalletInput extends OutpointRecord {
|
|
37
|
+
}
|
|
36
38
|
export declare function saveWalletStatePreservingUnlock(options: {
|
|
37
39
|
state: WalletStateV1;
|
|
38
40
|
provider: WalletSecretProvider;
|
|
@@ -49,8 +51,28 @@ export declare function updateMutationRecord(mutation: PendingMutationRecord, st
|
|
|
49
51
|
}): PendingMutationRecord;
|
|
50
52
|
export declare function unlockTemporaryBuilderLocks(rpc: Pick<WalletMutationRpcClient, "lockUnspent">, walletName: string, outpoints: OutpointRecord[]): Promise<void>;
|
|
51
53
|
export declare function diffTemporaryLockedOutpoints(before: RpcLockedUnspent[], after: RpcLockedUnspent[]): OutpointRecord[];
|
|
54
|
+
export declare function getDecodedInputScriptPubKeyHex(input: RpcVin): string | null;
|
|
55
|
+
export declare function getDecodedInputVout(input: RpcVin): number | null;
|
|
56
|
+
export declare function inputMatchesOutpoint(input: RpcVin, outpoint: OutpointRecord): boolean;
|
|
57
|
+
export declare function assertFixedInputPrefixMatches(inputs: RpcVin[], fixedInputs: FixedWalletInput[], errorCode: string): void;
|
|
58
|
+
export declare function assertFundingInputsAfterFixedPrefix(options: {
|
|
59
|
+
inputs: RpcVin[];
|
|
60
|
+
fixedInputs: FixedWalletInput[];
|
|
61
|
+
allowedFundingScriptPubKeyHex: string;
|
|
62
|
+
eligibleFundingOutpointKeys: Set<string>;
|
|
63
|
+
errorCode: string;
|
|
64
|
+
}): void;
|
|
65
|
+
export declare function reconcilePersistentPolicyLocks(options: {
|
|
66
|
+
rpc: Pick<WalletMutationRpcClient, "listLockUnspent" | "lockUnspent" | "listUnspent">;
|
|
67
|
+
walletName: string;
|
|
68
|
+
state: WalletStateV1;
|
|
69
|
+
fixedInputs: FixedWalletInput[];
|
|
70
|
+
temporarilyUnlockedOutpoints?: readonly OutpointRecord[];
|
|
71
|
+
cleanupInactiveTemporaryBuilderLocks?: boolean;
|
|
72
|
+
}): Promise<void>;
|
|
52
73
|
export declare function isBroadcastUnknownError(error: unknown): boolean;
|
|
53
74
|
export declare function isAlreadyAcceptedError(error: unknown): boolean;
|
|
75
|
+
export declare function isInsufficientFundsError(error: unknown): boolean;
|
|
54
76
|
export declare function assertWalletMutationContextReady(context: WalletReadContext, errorPrefix: string): asserts context is WalletReadContext & {
|
|
55
77
|
localState: {
|
|
56
78
|
availability: "ready";
|
|
@@ -67,11 +89,9 @@ export declare function pauseMiningForWalletMutation(options: {
|
|
|
67
89
|
export declare function buildWalletMutationTransaction<TPlan>(options: {
|
|
68
90
|
rpc: WalletMutationRpcClient;
|
|
69
91
|
walletName: string;
|
|
92
|
+
state: WalletStateV1;
|
|
70
93
|
plan: TPlan & {
|
|
71
|
-
|
|
72
|
-
txid: string;
|
|
73
|
-
vout: number;
|
|
74
|
-
}>;
|
|
94
|
+
fixedInputs: FixedWalletInput[];
|
|
75
95
|
outputs: unknown[];
|
|
76
96
|
changeAddress: string;
|
|
77
97
|
changePosition: number;
|
|
@@ -80,10 +100,21 @@ export declare function buildWalletMutationTransaction<TPlan>(options: {
|
|
|
80
100
|
finalizeErrorCode: string;
|
|
81
101
|
mempoolRejectPrefix: string;
|
|
82
102
|
feeRate?: number;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
temporarilyUnlockedPolicyOutpoints?: readonly OutpointRecord[];
|
|
104
|
+
}): Promise<BuiltWalletMutationTransaction>;
|
|
105
|
+
export declare function buildWalletMutationTransactionWithReserveFallback<TPlan>(options: {
|
|
106
|
+
rpc: WalletMutationRpcClient;
|
|
107
|
+
walletName: string;
|
|
108
|
+
state: WalletStateV1;
|
|
109
|
+
plan: TPlan & {
|
|
110
|
+
fixedInputs: FixedWalletInput[];
|
|
111
|
+
outputs: unknown[];
|
|
112
|
+
changeAddress: string;
|
|
113
|
+
changePosition: number;
|
|
88
114
|
};
|
|
115
|
+
validateFundedDraft(decoded: RpcDecodedPsbt, funded: RpcWalletCreateFundedPsbtResult, plan: TPlan): void;
|
|
116
|
+
finalizeErrorCode: string;
|
|
117
|
+
mempoolRejectPrefix: string;
|
|
118
|
+
feeRate?: number;
|
|
119
|
+
reserveCandidates: readonly OutpointRecord[];
|
|
89
120
|
}): Promise<BuiltWalletMutationTransaction>;
|
package/dist/wallet/tx/common.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { saveUnlockSession } from "../state/session.js";
|
|
3
3
|
import { saveWalletState } from "../state/storage.js";
|
|
4
4
|
import { createWalletSecretReference, } from "../state/provider.js";
|
|
5
|
+
import { reconcilePersistentPolicyLocks as reconcileWalletCoinControlLocks } from "../coin-control.js";
|
|
5
6
|
import { requestMiningGenerationPreemption } from "../mining/coordination.js";
|
|
6
7
|
export const DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB = 10;
|
|
7
8
|
function createUnlockSessionState(state, unlockUntilUnixMs, nowUnixMs) {
|
|
@@ -64,6 +65,53 @@ export function diffTemporaryLockedOutpoints(before, after) {
|
|
|
64
65
|
vout: entry.vout,
|
|
65
66
|
}));
|
|
66
67
|
}
|
|
68
|
+
export function getDecodedInputScriptPubKeyHex(input) {
|
|
69
|
+
return input.prevout?.scriptPubKey?.hex ?? null;
|
|
70
|
+
}
|
|
71
|
+
export function getDecodedInputVout(input) {
|
|
72
|
+
const vout = input.vout;
|
|
73
|
+
return typeof vout === "number" ? vout : null;
|
|
74
|
+
}
|
|
75
|
+
export function inputMatchesOutpoint(input, outpoint) {
|
|
76
|
+
return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
|
|
77
|
+
}
|
|
78
|
+
export function assertFixedInputPrefixMatches(inputs, fixedInputs, errorCode) {
|
|
79
|
+
if (inputs.length < fixedInputs.length) {
|
|
80
|
+
throw new Error(errorCode);
|
|
81
|
+
}
|
|
82
|
+
for (const [index, fixedInput] of fixedInputs.entries()) {
|
|
83
|
+
if (!inputMatchesOutpoint(inputs[index], fixedInput)) {
|
|
84
|
+
throw new Error(errorCode);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function assertFundingInputsAfterFixedPrefix(options) {
|
|
89
|
+
for (let index = options.fixedInputs.length; index < options.inputs.length; index += 1) {
|
|
90
|
+
const input = options.inputs[index];
|
|
91
|
+
const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
|
|
92
|
+
const vout = getDecodedInputVout(input);
|
|
93
|
+
if (scriptPubKeyHex !== options.allowedFundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
|
|
94
|
+
throw new Error(options.errorCode);
|
|
95
|
+
}
|
|
96
|
+
const key = outpointKey({
|
|
97
|
+
txid: input.txid,
|
|
98
|
+
vout,
|
|
99
|
+
});
|
|
100
|
+
if (!options.eligibleFundingOutpointKeys.has(key)) {
|
|
101
|
+
throw new Error(options.errorCode);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function reconcilePersistentPolicyLocks(options) {
|
|
106
|
+
await reconcileWalletCoinControlLocks({
|
|
107
|
+
rpc: options.rpc,
|
|
108
|
+
walletName: options.walletName,
|
|
109
|
+
state: options.state,
|
|
110
|
+
fixedInputs: options.fixedInputs,
|
|
111
|
+
temporarilyUnlockedOutpoints: options.temporarilyUnlockedOutpoints,
|
|
112
|
+
cleanupInactiveTemporaryBuilderLocks: options.cleanupInactiveTemporaryBuilderLocks,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
67
115
|
export function isBroadcastUnknownError(error) {
|
|
68
116
|
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
69
117
|
return message.includes("timeout")
|
|
@@ -80,6 +128,10 @@ export function isAlreadyAcceptedError(error) {
|
|
|
80
128
|
|| message.includes("already in blockchain")
|
|
81
129
|
|| message.includes("txn-already-known");
|
|
82
130
|
}
|
|
131
|
+
export function isInsufficientFundsError(error) {
|
|
132
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
133
|
+
return message.includes("insufficient funds");
|
|
134
|
+
}
|
|
83
135
|
export function assertWalletMutationContextReady(context, errorPrefix) {
|
|
84
136
|
if (context.localState.availability === "uninitialized") {
|
|
85
137
|
throw new Error("wallet_uninitialized");
|
|
@@ -110,16 +162,23 @@ export async function pauseMiningForWalletMutation(options) {
|
|
|
110
162
|
});
|
|
111
163
|
}
|
|
112
164
|
export async function buildWalletMutationTransaction(options) {
|
|
165
|
+
await reconcilePersistentPolicyLocks({
|
|
166
|
+
rpc: options.rpc,
|
|
167
|
+
walletName: options.walletName,
|
|
168
|
+
state: options.state,
|
|
169
|
+
fixedInputs: options.plan.fixedInputs,
|
|
170
|
+
temporarilyUnlockedOutpoints: options.temporarilyUnlockedPolicyOutpoints,
|
|
171
|
+
});
|
|
113
172
|
const lockedBefore = await options.rpc.listLockUnspent(options.walletName);
|
|
114
173
|
let temporaryBuilderLockedOutpoints = [];
|
|
115
174
|
try {
|
|
116
|
-
const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.
|
|
117
|
-
add_inputs:
|
|
118
|
-
include_unsafe:
|
|
119
|
-
minconf:
|
|
175
|
+
const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.fixedInputs, options.plan.outputs, 0, {
|
|
176
|
+
add_inputs: true,
|
|
177
|
+
include_unsafe: false,
|
|
178
|
+
minconf: 1,
|
|
120
179
|
changeAddress: options.plan.changeAddress,
|
|
121
180
|
changePosition: options.plan.changePosition,
|
|
122
|
-
lockUnspents:
|
|
181
|
+
lockUnspents: true,
|
|
123
182
|
fee_rate: options.feeRate ?? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB,
|
|
124
183
|
replaceable: true,
|
|
125
184
|
subtractFeeFromOutputs: [],
|
|
@@ -139,6 +198,14 @@ export async function buildWalletMutationTransaction(options) {
|
|
|
139
198
|
if (accepted == null || !accepted.allowed) {
|
|
140
199
|
throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
|
|
141
200
|
}
|
|
201
|
+
if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
|
|
202
|
+
await reconcilePersistentPolicyLocks({
|
|
203
|
+
rpc: options.rpc,
|
|
204
|
+
walletName: options.walletName,
|
|
205
|
+
state: options.state,
|
|
206
|
+
fixedInputs: options.plan.fixedInputs,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
142
209
|
return {
|
|
143
210
|
funded,
|
|
144
211
|
decoded,
|
|
@@ -151,6 +218,46 @@ export async function buildWalletMutationTransaction(options) {
|
|
|
151
218
|
}
|
|
152
219
|
catch (error) {
|
|
153
220
|
await unlockTemporaryBuilderLocks(options.rpc, options.walletName, temporaryBuilderLockedOutpoints);
|
|
221
|
+
if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
|
|
222
|
+
await reconcilePersistentPolicyLocks({
|
|
223
|
+
rpc: options.rpc,
|
|
224
|
+
walletName: options.walletName,
|
|
225
|
+
state: options.state,
|
|
226
|
+
fixedInputs: options.plan.fixedInputs,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
154
229
|
throw error;
|
|
155
230
|
}
|
|
156
231
|
}
|
|
232
|
+
export async function buildWalletMutationTransactionWithReserveFallback(options) {
|
|
233
|
+
let unlockedReserveOutpoints = [];
|
|
234
|
+
let lastError = null;
|
|
235
|
+
for (let attempt = 0; attempt <= options.reserveCandidates.length; attempt += 1) {
|
|
236
|
+
if (attempt > 0) {
|
|
237
|
+
unlockedReserveOutpoints = [
|
|
238
|
+
...unlockedReserveOutpoints,
|
|
239
|
+
options.reserveCandidates[attempt - 1],
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
return await buildWalletMutationTransaction({
|
|
244
|
+
rpc: options.rpc,
|
|
245
|
+
walletName: options.walletName,
|
|
246
|
+
state: options.state,
|
|
247
|
+
plan: options.plan,
|
|
248
|
+
validateFundedDraft: options.validateFundedDraft,
|
|
249
|
+
finalizeErrorCode: options.finalizeErrorCode,
|
|
250
|
+
mempoolRejectPrefix: options.mempoolRejectPrefix,
|
|
251
|
+
feeRate: options.feeRate,
|
|
252
|
+
temporarilyUnlockedPolicyOutpoints: unlockedReserveOutpoints,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
lastError = error;
|
|
257
|
+
if (!isInsufficientFundsError(error) || attempt === options.reserveCandidates.length) {
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
throw lastError;
|
|
263
|
+
}
|