@cogcoin/client 0.5.12 → 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 +1 -1
- package/dist/bitcoind/client/sync-engine.js +7 -2
- 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
|
@@ -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
|
+
}
|
|
@@ -9,7 +9,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
|
9
9
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
10
10
|
import { serializeSetCanonical, serializeSetDelegate, serializeSetEndpoint, serializeSetMiner, validateDomainName, } from "../cogop/index.js";
|
|
11
11
|
import { openWalletReadContext } from "../read/index.js";
|
|
12
|
-
import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
12
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
13
13
|
import { confirmYesNo } from "./confirm.js";
|
|
14
14
|
import { getCanonicalIdentitySelector } from "./identity-selector.js";
|
|
15
15
|
import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
|
|
@@ -133,9 +133,8 @@ function buildPlanForDomainAdminOperation(options) {
|
|
|
133
133
|
return {
|
|
134
134
|
sender: options.sender,
|
|
135
135
|
changeAddress: options.state.funding.address,
|
|
136
|
-
|
|
136
|
+
fixedInputs: [
|
|
137
137
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
138
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
139
138
|
],
|
|
140
139
|
outputs: [
|
|
141
140
|
{ data: Buffer.from(options.opReturnData).toString("hex") },
|
|
@@ -146,6 +145,7 @@ function buildPlanForDomainAdminOperation(options) {
|
|
|
146
145
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
147
146
|
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
148
147
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
148
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
149
149
|
errorPrefix: options.errorPrefix,
|
|
150
150
|
};
|
|
151
151
|
}
|
|
@@ -155,14 +155,17 @@ function validateFundedDraft(decoded, funded, plan) {
|
|
|
155
155
|
if (inputs.length === 0) {
|
|
156
156
|
throw new Error(`${plan.errorPrefix}_missing_sender_input`);
|
|
157
157
|
}
|
|
158
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
158
159
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
159
160
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
160
161
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
assertFundingInputsAfterFixedPrefix({
|
|
163
|
+
inputs,
|
|
164
|
+
fixedInputs: plan.fixedInputs,
|
|
165
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
166
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
167
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
168
|
+
});
|
|
166
169
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
167
170
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
168
171
|
}
|
|
@@ -189,6 +192,7 @@ async function buildTransaction(options) {
|
|
|
189
192
|
return buildWalletMutationTransaction({
|
|
190
193
|
rpc: options.rpc,
|
|
191
194
|
walletName: options.walletName,
|
|
195
|
+
state: options.state,
|
|
192
196
|
plan: options.plan,
|
|
193
197
|
validateFundedDraft,
|
|
194
198
|
finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
|
|
@@ -632,6 +636,7 @@ async function submitDomainAdminMutation(options) {
|
|
|
632
636
|
const built = await buildTransaction({
|
|
633
637
|
rpc,
|
|
634
638
|
walletName,
|
|
639
|
+
state: nextState,
|
|
635
640
|
plan: buildPlanForDomainAdminOperation({
|
|
636
641
|
state: nextState,
|
|
637
642
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -7,7 +7,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
|
7
7
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
8
8
|
import { serializeDomainBuy, serializeDomainSell, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
|
|
9
9
|
import { openWalletReadContext } from "../read/index.js";
|
|
10
|
-
import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
10
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, 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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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`,
|
|
@@ -768,6 +772,7 @@ export async function transferDomain(options) {
|
|
|
768
772
|
const built = await buildTransaction({
|
|
769
773
|
rpc,
|
|
770
774
|
walletName,
|
|
775
|
+
state: nextState,
|
|
771
776
|
plan: buildPlanForDomainOperation({
|
|
772
777
|
state: nextState,
|
|
773
778
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -998,6 +1003,7 @@ async function runSellMutation(options) {
|
|
|
998
1003
|
const built = await buildTransaction({
|
|
999
1004
|
rpc,
|
|
1000
1005
|
walletName,
|
|
1006
|
+
state: nextState,
|
|
1001
1007
|
plan: buildPlanForDomainOperation({
|
|
1002
1008
|
state: nextState,
|
|
1003
1009
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1233,6 +1239,7 @@ export async function buyDomain(options) {
|
|
|
1233
1239
|
const built = await buildTransaction({
|
|
1234
1240
|
rpc,
|
|
1235
1241
|
walletName,
|
|
1242
|
+
state: nextState,
|
|
1236
1243
|
plan: buildPlanForDomainOperation({
|
|
1237
1244
|
state: nextState,
|
|
1238
1245
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
package/dist/wallet/tx/field.js
CHANGED
|
@@ -5,12 +5,13 @@ import { getBalance, lookupDomain, } from "@cogcoin/indexer/queries";
|
|
|
5
5
|
import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
6
6
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
7
7
|
import { acquireFileLock } from "../fs/lock.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 { FIELD_FORMAT_BYTES, serializeDataUpdate, serializeFieldReg, } from "../cogop/index.js";
|
|
11
12
|
import { validateFieldName } from "../cogop/validate-name.js";
|
|
12
13
|
import { findDomainField, openWalletReadContext, } from "../read/index.js";
|
|
13
|
-
import { assertWalletMutationContextReady,
|
|
14
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
14
15
|
import { confirmTypedAcknowledgement as confirmSharedTypedAcknowledgement, confirmYesNo as confirmSharedYesNo, } from "./confirm.js";
|
|
15
16
|
import { getCanonicalIdentitySelector } from "./identity-selector.js";
|
|
16
17
|
import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
|
|
@@ -253,9 +254,8 @@ function buildAnchoredFieldPlan(options) {
|
|
|
253
254
|
return {
|
|
254
255
|
sender: options.sender,
|
|
255
256
|
changeAddress: options.state.funding.address,
|
|
256
|
-
|
|
257
|
+
fixedInputs: [
|
|
257
258
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
258
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
259
259
|
],
|
|
260
260
|
outputs: [
|
|
261
261
|
{ data: Buffer.from(options.opReturnData).toString("hex") },
|
|
@@ -266,6 +266,7 @@ function buildAnchoredFieldPlan(options) {
|
|
|
266
266
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
267
267
|
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
268
268
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
269
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
269
270
|
errorPrefix: options.errorPrefix,
|
|
270
271
|
};
|
|
271
272
|
}
|
|
@@ -277,10 +278,7 @@ function buildFieldFamilyTx2Plan(options) {
|
|
|
277
278
|
return {
|
|
278
279
|
sender: options.sender,
|
|
279
280
|
changeAddress: options.state.funding.address,
|
|
280
|
-
|
|
281
|
-
{ txid: options.tx1Txid, vout: 1 },
|
|
282
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
283
|
-
],
|
|
281
|
+
fixedInputs: [{ txid: options.tx1Txid, vout: 1 }],
|
|
284
282
|
outputs: [
|
|
285
283
|
{ data: Buffer.from(options.opReturnData).toString("hex") },
|
|
286
284
|
{ [options.sender.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
|
|
@@ -290,6 +288,7 @@ function buildFieldFamilyTx2Plan(options) {
|
|
|
290
288
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
291
289
|
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
292
290
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
291
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
293
292
|
errorPrefix: "wallet_field_create_tx2",
|
|
294
293
|
};
|
|
295
294
|
}
|
|
@@ -299,14 +298,17 @@ function validateFieldDraft(decoded, funded, plan) {
|
|
|
299
298
|
if (inputs.length === 0) {
|
|
300
299
|
throw new Error(`${plan.errorPrefix}_missing_sender_input`);
|
|
301
300
|
}
|
|
301
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
302
302
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
303
303
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
304
304
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
assertFundingInputsAfterFixedPrefix({
|
|
306
|
+
inputs,
|
|
307
|
+
fixedInputs: plan.fixedInputs,
|
|
308
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
309
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
310
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
311
|
+
});
|
|
310
312
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
311
313
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
312
314
|
}
|
|
@@ -330,14 +332,16 @@ function validateFieldDraft(decoded, funded, plan) {
|
|
|
330
332
|
}
|
|
331
333
|
}
|
|
332
334
|
async function buildFieldTransaction(options) {
|
|
333
|
-
|
|
335
|
+
const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
|
|
336
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
334
337
|
rpc: options.rpc,
|
|
335
338
|
walletName: options.walletName,
|
|
339
|
+
state: options.state,
|
|
336
340
|
plan: options.plan,
|
|
337
341
|
validateFundedDraft: validateFieldDraft,
|
|
338
342
|
finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
|
|
339
343
|
mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
|
|
340
|
-
|
|
344
|
+
reserveCandidates,
|
|
341
345
|
});
|
|
342
346
|
}
|
|
343
347
|
async function saveUpdatedState(options) {
|
|
@@ -1348,6 +1352,7 @@ async function submitStandaloneFieldMutation(options) {
|
|
|
1348
1352
|
const built = await buildFieldTransaction({
|
|
1349
1353
|
rpc,
|
|
1350
1354
|
walletName,
|
|
1355
|
+
state: nextState,
|
|
1351
1356
|
plan: buildAnchoredFieldPlan({
|
|
1352
1357
|
state: nextState,
|
|
1353
1358
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1544,6 +1549,7 @@ async function submitFieldCreateFamily(options) {
|
|
|
1544
1549
|
const tx1 = await buildFieldTransaction({
|
|
1545
1550
|
rpc,
|
|
1546
1551
|
walletName,
|
|
1552
|
+
state: nextState,
|
|
1547
1553
|
plan: buildAnchoredFieldPlan({
|
|
1548
1554
|
state: nextState,
|
|
1549
1555
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1576,6 +1582,7 @@ async function submitFieldCreateFamily(options) {
|
|
|
1576
1582
|
const tx2 = await buildFieldTransaction({
|
|
1577
1583
|
rpc,
|
|
1578
1584
|
walletName,
|
|
1585
|
+
state: workingState,
|
|
1579
1586
|
plan: buildFieldFamilyTx2Plan({
|
|
1580
1587
|
state: workingState,
|
|
1581
1588
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1583,10 +1590,6 @@ async function submitFieldCreateFamily(options) {
|
|
|
1583
1590
|
tx1Txid,
|
|
1584
1591
|
opReturnData: serializeDataUpdate(operation.chainDomain.domainId, resumedFamily.expectedFieldId ?? operation.chainDomain.nextFieldId, options.value.format, options.value.value).opReturnData,
|
|
1585
1592
|
}),
|
|
1586
|
-
builderOptions: {
|
|
1587
|
-
includeUnsafe: true,
|
|
1588
|
-
minConf: 0,
|
|
1589
|
-
},
|
|
1590
1593
|
});
|
|
1591
1594
|
const final = await sendFamilyTx2({
|
|
1592
1595
|
rpc,
|
|
@@ -8,7 +8,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
|
8
8
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
9
9
|
import { computeRootRegistrationPriceSats, serializeDomainReg } from "../cogop/index.js";
|
|
10
10
|
import { openWalletReadContext } from "../read/index.js";
|
|
11
|
-
import { assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
11
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
12
12
|
import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
|
|
13
13
|
import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
|
|
14
14
|
import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
|
|
@@ -401,14 +401,17 @@ function validateFundedDraft(decoded, funded, plan) {
|
|
|
401
401
|
if (inputs.length === 0) {
|
|
402
402
|
throw new Error("wallet_register_missing_sender_input");
|
|
403
403
|
}
|
|
404
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_register_sender_input_mismatch");
|
|
404
405
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
405
406
|
throw new Error("wallet_register_sender_input_mismatch");
|
|
406
407
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
408
|
+
assertFundingInputsAfterFixedPrefix({
|
|
409
|
+
inputs,
|
|
410
|
+
fixedInputs: plan.fixedInputs,
|
|
411
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
412
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
413
|
+
errorCode: "wallet_register_unexpected_funding_input",
|
|
414
|
+
});
|
|
412
415
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
413
416
|
throw new Error("wallet_register_opreturn_mismatch");
|
|
414
417
|
}
|
|
@@ -467,9 +470,8 @@ function buildRegisterPlan(options) {
|
|
|
467
470
|
registerKind: "root",
|
|
468
471
|
sender: options.sender,
|
|
469
472
|
changeAddress: options.state.funding.address,
|
|
470
|
-
|
|
473
|
+
fixedInputs: [
|
|
471
474
|
{ txid: senderInput.txid, vout: senderInput.vout },
|
|
472
|
-
...additionalFunding,
|
|
473
475
|
],
|
|
474
476
|
outputs: rootOutputs.outputs,
|
|
475
477
|
changePosition: rootOutputs.changePosition,
|
|
@@ -481,6 +483,7 @@ function buildRegisterPlan(options) {
|
|
|
481
483
|
expectedAnchorScriptHex: null,
|
|
482
484
|
expectedAnchorValueSats: null,
|
|
483
485
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
486
|
+
eligibleFundingOutpointKeys: new Set(additionalFunding.map((entry) => outpointKey(entry))),
|
|
484
487
|
};
|
|
485
488
|
}
|
|
486
489
|
const anchorUtxo = options.allUtxos.find((entry) => entry.txid === options.anchorOutpoint?.txid
|
|
@@ -494,9 +497,8 @@ function buildRegisterPlan(options) {
|
|
|
494
497
|
registerKind: "root",
|
|
495
498
|
sender: options.sender,
|
|
496
499
|
changeAddress: options.state.funding.address,
|
|
497
|
-
|
|
500
|
+
fixedInputs: [
|
|
498
501
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
499
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
500
502
|
],
|
|
501
503
|
outputs: rootOutputs.outputs,
|
|
502
504
|
changePosition: rootOutputs.changePosition,
|
|
@@ -508,6 +510,7 @@ function buildRegisterPlan(options) {
|
|
|
508
510
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
509
511
|
expectedAnchorValueSats: options.anchorValueSats,
|
|
510
512
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
513
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey(entry))),
|
|
511
514
|
};
|
|
512
515
|
}
|
|
513
516
|
const anchor = options.anchorOutpoint;
|
|
@@ -530,9 +533,8 @@ function buildRegisterPlan(options) {
|
|
|
530
533
|
registerKind: "subdomain",
|
|
531
534
|
sender: options.sender,
|
|
532
535
|
changeAddress: options.state.funding.address,
|
|
533
|
-
|
|
536
|
+
fixedInputs: [
|
|
534
537
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
535
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
536
538
|
],
|
|
537
539
|
outputs: subdomainOutputs.outputs,
|
|
538
540
|
changePosition: subdomainOutputs.changePosition,
|
|
@@ -544,12 +546,14 @@ function buildRegisterPlan(options) {
|
|
|
544
546
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
545
547
|
expectedAnchorValueSats: options.anchorValueSats,
|
|
546
548
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
549
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey(entry))),
|
|
547
550
|
};
|
|
548
551
|
}
|
|
549
552
|
async function buildRegisterTransaction(options) {
|
|
550
553
|
return buildWalletMutationTransaction({
|
|
551
554
|
rpc: options.rpc,
|
|
552
555
|
walletName: options.walletName,
|
|
556
|
+
state: options.state,
|
|
553
557
|
plan: options.plan,
|
|
554
558
|
validateFundedDraft,
|
|
555
559
|
finalizeErrorCode: "wallet_register_finalize_failed",
|
|
@@ -810,6 +814,7 @@ export async function registerDomain(options) {
|
|
|
810
814
|
const built = await buildRegisterTransaction({
|
|
811
815
|
rpc,
|
|
812
816
|
walletName,
|
|
817
|
+
state: nextState,
|
|
813
818
|
plan,
|
|
814
819
|
});
|
|
815
820
|
const currentMutation = nextState.pendingMutations?.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex)
|