@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
package/dist/wallet/tx/common.js
CHANGED
|
@@ -2,8 +2,17 @@ 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;
|
|
8
|
+
function btcNumberToSats(value) {
|
|
9
|
+
return BigInt(Math.round(value * 100_000_000));
|
|
10
|
+
}
|
|
11
|
+
function valueToSats(value) {
|
|
12
|
+
return typeof value === "string"
|
|
13
|
+
? BigInt(Math.round(Number(value) * 100_000_000))
|
|
14
|
+
: btcNumberToSats(value);
|
|
15
|
+
}
|
|
7
16
|
function createUnlockSessionState(state, unlockUntilUnixMs, nowUnixMs) {
|
|
8
17
|
return {
|
|
9
18
|
schemaVersion: 1,
|
|
@@ -39,6 +48,12 @@ export function formatCogAmount(value) {
|
|
|
39
48
|
export function outpointKey(outpoint) {
|
|
40
49
|
return `${outpoint.txid}:${outpoint.vout}`;
|
|
41
50
|
}
|
|
51
|
+
function isSpendableConfirmedFundingUtxo(entry, fundingScriptPubKeyHex) {
|
|
52
|
+
return entry.scriptPubKey === fundingScriptPubKeyHex
|
|
53
|
+
&& entry.confirmations >= 1
|
|
54
|
+
&& entry.spendable !== false
|
|
55
|
+
&& entry.safe !== false;
|
|
56
|
+
}
|
|
42
57
|
export function updateMutationRecord(mutation, status, nowUnixMs, options = {}) {
|
|
43
58
|
return {
|
|
44
59
|
...mutation,
|
|
@@ -64,6 +79,53 @@ export function diffTemporaryLockedOutpoints(before, after) {
|
|
|
64
79
|
vout: entry.vout,
|
|
65
80
|
}));
|
|
66
81
|
}
|
|
82
|
+
export function getDecodedInputScriptPubKeyHex(input) {
|
|
83
|
+
return input.prevout?.scriptPubKey?.hex ?? null;
|
|
84
|
+
}
|
|
85
|
+
export function getDecodedInputVout(input) {
|
|
86
|
+
const vout = input.vout;
|
|
87
|
+
return typeof vout === "number" ? vout : null;
|
|
88
|
+
}
|
|
89
|
+
export function inputMatchesOutpoint(input, outpoint) {
|
|
90
|
+
return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
|
|
91
|
+
}
|
|
92
|
+
export function assertFixedInputPrefixMatches(inputs, fixedInputs, errorCode) {
|
|
93
|
+
if (inputs.length < fixedInputs.length) {
|
|
94
|
+
throw new Error(errorCode);
|
|
95
|
+
}
|
|
96
|
+
for (const [index, fixedInput] of fixedInputs.entries()) {
|
|
97
|
+
if (!inputMatchesOutpoint(inputs[index], fixedInput)) {
|
|
98
|
+
throw new Error(errorCode);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function assertFundingInputsAfterFixedPrefix(options) {
|
|
103
|
+
for (let index = options.fixedInputs.length; index < options.inputs.length; index += 1) {
|
|
104
|
+
const input = options.inputs[index];
|
|
105
|
+
const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
|
|
106
|
+
const vout = getDecodedInputVout(input);
|
|
107
|
+
if (scriptPubKeyHex !== options.allowedFundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
|
|
108
|
+
throw new Error(options.errorCode);
|
|
109
|
+
}
|
|
110
|
+
const key = outpointKey({
|
|
111
|
+
txid: input.txid,
|
|
112
|
+
vout,
|
|
113
|
+
});
|
|
114
|
+
if (!options.eligibleFundingOutpointKeys.has(key)) {
|
|
115
|
+
throw new Error(options.errorCode);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function reconcilePersistentPolicyLocks(options) {
|
|
120
|
+
await reconcileWalletCoinControlLocks({
|
|
121
|
+
rpc: options.rpc,
|
|
122
|
+
walletName: options.walletName,
|
|
123
|
+
state: options.state,
|
|
124
|
+
fixedInputs: options.fixedInputs,
|
|
125
|
+
temporarilyUnlockedOutpoints: options.temporarilyUnlockedOutpoints,
|
|
126
|
+
cleanupInactiveTemporaryBuilderLocks: options.cleanupInactiveTemporaryBuilderLocks,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
67
129
|
export function isBroadcastUnknownError(error) {
|
|
68
130
|
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
69
131
|
return message.includes("timeout")
|
|
@@ -80,6 +142,38 @@ export function isAlreadyAcceptedError(error) {
|
|
|
80
142
|
|| message.includes("already in blockchain")
|
|
81
143
|
|| message.includes("txn-already-known");
|
|
82
144
|
}
|
|
145
|
+
export function isInsufficientFundsError(error) {
|
|
146
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
147
|
+
return message.includes("insufficient funds");
|
|
148
|
+
}
|
|
149
|
+
function isReserveFloorFundingError(error) {
|
|
150
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
151
|
+
return message.includes("insufficient_funding_after_reserve");
|
|
152
|
+
}
|
|
153
|
+
function computeRemainingFundingValueSats(options) {
|
|
154
|
+
let remaining = 0n;
|
|
155
|
+
for (const value of options.availableFundingValueByKey.values()) {
|
|
156
|
+
remaining += value;
|
|
157
|
+
}
|
|
158
|
+
for (const input of options.transaction.vin) {
|
|
159
|
+
const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
|
|
160
|
+
const vout = getDecodedInputVout(input);
|
|
161
|
+
if (scriptPubKeyHex !== options.fundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
remaining -= options.availableFundingValueByKey.get(outpointKey({
|
|
165
|
+
txid: input.txid,
|
|
166
|
+
vout,
|
|
167
|
+
})) ?? 0n;
|
|
168
|
+
}
|
|
169
|
+
for (const output of options.transaction.vout) {
|
|
170
|
+
if (output.scriptPubKey?.hex !== options.fundingScriptPubKeyHex) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
remaining += valueToSats(output.value);
|
|
174
|
+
}
|
|
175
|
+
return remaining;
|
|
176
|
+
}
|
|
83
177
|
export function assertWalletMutationContextReady(context, errorPrefix) {
|
|
84
178
|
if (context.localState.availability === "uninitialized") {
|
|
85
179
|
throw new Error("wallet_uninitialized");
|
|
@@ -110,16 +204,36 @@ export async function pauseMiningForWalletMutation(options) {
|
|
|
110
204
|
});
|
|
111
205
|
}
|
|
112
206
|
export async function buildWalletMutationTransaction(options) {
|
|
207
|
+
await reconcilePersistentPolicyLocks({
|
|
208
|
+
rpc: options.rpc,
|
|
209
|
+
walletName: options.walletName,
|
|
210
|
+
state: options.state,
|
|
211
|
+
fixedInputs: options.plan.fixedInputs,
|
|
212
|
+
temporarilyUnlockedOutpoints: options.temporarilyUnlockedPolicyOutpoints,
|
|
213
|
+
});
|
|
214
|
+
const availableFundingUtxos = (await options.rpc.listUnspent(options.walletName, 1))
|
|
215
|
+
.filter((entry) => isSpendableConfirmedFundingUtxo(entry, options.plan.allowedFundingScriptPubKeyHex));
|
|
216
|
+
const availableFundingValueByKey = new Map(availableFundingUtxos.map((entry) => [
|
|
217
|
+
outpointKey({ txid: entry.txid, vout: entry.vout }),
|
|
218
|
+
btcNumberToSats(entry.amount),
|
|
219
|
+
]));
|
|
220
|
+
const validationPlan = {
|
|
221
|
+
...options.plan,
|
|
222
|
+
eligibleFundingOutpointKeys: new Set([
|
|
223
|
+
...options.plan.eligibleFundingOutpointKeys,
|
|
224
|
+
...availableFundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout })),
|
|
225
|
+
]),
|
|
226
|
+
};
|
|
113
227
|
const lockedBefore = await options.rpc.listLockUnspent(options.walletName);
|
|
114
228
|
let temporaryBuilderLockedOutpoints = [];
|
|
115
229
|
try {
|
|
116
|
-
const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.
|
|
117
|
-
add_inputs:
|
|
118
|
-
include_unsafe:
|
|
119
|
-
minconf:
|
|
230
|
+
const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.fixedInputs, options.plan.outputs, 0, {
|
|
231
|
+
add_inputs: true,
|
|
232
|
+
include_unsafe: false,
|
|
233
|
+
minconf: 1,
|
|
120
234
|
changeAddress: options.plan.changeAddress,
|
|
121
235
|
changePosition: options.plan.changePosition,
|
|
122
|
-
lockUnspents:
|
|
236
|
+
lockUnspents: true,
|
|
123
237
|
fee_rate: options.feeRate ?? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB,
|
|
124
238
|
replaceable: true,
|
|
125
239
|
subtractFeeFromOutputs: [],
|
|
@@ -127,7 +241,17 @@ export async function buildWalletMutationTransaction(options) {
|
|
|
127
241
|
const lockedAfter = await options.rpc.listLockUnspent(options.walletName);
|
|
128
242
|
temporaryBuilderLockedOutpoints = diffTemporaryLockedOutpoints(lockedBefore, lockedAfter);
|
|
129
243
|
const decoded = await options.rpc.decodePsbt(funded.psbt);
|
|
130
|
-
options.validateFundedDraft(decoded, funded,
|
|
244
|
+
options.validateFundedDraft(decoded, funded, validationPlan);
|
|
245
|
+
if (options.state.proactiveReserveSats > 0) {
|
|
246
|
+
const remainingFundingValueSats = computeRemainingFundingValueSats({
|
|
247
|
+
transaction: decoded.tx,
|
|
248
|
+
fundingScriptPubKeyHex: options.plan.allowedFundingScriptPubKeyHex,
|
|
249
|
+
availableFundingValueByKey,
|
|
250
|
+
});
|
|
251
|
+
if (remainingFundingValueSats < BigInt(options.state.proactiveReserveSats)) {
|
|
252
|
+
throw new Error("wallet_mutation_insufficient_funding_after_reserve");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
131
255
|
const signed = await options.rpc.walletProcessPsbt(options.walletName, funded.psbt, true, "DEFAULT");
|
|
132
256
|
const finalized = await options.rpc.finalizePsbt(signed.psbt, true);
|
|
133
257
|
if (!finalized.complete || finalized.hex == null) {
|
|
@@ -139,6 +263,14 @@ export async function buildWalletMutationTransaction(options) {
|
|
|
139
263
|
if (accepted == null || !accepted.allowed) {
|
|
140
264
|
throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
|
|
141
265
|
}
|
|
266
|
+
if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
|
|
267
|
+
await reconcilePersistentPolicyLocks({
|
|
268
|
+
rpc: options.rpc,
|
|
269
|
+
walletName: options.walletName,
|
|
270
|
+
state: options.state,
|
|
271
|
+
fixedInputs: options.plan.fixedInputs,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
142
274
|
return {
|
|
143
275
|
funded,
|
|
144
276
|
decoded,
|
|
@@ -151,6 +283,46 @@ export async function buildWalletMutationTransaction(options) {
|
|
|
151
283
|
}
|
|
152
284
|
catch (error) {
|
|
153
285
|
await unlockTemporaryBuilderLocks(options.rpc, options.walletName, temporaryBuilderLockedOutpoints);
|
|
286
|
+
if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
|
|
287
|
+
await reconcilePersistentPolicyLocks({
|
|
288
|
+
rpc: options.rpc,
|
|
289
|
+
walletName: options.walletName,
|
|
290
|
+
state: options.state,
|
|
291
|
+
fixedInputs: options.plan.fixedInputs,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
154
294
|
throw error;
|
|
155
295
|
}
|
|
156
296
|
}
|
|
297
|
+
export async function buildWalletMutationTransactionWithReserveFallback(options) {
|
|
298
|
+
let unlockedReserveOutpoints = [];
|
|
299
|
+
let lastError = null;
|
|
300
|
+
for (let attempt = 0; attempt <= options.reserveCandidates.length; attempt += 1) {
|
|
301
|
+
if (attempt > 0) {
|
|
302
|
+
unlockedReserveOutpoints = [
|
|
303
|
+
...unlockedReserveOutpoints,
|
|
304
|
+
options.reserveCandidates[attempt - 1],
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
return await buildWalletMutationTransaction({
|
|
309
|
+
rpc: options.rpc,
|
|
310
|
+
walletName: options.walletName,
|
|
311
|
+
state: options.state,
|
|
312
|
+
plan: options.plan,
|
|
313
|
+
validateFundedDraft: options.validateFundedDraft,
|
|
314
|
+
finalizeErrorCode: options.finalizeErrorCode,
|
|
315
|
+
mempoolRejectPrefix: options.mempoolRejectPrefix,
|
|
316
|
+
feeRate: options.feeRate,
|
|
317
|
+
temporarilyUnlockedPolicyOutpoints: unlockedReserveOutpoints,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
lastError = error;
|
|
322
|
+
if ((!isInsufficientFundsError(error) && !isReserveFloorFundingError(error)) || attempt === options.reserveCandidates.length) {
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
throw lastError;
|
|
328
|
+
}
|
|
@@ -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,
|
|
12
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, 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
|
}
|
|
@@ -186,13 +189,15 @@ function validateFundedDraft(decoded, funded, plan) {
|
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
async function buildTransaction(options) {
|
|
189
|
-
return
|
|
192
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
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`,
|
|
195
199
|
mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
|
|
200
|
+
reserveCandidates: options.state.proactiveReserveOutpoints,
|
|
196
201
|
});
|
|
197
202
|
}
|
|
198
203
|
function createDraftMutation(options) {
|
|
@@ -632,6 +637,7 @@ async function submitDomainAdminMutation(options) {
|
|
|
632
637
|
const built = await buildTransaction({
|
|
633
638
|
rpc,
|
|
634
639
|
walletName,
|
|
640
|
+
state: nextState,
|
|
635
641
|
plan: buildPlanForDomainAdminOperation({
|
|
636
642
|
state: nextState,
|
|
637
643
|
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,
|
|
10
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, 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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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) {
|
|
@@ -768,6 +762,7 @@ export async function transferDomain(options) {
|
|
|
768
762
|
const built = await buildTransaction({
|
|
769
763
|
rpc,
|
|
770
764
|
walletName,
|
|
765
|
+
state: nextState,
|
|
771
766
|
plan: buildPlanForDomainOperation({
|
|
772
767
|
state: nextState,
|
|
773
768
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -998,6 +993,7 @@ async function runSellMutation(options) {
|
|
|
998
993
|
const built = await buildTransaction({
|
|
999
994
|
rpc,
|
|
1000
995
|
walletName,
|
|
996
|
+
state: nextState,
|
|
1001
997
|
plan: buildPlanForDomainOperation({
|
|
1002
998
|
state: nextState,
|
|
1003
999
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1233,6 +1229,7 @@ export async function buyDomain(options) {
|
|
|
1233
1229
|
const built = await buildTransaction({
|
|
1234
1230
|
rpc,
|
|
1235
1231
|
walletName,
|
|
1232
|
+
state: nextState,
|
|
1236
1233
|
plan: buildPlanForDomainOperation({
|
|
1237
1234
|
state: nextState,
|
|
1238
1235
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
package/dist/wallet/tx/field.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
|
10
10
|
import { FIELD_FORMAT_BYTES, serializeDataUpdate, serializeFieldReg, } from "../cogop/index.js";
|
|
11
11
|
import { validateFieldName } from "../cogop/validate-name.js";
|
|
12
12
|
import { findDomainField, openWalletReadContext, } from "../read/index.js";
|
|
13
|
-
import { assertWalletMutationContextReady,
|
|
13
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
14
14
|
import { confirmTypedAcknowledgement as confirmSharedTypedAcknowledgement, confirmYesNo as confirmSharedYesNo, } from "./confirm.js";
|
|
15
15
|
import { getCanonicalIdentitySelector } from "./identity-selector.js";
|
|
16
16
|
import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
|
|
@@ -253,9 +253,8 @@ function buildAnchoredFieldPlan(options) {
|
|
|
253
253
|
return {
|
|
254
254
|
sender: options.sender,
|
|
255
255
|
changeAddress: options.state.funding.address,
|
|
256
|
-
|
|
256
|
+
fixedInputs: [
|
|
257
257
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
258
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
259
258
|
],
|
|
260
259
|
outputs: [
|
|
261
260
|
{ data: Buffer.from(options.opReturnData).toString("hex") },
|
|
@@ -266,6 +265,7 @@ function buildAnchoredFieldPlan(options) {
|
|
|
266
265
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
267
266
|
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
268
267
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
268
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
269
269
|
errorPrefix: options.errorPrefix,
|
|
270
270
|
};
|
|
271
271
|
}
|
|
@@ -277,10 +277,7 @@ function buildFieldFamilyTx2Plan(options) {
|
|
|
277
277
|
return {
|
|
278
278
|
sender: options.sender,
|
|
279
279
|
changeAddress: options.state.funding.address,
|
|
280
|
-
|
|
281
|
-
{ txid: options.tx1Txid, vout: 1 },
|
|
282
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
283
|
-
],
|
|
280
|
+
fixedInputs: [{ txid: options.tx1Txid, vout: 1 }],
|
|
284
281
|
outputs: [
|
|
285
282
|
{ data: Buffer.from(options.opReturnData).toString("hex") },
|
|
286
283
|
{ [options.sender.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
|
|
@@ -290,6 +287,7 @@ function buildFieldFamilyTx2Plan(options) {
|
|
|
290
287
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
291
288
|
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
292
289
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
290
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
293
291
|
errorPrefix: "wallet_field_create_tx2",
|
|
294
292
|
};
|
|
295
293
|
}
|
|
@@ -299,14 +297,17 @@ function validateFieldDraft(decoded, funded, plan) {
|
|
|
299
297
|
if (inputs.length === 0) {
|
|
300
298
|
throw new Error(`${plan.errorPrefix}_missing_sender_input`);
|
|
301
299
|
}
|
|
300
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
302
301
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
303
302
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
304
303
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
assertFundingInputsAfterFixedPrefix({
|
|
305
|
+
inputs,
|
|
306
|
+
fixedInputs: plan.fixedInputs,
|
|
307
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
308
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
309
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
310
|
+
});
|
|
310
311
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
311
312
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
312
313
|
}
|
|
@@ -330,14 +331,15 @@ function validateFieldDraft(decoded, funded, plan) {
|
|
|
330
331
|
}
|
|
331
332
|
}
|
|
332
333
|
async function buildFieldTransaction(options) {
|
|
333
|
-
return
|
|
334
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
334
335
|
rpc: options.rpc,
|
|
335
336
|
walletName: options.walletName,
|
|
337
|
+
state: options.state,
|
|
336
338
|
plan: options.plan,
|
|
337
339
|
validateFundedDraft: validateFieldDraft,
|
|
338
340
|
finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
|
|
339
341
|
mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
|
|
340
|
-
|
|
342
|
+
reserveCandidates: options.state.proactiveReserveOutpoints,
|
|
341
343
|
});
|
|
342
344
|
}
|
|
343
345
|
async function saveUpdatedState(options) {
|
|
@@ -1348,6 +1350,7 @@ async function submitStandaloneFieldMutation(options) {
|
|
|
1348
1350
|
const built = await buildFieldTransaction({
|
|
1349
1351
|
rpc,
|
|
1350
1352
|
walletName,
|
|
1353
|
+
state: nextState,
|
|
1351
1354
|
plan: buildAnchoredFieldPlan({
|
|
1352
1355
|
state: nextState,
|
|
1353
1356
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1544,6 +1547,7 @@ async function submitFieldCreateFamily(options) {
|
|
|
1544
1547
|
const tx1 = await buildFieldTransaction({
|
|
1545
1548
|
rpc,
|
|
1546
1549
|
walletName,
|
|
1550
|
+
state: nextState,
|
|
1547
1551
|
plan: buildAnchoredFieldPlan({
|
|
1548
1552
|
state: nextState,
|
|
1549
1553
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1576,6 +1580,7 @@ async function submitFieldCreateFamily(options) {
|
|
|
1576
1580
|
const tx2 = await buildFieldTransaction({
|
|
1577
1581
|
rpc,
|
|
1578
1582
|
walletName,
|
|
1583
|
+
state: workingState,
|
|
1579
1584
|
plan: buildFieldFamilyTx2Plan({
|
|
1580
1585
|
state: workingState,
|
|
1581
1586
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1583,10 +1588,6 @@ async function submitFieldCreateFamily(options) {
|
|
|
1583
1588
|
tx1Txid,
|
|
1584
1589
|
opReturnData: serializeDataUpdate(operation.chainDomain.domainId, resumedFamily.expectedFieldId ?? operation.chainDomain.nextFieldId, options.value.format, options.value.value).opReturnData,
|
|
1585
1590
|
}),
|
|
1586
|
-
builderOptions: {
|
|
1587
|
-
includeUnsafe: true,
|
|
1588
|
-
minConf: 0,
|
|
1589
|
-
},
|
|
1590
1591
|
});
|
|
1591
1592
|
const final = await sendFamilyTx2({
|
|
1592
1593
|
rpc,
|