@cogcoin/client 1.1.4 → 1.1.6
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 +4 -5
- package/dist/bitcoind/indexer-daemon.d.ts +3 -7
- package/dist/bitcoind/indexer-daemon.js +43 -158
- package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
- package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/status.js +59 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +37 -0
- package/dist/bitcoind/managed-runtime/types.js +1 -0
- package/dist/bitcoind/progress/tty-renderer.js +3 -2
- package/dist/bitcoind/service.d.ts +2 -7
- package/dist/bitcoind/service.js +46 -94
- package/dist/cli/command-registry.d.ts +39 -0
- package/dist/cli/command-registry.js +1132 -0
- package/dist/cli/commands/client-admin.js +6 -56
- package/dist/cli/commands/mining-admin.js +9 -32
- package/dist/cli/commands/mining-read.js +15 -56
- package/dist/cli/commands/mining-runtime.js +258 -57
- package/dist/cli/commands/service-runtime.js +1 -64
- package/dist/cli/commands/status.js +2 -15
- package/dist/cli/commands/update.js +6 -21
- package/dist/cli/commands/wallet-admin.js +18 -120
- package/dist/cli/commands/wallet-mutation.js +4 -7
- package/dist/cli/commands/wallet-read.js +31 -138
- package/dist/cli/context.js +2 -4
- package/dist/cli/mining-format.js +8 -2
- package/dist/cli/mutation-command-groups.d.ts +11 -11
- package/dist/cli/mutation-command-groups.js +9 -18
- package/dist/cli/mutation-json.d.ts +1 -17
- package/dist/cli/mutation-json.js +1 -28
- package/dist/cli/mutation-success.d.ts +0 -1
- package/dist/cli/mutation-success.js +0 -19
- package/dist/cli/output.d.ts +1 -10
- package/dist/cli/output.js +52 -481
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +38 -695
- package/dist/cli/runner.js +28 -113
- package/dist/cli/types.d.ts +7 -8
- package/dist/cli/update-notifier.js +1 -1
- package/dist/cli/wallet-format.js +1 -1
- package/dist/wallet/lifecycle/access.d.ts +5 -0
- package/dist/wallet/lifecycle/access.js +79 -0
- package/dist/wallet/lifecycle/context.d.ts +26 -0
- package/dist/wallet/lifecycle/context.js +58 -0
- package/dist/wallet/lifecycle/managed-core.d.ts +15 -0
- package/dist/wallet/lifecycle/managed-core.js +197 -0
- package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
- package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
- package/dist/wallet/lifecycle/repair-indexer.js +117 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
- package/dist/wallet/lifecycle/repair-mining.js +304 -0
- package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
- package/dist/wallet/lifecycle/repair-runtime.js +206 -0
- package/dist/wallet/lifecycle/repair.d.ts +9 -0
- package/dist/wallet/lifecycle/repair.js +127 -0
- package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
- package/dist/wallet/lifecycle/setup-prompts.js +88 -0
- package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
- package/dist/wallet/lifecycle/setup-state.js +159 -0
- package/dist/wallet/lifecycle/setup.d.ts +15 -0
- package/dist/wallet/lifecycle/setup.js +124 -0
- package/dist/wallet/lifecycle/types.d.ts +156 -0
- package/dist/wallet/lifecycle/types.js +1 -0
- package/dist/wallet/lifecycle.d.ts +4 -165
- package/dist/wallet/lifecycle.js +3 -1656
- package/dist/wallet/mining/candidate.d.ts +60 -0
- package/dist/wallet/mining/candidate.js +290 -0
- package/dist/wallet/mining/competitiveness.d.ts +22 -0
- package/dist/wallet/mining/competitiveness.js +640 -0
- package/dist/wallet/mining/control.js +7 -251
- package/dist/wallet/mining/cycle.d.ts +39 -0
- package/dist/wallet/mining/cycle.js +542 -0
- package/dist/wallet/mining/engine-state.d.ts +66 -0
- package/dist/wallet/mining/engine-state.js +211 -0
- package/dist/wallet/mining/engine-types.d.ts +173 -0
- package/dist/wallet/mining/engine-types.js +1 -0
- package/dist/wallet/mining/engine-utils.d.ts +7 -0
- package/dist/wallet/mining/engine-utils.js +75 -0
- package/dist/wallet/mining/events.d.ts +2 -0
- package/dist/wallet/mining/events.js +19 -0
- package/dist/wallet/mining/lifecycle.d.ts +71 -0
- package/dist/wallet/mining/lifecycle.js +355 -0
- package/dist/wallet/mining/projection.d.ts +61 -0
- package/dist/wallet/mining/projection.js +319 -0
- package/dist/wallet/mining/publish.d.ts +79 -0
- package/dist/wallet/mining/publish.js +614 -0
- package/dist/wallet/mining/runner.d.ts +12 -418
- package/dist/wallet/mining/runner.js +274 -3433
- package/dist/wallet/mining/supervisor.d.ts +134 -0
- package/dist/wallet/mining/supervisor.js +558 -0
- package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
- package/dist/wallet/mining/visualizer-sync.js +166 -0
- package/dist/wallet/mining/visualizer.d.ts +1 -0
- package/dist/wallet/mining/visualizer.js +33 -18
- package/dist/wallet/read/context.js +13 -188
- package/dist/wallet/reset.d.ts +1 -1
- package/dist/wallet/reset.js +35 -11
- package/dist/wallet/runtime.d.ts +0 -6
- package/dist/wallet/runtime.js +2 -38
- package/dist/wallet/tx/common.d.ts +18 -0
- package/dist/wallet/tx/common.js +40 -26
- package/package.json +1 -1
- package/dist/wallet/state/seed-index.d.ts +0 -43
- package/dist/wallet/state/seed-index.js +0 -151
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { getBlockWinners } from "@cogcoin/indexer/queries";
|
|
3
|
+
import { deriveBlendSeed, displayToInternalBlockhash } from "@cogcoin/scoring";
|
|
4
|
+
import { createRpcClient } from "../../bitcoind/node.js";
|
|
5
|
+
import { serializeMine } from "../cogop/index.js";
|
|
6
|
+
import { openWalletReadContext } from "../read/index.js";
|
|
7
|
+
import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, fundAndValidateWalletMutationDraft, isAlreadyAcceptedError, isBroadcastUnknownError, isInsufficientFundsError, outpointKey as walletMutationOutpointKey, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
8
|
+
import { createMiningEventRecord } from "./events.js";
|
|
9
|
+
import {} from "./engine-types.js";
|
|
10
|
+
import { cloneMiningState, defaultMiningStatePatch, livePublishTargetsCandidateTip, miningCandidateIsCurrent, resolveSharedMiningConflictOutpoint, } from "./engine-state.js";
|
|
11
|
+
import { deriveMiningWordIndices, numberToSats, resolveBip39WordsFromIndices, } from "./engine-utils.js";
|
|
12
|
+
import { clearMiningPublishState, miningPublishMayStillExist, } from "./state.js";
|
|
13
|
+
import { refreshMiningCandidateFromCurrentState } from "./candidate.js";
|
|
14
|
+
import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
15
|
+
const MINING_FUNDING_MIN_CONF = 0;
|
|
16
|
+
const MINING_FUNDING_PROBE_PLACEHOLDER_SENTENCE = "m".repeat(60);
|
|
17
|
+
export class MiningPublishRejectedError extends Error {
|
|
18
|
+
revertedState;
|
|
19
|
+
constructor(message, revertedState) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "MiningPublishRejectedError";
|
|
22
|
+
this.revertedState = revertedState;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function createStaleMiningCandidateWaitingNote() {
|
|
26
|
+
return "Mining candidate changed before broadcast: the selected root domain is no longer locally mineable. Skipping this tip and waiting for the next block.";
|
|
27
|
+
}
|
|
28
|
+
export function createRetryableMiningPublishWaitingNote() {
|
|
29
|
+
return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
|
|
30
|
+
}
|
|
31
|
+
export function createInsufficientFundsMiningPublishWaitingNote() {
|
|
32
|
+
return "Insufficient BTC to mine.";
|
|
33
|
+
}
|
|
34
|
+
export function createInsufficientFundsMiningPublishErrorMessage() {
|
|
35
|
+
return "Bitcoin Core could not fund the next mining publish with safe BTC.";
|
|
36
|
+
}
|
|
37
|
+
function createMiningFundingProbeCandidate(options) {
|
|
38
|
+
const referencedBlockHashInternal = Buffer.from(displayToInternalBlockhash(options.referencedBlockHashDisplay), "hex");
|
|
39
|
+
const bip39WordIndices = deriveMiningWordIndices(referencedBlockHashInternal, options.domain.domainId);
|
|
40
|
+
return {
|
|
41
|
+
domainId: options.domain.domainId,
|
|
42
|
+
domainName: options.domain.domainName,
|
|
43
|
+
localIndex: options.domain.localIndex,
|
|
44
|
+
sender: options.domain.sender,
|
|
45
|
+
sentence: MINING_FUNDING_PROBE_PLACEHOLDER_SENTENCE,
|
|
46
|
+
encodedSentenceBytes: Buffer.from(MINING_FUNDING_PROBE_PLACEHOLDER_SENTENCE, "utf8"),
|
|
47
|
+
bip39WordIndices,
|
|
48
|
+
bip39Words: resolveBip39WordsFromIndices(bip39WordIndices),
|
|
49
|
+
canonicalBlend: 0n,
|
|
50
|
+
referencedBlockHashDisplay: options.referencedBlockHashDisplay,
|
|
51
|
+
referencedBlockHashInternal,
|
|
52
|
+
targetBlockHeight: options.targetBlockHeight,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function resolveMiningConflictOutpoint(options) {
|
|
56
|
+
void options.allUtxos;
|
|
57
|
+
return resolveSharedMiningConflictOutpoint(options.state.miningState);
|
|
58
|
+
}
|
|
59
|
+
export function createMiningPlan(options) {
|
|
60
|
+
const fundingUtxos = options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
|
|
61
|
+
&& entry.confirmations >= MINING_FUNDING_MIN_CONF
|
|
62
|
+
&& entry.spendable !== false
|
|
63
|
+
&& entry.safe !== false
|
|
64
|
+
&& !(options.conflictOutpoint !== null
|
|
65
|
+
&& entry.txid === options.conflictOutpoint.txid
|
|
66
|
+
&& entry.vout === options.conflictOutpoint.vout));
|
|
67
|
+
const opReturnData = serializeMine(options.candidate.domainId, options.candidate.referencedBlockHashInternal, options.candidate.encodedSentenceBytes).opReturnData;
|
|
68
|
+
const expectedOpReturnScriptHex = Buffer.concat([
|
|
69
|
+
Buffer.from([0x6a, opReturnData.length]),
|
|
70
|
+
Buffer.from(opReturnData),
|
|
71
|
+
]).toString("hex");
|
|
72
|
+
return {
|
|
73
|
+
sender: options.candidate.sender,
|
|
74
|
+
fixedInputs: options.conflictOutpoint === null ? [] : [options.conflictOutpoint],
|
|
75
|
+
outputs: [{ data: Buffer.from(opReturnData).toString("hex") }],
|
|
76
|
+
changeAddress: options.state.funding.address,
|
|
77
|
+
changePosition: 1,
|
|
78
|
+
expectedOpReturnScriptHex,
|
|
79
|
+
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
80
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => walletMutationOutpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
81
|
+
expectedConflictOutpoint: options.conflictOutpoint,
|
|
82
|
+
feeRateSatVb: options.feeRateSatVb,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function validateMiningDraft(decoded, funded, plan) {
|
|
86
|
+
const inputs = decoded.tx.vin;
|
|
87
|
+
const outputs = decoded.tx.vout;
|
|
88
|
+
if (inputs.length === 0) {
|
|
89
|
+
throw new Error("wallet_mining_missing_inputs");
|
|
90
|
+
}
|
|
91
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_mining_missing_inputs");
|
|
92
|
+
if (plan.expectedConflictOutpoint !== null
|
|
93
|
+
&& (inputs[0]?.txid !== plan.expectedConflictOutpoint.txid
|
|
94
|
+
|| inputs[0]?.vout !== plan.expectedConflictOutpoint.vout)) {
|
|
95
|
+
throw new Error("wallet_mining_conflict_input_mismatch");
|
|
96
|
+
}
|
|
97
|
+
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
98
|
+
throw new Error("wallet_mining_opreturn_mismatch");
|
|
99
|
+
}
|
|
100
|
+
if (funded.changepos !== -1
|
|
101
|
+
&& (funded.changepos !== plan.changePosition
|
|
102
|
+
|| outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex)) {
|
|
103
|
+
throw new Error("wallet_mining_change_output_mismatch");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function buildMiningTransaction(options) {
|
|
107
|
+
return buildWalletMutationTransaction({
|
|
108
|
+
rpc: options.rpc,
|
|
109
|
+
walletName: options.walletName,
|
|
110
|
+
state: options.state,
|
|
111
|
+
plan: options.plan,
|
|
112
|
+
validateFundedDraft: validateMiningDraft,
|
|
113
|
+
finalizeErrorCode: "wallet_mining_finalize_failed",
|
|
114
|
+
mempoolRejectPrefix: "wallet_mining_mempool_rejected",
|
|
115
|
+
feeRate: options.plan.feeRateSatVb,
|
|
116
|
+
availableFundingMinConf: MINING_FUNDING_MIN_CONF,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export async function probeMiningFundingAvailability(options) {
|
|
120
|
+
const templateDomain = options.domains[0];
|
|
121
|
+
if (templateDomain === undefined) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const allUtxos = await options.rpc.listUnspent(options.walletName, MINING_FUNDING_MIN_CONF);
|
|
125
|
+
const conflictOutpoint = resolveMiningConflictOutpoint({
|
|
126
|
+
state: options.state,
|
|
127
|
+
allUtxos,
|
|
128
|
+
});
|
|
129
|
+
const feeSelection = await resolveWalletMutationFeeSelection({
|
|
130
|
+
rpc: options.rpc,
|
|
131
|
+
});
|
|
132
|
+
const plan = createMiningPlan({
|
|
133
|
+
state: options.state,
|
|
134
|
+
candidate: createMiningFundingProbeCandidate({
|
|
135
|
+
domain: templateDomain,
|
|
136
|
+
referencedBlockHashDisplay: options.referencedBlockHashDisplay,
|
|
137
|
+
targetBlockHeight: options.targetBlockHeight,
|
|
138
|
+
}),
|
|
139
|
+
conflictOutpoint,
|
|
140
|
+
allUtxos,
|
|
141
|
+
feeRateSatVb: feeSelection.feeRateSatVb,
|
|
142
|
+
});
|
|
143
|
+
await fundAndValidateWalletMutationDraft({
|
|
144
|
+
rpc: options.rpc,
|
|
145
|
+
walletName: options.walletName,
|
|
146
|
+
plan,
|
|
147
|
+
validateFundedDraft: validateMiningDraft,
|
|
148
|
+
feeRate: plan.feeRateSatVb,
|
|
149
|
+
availableFundingMinConf: MINING_FUNDING_MIN_CONF,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function findRecentMiningWin(snapshotState, txid, targetBlockHeight) {
|
|
153
|
+
if (snapshotState === null || snapshotState === undefined || txid === null || targetBlockHeight === null) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const winners = getBlockWinners(snapshotState, targetBlockHeight) ?? [];
|
|
157
|
+
const winner = winners.find((entry) => entry.txidHex === txid) ?? null;
|
|
158
|
+
if (winner === null) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
rank: winner.rank,
|
|
163
|
+
rewardCogtoshi: winner.rewardCogtoshi,
|
|
164
|
+
blockHeight: winner.height,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function computeIntentFingerprint(state, candidate) {
|
|
168
|
+
return createHash("sha256")
|
|
169
|
+
.update([
|
|
170
|
+
"mine",
|
|
171
|
+
state.walletRootId,
|
|
172
|
+
candidate.domainId,
|
|
173
|
+
candidate.referencedBlockHashDisplay,
|
|
174
|
+
Buffer.from(candidate.encodedSentenceBytes).toString("hex"),
|
|
175
|
+
].join("\n"))
|
|
176
|
+
.digest("hex");
|
|
177
|
+
}
|
|
178
|
+
export async function reconcileLiveMiningState(options) {
|
|
179
|
+
let state = {
|
|
180
|
+
...options.state,
|
|
181
|
+
miningState: cloneMiningState(options.state.miningState),
|
|
182
|
+
};
|
|
183
|
+
const currentTxid = state.miningState.currentTxid;
|
|
184
|
+
if (currentTxid === null || !miningPublishMayStillExist(state.miningState)) {
|
|
185
|
+
await reconcilePersistentPolicyLocks({
|
|
186
|
+
rpc: options.rpc,
|
|
187
|
+
walletName: state.managedCoreWallet.walletName,
|
|
188
|
+
state,
|
|
189
|
+
fixedInputs: [],
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
state,
|
|
193
|
+
recentWin: null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const walletName = state.managedCoreWallet.walletName;
|
|
197
|
+
const [mempoolVerbose, walletTx] = await Promise.all([
|
|
198
|
+
options.rpc.getRawMempoolVerbose().catch(() => ({
|
|
199
|
+
txids: [],
|
|
200
|
+
mempool_sequence: "unknown",
|
|
201
|
+
})),
|
|
202
|
+
options.rpc.getTransaction(walletName, currentTxid).catch(() => null),
|
|
203
|
+
]);
|
|
204
|
+
const inMempool = mempoolVerbose.txids.includes(currentTxid);
|
|
205
|
+
if (walletTx !== null && walletTx.confirmations > 0) {
|
|
206
|
+
const recentWin = findRecentMiningWin(options.snapshotState ?? null, currentTxid, state.miningState.currentBlockTargetHeight);
|
|
207
|
+
state = {
|
|
208
|
+
...state,
|
|
209
|
+
miningState: {
|
|
210
|
+
...clearMiningPublishState(state.miningState),
|
|
211
|
+
currentPublishDecision: "tx-confirmed-while-down",
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
await reconcilePersistentPolicyLocks({
|
|
215
|
+
rpc: options.rpc,
|
|
216
|
+
walletName: state.managedCoreWallet.walletName,
|
|
217
|
+
state,
|
|
218
|
+
fixedInputs: [],
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
state,
|
|
222
|
+
recentWin,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (inMempool) {
|
|
226
|
+
const stale = !miningCandidateIsCurrent({
|
|
227
|
+
state: state.miningState,
|
|
228
|
+
nodeBestHash: options.nodeBestHash,
|
|
229
|
+
nodeBestHeight: options.nodeBestHeight,
|
|
230
|
+
});
|
|
231
|
+
state = defaultMiningStatePatch(state, {
|
|
232
|
+
livePublishInMempool: true,
|
|
233
|
+
currentPublishState: "in-mempool",
|
|
234
|
+
state: stale
|
|
235
|
+
? "paused-stale"
|
|
236
|
+
: state.miningState.runMode === "stopped"
|
|
237
|
+
? "paused"
|
|
238
|
+
: "live",
|
|
239
|
+
pauseReason: stale
|
|
240
|
+
? "stale-block-context"
|
|
241
|
+
: state.miningState.runMode === "stopped"
|
|
242
|
+
? "user-stopped"
|
|
243
|
+
: null,
|
|
244
|
+
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-publish",
|
|
245
|
+
});
|
|
246
|
+
await reconcilePersistentPolicyLocks({
|
|
247
|
+
rpc: options.rpc,
|
|
248
|
+
walletName: state.managedCoreWallet.walletName,
|
|
249
|
+
state,
|
|
250
|
+
fixedInputs: [],
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
state,
|
|
254
|
+
recentWin: null,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
|
|
258
|
+
state = defaultMiningStatePatch(state, {
|
|
259
|
+
state: "repair-required",
|
|
260
|
+
pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
|
|
261
|
+
? "broadcast-unknown-conflict"
|
|
262
|
+
: "wallet-conflict-observed",
|
|
263
|
+
livePublishInMempool: false,
|
|
264
|
+
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
265
|
+
? "repair-required-broadcast-conflict"
|
|
266
|
+
: "repair-required-wallet-conflict",
|
|
267
|
+
});
|
|
268
|
+
await reconcilePersistentPolicyLocks({
|
|
269
|
+
rpc: options.rpc,
|
|
270
|
+
walletName: state.managedCoreWallet.walletName,
|
|
271
|
+
state,
|
|
272
|
+
fixedInputs: [],
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
state,
|
|
276
|
+
recentWin: null,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
state = defaultMiningStatePatch(state, {
|
|
280
|
+
...clearMiningPublishState(state.miningState),
|
|
281
|
+
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
282
|
+
? "broadcast-unknown-not-seen"
|
|
283
|
+
: "live-publish-not-seen",
|
|
284
|
+
});
|
|
285
|
+
await reconcilePersistentPolicyLocks({
|
|
286
|
+
rpc: options.rpc,
|
|
287
|
+
walletName: state.managedCoreWallet.walletName,
|
|
288
|
+
state,
|
|
289
|
+
fixedInputs: [],
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
state,
|
|
293
|
+
recentWin: null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export async function publishCandidateOnce(options) {
|
|
297
|
+
const appendEventFn = options.appendEventFn;
|
|
298
|
+
const service = await options.attachService({
|
|
299
|
+
dataDir: options.dataDir,
|
|
300
|
+
chain: "main",
|
|
301
|
+
startHeight: 0,
|
|
302
|
+
walletRootId: options.readContext.localState.state.walletRootId,
|
|
303
|
+
});
|
|
304
|
+
const rpc = options.rpcFactory(service.rpc);
|
|
305
|
+
let state = (await reconcileLiveMiningState({
|
|
306
|
+
state: options.readContext.localState.state,
|
|
307
|
+
rpc,
|
|
308
|
+
nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
309
|
+
nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
310
|
+
snapshotState: options.readContext.snapshot.state,
|
|
311
|
+
})).state;
|
|
312
|
+
const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, MINING_FUNDING_MIN_CONF);
|
|
313
|
+
const conflictOutpoint = resolveMiningConflictOutpoint({
|
|
314
|
+
state,
|
|
315
|
+
allUtxos,
|
|
316
|
+
});
|
|
317
|
+
const priorMiningState = cloneMiningState(state.miningState);
|
|
318
|
+
if (livePublishTargetsCandidateTip({
|
|
319
|
+
liveState: state.miningState,
|
|
320
|
+
candidate: options.candidate,
|
|
321
|
+
})) {
|
|
322
|
+
return {
|
|
323
|
+
state: defaultMiningStatePatch(state, {
|
|
324
|
+
currentPublishDecision: "kept-live-publish",
|
|
325
|
+
}),
|
|
326
|
+
txid: state.miningState.currentTxid,
|
|
327
|
+
decision: "kept-live-publish",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const feeSelection = await resolveWalletMutationFeeSelection({
|
|
331
|
+
rpc,
|
|
332
|
+
});
|
|
333
|
+
const nextFeeRate = feeSelection.feeRateSatVb;
|
|
334
|
+
const plan = createMiningPlan({
|
|
335
|
+
state,
|
|
336
|
+
candidate: options.candidate,
|
|
337
|
+
conflictOutpoint,
|
|
338
|
+
allUtxos,
|
|
339
|
+
feeRateSatVb: nextFeeRate,
|
|
340
|
+
});
|
|
341
|
+
const built = await buildMiningTransaction({
|
|
342
|
+
rpc,
|
|
343
|
+
walletName: state.managedCoreWallet.walletName,
|
|
344
|
+
state,
|
|
345
|
+
plan,
|
|
346
|
+
});
|
|
347
|
+
const intentFingerprintHex = computeIntentFingerprint(state, options.candidate);
|
|
348
|
+
state = defaultMiningStatePatch(state, {
|
|
349
|
+
state: "live",
|
|
350
|
+
currentPublishState: "broadcasting",
|
|
351
|
+
currentDomain: options.candidate.domainName,
|
|
352
|
+
currentDomainId: options.candidate.domainId,
|
|
353
|
+
currentDomainIndex: options.candidate.localIndex,
|
|
354
|
+
currentSenderScriptPubKeyHex: options.candidate.sender.scriptPubKeyHex,
|
|
355
|
+
currentTxid: built.txid,
|
|
356
|
+
currentWtxid: built.wtxid,
|
|
357
|
+
currentFeeRateSatVb: nextFeeRate,
|
|
358
|
+
currentAbsoluteFeeSats: numberToSats(built.funded.fee).toString() === "0" ? 0 : Number(numberToSats(built.funded.fee)),
|
|
359
|
+
currentScore: options.candidate.canonicalBlend.toString(),
|
|
360
|
+
currentSentence: options.candidate.sentence,
|
|
361
|
+
currentEncodedSentenceBytesHex: Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
|
|
362
|
+
currentBip39WordIndices: [...options.candidate.bip39WordIndices],
|
|
363
|
+
currentBlendSeedHex: Buffer.from(deriveBlendSeed(options.candidate.referencedBlockHashInternal)).toString("hex"),
|
|
364
|
+
currentBlockTargetHeight: options.candidate.targetBlockHeight,
|
|
365
|
+
currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
366
|
+
currentIntentFingerprintHex: intentFingerprintHex,
|
|
367
|
+
sharedMiningConflictOutpoint: conflictOutpoint,
|
|
368
|
+
livePublishInMempool: null,
|
|
369
|
+
currentPublishDecision: priorMiningState.currentTxid === null
|
|
370
|
+
? "publishing"
|
|
371
|
+
: "replacing",
|
|
372
|
+
});
|
|
373
|
+
await saveWalletStatePreservingUnlock({
|
|
374
|
+
state,
|
|
375
|
+
provider: options.provider,
|
|
376
|
+
paths: options.paths,
|
|
377
|
+
});
|
|
378
|
+
try {
|
|
379
|
+
await rpc.sendRawTransaction(built.rawHex);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
if (isAlreadyAcceptedError(error)) {
|
|
383
|
+
state = defaultMiningStatePatch(state, {
|
|
384
|
+
currentPublishState: "in-mempool",
|
|
385
|
+
livePublishInMempool: true,
|
|
386
|
+
});
|
|
387
|
+
await saveWalletStatePreservingUnlock({
|
|
388
|
+
state,
|
|
389
|
+
provider: options.provider,
|
|
390
|
+
paths: options.paths,
|
|
391
|
+
});
|
|
392
|
+
if (appendEventFn !== undefined) {
|
|
393
|
+
await appendEventFn(options.paths, createMiningEventRecord(state.miningState.currentPublishDecision === "replacing" ? "tx-replaced" : "tx-broadcast", `Mining transaction ${built.txid} is already accepted by the local node.`, {
|
|
394
|
+
runId: options.runId,
|
|
395
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
396
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
397
|
+
domainId: options.candidate.domainId,
|
|
398
|
+
domainName: options.candidate.domainName,
|
|
399
|
+
txid: built.txid,
|
|
400
|
+
feeRateSatVb: nextFeeRate,
|
|
401
|
+
feeSats: numberToSats(built.funded.fee).toString(),
|
|
402
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
state,
|
|
407
|
+
txid: built.txid,
|
|
408
|
+
decision: state.miningState.currentPublishDecision === "replacing"
|
|
409
|
+
? "replaced"
|
|
410
|
+
: "broadcast",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (isBroadcastUnknownError(error)) {
|
|
414
|
+
state = defaultMiningStatePatch(state, {
|
|
415
|
+
currentPublishState: "broadcast-unknown",
|
|
416
|
+
currentPublishDecision: "broadcast-unknown",
|
|
417
|
+
});
|
|
418
|
+
await saveWalletStatePreservingUnlock({
|
|
419
|
+
state,
|
|
420
|
+
provider: options.provider,
|
|
421
|
+
paths: options.paths,
|
|
422
|
+
});
|
|
423
|
+
if (appendEventFn !== undefined) {
|
|
424
|
+
await appendEventFn(options.paths, createMiningEventRecord("error", `Mining broadcast became uncertain for ${built.txid}.`, {
|
|
425
|
+
level: "warn",
|
|
426
|
+
runId: options.runId,
|
|
427
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
428
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
429
|
+
domainId: options.candidate.domainId,
|
|
430
|
+
domainName: options.candidate.domainName,
|
|
431
|
+
txid: built.txid,
|
|
432
|
+
feeRateSatVb: nextFeeRate,
|
|
433
|
+
feeSats: numberToSats(built.funded.fee).toString(),
|
|
434
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
435
|
+
reason: "broadcast-unknown",
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
state,
|
|
440
|
+
txid: built.txid,
|
|
441
|
+
decision: "broadcast-unknown",
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
state = {
|
|
445
|
+
...state,
|
|
446
|
+
miningState: cloneMiningState(priorMiningState),
|
|
447
|
+
};
|
|
448
|
+
await saveWalletStatePreservingUnlock({
|
|
449
|
+
state,
|
|
450
|
+
provider: options.provider,
|
|
451
|
+
paths: options.paths,
|
|
452
|
+
});
|
|
453
|
+
throw new MiningPublishRejectedError(error instanceof Error ? error.message : String(error), state);
|
|
454
|
+
}
|
|
455
|
+
const absoluteFeeSats = numberToSats(built.funded.fee);
|
|
456
|
+
const replacementCount = priorMiningState.currentTxid === null
|
|
457
|
+
? priorMiningState.replacementCount
|
|
458
|
+
: priorMiningState.replacementCount + 1;
|
|
459
|
+
state = defaultMiningStatePatch(state, {
|
|
460
|
+
currentPublishState: "in-mempool",
|
|
461
|
+
livePublishInMempool: true,
|
|
462
|
+
currentPublishDecision: state.miningState.currentPublishDecision === "replacing"
|
|
463
|
+
? "replaced"
|
|
464
|
+
: "broadcast",
|
|
465
|
+
replacementCount,
|
|
466
|
+
currentAbsoluteFeeSats: Number(absoluteFeeSats),
|
|
467
|
+
currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
|
|
468
|
+
sessionFeeSpentSats: (BigInt(state.miningState.sessionFeeSpentSats) + absoluteFeeSats).toString(),
|
|
469
|
+
lifetimeFeeSpentSats: (BigInt(state.miningState.lifetimeFeeSpentSats) + absoluteFeeSats).toString(),
|
|
470
|
+
});
|
|
471
|
+
await saveWalletStatePreservingUnlock({
|
|
472
|
+
state,
|
|
473
|
+
provider: options.provider,
|
|
474
|
+
paths: options.paths,
|
|
475
|
+
});
|
|
476
|
+
if (appendEventFn !== undefined) {
|
|
477
|
+
await appendEventFn(options.paths, createMiningEventRecord(state.miningState.currentPublishDecision === "replaced"
|
|
478
|
+
? "tx-replaced"
|
|
479
|
+
: "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
|
|
480
|
+
? "Replaced"
|
|
481
|
+
: "Broadcast"} mining transaction ${built.txid}.`, {
|
|
482
|
+
runId: options.runId,
|
|
483
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
484
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
485
|
+
domainId: options.candidate.domainId,
|
|
486
|
+
domainName: options.candidate.domainName,
|
|
487
|
+
txid: built.txid,
|
|
488
|
+
feeRateSatVb: nextFeeRate,
|
|
489
|
+
feeSats: absoluteFeeSats.toString(),
|
|
490
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
state,
|
|
495
|
+
txid: built.txid,
|
|
496
|
+
decision: state.miningState.currentPublishDecision === "replaced"
|
|
497
|
+
? "replaced"
|
|
498
|
+
: "broadcast",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
export async function publishCandidate(options) {
|
|
502
|
+
const publishAttempt = options.publishAttempt ?? publishCandidateOnce;
|
|
503
|
+
const createStaleCandidateSkipResult = async (state) => {
|
|
504
|
+
const note = createStaleMiningCandidateWaitingNote();
|
|
505
|
+
await options.appendEventFn(options.paths, createMiningEventRecord("publish-skipped-stale-candidate", "Skipped mining publish for the current tip because the selected root domain is no longer locally mineable.", {
|
|
506
|
+
level: "warn",
|
|
507
|
+
runId: options.runId,
|
|
508
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
509
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
510
|
+
domainId: options.candidate.domainId,
|
|
511
|
+
domainName: options.candidate.domainName,
|
|
512
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
513
|
+
reason: "candidate-unavailable",
|
|
514
|
+
}));
|
|
515
|
+
return {
|
|
516
|
+
state,
|
|
517
|
+
txid: null,
|
|
518
|
+
decision: "publish-skipped-stale-candidate",
|
|
519
|
+
note,
|
|
520
|
+
skipped: true,
|
|
521
|
+
candidate: null,
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
const lockedReadContext = await options.openReadContext({
|
|
525
|
+
dataDir: options.dataDir,
|
|
526
|
+
databasePath: options.databasePath,
|
|
527
|
+
secretProvider: options.provider,
|
|
528
|
+
walletControlLockHeld: true,
|
|
529
|
+
paths: options.paths,
|
|
530
|
+
});
|
|
531
|
+
try {
|
|
532
|
+
if (lockedReadContext.localState.availability !== "ready"
|
|
533
|
+
|| lockedReadContext.localState.state === null
|
|
534
|
+
|| lockedReadContext.snapshot === null
|
|
535
|
+
|| lockedReadContext.model === null) {
|
|
536
|
+
return await createStaleCandidateSkipResult(options.fallbackState);
|
|
537
|
+
}
|
|
538
|
+
const readyReadContext = lockedReadContext;
|
|
539
|
+
const refreshedCandidate = refreshMiningCandidateFromCurrentState(readyReadContext, options.candidate);
|
|
540
|
+
if (refreshedCandidate === null) {
|
|
541
|
+
return await createStaleCandidateSkipResult(readyReadContext.localState.state);
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
const published = await publishAttempt({
|
|
545
|
+
readContext: readyReadContext,
|
|
546
|
+
candidate: refreshedCandidate,
|
|
547
|
+
dataDir: options.dataDir,
|
|
548
|
+
provider: options.provider,
|
|
549
|
+
paths: options.paths,
|
|
550
|
+
attachService: options.attachService,
|
|
551
|
+
rpcFactory: options.rpcFactory,
|
|
552
|
+
runId: options.runId,
|
|
553
|
+
appendEventFn: options.appendEventFn,
|
|
554
|
+
});
|
|
555
|
+
return {
|
|
556
|
+
...published,
|
|
557
|
+
candidate: refreshedCandidate,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (error instanceof Error && error.message === "wallet_mining_mempool_rejected_missing-inputs") {
|
|
562
|
+
const note = createRetryableMiningPublishWaitingNote();
|
|
563
|
+
const revertedState = error instanceof MiningPublishRejectedError
|
|
564
|
+
? error.revertedState
|
|
565
|
+
: readyReadContext.localState.state;
|
|
566
|
+
await options.appendEventFn(options.paths, createMiningEventRecord("publish-retry-pending", "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.", {
|
|
567
|
+
level: "warn",
|
|
568
|
+
runId: options.runId,
|
|
569
|
+
targetBlockHeight: refreshedCandidate.targetBlockHeight,
|
|
570
|
+
referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
|
|
571
|
+
domainId: refreshedCandidate.domainId,
|
|
572
|
+
domainName: refreshedCandidate.domainName,
|
|
573
|
+
score: refreshedCandidate.canonicalBlend.toString(),
|
|
574
|
+
reason: "missing-inputs",
|
|
575
|
+
}));
|
|
576
|
+
return {
|
|
577
|
+
state: revertedState,
|
|
578
|
+
txid: null,
|
|
579
|
+
decision: "publish-retry-pending",
|
|
580
|
+
note,
|
|
581
|
+
retryable: true,
|
|
582
|
+
candidate: refreshedCandidate,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (isInsufficientFundsError(error)) {
|
|
586
|
+
const note = createInsufficientFundsMiningPublishWaitingNote();
|
|
587
|
+
const lastError = createInsufficientFundsMiningPublishErrorMessage();
|
|
588
|
+
await options.appendEventFn(options.paths, createMiningEventRecord("publish-paused-insufficient-funds", "Paused mining publish because Bitcoin Core could not fund the next mining transaction with safe BTC.", {
|
|
589
|
+
level: "warn",
|
|
590
|
+
runId: options.runId,
|
|
591
|
+
targetBlockHeight: refreshedCandidate.targetBlockHeight,
|
|
592
|
+
referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
|
|
593
|
+
domainId: refreshedCandidate.domainId,
|
|
594
|
+
domainName: refreshedCandidate.domainName,
|
|
595
|
+
score: refreshedCandidate.canonicalBlend.toString(),
|
|
596
|
+
reason: "insufficient-funds",
|
|
597
|
+
}));
|
|
598
|
+
return {
|
|
599
|
+
state: readyReadContext.localState.state,
|
|
600
|
+
txid: null,
|
|
601
|
+
decision: "publish-paused-insufficient-funds",
|
|
602
|
+
note,
|
|
603
|
+
lastError,
|
|
604
|
+
skipped: true,
|
|
605
|
+
candidate: null,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
finally {
|
|
612
|
+
await lockedReadContext.close();
|
|
613
|
+
}
|
|
614
|
+
}
|