@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
|
@@ -8,7 +8,7 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
|
8
8
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
9
9
|
import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
|
|
10
10
|
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
11
|
-
import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
11
|
+
import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
12
12
|
import { acquireFileLock } from "../fs/lock.js";
|
|
13
13
|
import { loadOrAutoUnlockWalletState } from "../lifecycle.js";
|
|
14
14
|
import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
|
|
@@ -584,10 +584,9 @@ function createMiningPlan(options) {
|
|
|
584
584
|
]).toString("hex");
|
|
585
585
|
return {
|
|
586
586
|
sender: options.candidate.sender,
|
|
587
|
-
|
|
587
|
+
fixedInputs: [
|
|
588
588
|
options.candidate.anchorOutpoint,
|
|
589
589
|
options.conflictOutpoint,
|
|
590
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
591
590
|
],
|
|
592
591
|
outputs: [
|
|
593
592
|
{ data: Buffer.from(opReturnData).toString("hex") },
|
|
@@ -599,6 +598,7 @@ function createMiningPlan(options) {
|
|
|
599
598
|
expectedAnchorScriptHex: options.candidate.sender.scriptPubKeyHex,
|
|
600
599
|
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
601
600
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
601
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => walletMutationOutpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
602
602
|
expectedConflictOutpoint: options.conflictOutpoint,
|
|
603
603
|
feeRateSatVb: options.feeRateSatVb,
|
|
604
604
|
};
|
|
@@ -609,17 +609,22 @@ function validateMiningDraft(decoded, funded, plan) {
|
|
|
609
609
|
if (inputs.length < 2) {
|
|
610
610
|
throw new Error("wallet_mining_missing_inputs");
|
|
611
611
|
}
|
|
612
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_mining_missing_inputs");
|
|
612
613
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
613
614
|
throw new Error("wallet_mining_sender_input_mismatch");
|
|
614
615
|
}
|
|
615
|
-
if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex
|
|
616
|
+
if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex
|
|
617
|
+
|| inputs[1]?.txid !== plan.expectedConflictOutpoint.txid
|
|
618
|
+
|| inputs[1].vout !== plan.expectedConflictOutpoint.vout) {
|
|
616
619
|
throw new Error("wallet_mining_conflict_input_mismatch");
|
|
617
620
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
621
|
+
assertFundingInputsAfterFixedPrefix({
|
|
622
|
+
inputs,
|
|
623
|
+
fixedInputs: plan.fixedInputs,
|
|
624
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
625
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
626
|
+
errorCode: "wallet_mining_unexpected_funding_input",
|
|
627
|
+
});
|
|
623
628
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
624
629
|
throw new Error("wallet_mining_opreturn_mismatch");
|
|
625
630
|
}
|
|
@@ -637,17 +642,12 @@ async function buildMiningTransaction(options) {
|
|
|
637
642
|
return buildWalletMutationTransaction({
|
|
638
643
|
rpc: options.rpc,
|
|
639
644
|
walletName: options.walletName,
|
|
645
|
+
state: options.state,
|
|
640
646
|
plan: options.plan,
|
|
641
647
|
validateFundedDraft: validateMiningDraft,
|
|
642
648
|
finalizeErrorCode: "wallet_mining_finalize_failed",
|
|
643
649
|
mempoolRejectPrefix: "wallet_mining_mempool_rejected",
|
|
644
650
|
feeRate: options.plan.feeRateSatVb,
|
|
645
|
-
builderOptions: {
|
|
646
|
-
addInputs: true,
|
|
647
|
-
includeUnsafe: true,
|
|
648
|
-
minConf: 0,
|
|
649
|
-
lockUnspents: true,
|
|
650
|
-
},
|
|
651
651
|
});
|
|
652
652
|
}
|
|
653
653
|
function resolveEligibleAnchoredRoots(context) {
|
|
@@ -1244,30 +1244,6 @@ function miningCandidateIsCurrent(options) {
|
|
|
1244
1244
|
&& options.nodeBestHeight !== null
|
|
1245
1245
|
&& options.state.currentBlockTargetHeight === (options.nodeBestHeight + 1);
|
|
1246
1246
|
}
|
|
1247
|
-
async function rebuildPersistentAnchorLocks(options) {
|
|
1248
|
-
const walletName = options.state.managedCoreWallet.walletName;
|
|
1249
|
-
const [locked, spendable] = await Promise.all([
|
|
1250
|
-
options.rpc.listLockUnspent(walletName).catch(() => []),
|
|
1251
|
-
options.rpc.listUnspent(walletName, 0).catch(() => []),
|
|
1252
|
-
]);
|
|
1253
|
-
const spendableKeys = new Set(spendable.map((entry) => `${entry.txid}:${entry.vout}`));
|
|
1254
|
-
const expected = options.state.domains
|
|
1255
|
-
.map((domain) => domain.currentCanonicalAnchorOutpoint)
|
|
1256
|
-
.filter((outpoint) => outpoint !== null)
|
|
1257
|
-
.map((outpoint) => ({ txid: outpoint.txid, vout: outpoint.vout }))
|
|
1258
|
-
.filter((outpoint) => spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1259
|
-
const expectedKeys = new Set(expected.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
|
|
1260
|
-
const lockedKeys = new Set(locked.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
|
|
1261
|
-
const staleLocked = locked.filter((outpoint) => !expectedKeys.has(`${outpoint.txid}:${outpoint.vout}`)
|
|
1262
|
-
|| !spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1263
|
-
const missingLocked = expected.filter((outpoint) => !lockedKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1264
|
-
if (staleLocked.length > 0) {
|
|
1265
|
-
await options.rpc.lockUnspent(walletName, true, staleLocked).catch(() => undefined);
|
|
1266
|
-
}
|
|
1267
|
-
if (missingLocked.length > 0) {
|
|
1268
|
-
await options.rpc.lockUnspent(walletName, false, missingLocked).catch(() => undefined);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
1247
|
async function reconcileLiveMiningState(options) {
|
|
1272
1248
|
let state = {
|
|
1273
1249
|
...options.state,
|
|
@@ -1275,7 +1251,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1275
1251
|
};
|
|
1276
1252
|
const currentTxid = state.miningState.currentTxid;
|
|
1277
1253
|
if (currentTxid === null || !miningFamilyMayStillExist(state.miningState)) {
|
|
1278
|
-
await
|
|
1254
|
+
await reconcilePersistentPolicyLocks({
|
|
1255
|
+
rpc: options.rpc,
|
|
1256
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1257
|
+
state,
|
|
1258
|
+
fixedInputs: [],
|
|
1259
|
+
});
|
|
1279
1260
|
return state;
|
|
1280
1261
|
}
|
|
1281
1262
|
const walletName = state.managedCoreWallet.walletName;
|
|
@@ -1295,7 +1276,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1295
1276
|
currentPublishDecision: "tx-confirmed-while-down",
|
|
1296
1277
|
},
|
|
1297
1278
|
};
|
|
1298
|
-
await
|
|
1279
|
+
await reconcilePersistentPolicyLocks({
|
|
1280
|
+
rpc: options.rpc,
|
|
1281
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1282
|
+
state,
|
|
1283
|
+
fixedInputs: [],
|
|
1284
|
+
});
|
|
1299
1285
|
return state;
|
|
1300
1286
|
}
|
|
1301
1287
|
if (inMempool) {
|
|
@@ -1319,7 +1305,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1319
1305
|
: null,
|
|
1320
1306
|
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-family",
|
|
1321
1307
|
});
|
|
1322
|
-
await
|
|
1308
|
+
await reconcilePersistentPolicyLocks({
|
|
1309
|
+
rpc: options.rpc,
|
|
1310
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1311
|
+
state,
|
|
1312
|
+
fixedInputs: [],
|
|
1313
|
+
});
|
|
1323
1314
|
return state;
|
|
1324
1315
|
}
|
|
1325
1316
|
if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
|
|
@@ -1333,7 +1324,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1333
1324
|
? "repair-required-broadcast-conflict"
|
|
1334
1325
|
: "repair-required-wallet-conflict",
|
|
1335
1326
|
});
|
|
1336
|
-
await
|
|
1327
|
+
await reconcilePersistentPolicyLocks({
|
|
1328
|
+
rpc: options.rpc,
|
|
1329
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1330
|
+
state,
|
|
1331
|
+
fixedInputs: [],
|
|
1332
|
+
});
|
|
1337
1333
|
return state;
|
|
1338
1334
|
}
|
|
1339
1335
|
state = defaultMiningStatePatch(state, {
|
|
@@ -1342,7 +1338,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1342
1338
|
? "broadcast-unknown-not-seen"
|
|
1343
1339
|
: "live-family-not-seen",
|
|
1344
1340
|
});
|
|
1345
|
-
await
|
|
1341
|
+
await reconcilePersistentPolicyLocks({
|
|
1342
|
+
rpc: options.rpc,
|
|
1343
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1344
|
+
state,
|
|
1345
|
+
fixedInputs: [],
|
|
1346
|
+
});
|
|
1346
1347
|
return state;
|
|
1347
1348
|
}
|
|
1348
1349
|
async function publishCandidate(options) {
|
|
@@ -1405,6 +1406,7 @@ async function publishCandidate(options) {
|
|
|
1405
1406
|
const built = await buildMiningTransaction({
|
|
1406
1407
|
rpc,
|
|
1407
1408
|
walletName: state.managedCoreWallet.walletName,
|
|
1409
|
+
state,
|
|
1408
1410
|
plan,
|
|
1409
1411
|
});
|
|
1410
1412
|
const intentFingerprintHex = computeIntentFingerprint(state, options.candidate);
|
|
@@ -7,6 +7,7 @@ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, } fro
|
|
|
7
7
|
import { resolveCogcoinProcessingStartHeight } from "../../bitcoind/processing-start-height.js";
|
|
8
8
|
import {} from "../../bitcoind/types.js";
|
|
9
9
|
import { loadOrAutoUnlockWalletState, verifyManagedCoreWalletReplica, } from "../lifecycle.js";
|
|
10
|
+
import { normalizeWalletStateRecord, persistWalletCoinControlStateIfNeeded } from "../coin-control.js";
|
|
10
11
|
import { persistNormalizedWalletDescriptorStateIfNeeded } from "../descriptor-normalization.js";
|
|
11
12
|
import { inspectMiningControlPlane } from "../mining/index.js";
|
|
12
13
|
import { normalizeMiningStateRecord } from "../mining/state.js";
|
|
@@ -76,9 +77,17 @@ async function normalizeLoadedWalletStateForRead(options) {
|
|
|
76
77
|
replacePrimary: options.loaded.source === "backup",
|
|
77
78
|
rpc: createRpcClient(node.rpc),
|
|
78
79
|
});
|
|
79
|
-
|
|
80
|
-
source: normalized.changed ? "primary" : options.loaded.source,
|
|
80
|
+
const coinControl = await persistWalletCoinControlStateIfNeeded({
|
|
81
81
|
state: normalized.state,
|
|
82
|
+
access,
|
|
83
|
+
paths: options.paths,
|
|
84
|
+
nowUnixMs: options.now,
|
|
85
|
+
replacePrimary: (normalized.changed ? "primary" : options.loaded.source) === "backup",
|
|
86
|
+
rpc: createRpcClient(node.rpc),
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
source: coinControl.changed ? "primary" : normalized.changed ? "primary" : options.loaded.source,
|
|
90
|
+
state: coinControl.state,
|
|
82
91
|
};
|
|
83
92
|
}
|
|
84
93
|
finally {
|
|
@@ -186,10 +195,10 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
186
195
|
return {
|
|
187
196
|
availability: "ready",
|
|
188
197
|
walletRootId: unlocked.state.walletRootId,
|
|
189
|
-
state: {
|
|
198
|
+
state: normalizeWalletStateRecord({
|
|
190
199
|
...unlocked.state,
|
|
191
200
|
miningState: normalizeMiningStateRecord(unlocked.state.miningState),
|
|
192
|
-
},
|
|
201
|
+
}),
|
|
193
202
|
source: unlocked.source,
|
|
194
203
|
unlockUntilUnixMs: unlocked.session.unlockUntilUnixMs,
|
|
195
204
|
hasPrimaryStateFile,
|
|
@@ -226,10 +235,10 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
226
235
|
return {
|
|
227
236
|
availability: "ready",
|
|
228
237
|
walletRootId: loaded.state.walletRootId,
|
|
229
|
-
state: {
|
|
238
|
+
state: normalizeWalletStateRecord({
|
|
230
239
|
...loaded.state,
|
|
231
240
|
miningState: normalizeMiningStateRecord(loaded.state.miningState),
|
|
232
|
-
},
|
|
241
|
+
}),
|
|
233
242
|
source: loaded.source,
|
|
234
243
|
unlockUntilUnixMs: null,
|
|
235
244
|
hasPrimaryStateFile,
|
package/dist/wallet/reset.js
CHANGED
|
@@ -187,6 +187,8 @@ function createEntropyRetainedWalletState(previousState, nowUnixMs) {
|
|
|
187
187
|
walletRootId,
|
|
188
188
|
network: previousState.network,
|
|
189
189
|
anchorValueSats: previousState.anchorValueSats,
|
|
190
|
+
proactiveReserveSats: previousState.proactiveReserveSats,
|
|
191
|
+
proactiveReserveOutpoints: previousState.proactiveReserveOutpoints,
|
|
190
192
|
nextDedicatedIndex: previousState.nextDedicatedIndex,
|
|
191
193
|
fundingIndex: previousState.fundingIndex,
|
|
192
194
|
mnemonic: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { writeJsonFileAtomic } from "../fs/atomic.js";
|
|
3
|
+
import { normalizeWalletStateRecord } from "../coin-control.js";
|
|
3
4
|
import { decryptJsonWithPassphrase, decryptJsonWithSecretProvider, encryptJsonWithPassphrase, encryptJsonWithSecretProvider, } from "./crypto.js";
|
|
4
5
|
async function readEnvelope(path) {
|
|
5
6
|
const raw = await readFile(path, "utf8");
|
|
@@ -73,18 +74,18 @@ export async function loadWalletState(paths, access) {
|
|
|
73
74
|
try {
|
|
74
75
|
return {
|
|
75
76
|
source: "primary",
|
|
76
|
-
state: typeof access === "string" || access instanceof Uint8Array
|
|
77
|
+
state: normalizeWalletStateRecord(typeof access === "string" || access instanceof Uint8Array
|
|
77
78
|
? await decryptJsonWithPassphrase(await readEnvelope(paths.primaryPath), access)
|
|
78
|
-
: await decryptJsonWithSecretProvider(await readEnvelope(paths.primaryPath), access.provider),
|
|
79
|
+
: await decryptJsonWithSecretProvider(await readEnvelope(paths.primaryPath), access.provider)),
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
catch (primaryError) {
|
|
82
83
|
try {
|
|
83
84
|
return {
|
|
84
85
|
source: "backup",
|
|
85
|
-
state: typeof access === "string" || access instanceof Uint8Array
|
|
86
|
+
state: normalizeWalletStateRecord(typeof access === "string" || access instanceof Uint8Array
|
|
86
87
|
? await decryptJsonWithPassphrase(await readEnvelope(paths.backupPath), access)
|
|
87
|
-
: await decryptJsonWithSecretProvider(await readEnvelope(paths.backupPath), access.provider),
|
|
88
|
+
: await decryptJsonWithSecretProvider(await readEnvelope(paths.backupPath), access.provider)),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
catch {
|
package/dist/wallet/tx/anchor.js
CHANGED
|
@@ -5,11 +5,12 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
|
5
5
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
6
6
|
import { acquireFileLock } from "../fs/lock.js";
|
|
7
7
|
import { deriveWalletIdentityMaterial, } from "../material.js";
|
|
8
|
+
import { computeDesignatedProactiveReserveOutpoints } from "../coin-control.js";
|
|
8
9
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
9
10
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
10
11
|
import { serializeDomainAnchor, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
|
|
11
12
|
import { openWalletReadContext } from "../read/index.js";
|
|
12
|
-
import { assertWalletMutationContextReady,
|
|
13
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, inputMatchesOutpoint, } from "./common.js";
|
|
13
14
|
import { confirmYesNo } from "./confirm.js";
|
|
14
15
|
const ACTIVE_FAMILY_STATUSES = new Set([
|
|
15
16
|
"draft",
|
|
@@ -463,9 +464,7 @@ function buildTx1Plan(options) {
|
|
|
463
464
|
return {
|
|
464
465
|
sender: options.operation.sourceSender,
|
|
465
466
|
changeAddress: options.state.funding.address,
|
|
466
|
-
|
|
467
|
-
? { txid: senderInput.txid, vout: senderInput.vout }
|
|
468
|
-
: { txid: entry.txid, vout: entry.vout }),
|
|
467
|
+
fixedInputs: [{ txid: senderInput.txid, vout: senderInput.vout }],
|
|
469
468
|
outputs,
|
|
470
469
|
changePosition: 2,
|
|
471
470
|
expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
|
|
@@ -474,6 +473,9 @@ function buildTx1Plan(options) {
|
|
|
474
473
|
expectedReplacementAnchorScriptHex: null,
|
|
475
474
|
expectedReplacementAnchorValueSats: null,
|
|
476
475
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
476
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos
|
|
477
|
+
.filter((entry) => !(entry.txid === senderInput.txid && entry.vout === senderInput.vout))
|
|
478
|
+
.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
477
479
|
requiredSenderOutpoint: null,
|
|
478
480
|
requiredProvisionalOutpoint: null,
|
|
479
481
|
errorPrefix: "wallet_anchor_tx1",
|
|
@@ -492,10 +494,7 @@ function buildTx1Plan(options) {
|
|
|
492
494
|
return {
|
|
493
495
|
sender: options.operation.sourceSender,
|
|
494
496
|
changeAddress: options.state.funding.address,
|
|
495
|
-
|
|
496
|
-
{ txid: sourceAnchor.txid, vout: sourceAnchor.vout },
|
|
497
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
498
|
-
],
|
|
497
|
+
fixedInputs: [{ txid: sourceAnchor.txid, vout: sourceAnchor.vout }],
|
|
499
498
|
outputs,
|
|
500
499
|
changePosition: 3,
|
|
501
500
|
expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
|
|
@@ -504,6 +503,7 @@ function buildTx1Plan(options) {
|
|
|
504
503
|
expectedReplacementAnchorScriptHex: options.operation.sourceSender.scriptPubKeyHex,
|
|
505
504
|
expectedReplacementAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
506
505
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
506
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
507
507
|
requiredSenderOutpoint: options.operation.sourceAnchorOutpoint,
|
|
508
508
|
requiredProvisionalOutpoint: null,
|
|
509
509
|
errorPrefix: "wallet_anchor_tx1",
|
|
@@ -535,10 +535,7 @@ function buildTx2Plan(options) {
|
|
|
535
535
|
address: options.operation.targetIdentity.address,
|
|
536
536
|
},
|
|
537
537
|
changeAddress: options.state.funding.address,
|
|
538
|
-
|
|
539
|
-
{ txid: provisional.txid, vout: provisional.vout },
|
|
540
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
541
|
-
],
|
|
538
|
+
fixedInputs: [{ txid: provisional.txid, vout: provisional.vout }],
|
|
542
539
|
outputs: [
|
|
543
540
|
{ data: Buffer.from(opReturnData).toString("hex") },
|
|
544
541
|
{ [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
|
|
@@ -550,6 +547,7 @@ function buildTx2Plan(options) {
|
|
|
550
547
|
expectedReplacementAnchorScriptHex: null,
|
|
551
548
|
expectedReplacementAnchorValueSats: null,
|
|
552
549
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
550
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
553
551
|
requiredSenderOutpoint: null,
|
|
554
552
|
requiredProvisionalOutpoint: {
|
|
555
553
|
txid: provisional.txid,
|
|
@@ -558,30 +556,13 @@ function buildTx2Plan(options) {
|
|
|
558
556
|
errorPrefix: "wallet_anchor_tx2",
|
|
559
557
|
};
|
|
560
558
|
}
|
|
561
|
-
function getDecodedInputScriptPubKeyHex(input) {
|
|
562
|
-
return input.prevout?.scriptPubKey?.hex ?? null;
|
|
563
|
-
}
|
|
564
|
-
function getDecodedInputVout(input) {
|
|
565
|
-
const vout = input.vout;
|
|
566
|
-
return typeof vout === "number" ? vout : null;
|
|
567
|
-
}
|
|
568
|
-
function inputMatchesOutpoint(input, outpoint) {
|
|
569
|
-
return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
|
|
570
|
-
}
|
|
571
|
-
function assertNoUnexpectedAnchorInputs(inputs, allowedScripts, unexpectedInputErrorCode) {
|
|
572
|
-
for (const input of inputs) {
|
|
573
|
-
const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
|
|
574
|
-
if (scriptPubKeyHex === null || !allowedScripts.has(scriptPubKeyHex)) {
|
|
575
|
-
throw new Error(unexpectedInputErrorCode);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
559
|
function validateTx1Draft(decoded, funded, plan) {
|
|
580
560
|
const inputs = decoded.tx.vin;
|
|
581
561
|
const outputs = decoded.tx.vout;
|
|
582
562
|
if (inputs.length === 0) {
|
|
583
563
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
584
564
|
}
|
|
565
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
585
566
|
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
586
567
|
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
|
|
587
568
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
@@ -590,14 +571,14 @@ function validateTx1Draft(decoded, funded, plan) {
|
|
|
590
571
|
if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
|
|
591
572
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
592
573
|
}
|
|
593
|
-
if (inputs.length < 2 || getDecodedInputScriptPubKeyHex(inputs[1]) !== plan.allowedFundingScriptPubKeyHex) {
|
|
594
|
-
throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
|
|
595
|
-
}
|
|
596
|
-
assertNoUnexpectedAnchorInputs(inputs.slice(2), new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
assertNoUnexpectedAnchorInputs(inputs, new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
|
|
600
574
|
}
|
|
575
|
+
assertFundingInputsAfterFixedPrefix({
|
|
576
|
+
inputs,
|
|
577
|
+
fixedInputs: plan.fixedInputs,
|
|
578
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
579
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
580
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
581
|
+
});
|
|
601
582
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
602
583
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
603
584
|
}
|
|
@@ -635,15 +616,19 @@ function validateTx2Draft(decoded, funded, plan) {
|
|
|
635
616
|
if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
|
|
636
617
|
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
637
618
|
}
|
|
619
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
|
|
638
620
|
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
639
621
|
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
|
|
640
622
|
|| !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
|
|
641
623
|
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
642
624
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
625
|
+
assertFundingInputsAfterFixedPrefix({
|
|
626
|
+
inputs,
|
|
627
|
+
fixedInputs: plan.fixedInputs,
|
|
628
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
629
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
630
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
631
|
+
});
|
|
647
632
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
648
633
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
649
634
|
}
|
|
@@ -668,31 +653,29 @@ function validateTx2Draft(decoded, funded, plan) {
|
|
|
668
653
|
}
|
|
669
654
|
}
|
|
670
655
|
async function buildTx1(options) {
|
|
671
|
-
|
|
656
|
+
const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
|
|
657
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
672
658
|
rpc: options.rpc,
|
|
673
659
|
walletName: options.walletName,
|
|
660
|
+
state: options.state,
|
|
674
661
|
plan: options.plan,
|
|
675
662
|
validateFundedDraft: validateTx1Draft,
|
|
676
663
|
finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
|
|
677
664
|
mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
|
|
678
|
-
|
|
679
|
-
addInputs: false,
|
|
680
|
-
},
|
|
665
|
+
reserveCandidates,
|
|
681
666
|
});
|
|
682
667
|
}
|
|
683
668
|
async function buildTx2(options) {
|
|
684
|
-
|
|
669
|
+
const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
|
|
670
|
+
return buildWalletMutationTransactionWithReserveFallback({
|
|
685
671
|
rpc: options.rpc,
|
|
686
672
|
walletName: options.walletName,
|
|
673
|
+
state: options.state,
|
|
687
674
|
plan: options.plan,
|
|
688
675
|
validateFundedDraft: validateTx2Draft,
|
|
689
676
|
finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
|
|
690
677
|
mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
|
|
691
|
-
|
|
692
|
-
addInputs: false,
|
|
693
|
-
includeUnsafe: true,
|
|
694
|
-
minConf: 0,
|
|
695
|
-
},
|
|
678
|
+
reserveCandidates,
|
|
696
679
|
});
|
|
697
680
|
}
|
|
698
681
|
async function relockAnchorOutpoint(rpc, walletName, outpoint) {
|
|
@@ -998,6 +981,7 @@ async function submitTx2(options) {
|
|
|
998
981
|
const builtTx2 = await buildTx2({
|
|
999
982
|
rpc: options.rpc,
|
|
1000
983
|
walletName: options.walletName,
|
|
984
|
+
state: nextState,
|
|
1001
985
|
plan: tx2Plan,
|
|
1002
986
|
});
|
|
1003
987
|
const broadcastingTx2 = createBroadcastingTxRecord(builtTx2);
|
|
@@ -1232,6 +1216,7 @@ export async function anchorDomain(options) {
|
|
|
1232
1216
|
const builtTx1 = await buildTx1({
|
|
1233
1217
|
rpc,
|
|
1234
1218
|
walletName,
|
|
1219
|
+
state: nextState,
|
|
1235
1220
|
plan: tx1Plan,
|
|
1236
1221
|
});
|
|
1237
1222
|
const broadcastingTx1 = createBroadcastingTxRecord(builtTx1);
|
package/dist/wallet/tx/cog.js
CHANGED
|
@@ -7,7 +7,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
|
7
7
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
8
8
|
import { serializeCogClaim, serializeCogLock, serializeCogTransfer, } from "../cogop/index.js";
|
|
9
9
|
import { openWalletReadContext } from "../read/index.js";
|
|
10
|
-
import { assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
10
|
+
import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
|
|
11
11
|
import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
|
|
12
12
|
import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
|
|
13
13
|
import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
|
|
@@ -258,11 +258,8 @@ function buildPlanForCogOperation(options) {
|
|
|
258
258
|
return {
|
|
259
259
|
sender: options.sender,
|
|
260
260
|
changeAddress: options.state.funding.address,
|
|
261
|
-
|
|
261
|
+
fixedInputs: [
|
|
262
262
|
{ txid: senderUtxo.txid, vout: senderUtxo.vout },
|
|
263
|
-
...fundingUtxos
|
|
264
|
-
.filter((entry) => !(entry.txid === senderUtxo.txid && entry.vout === senderUtxo.vout))
|
|
265
|
-
.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
266
263
|
],
|
|
267
264
|
outputs,
|
|
268
265
|
changePosition: 1,
|
|
@@ -270,6 +267,9 @@ function buildPlanForCogOperation(options) {
|
|
|
270
267
|
expectedAnchorScriptHex: null,
|
|
271
268
|
expectedAnchorValueSats: null,
|
|
272
269
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
270
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos
|
|
271
|
+
.filter((entry) => !(entry.txid === senderUtxo.txid && entry.vout === senderUtxo.vout))
|
|
272
|
+
.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
273
273
|
errorPrefix: options.errorPrefix,
|
|
274
274
|
};
|
|
275
275
|
}
|
|
@@ -288,9 +288,8 @@ function buildPlanForCogOperation(options) {
|
|
|
288
288
|
return {
|
|
289
289
|
sender: options.sender,
|
|
290
290
|
changeAddress: options.state.funding.address,
|
|
291
|
-
|
|
291
|
+
fixedInputs: [
|
|
292
292
|
{ txid: anchorUtxo.txid, vout: anchorUtxo.vout },
|
|
293
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
294
293
|
],
|
|
295
294
|
outputs,
|
|
296
295
|
changePosition: 2,
|
|
@@ -298,6 +297,7 @@ function buildPlanForCogOperation(options) {
|
|
|
298
297
|
expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
|
|
299
298
|
expectedAnchorValueSats: options.anchorValueSats,
|
|
300
299
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
300
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
301
301
|
errorPrefix: options.errorPrefix,
|
|
302
302
|
};
|
|
303
303
|
}
|
|
@@ -307,14 +307,17 @@ function validateFundedDraft(decoded, funded, plan) {
|
|
|
307
307
|
if (inputs.length === 0) {
|
|
308
308
|
throw new Error(`${plan.errorPrefix}_missing_sender_input`);
|
|
309
309
|
}
|
|
310
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
|
|
310
311
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
311
312
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
assertFundingInputsAfterFixedPrefix({
|
|
315
|
+
inputs,
|
|
316
|
+
fixedInputs: plan.fixedInputs,
|
|
317
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
318
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
319
|
+
errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
|
|
320
|
+
});
|
|
318
321
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
319
322
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
320
323
|
}
|
|
@@ -344,6 +347,7 @@ async function buildTransaction(options) {
|
|
|
344
347
|
return buildWalletMutationTransaction({
|
|
345
348
|
rpc: options.rpc,
|
|
346
349
|
walletName: options.walletName,
|
|
350
|
+
state: options.state,
|
|
347
351
|
plan: options.plan,
|
|
348
352
|
validateFundedDraft,
|
|
349
353
|
finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
|
|
@@ -699,6 +703,7 @@ export async function sendCog(options) {
|
|
|
699
703
|
const built = await buildTransaction({
|
|
700
704
|
rpc,
|
|
701
705
|
walletName,
|
|
706
|
+
state: nextState,
|
|
702
707
|
plan: buildPlanForCogOperation({
|
|
703
708
|
state: nextState,
|
|
704
709
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -860,6 +865,7 @@ export async function lockCogToDomain(options) {
|
|
|
860
865
|
const built = await buildTransaction({
|
|
861
866
|
rpc,
|
|
862
867
|
walletName,
|
|
868
|
+
state: nextState,
|
|
863
869
|
plan: buildPlanForCogOperation({
|
|
864
870
|
state: nextState,
|
|
865
871
|
allUtxos: await rpc.listUnspent(walletName, 1),
|
|
@@ -1004,6 +1010,7 @@ async function runClaimLikeMutation(options, reclaim) {
|
|
|
1004
1010
|
const built = await buildTransaction({
|
|
1005
1011
|
rpc,
|
|
1006
1012
|
walletName,
|
|
1013
|
+
state: nextState,
|
|
1007
1014
|
plan: buildPlanForCogOperation({
|
|
1008
1015
|
state: nextState,
|
|
1009
1016
|
allUtxos: await rpc.listUnspent(walletName, 1),
|