@cogcoin/client 0.5.12 → 0.5.14
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 +1 -1
- package/dist/bitcoind/bootstrap/getblock-archive.d.ts +23 -1
- package/dist/bitcoind/bootstrap/getblock-archive.js +127 -37
- package/dist/bitcoind/bootstrap.d.ts +1 -1
- package/dist/bitcoind/bootstrap.js +1 -1
- package/dist/bitcoind/client/managed-client.js +62 -40
- package/dist/bitcoind/client/sync-engine.js +7 -2
- package/dist/bitcoind/testing.d.ts +1 -1
- package/dist/bitcoind/testing.js +1 -1
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/commands/sync.js +99 -1
- package/dist/cli/commands/wallet-mutation.js +39 -2
- package/dist/cli/context.js +20 -3
- package/dist/cli/mutation-success.d.ts +2 -0
- package/dist/cli/mutation-success.js +2 -0
- package/dist/cli/mutation-text-write.d.ts +2 -0
- package/dist/cli/mutation-text-write.js +7 -0
- package/dist/cli/output.js +22 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/wallet-format.d.ts +1 -1
- package/dist/cli/wallet-format.js +2 -2
- package/dist/wallet/archive.js +10 -8
- package/dist/wallet/coin-control.d.ts +41 -0
- package/dist/wallet/coin-control.js +406 -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.d.ts +2 -0
- package/dist/wallet/tx/anchor.js +76 -56
- package/dist/wallet/tx/cog.js +19 -22
- package/dist/wallet/tx/common.d.ts +45 -10
- package/dist/wallet/tx/common.js +178 -6
- package/dist/wallet/tx/domain-admin.js +15 -9
- package/dist/wallet/tx/domain-market.js +19 -22
- package/dist/wallet/tx/field.js +19 -18
- package/dist/wallet/tx/register.js +19 -22
- package/dist/wallet/tx/reputation.js +15 -9
- 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 {
|
|
@@ -18,6 +18,7 @@ interface WalletAnchorRpcClient extends WalletMutationRpcClient {
|
|
|
18
18
|
export interface AnchorDomainOptions {
|
|
19
19
|
domainName: string;
|
|
20
20
|
foundingMessageText?: string | null;
|
|
21
|
+
promptForFoundingMessageWhenMissing?: boolean;
|
|
21
22
|
dataDir: string;
|
|
22
23
|
databasePath: string;
|
|
23
24
|
provider?: WalletSecretProvider;
|
|
@@ -36,6 +37,7 @@ export interface AnchorDomainResult {
|
|
|
36
37
|
dedicatedIndex: number;
|
|
37
38
|
status: "live" | "confirmed";
|
|
38
39
|
reusedExisting: boolean;
|
|
40
|
+
foundingMessageText?: string | null;
|
|
39
41
|
}
|
|
40
42
|
export interface ClearPendingAnchorOptions {
|
|
41
43
|
domainName: string;
|
package/dist/wallet/tx/anchor.js
CHANGED
|
@@ -9,7 +9,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
|
9
9
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
10
10
|
import { serializeDomainAnchor, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
|
|
11
11
|
import { openWalletReadContext } from "../read/index.js";
|
|
12
|
-
import { assertWalletMutationContextReady,
|
|
12
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, inputMatchesOutpoint, } from "./common.js";
|
|
13
13
|
import { confirmYesNo } from "./confirm.js";
|
|
14
14
|
const ACTIVE_FAMILY_STATUSES = new Set([
|
|
15
15
|
"draft",
|
|
@@ -115,6 +115,11 @@ function findActiveAnchorFamilyByDomain(state, domainName) {
|
|
|
115
115
|
&& family.domainName === domainName
|
|
116
116
|
&& ACTIVE_FAMILY_STATUSES.has(family.status)) ?? null;
|
|
117
117
|
}
|
|
118
|
+
function isClearableReservedAnchorFamily(family) {
|
|
119
|
+
return family?.type === "anchor"
|
|
120
|
+
&& family.status === "draft"
|
|
121
|
+
&& family.currentStep === "reserved";
|
|
122
|
+
}
|
|
118
123
|
function findAnchorFamilyById(state, familyId) {
|
|
119
124
|
return state.proactiveFamilies.find((family) => family.familyId === familyId) ?? null;
|
|
120
125
|
}
|
|
@@ -215,6 +220,35 @@ function encodeFoundingMessage(foundingMessageText) {
|
|
|
215
220
|
throw new Error(error instanceof Error ? `wallet_anchor_invalid_message_${error.message}` : "wallet_anchor_invalid_message");
|
|
216
221
|
});
|
|
217
222
|
}
|
|
223
|
+
function extractAnchorInvalidMessageReason(error) {
|
|
224
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
225
|
+
if (message === "wallet_anchor_invalid_message") {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (!message.startsWith("wallet_anchor_invalid_message_")) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const reason = message.slice("wallet_anchor_invalid_message_".length).trim();
|
|
232
|
+
return reason === "" ? null : reason;
|
|
233
|
+
}
|
|
234
|
+
async function resolveFoundingMessage(options) {
|
|
235
|
+
if (!options.promptForFoundingMessageWhenMissing || options.foundingMessageText != null) {
|
|
236
|
+
return encodeFoundingMessage(options.foundingMessageText ?? null);
|
|
237
|
+
}
|
|
238
|
+
for (;;) {
|
|
239
|
+
const answer = await options.prompter.prompt("Founding message (optional, press Enter to skip): ");
|
|
240
|
+
try {
|
|
241
|
+
return await encodeFoundingMessage(answer);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
const reason = extractAnchorInvalidMessageReason(error);
|
|
245
|
+
options.prompter.writeLine("Founding message cannot be encoded in canonical Coglex.");
|
|
246
|
+
if (reason !== null) {
|
|
247
|
+
options.prompter.writeLine(`Reason: ${reason}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
218
252
|
function resolveAnchorOutpointForSender(state, senderIndex) {
|
|
219
253
|
const anchoredDomain = state.domains.find((domain) => domain.currentOwnerLocalIndex === senderIndex
|
|
220
254
|
&& domain.canonicalChainStatus === "anchored"
|
|
@@ -456,16 +490,10 @@ function buildTx1Plan(options) {
|
|
|
456
490
|
{ [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
|
|
457
491
|
];
|
|
458
492
|
if (options.operation.sourceAnchorOutpoint === null) {
|
|
459
|
-
if (fundingUtxos.length === 0) {
|
|
460
|
-
throw new Error("wallet_anchor_sender_utxo_unavailable");
|
|
461
|
-
}
|
|
462
|
-
const senderInput = fundingUtxos[0];
|
|
463
493
|
return {
|
|
464
494
|
sender: options.operation.sourceSender,
|
|
465
495
|
changeAddress: options.state.funding.address,
|
|
466
|
-
|
|
467
|
-
? { txid: senderInput.txid, vout: senderInput.vout }
|
|
468
|
-
: { txid: entry.txid, vout: entry.vout }),
|
|
496
|
+
fixedInputs: [],
|
|
469
497
|
outputs,
|
|
470
498
|
changePosition: 2,
|
|
471
499
|
expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
|
|
@@ -474,6 +502,7 @@ function buildTx1Plan(options) {
|
|
|
474
502
|
expectedReplacementAnchorScriptHex: null,
|
|
475
503
|
expectedReplacementAnchorValueSats: null,
|
|
476
504
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
505
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
477
506
|
requiredSenderOutpoint: null,
|
|
478
507
|
requiredProvisionalOutpoint: null,
|
|
479
508
|
errorPrefix: "wallet_anchor_tx1",
|
|
@@ -492,10 +521,7 @@ function buildTx1Plan(options) {
|
|
|
492
521
|
return {
|
|
493
522
|
sender: options.operation.sourceSender,
|
|
494
523
|
changeAddress: options.state.funding.address,
|
|
495
|
-
|
|
496
|
-
{ txid: sourceAnchor.txid, vout: sourceAnchor.vout },
|
|
497
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
498
|
-
],
|
|
524
|
+
fixedInputs: [{ txid: sourceAnchor.txid, vout: sourceAnchor.vout }],
|
|
499
525
|
outputs,
|
|
500
526
|
changePosition: 3,
|
|
501
527
|
expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
|
|
@@ -504,6 +530,7 @@ function buildTx1Plan(options) {
|
|
|
504
530
|
expectedReplacementAnchorScriptHex: options.operation.sourceSender.scriptPubKeyHex,
|
|
505
531
|
expectedReplacementAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
506
532
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
533
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
507
534
|
requiredSenderOutpoint: options.operation.sourceAnchorOutpoint,
|
|
508
535
|
requiredProvisionalOutpoint: null,
|
|
509
536
|
errorPrefix: "wallet_anchor_tx1",
|
|
@@ -535,10 +562,7 @@ function buildTx2Plan(options) {
|
|
|
535
562
|
address: options.operation.targetIdentity.address,
|
|
536
563
|
},
|
|
537
564
|
changeAddress: options.state.funding.address,
|
|
538
|
-
|
|
539
|
-
{ txid: provisional.txid, vout: provisional.vout },
|
|
540
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
541
|
-
],
|
|
565
|
+
fixedInputs: [{ txid: provisional.txid, vout: provisional.vout }],
|
|
542
566
|
outputs: [
|
|
543
567
|
{ data: Buffer.from(opReturnData).toString("hex") },
|
|
544
568
|
{ [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
|
|
@@ -550,6 +574,7 @@ function buildTx2Plan(options) {
|
|
|
550
574
|
expectedReplacementAnchorScriptHex: null,
|
|
551
575
|
expectedReplacementAnchorValueSats: null,
|
|
552
576
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
577
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
553
578
|
requiredSenderOutpoint: null,
|
|
554
579
|
requiredProvisionalOutpoint: {
|
|
555
580
|
txid: provisional.txid,
|
|
@@ -558,30 +583,13 @@ function buildTx2Plan(options) {
|
|
|
558
583
|
errorPrefix: "wallet_anchor_tx2",
|
|
559
584
|
};
|
|
560
585
|
}
|
|
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
586
|
function validateTx1Draft(decoded, funded, plan) {
|
|
580
587
|
const inputs = decoded.tx.vin;
|
|
581
588
|
const outputs = decoded.tx.vout;
|
|
582
589
|
if (inputs.length === 0) {
|
|
583
590
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
584
591
|
}
|
|
592
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
585
593
|
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
586
594
|
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
|
|
587
595
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
@@ -590,14 +598,14 @@ function validateTx1Draft(decoded, funded, plan) {
|
|
|
590
598
|
if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
|
|
591
599
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
592
600
|
}
|
|
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
601
|
}
|
|
602
|
+
assertFundingInputsAfterFixedPrefix({
|
|
603
|
+
inputs,
|
|
604
|
+
fixedInputs: plan.fixedInputs,
|
|
605
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
606
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
607
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
608
|
+
});
|
|
601
609
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
602
610
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
603
611
|
}
|
|
@@ -635,15 +643,19 @@ function validateTx2Draft(decoded, funded, plan) {
|
|
|
635
643
|
if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
|
|
636
644
|
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
637
645
|
}
|
|
646
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
|
|
638
647
|
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
639
648
|
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
|
|
640
649
|
|| !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
|
|
641
650
|
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
642
651
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
652
|
+
assertFundingInputsAfterFixedPrefix({
|
|
653
|
+
inputs,
|
|
654
|
+
fixedInputs: plan.fixedInputs,
|
|
655
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
656
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
657
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
658
|
+
});
|
|
647
659
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
648
660
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
649
661
|
}
|
|
@@ -668,31 +680,27 @@ function validateTx2Draft(decoded, funded, plan) {
|
|
|
668
680
|
}
|
|
669
681
|
}
|
|
670
682
|
async function buildTx1(options) {
|
|
671
|
-
return
|
|
683
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
672
684
|
rpc: options.rpc,
|
|
673
685
|
walletName: options.walletName,
|
|
686
|
+
state: options.state,
|
|
674
687
|
plan: options.plan,
|
|
675
688
|
validateFundedDraft: validateTx1Draft,
|
|
676
689
|
finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
|
|
677
690
|
mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
|
|
678
|
-
|
|
679
|
-
addInputs: false,
|
|
680
|
-
},
|
|
691
|
+
reserveCandidates: options.state.proactiveReserveOutpoints,
|
|
681
692
|
});
|
|
682
693
|
}
|
|
683
694
|
async function buildTx2(options) {
|
|
684
|
-
return
|
|
695
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
685
696
|
rpc: options.rpc,
|
|
686
697
|
walletName: options.walletName,
|
|
698
|
+
state: options.state,
|
|
687
699
|
plan: options.plan,
|
|
688
700
|
validateFundedDraft: validateTx2Draft,
|
|
689
701
|
finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
|
|
690
702
|
mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
|
|
691
|
-
|
|
692
|
-
addInputs: false,
|
|
693
|
-
includeUnsafe: true,
|
|
694
|
-
minConf: 0,
|
|
695
|
-
},
|
|
703
|
+
reserveCandidates: options.state.proactiveReserveOutpoints,
|
|
696
704
|
});
|
|
697
705
|
}
|
|
698
706
|
async function relockAnchorOutpoint(rpc, walletName, outpoint) {
|
|
@@ -998,6 +1006,7 @@ async function submitTx2(options) {
|
|
|
998
1006
|
const builtTx2 = await buildTx2({
|
|
999
1007
|
rpc: options.rpc,
|
|
1000
1008
|
walletName: options.walletName,
|
|
1009
|
+
state: nextState,
|
|
1001
1010
|
plan: tx2Plan,
|
|
1002
1011
|
});
|
|
1003
1012
|
const broadcastingTx2 = createBroadcastingTxRecord(builtTx2);
|
|
@@ -1123,6 +1132,7 @@ async function submitTx2(options) {
|
|
|
1123
1132
|
dedicatedIndex: options.operation.targetIdentity.localIndex,
|
|
1124
1133
|
status: finalStatus,
|
|
1125
1134
|
reusedExisting: false,
|
|
1135
|
+
foundingMessageText: options.operation.foundingMessageText,
|
|
1126
1136
|
};
|
|
1127
1137
|
}
|
|
1128
1138
|
function ensureSameTipHeight(context, bestHeight, errorCode) {
|
|
@@ -1147,7 +1157,11 @@ export async function anchorDomain(options) {
|
|
|
1147
1157
|
paths,
|
|
1148
1158
|
reason: "wallet-anchor",
|
|
1149
1159
|
});
|
|
1150
|
-
const message = await
|
|
1160
|
+
const message = await resolveFoundingMessage({
|
|
1161
|
+
foundingMessageText: options.foundingMessageText,
|
|
1162
|
+
promptForFoundingMessageWhenMissing: options.promptForFoundingMessageWhenMissing,
|
|
1163
|
+
prompter: options.prompter,
|
|
1164
|
+
});
|
|
1151
1165
|
const readContext = await (options.openReadContext ?? openWalletReadContext)({
|
|
1152
1166
|
dataDir: options.dataDir,
|
|
1153
1167
|
databasePath: options.databasePath,
|
|
@@ -1160,6 +1174,9 @@ export async function anchorDomain(options) {
|
|
|
1160
1174
|
const initialFamily = createDraftAnchorFamily(operation, nowUnixMs);
|
|
1161
1175
|
const existingFamily = findAnchorFamilyByIntent(operation.state, initialFamily.intentFingerprintHex);
|
|
1162
1176
|
const conflictingFamily = findActiveAnchorFamilyByDomain(operation.state, normalizedDomainName);
|
|
1177
|
+
if (existingFamily === null && isClearableReservedAnchorFamily(conflictingFamily)) {
|
|
1178
|
+
throw new Error(`wallet_anchor_clear_pending_first_${conflictingFamily.domainName}`);
|
|
1179
|
+
}
|
|
1163
1180
|
if (existingFamily === null && conflictingFamily !== null) {
|
|
1164
1181
|
throw new Error("wallet_anchor_prior_family_unresolved");
|
|
1165
1182
|
}
|
|
@@ -1201,6 +1218,7 @@ export async function anchorDomain(options) {
|
|
|
1201
1218
|
dedicatedIndex: reconciled.family.reservedDedicatedIndex ?? existingTargetIdentity.localIndex,
|
|
1202
1219
|
status: reconciled.resolution,
|
|
1203
1220
|
reusedExisting: true,
|
|
1221
|
+
foundingMessageText: reconciled.family.foundingMessageText,
|
|
1204
1222
|
};
|
|
1205
1223
|
}
|
|
1206
1224
|
if (reconciled.resolution === "repair-required") {
|
|
@@ -1232,6 +1250,7 @@ export async function anchorDomain(options) {
|
|
|
1232
1250
|
const builtTx1 = await buildTx1({
|
|
1233
1251
|
rpc,
|
|
1234
1252
|
walletName,
|
|
1253
|
+
state: nextState,
|
|
1235
1254
|
plan: tx1Plan,
|
|
1236
1255
|
});
|
|
1237
1256
|
const broadcastingTx1 = createBroadcastingTxRecord(builtTx1);
|
|
@@ -1351,6 +1370,7 @@ export async function anchorDomain(options) {
|
|
|
1351
1370
|
return {
|
|
1352
1371
|
...result,
|
|
1353
1372
|
reusedExisting: resumedExisting,
|
|
1373
|
+
foundingMessageText: result.foundingMessageText ?? operation.foundingMessageText,
|
|
1354
1374
|
};
|
|
1355
1375
|
}
|
|
1356
1376
|
finally {
|
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,
|
|
10
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, 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";
|
|
@@ -248,28 +248,17 @@ function buildPlanForCogOperation(options) {
|
|
|
248
248
|
&& entry.safe !== false);
|
|
249
249
|
const outputs = [{ data: Buffer.from(options.opReturnData).toString("hex") }];
|
|
250
250
|
if (options.anchorOutpoint === null) {
|
|
251
|
-
const senderUtxo = options.allUtxos.find((entry) => entry.scriptPubKey === options.sender.scriptPubKeyHex
|
|
252
|
-
&& entry.confirmations >= 1
|
|
253
|
-
&& entry.spendable !== false
|
|
254
|
-
&& entry.safe !== false);
|
|
255
|
-
if (senderUtxo === undefined) {
|
|
256
|
-
throw new Error(`${options.errorPrefix}_sender_utxo_unavailable`);
|
|
257
|
-
}
|
|
258
251
|
return {
|
|
259
252
|
sender: options.sender,
|
|
260
253
|
changeAddress: options.state.funding.address,
|
|
261
|
-
|
|
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
|
-
],
|
|
254
|
+
fixedInputs: [],
|
|
267
255
|
outputs,
|
|
268
256
|
changePosition: 1,
|
|
269
257
|
expectedOpReturnScriptHex: encodeOpReturnScript(options.opReturnData),
|
|
270
258
|
expectedAnchorScriptHex: null,
|
|
271
259
|
expectedAnchorValueSats: null,
|
|
272
260
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
261
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
273
262
|
errorPrefix: options.errorPrefix,
|
|
274
263
|
};
|
|
275
264
|
}
|
|
@@ -288,9 +277,8 @@ function buildPlanForCogOperation(options) {
|
|
|
288
277
|
return {
|
|
289
278
|
sender: options.sender,
|
|
290
279
|
changeAddress: options.state.funding.address,
|
|
291
|
-
|
|
280
|
+
fixedInputs: [
|
|
292
281
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
293
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
294
282
|
],
|
|
295
283
|
outputs,
|
|
296
284
|
changePosition: 2,
|
|
@@ -298,6 +286,7 @@ function buildPlanForCogOperation(options) {
|
|
|
298
286
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
299
287
|
expectedAnchorValueSats: options.anchorValueSats,
|
|
300
288
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
289
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
301
290
|
errorPrefix: options.errorPrefix,
|
|
302
291
|
};
|
|
303
292
|
}
|
|
@@ -307,14 +296,17 @@ function validateFundedDraft(decoded, funded, plan) {
|
|
|
307
296
|
if (inputs.length === 0) {
|
|
308
297
|
throw new Error(`${plan.errorPrefix}_missing_sender_input`);
|
|
309
298
|
}
|
|
299
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
310
300
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
311
301
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
312
302
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
303
|
+
assertFundingInputsAfterFixedPrefix({
|
|
304
|
+
inputs,
|
|
305
|
+
fixedInputs: plan.fixedInputs,
|
|
306
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
307
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
308
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
309
|
+
});
|
|
318
310
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
319
311
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
320
312
|
}
|
|
@@ -341,13 +333,15 @@ function validateFundedDraft(decoded, funded, plan) {
|
|
|
341
333
|
}
|
|
342
334
|
}
|
|
343
335
|
async function buildTransaction(options) {
|
|
344
|
-
return
|
|
336
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
345
337
|
rpc: options.rpc,
|
|
346
338
|
walletName: options.walletName,
|
|
339
|
+
state: options.state,
|
|
347
340
|
plan: options.plan,
|
|
348
341
|
validateFundedDraft,
|
|
349
342
|
finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
|
|
350
343
|
mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
|
|
344
|
+
reserveCandidates: options.state.proactiveReserveOutpoints,
|
|
351
345
|
});
|
|
352
346
|
}
|
|
353
347
|
function createDraftMutation(options) {
|
|
@@ -699,6 +693,7 @@ export async function sendCog(options) {
|
|
|
699
693
|
const built = await buildTransaction({
|
|
700
694
|
rpc,
|
|
701
695
|
walletName,
|
|
696
|
+
state: nextState,
|
|
702
697
|
plan: buildPlanForCogOperation({
|
|
703
698
|
state: nextState,
|
|
704
699
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -860,6 +855,7 @@ export async function lockCogToDomain(options) {
|
|
|
860
855
|
const built = await buildTransaction({
|
|
861
856
|
rpc,
|
|
862
857
|
walletName,
|
|
858
|
+
state: nextState,
|
|
863
859
|
plan: buildPlanForCogOperation({
|
|
864
860
|
state: nextState,
|
|
865
861
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1004,6 +1000,7 @@ async function runClaimLikeMutation(options, reclaim) {
|
|
|
1004
1000
|
const built = await buildTransaction({
|
|
1005
1001
|
rpc,
|
|
1006
1002
|
walletName,
|
|
1003
|
+
state: nextState,
|
|
1007
1004
|
plan: buildPlanForCogOperation({
|
|
1008
1005
|
state: nextState,
|
|
1009
1006
|
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,23 +89,36 @@ 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;
|
|
98
|
+
allowedFundingScriptPubKeyHex: string;
|
|
99
|
+
eligibleFundingOutpointKeys: Set<string>;
|
|
78
100
|
};
|
|
79
101
|
validateFundedDraft(decoded: RpcDecodedPsbt, funded: RpcWalletCreateFundedPsbtResult, plan: TPlan): void;
|
|
80
102
|
finalizeErrorCode: string;
|
|
81
103
|
mempoolRejectPrefix: string;
|
|
82
104
|
feeRate?: number;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
temporarilyUnlockedPolicyOutpoints?: readonly OutpointRecord[];
|
|
106
|
+
}): Promise<BuiltWalletMutationTransaction>;
|
|
107
|
+
export declare function buildWalletMutationTransactionWithReserveFallback<TPlan>(options: {
|
|
108
|
+
rpc: WalletMutationRpcClient;
|
|
109
|
+
walletName: string;
|
|
110
|
+
state: WalletStateV1;
|
|
111
|
+
plan: TPlan & {
|
|
112
|
+
fixedInputs: FixedWalletInput[];
|
|
113
|
+
outputs: unknown[];
|
|
114
|
+
changeAddress: string;
|
|
115
|
+
changePosition: number;
|
|
116
|
+
allowedFundingScriptPubKeyHex: string;
|
|
117
|
+
eligibleFundingOutpointKeys: Set<string>;
|
|
88
118
|
};
|
|
119
|
+
validateFundedDraft(decoded: RpcDecodedPsbt, funded: RpcWalletCreateFundedPsbtResult, plan: TPlan): void;
|
|
120
|
+
finalizeErrorCode: string;
|
|
121
|
+
mempoolRejectPrefix: string;
|
|
122
|
+
feeRate?: number;
|
|
123
|
+
reserveCandidates: readonly OutpointRecord[];
|
|
89
124
|
}): Promise<BuiltWalletMutationTransaction>;
|