@cogcoin/client 0.5.15 → 1.0.0

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.
Files changed (172) hide show
  1. package/README.md +80 -25
  2. package/dist/app-paths.d.ts +5 -6
  3. package/dist/app-paths.js +8 -16
  4. package/dist/art/balance.txt +10 -0
  5. package/dist/art/welcome.txt +16 -0
  6. package/dist/bitcoind/bootstrap/controller.d.ts +1 -0
  7. package/dist/bitcoind/bootstrap/controller.js +53 -1
  8. package/dist/bitcoind/client/follow-block-times.d.ts +1 -0
  9. package/dist/bitcoind/client/follow-block-times.js +1 -1
  10. package/dist/bitcoind/client/internal-types.d.ts +7 -3
  11. package/dist/bitcoind/client/managed-client.d.ts +4 -2
  12. package/dist/bitcoind/client/managed-client.js +14 -0
  13. package/dist/bitcoind/client/sync-engine.js +72 -11
  14. package/dist/bitcoind/hash-order.d.ts +4 -0
  15. package/dist/bitcoind/hash-order.js +13 -0
  16. package/dist/bitcoind/indexer-daemon-main.js +11 -3
  17. package/dist/bitcoind/normalize.js +3 -2
  18. package/dist/bitcoind/processing-start-height.d.ts +5 -0
  19. package/dist/bitcoind/processing-start-height.js +7 -0
  20. package/dist/bitcoind/progress/constants.d.ts +4 -0
  21. package/dist/bitcoind/progress/constants.js +4 -0
  22. package/dist/bitcoind/progress/controller.d.ts +2 -1
  23. package/dist/bitcoind/progress/controller.js +3 -3
  24. package/dist/bitcoind/progress/follow-scene.d.ts +6 -2
  25. package/dist/bitcoind/progress/follow-scene.js +29 -6
  26. package/dist/bitcoind/progress/formatting.d.ts +1 -0
  27. package/dist/bitcoind/progress/formatting.js +6 -0
  28. package/dist/bitcoind/progress/train-scene.js +37 -18
  29. package/dist/bitcoind/progress/tty-renderer.d.ts +6 -1
  30. package/dist/bitcoind/progress/tty-renderer.js +8 -4
  31. package/dist/bitcoind/rpc.d.ts +2 -1
  32. package/dist/bitcoind/rpc.js +3 -0
  33. package/dist/bitcoind/types.d.ts +6 -0
  34. package/dist/bytes.d.ts +1 -0
  35. package/dist/bytes.js +3 -0
  36. package/dist/cli/art.d.ts +2 -0
  37. package/dist/cli/art.js +37 -0
  38. package/dist/cli/commands/client-admin.d.ts +2 -0
  39. package/dist/cli/commands/client-admin.js +91 -0
  40. package/dist/cli/commands/follow.js +0 -2
  41. package/dist/cli/commands/mining-admin.js +6 -47
  42. package/dist/cli/commands/mining-read.js +11 -50
  43. package/dist/cli/commands/mining-runtime.js +38 -3
  44. package/dist/cli/commands/service-runtime.js +0 -2
  45. package/dist/cli/commands/status.js +8 -2
  46. package/dist/cli/commands/sync.js +51 -4
  47. package/dist/cli/commands/wallet-admin.js +142 -136
  48. package/dist/cli/commands/wallet-mutation.js +91 -79
  49. package/dist/cli/commands/wallet-read.js +15 -18
  50. package/dist/cli/context.js +4 -14
  51. package/dist/cli/mining-format.d.ts +0 -1
  52. package/dist/cli/mining-format.js +5 -37
  53. package/dist/cli/mining-json.d.ts +0 -18
  54. package/dist/cli/mining-json.js +0 -35
  55. package/dist/cli/mutation-command-groups.d.ts +1 -2
  56. package/dist/cli/mutation-command-groups.js +0 -5
  57. package/dist/cli/mutation-json.d.ts +24 -145
  58. package/dist/cli/mutation-json.js +30 -136
  59. package/dist/cli/mutation-resolved-json.d.ts +0 -7
  60. package/dist/cli/mutation-resolved-json.js +4 -10
  61. package/dist/cli/mutation-success.d.ts +2 -0
  62. package/dist/cli/mutation-success.js +11 -1
  63. package/dist/cli/mutation-text-format.js +1 -3
  64. package/dist/cli/output.d.ts +1 -1
  65. package/dist/cli/output.js +254 -231
  66. package/dist/cli/parse.d.ts +1 -1
  67. package/dist/cli/parse.js +93 -122
  68. package/dist/cli/preview-json.d.ts +17 -120
  69. package/dist/cli/preview-json.js +14 -97
  70. package/dist/cli/prompt.js +8 -13
  71. package/dist/cli/read-json.d.ts +15 -37
  72. package/dist/cli/read-json.js +44 -140
  73. package/dist/cli/runner.js +10 -13
  74. package/dist/cli/types.d.ts +8 -17
  75. package/dist/cli/types.js +0 -2
  76. package/dist/cli/wallet-format.d.ts +1 -0
  77. package/dist/cli/wallet-format.js +205 -144
  78. package/dist/cli/workflow-hints.d.ts +3 -3
  79. package/dist/cli/workflow-hints.js +11 -8
  80. package/dist/client/default-client.d.ts +3 -1
  81. package/dist/client/default-client.js +45 -2
  82. package/dist/client/factory.js +1 -1
  83. package/dist/client/initialization.js +23 -0
  84. package/dist/client/persistence.js +5 -5
  85. package/dist/client/store-adapter.js +1 -0
  86. package/dist/sqlite/checkpoints.d.ts +1 -0
  87. package/dist/sqlite/checkpoints.js +7 -0
  88. package/dist/sqlite/store.js +14 -1
  89. package/dist/types.d.ts +1 -0
  90. package/dist/wallet/coin-control.d.ts +41 -12
  91. package/dist/wallet/coin-control.js +100 -428
  92. package/dist/wallet/descriptor-normalization.d.ts +1 -3
  93. package/dist/wallet/descriptor-normalization.js +0 -16
  94. package/dist/wallet/lifecycle.d.ts +7 -99
  95. package/dist/wallet/lifecycle.js +513 -968
  96. package/dist/wallet/managed-core-wallet.d.ts +13 -0
  97. package/dist/wallet/managed-core-wallet.js +20 -0
  98. package/dist/wallet/mining/constants.d.ts +5 -12
  99. package/dist/wallet/mining/constants.js +5 -12
  100. package/dist/wallet/mining/control.d.ts +1 -13
  101. package/dist/wallet/mining/control.js +45 -349
  102. package/dist/wallet/mining/index.d.ts +3 -4
  103. package/dist/wallet/mining/index.js +1 -2
  104. package/dist/wallet/mining/runner.d.ts +116 -13
  105. package/dist/wallet/mining/runner.js +885 -501
  106. package/dist/wallet/mining/runtime-artifacts.js +23 -3
  107. package/dist/wallet/mining/sentence-protocol.d.ts +44 -0
  108. package/dist/wallet/mining/sentence-protocol.js +123 -0
  109. package/dist/wallet/mining/sentences.d.ts +4 -8
  110. package/dist/wallet/mining/sentences.js +3 -52
  111. package/dist/wallet/mining/state.d.ts +11 -6
  112. package/dist/wallet/mining/state.js +7 -6
  113. package/dist/wallet/mining/types.d.ts +2 -30
  114. package/dist/wallet/mining/visualizer.d.ts +31 -3
  115. package/dist/wallet/mining/visualizer.js +135 -13
  116. package/dist/wallet/read/context.d.ts +0 -2
  117. package/dist/wallet/read/context.js +119 -140
  118. package/dist/wallet/read/filter.js +2 -11
  119. package/dist/wallet/read/index.d.ts +1 -1
  120. package/dist/wallet/read/project.js +24 -77
  121. package/dist/wallet/read/types.d.ts +10 -25
  122. package/dist/wallet/reset.d.ts +0 -1
  123. package/dist/wallet/reset.js +60 -138
  124. package/dist/wallet/root-resolution.d.ts +1 -5
  125. package/dist/wallet/root-resolution.js +0 -18
  126. package/dist/wallet/runtime.d.ts +0 -6
  127. package/dist/wallet/runtime.js +0 -8
  128. package/dist/wallet/state/client-password-agent.js +208 -0
  129. package/dist/wallet/state/client-password.d.ts +65 -0
  130. package/dist/wallet/state/client-password.js +952 -0
  131. package/dist/wallet/state/crypto.d.ts +1 -20
  132. package/dist/wallet/state/crypto.js +0 -63
  133. package/dist/wallet/state/provider.d.ts +23 -11
  134. package/dist/wallet/state/provider.js +248 -290
  135. package/dist/wallet/state/storage.d.ts +2 -2
  136. package/dist/wallet/state/storage.js +48 -16
  137. package/dist/wallet/tx/anchor.d.ts +3 -28
  138. package/dist/wallet/tx/anchor.js +349 -1250
  139. package/dist/wallet/tx/bitcoin-transfer.d.ts +35 -0
  140. package/dist/wallet/tx/bitcoin-transfer.js +200 -0
  141. package/dist/wallet/tx/cog.d.ts +5 -1
  142. package/dist/wallet/tx/cog.js +149 -185
  143. package/dist/wallet/tx/common.d.ts +61 -8
  144. package/dist/wallet/tx/common.js +266 -146
  145. package/dist/wallet/tx/domain-admin.d.ts +3 -1
  146. package/dist/wallet/tx/domain-admin.js +61 -99
  147. package/dist/wallet/tx/domain-market.d.ts +5 -1
  148. package/dist/wallet/tx/domain-market.js +221 -228
  149. package/dist/wallet/tx/field.d.ts +4 -10
  150. package/dist/wallet/tx/field.js +83 -924
  151. package/dist/wallet/tx/identity-selector.d.ts +9 -3
  152. package/dist/wallet/tx/identity-selector.js +17 -35
  153. package/dist/wallet/tx/index.d.ts +3 -1
  154. package/dist/wallet/tx/index.js +2 -1
  155. package/dist/wallet/tx/register.d.ts +3 -1
  156. package/dist/wallet/tx/register.js +62 -220
  157. package/dist/wallet/tx/reputation.d.ts +3 -1
  158. package/dist/wallet/tx/reputation.js +58 -95
  159. package/dist/wallet/types.d.ts +8 -122
  160. package/package.json +5 -5
  161. package/dist/wallet/archive.d.ts +0 -4
  162. package/dist/wallet/archive.js +0 -41
  163. package/dist/wallet/mining/hook-protocol.d.ts +0 -47
  164. package/dist/wallet/mining/hook-protocol.js +0 -161
  165. package/dist/wallet/mining/hook-runner.js +0 -52
  166. package/dist/wallet/mining/hooks.d.ts +0 -38
  167. package/dist/wallet/mining/hooks.js +0 -520
  168. package/dist/wallet/state/explicit-lock.d.ts +0 -4
  169. package/dist/wallet/state/explicit-lock.js +0 -19
  170. package/dist/wallet/state/session.d.ts +0 -12
  171. package/dist/wallet/state/session.js +0 -23
  172. /package/dist/wallet/{mining/hook-runner.d.ts → state/client-password-agent.d.ts} +0 -0
@@ -1,23 +1,15 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { encodeSentence } from "@cogcoin/scoring";
3
- import { getListing, lookupDomain } from "@cogcoin/indexer/queries";
3
+ import { lookupDomain } from "@cogcoin/indexer/queries";
4
4
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
5
5
  import { createRpcClient } from "../../bitcoind/node.js";
6
6
  import { acquireFileLock } from "../fs/lock.js";
7
- import { deriveWalletIdentityMaterial, } from "../material.js";
8
7
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
9
8
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
10
- import { serializeDomainAnchor, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
9
+ import { serializeDomainAnchor, validateDomainName, } from "../cogop/index.js";
11
10
  import { openWalletReadContext } from "../read/index.js";
12
- import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, findSpendableFundingInputsFromTransaction, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, inputMatchesOutpoint, } from "./common.js";
13
- import { confirmYesNo } from "./confirm.js";
14
- const ACTIVE_FAMILY_STATUSES = new Set([
15
- "draft",
16
- "broadcasting",
17
- "broadcast-unknown",
18
- "live",
19
- "repair-required",
20
- ]);
11
+ import { assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, createBuiltWalletMutationFeeSummary, createWalletMutationFeeMetadata, isAlreadyAcceptedError, isBroadcastUnknownError, mergeFixedWalletInputs, outpointKey, pauseMiningForWalletMutation, resolvePendingMutationReuseDecision, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
12
+ import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
21
13
  function normalizeDomainName(domainName) {
22
14
  const normalized = domainName.trim().toLowerCase();
23
15
  if (normalized.length === 0) {
@@ -57,11 +49,6 @@ function createIntentFingerprint(parts) {
57
49
  .update(parts.map((part) => String(part)).join("\n"))
58
50
  .digest("hex");
59
51
  }
60
- function isSpendableConfirmedUtxo(entry) {
61
- return entry.confirmations >= 1
62
- && entry.spendable !== false
63
- && entry.safe !== false;
64
- }
65
52
  function sortUtxos(entries) {
66
53
  return entries
67
54
  .slice()
@@ -69,139 +56,11 @@ function sortUtxos(entries) {
69
56
  || left.txid.localeCompare(right.txid)
70
57
  || left.vout - right.vout);
71
58
  }
72
- function createReservedIdentityRecord(target) {
73
- return {
74
- index: target.localIndex,
75
- scriptPubKeyHex: target.scriptPubKeyHex,
76
- address: target.address,
77
- status: "dedicated",
78
- assignedDomainNames: [],
79
- };
80
- }
81
- function withUpdatedAssignedDomain(options) {
82
- return options.identities.map((identity) => {
83
- let assigned = identity.assignedDomainNames.filter((name) => name !== options.domainName);
84
- if (identity.index === options.targetLocalIndex) {
85
- assigned = [...assigned, options.domainName];
86
- }
87
- if (identity.index !== options.sourceLocalIndex && identity.index !== options.targetLocalIndex) {
88
- assigned = identity.assignedDomainNames.slice();
89
- }
90
- return {
91
- ...identity,
92
- assignedDomainNames: assigned.sort((left, right) => left.localeCompare(right)),
93
- };
94
- });
95
- }
96
- function upsertProactiveFamily(state, family) {
97
- const families = state.proactiveFamilies.slice();
98
- const existingIndex = families.findIndex((entry) => entry.familyId === family.familyId);
99
- if (existingIndex >= 0) {
100
- families[existingIndex] = family;
101
- }
102
- else {
103
- families.push(family);
104
- }
105
- return {
106
- ...state,
107
- proactiveFamilies: families,
108
- };
109
- }
110
- function findAnchorFamilyByIntent(state, intentFingerprintHex) {
111
- return state.proactiveFamilies.find((family) => family.type === "anchor" && family.intentFingerprintHex === intentFingerprintHex) ?? null;
112
- }
113
- function findActiveAnchorFamilyByDomain(state, domainName) {
114
- return state.proactiveFamilies.find((family) => family.type === "anchor"
115
- && family.domainName === domainName
116
- && ACTIVE_FAMILY_STATUSES.has(family.status)) ?? null;
117
- }
118
- function isClearableReservedAnchorFamily(family) {
119
- return family?.type === "anchor"
120
- && family.status === "draft"
121
- && family.currentStep === "reserved";
122
- }
123
- function findAnchorFamilyById(state, familyId) {
124
- return state.proactiveFamilies.find((family) => family.familyId === familyId) ?? null;
125
- }
126
- function collectActivelyReservedDedicatedIndices(state) {
127
- const reservedIndices = new Set();
128
- for (const domain of state.domains) {
129
- if (domain.dedicatedIndex !== null && domain.localAnchorIntent !== "none") {
130
- reservedIndices.add(domain.dedicatedIndex);
131
- }
132
- }
133
- for (const family of state.proactiveFamilies) {
134
- if (family.type === "anchor"
135
- && ACTIVE_FAMILY_STATUSES.has(family.status)
136
- && family.reservedDedicatedIndex !== null
137
- && family.reservedDedicatedIndex !== undefined) {
138
- reservedIndices.add(family.reservedDedicatedIndex);
139
- }
140
- }
141
- return reservedIndices;
142
- }
143
- function selectReusableDedicatedIdentityTarget(state) {
144
- const reservedIndices = collectActivelyReservedDedicatedIndices(state);
145
- const reusableIdentity = state.identities
146
- .filter((identity) => identity.status === "dedicated"
147
- && identity.address !== null
148
- && identity.assignedDomainNames.length === 0
149
- && !reservedIndices.has(identity.index))
150
- .sort((left, right) => left.index - right.index)[0];
151
- if (reusableIdentity == null) {
152
- return null;
153
- }
154
- const material = deriveWalletIdentityMaterial(state.keys.accountXprv, reusableIdentity.index);
155
- const reusableAddress = reusableIdentity.address;
156
- if (reusableAddress === null) {
157
- return null;
158
- }
159
- return {
160
- ...material,
161
- localIndex: reusableIdentity.index,
162
- address: reusableAddress,
163
- scriptPubKeyHex: reusableIdentity.scriptPubKeyHex,
164
- };
165
- }
166
- function selectFreshDedicatedIdentityTarget(state) {
167
- const unavailableIndices = new Set();
168
- for (const identity of state.identities) {
169
- unavailableIndices.add(identity.index);
170
- }
171
- for (const domain of state.domains) {
172
- if (domain.dedicatedIndex !== null) {
173
- unavailableIndices.add(domain.dedicatedIndex);
174
- }
175
- }
176
- for (const index of collectActivelyReservedDedicatedIndices(state)) {
177
- unavailableIndices.add(index);
178
- }
179
- const startIndex = Math.max(1, state.nextDedicatedIndex);
180
- for (let index = startIndex; index <= state.descriptor.rangeEnd; index += 1) {
181
- if (unavailableIndices.has(index)) {
182
- continue;
183
- }
184
- const material = deriveWalletIdentityMaterial(state.keys.accountXprv, index);
185
- return {
186
- ...material,
187
- localIndex: index,
188
- };
189
- }
190
- throw new Error("wallet_anchor_no_fresh_dedicated_index");
191
- }
192
- function selectNextDedicatedIdentityTarget(state) {
193
- return selectReusableDedicatedIdentityTarget(state) ?? selectFreshDedicatedIdentityTarget(state);
194
- }
195
- function deriveAnchorTargetIdentityForIndex(state, localIndex) {
196
- const existingIdentity = state.identities.find((identity) => identity.index === localIndex
197
- && identity.address !== null) ?? null;
198
- const material = deriveWalletIdentityMaterial(state.keys.accountXprv, localIndex);
199
- return {
200
- ...material,
201
- localIndex,
202
- address: existingIdentity?.address ?? material.address,
203
- scriptPubKeyHex: existingIdentity?.scriptPubKeyHex ?? material.scriptPubKeyHex,
204
- };
59
+ function isSpendableFundingUtxo(entry, fundingScriptPubKeyHex) {
60
+ return entry.scriptPubKey === fundingScriptPubKeyHex
61
+ && entry.confirmations >= 1
62
+ && entry.spendable !== false
63
+ && entry.safe !== false;
205
64
  }
206
65
  function encodeFoundingMessage(foundingMessageText) {
207
66
  const trimmed = foundingMessageText?.trim() ?? "";
@@ -249,902 +108,226 @@ async function resolveFoundingMessage(options) {
249
108
  }
250
109
  }
251
110
  }
252
- function resolveAnchorOutpointForSender(state, senderIndex) {
253
- const anchoredDomain = state.domains.find((domain) => domain.currentOwnerLocalIndex === senderIndex
254
- && domain.canonicalChainStatus === "anchored"
255
- && domain.currentCanonicalAnchorOutpoint !== null) ?? null;
256
- if (anchoredDomain?.currentCanonicalAnchorOutpoint === null || anchoredDomain === null) {
257
- return null;
258
- }
259
- return {
260
- txid: anchoredDomain.currentCanonicalAnchorOutpoint.txid,
261
- vout: anchoredDomain.currentCanonicalAnchorOutpoint.vout,
262
- };
263
- }
264
- function isFundingSender(state, sender) {
265
- return sender.scriptPubKeyHex === state.funding.scriptPubKeyHex;
266
- }
267
- async function confirmAnchor(prompter, operation) {
268
- prompter.writeLine(`You are anchoring "${operation.chainDomain.name}" onto dedicated index ${operation.targetIdentity.localIndex}.`);
269
- prompter.writeLine("Anchoring is permanent chain state. This flow uses two transactions and is not rolled back automatically.");
270
- prompter.writeLine(`Dedicated BTC address: ${operation.targetIdentity.address}`);
271
- prompter.writeLine(`Dedicated Ethereum address: ${operation.targetIdentity.ethereumAddress}`);
272
- prompter.writeLine(`Dedicated Nostr npub: ${operation.targetIdentity.nostrNpub}`);
273
- if (operation.foundingMessageText !== null) {
111
+ async function confirmDirectAnchor(prompter, options) {
112
+ prompter.writeLine(`You are anchoring "${options.domainName}".`);
113
+ prompter.writeLine(`Wallet address: ${options.walletAddress}`);
114
+ prompter.writeLine("Anchoring publishes a standalone DOMAIN_ANCHOR from the local wallet address.");
115
+ if (options.foundingMessageText !== null) {
274
116
  prompter.writeLine("The founding message bytes will be public in mempool and on-chain.");
275
- prompter.writeLine(`Founding message: ${operation.foundingMessageText}`);
276
- }
277
- if (operation.hadListing) {
278
- prompter.writeLine("Warning: Tx1 will cancel the current listing for this domain.");
279
- prompter.writeLine("That listing-cancel side effect is not rolled back automatically if Tx2 later fails.");
117
+ prompter.writeLine(`Founding message: ${options.foundingMessageText}`);
280
118
  }
281
119
  const answer = (await prompter.prompt("Type the domain name to continue: ")).trim();
282
- if (answer !== operation.chainDomain.name) {
120
+ if (answer !== options.domainName) {
283
121
  throw new Error("wallet_anchor_confirmation_rejected");
284
122
  }
285
123
  }
286
- async function confirmAnchorClear(prompter, domainName, dedicatedIndex, assumeYes = false) {
287
- const releaseLine = dedicatedIndex === null
288
- ? "This will cancel the local pending anchor reservation."
289
- : `This will cancel the local pending anchor reservation and release dedicated index ${dedicatedIndex} for reuse.`;
290
- await confirmYesNo(prompter, releaseLine, {
291
- assumeYes,
292
- errorCode: "wallet_anchor_clear_confirmation_rejected",
293
- requiresTtyErrorCode: "wallet_anchor_clear_requires_tty",
294
- prompt: `Clear pending anchor for "${domainName}"? [y/N]: `,
295
- });
124
+ function buildDirectAnchorPlan(options) {
125
+ const fundingUtxos = sortUtxos(options.allUtxos.filter((entry) => isSpendableFundingUtxo(entry, options.state.funding.scriptPubKeyHex)));
126
+ const foundingPayload = options.foundingMessagePayloadHex === null
127
+ ? undefined
128
+ : Buffer.from(options.foundingMessagePayloadHex, "hex");
129
+ const opReturnData = serializeDomainAnchor(options.domainId, foundingPayload).opReturnData;
130
+ return {
131
+ fixedInputs: [],
132
+ outputs: [{ data: Buffer.from(opReturnData).toString("hex") }],
133
+ changeAddress: options.state.funding.address,
134
+ changePosition: 1,
135
+ expectedOpReturnScriptHex: encodeOpReturnScript(opReturnData),
136
+ allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
137
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
138
+ };
296
139
  }
297
- function resolveAnchorOperation(context, domainName, foundingMessageText, foundingMessagePayloadHex) {
298
- assertWalletMutationContextReady(context, "wallet_anchor");
299
- const chainDomain = lookupDomain(context.snapshot.state, domainName);
300
- if (chainDomain === null) {
301
- throw new Error("wallet_anchor_domain_not_found");
302
- }
303
- if (chainDomain.anchored) {
304
- throw new Error("wallet_anchor_domain_already_anchored");
140
+ function validateDirectAnchorDraft(decoded, funded, plan) {
141
+ const outputs = decoded.tx.vout;
142
+ if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
143
+ throw new Error("wallet_anchor_opreturn_mismatch");
305
144
  }
306
- const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
307
- const senderIdentity = context.model.identities.find((identity) => identity.scriptPubKeyHex === ownerHex) ?? null;
308
- if (senderIdentity === null || senderIdentity.address === null) {
309
- throw new Error("wallet_anchor_owner_not_locally_controlled");
145
+ if (funded.changepos === -1) {
146
+ if (outputs.length !== 1) {
147
+ throw new Error("wallet_anchor_unexpected_output_count");
148
+ }
149
+ return;
310
150
  }
311
- if (senderIdentity.readOnly) {
312
- throw new Error("wallet_anchor_owner_read_only");
151
+ if (funded.changepos !== plan.changePosition || outputs.length !== 2) {
152
+ throw new Error("wallet_anchor_change_position_mismatch");
313
153
  }
314
- const sourceAnchorOutpoint = isFundingSender(context.localState.state, {
315
- localIndex: senderIdentity.index,
316
- scriptPubKeyHex: senderIdentity.scriptPubKeyHex,
317
- address: senderIdentity.address,
318
- })
319
- ? null
320
- : resolveAnchorOutpointForSender(context.localState.state, senderIdentity.index);
321
- if (sourceAnchorOutpoint === null
322
- && senderIdentity.scriptPubKeyHex !== context.localState.state.funding.scriptPubKeyHex) {
323
- throw new Error("wallet_anchor_owner_identity_not_supported");
154
+ if (outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
155
+ throw new Error("wallet_anchor_change_output_mismatch");
324
156
  }
325
- const targetIdentity = selectNextDedicatedIdentityTarget(context.localState.state);
326
- return {
327
- readContext: context,
328
- state: context.localState.state,
329
- unlockUntilUnixMs: context.localState.unlockUntilUnixMs,
330
- sourceSender: {
331
- localIndex: senderIdentity.index,
332
- scriptPubKeyHex: senderIdentity.scriptPubKeyHex,
333
- address: senderIdentity.address,
334
- },
335
- sourceAnchorOutpoint,
336
- chainDomain,
337
- targetIdentity,
338
- foundingMessageText,
339
- foundingMessagePayloadHex,
340
- hadListing: getListing(context.snapshot.state, chainDomain.domainId) !== null,
341
- };
342
157
  }
343
- function releaseClearedAnchorReservationState(options) {
344
- const family = findAnchorFamilyById(options.state, options.familyId);
345
- const domains = options.state.domains.map((domain) => {
346
- if (domain.name !== options.domainName) {
347
- return domain;
348
- }
158
+ function createDraftAnchorMutation(options) {
159
+ const existing = options.existing ?? null;
160
+ if (existing !== null) {
349
161
  return {
350
- ...domain,
351
- dedicatedIndex: null,
352
- localAnchorIntent: "none",
162
+ ...existing,
163
+ kind: "anchor",
164
+ domainName: options.domainName,
165
+ parentDomainName: null,
166
+ senderScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
167
+ senderLocalIndex: 0,
168
+ intentFingerprintHex: options.intentFingerprintHex,
169
+ status: "draft",
170
+ lastUpdatedAtUnixMs: options.nowUnixMs,
171
+ attemptedTxid: null,
172
+ attemptedWtxid: null,
173
+ ...createWalletMutationFeeMetadata(options.feeSelection),
174
+ temporaryBuilderLockedOutpoints: [],
353
175
  };
354
- });
355
- const nextState = {
356
- ...options.state,
357
- domains,
358
- };
359
- if (family === null) {
360
- return nextState;
361
176
  }
362
- return upsertProactiveFamily(nextState, {
363
- ...family,
364
- status: "canceled",
365
- lastUpdatedAtUnixMs: options.nowUnixMs,
366
- tx1: family.tx1 == null ? family.tx1 : {
367
- ...family.tx1,
368
- status: "canceled",
369
- temporaryBuilderLockedOutpoints: [],
370
- },
371
- tx2: family.tx2 == null ? family.tx2 : {
372
- ...family.tx2,
373
- status: "canceled",
374
- temporaryBuilderLockedOutpoints: [],
375
- },
376
- });
377
- }
378
- function createFamilyTransactionRecord() {
379
177
  return {
178
+ mutationId: randomBytes(12).toString("hex"),
179
+ kind: "anchor",
180
+ domainName: options.domainName,
181
+ parentDomainName: null,
182
+ senderScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
183
+ senderLocalIndex: 0,
184
+ intentFingerprintHex: options.intentFingerprintHex,
380
185
  status: "draft",
186
+ createdAtUnixMs: options.nowUnixMs,
187
+ lastUpdatedAtUnixMs: options.nowUnixMs,
381
188
  attemptedTxid: null,
382
189
  attemptedWtxid: null,
190
+ ...createWalletMutationFeeMetadata(options.feeSelection),
383
191
  temporaryBuilderLockedOutpoints: [],
384
- rawHex: null,
385
192
  };
386
193
  }
387
- function createDraftAnchorFamily(operation, nowUnixMs) {
388
- return {
389
- familyId: randomBytes(12).toString("hex"),
390
- type: "anchor",
391
- status: "draft",
392
- intentFingerprintHex: createIntentFingerprint([
393
- "anchor",
394
- operation.state.walletRootId,
395
- operation.chainDomain.name,
396
- operation.sourceSender.scriptPubKeyHex,
397
- operation.foundingMessagePayloadHex ?? "",
398
- ]),
399
- createdAtUnixMs: nowUnixMs,
400
- lastUpdatedAtUnixMs: nowUnixMs,
401
- domainName: operation.chainDomain.name,
402
- domainId: operation.chainDomain.domainId,
403
- sourceSenderLocalIndex: operation.sourceSender.localIndex,
404
- sourceSenderScriptPubKeyHex: operation.sourceSender.scriptPubKeyHex,
405
- reservedDedicatedIndex: operation.targetIdentity.localIndex,
406
- reservedScriptPubKeyHex: operation.targetIdentity.scriptPubKeyHex,
407
- foundingMessageText: operation.foundingMessageText,
408
- foundingMessagePayloadHex: operation.foundingMessagePayloadHex,
409
- listingCancelCommitted: false,
410
- currentStep: "reserved",
411
- tx1: createFamilyTransactionRecord(),
412
- tx2: createFamilyTransactionRecord(),
194
+ function upsertAnchoredDomainRecord(options) {
195
+ const domains = options.state.domains.slice();
196
+ const existingIndex = domains.findIndex((entry) => entry.name === options.domainName);
197
+ const current = existingIndex >= 0 ? domains[existingIndex] : null;
198
+ const nextRecord = {
199
+ name: options.domainName,
200
+ domainId: options.domainId,
201
+ currentOwnerScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
202
+ canonicalChainStatus: "anchored",
203
+ foundingMessageText: options.foundingMessageText ?? current?.foundingMessageText ?? null,
204
+ birthTime: current?.birthTime ?? options.state.lastWrittenAtUnixMs,
413
205
  };
414
- }
415
- function ensureReservedIdentity(identities, target) {
416
- if (identities.some((identity) => identity.index === target.localIndex)) {
417
- return identities;
206
+ if (existingIndex >= 0) {
207
+ domains[existingIndex] = nextRecord;
418
208
  }
419
- return [...identities, createReservedIdentityRecord(target)]
420
- .sort((left, right) => left.index - right.index);
421
- }
422
- function reserveAnchorFamilyState(state, family, target, foundingMessageText) {
423
- const domains = state.domains.map((domain) => {
424
- if (domain.name !== family.domainName) {
425
- return domain;
426
- }
427
- return {
428
- ...domain,
429
- dedicatedIndex: target.localIndex,
430
- localAnchorIntent: "reserved",
431
- foundingMessageText: foundingMessageText ?? domain.foundingMessageText,
432
- };
433
- });
434
- return {
435
- ...upsertProactiveFamily(state, family),
436
- nextDedicatedIndex: Math.max(state.nextDedicatedIndex, target.localIndex + 1),
437
- identities: ensureReservedIdentity(state.identities, target),
438
- domains,
439
- };
440
- }
441
- function updateAnchorFamilyState(options) {
442
- const nextFamily = {
443
- ...options.family,
444
- status: options.status,
445
- currentStep: options.currentStep,
446
- lastUpdatedAtUnixMs: options.nowUnixMs,
447
- listingCancelCommitted: options.listingCancelCommitted ?? options.family.listingCancelCommitted,
448
- tx1: options.tx1 ?? options.family.tx1 ?? createFamilyTransactionRecord(),
449
- tx2: options.tx2 ?? options.family.tx2 ?? createFamilyTransactionRecord(),
450
- };
451
- let identities = ensureReservedIdentity(options.state.identities, options.target);
452
- if (options.moveOwnershipToTarget) {
453
- identities = withUpdatedAssignedDomain({
454
- identities,
455
- sourceLocalIndex: options.family.sourceSenderLocalIndex ?? null,
456
- targetLocalIndex: options.target.localIndex,
457
- domainName: options.family.domainName ?? "",
458
- });
209
+ else {
210
+ domains.push(nextRecord);
459
211
  }
460
- const domains = options.state.domains.map((domain) => {
461
- if (domain.name !== options.family.domainName) {
462
- return domain;
463
- }
464
- return {
465
- ...domain,
466
- dedicatedIndex: options.target.localIndex,
467
- currentOwnerScriptPubKeyHex: options.moveOwnershipToTarget
468
- ? options.target.scriptPubKeyHex
469
- : domain.currentOwnerScriptPubKeyHex,
470
- currentOwnerLocalIndex: options.moveOwnershipToTarget
471
- ? options.target.localIndex
472
- : domain.currentOwnerLocalIndex,
473
- localAnchorIntent: options.localAnchorIntent,
474
- canonicalChainStatus: options.canonicalChainStatus ?? domain.canonicalChainStatus,
475
- currentCanonicalAnchorOutpoint: options.currentCanonicalAnchorOutpoint ?? domain.currentCanonicalAnchorOutpoint,
476
- foundingMessageText: options.family.foundingMessageText ?? domain.foundingMessageText,
477
- };
478
- });
479
212
  return {
480
- ...upsertProactiveFamily(options.state, nextFamily),
481
- identities,
213
+ ...options.state,
482
214
  domains,
483
215
  };
484
216
  }
485
- function buildTx1Plan(options) {
486
- const fundingUtxos = sortUtxos(options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
487
- && isSpendableConfirmedUtxo(entry)));
488
- const outputs = [
489
- { data: Buffer.from(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData).toString("hex") },
490
- { [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
491
- ];
492
- if (options.operation.sourceAnchorOutpoint === null) {
493
- return {
494
- sender: options.operation.sourceSender,
495
- changeAddress: options.state.funding.address,
496
- fixedInputs: [],
497
- outputs,
498
- changePosition: 2,
499
- expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
500
- expectedProvisionalAnchorScriptHex: options.operation.targetIdentity.scriptPubKeyHex,
501
- expectedProvisionalAnchorValueSats: BigInt(options.state.anchorValueSats),
502
- expectedReplacementAnchorScriptHex: null,
503
- expectedReplacementAnchorValueSats: null,
504
- allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
505
- eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
506
- requiredSenderOutpoint: null,
507
- requiredProvisionalOutpoint: null,
508
- errorPrefix: "wallet_anchor_tx1",
509
- };
217
+ function anchorConfirmedOnSnapshot(options) {
218
+ const chainDomain = lookupDomain(options.snapshot.state, options.domainName);
219
+ if (chainDomain === null || !chainDomain.anchored) {
220
+ return false;
510
221
  }
511
- const sourceAnchor = options.allUtxos.find((entry) => entry.txid === options.operation.sourceAnchorOutpoint?.txid
512
- && entry.vout === options.operation.sourceAnchorOutpoint.vout
513
- && entry.scriptPubKey === options.operation.sourceSender.scriptPubKeyHex
514
- && isSpendableConfirmedUtxo(entry));
515
- if (sourceAnchor === undefined) {
516
- throw new Error("wallet_anchor_source_anchor_missing");
517
- }
518
- outputs.push({
519
- [options.operation.sourceSender.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)),
520
- });
521
- return {
522
- sender: options.operation.sourceSender,
523
- changeAddress: options.state.funding.address,
524
- fixedInputs: [{ txid: sourceAnchor.txid, vout: sourceAnchor.vout }],
525
- outputs,
526
- changePosition: 3,
527
- expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
528
- expectedProvisionalAnchorScriptHex: options.operation.targetIdentity.scriptPubKeyHex,
529
- expectedProvisionalAnchorValueSats: BigInt(options.state.anchorValueSats),
530
- expectedReplacementAnchorScriptHex: options.operation.sourceSender.scriptPubKeyHex,
531
- expectedReplacementAnchorValueSats: BigInt(options.state.anchorValueSats),
532
- allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
533
- eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
534
- requiredSenderOutpoint: options.operation.sourceAnchorOutpoint,
535
- requiredProvisionalOutpoint: null,
536
- errorPrefix: "wallet_anchor_tx1",
537
- };
222
+ const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
223
+ return ownerHex === options.state.funding.scriptPubKeyHex
224
+ || (options.state.localScriptPubKeyHexes ?? []).includes(ownerHex);
538
225
  }
539
- function buildTx2Plan(options) {
540
- const tx1Txid = options.family.tx1?.attemptedTxid;
541
- if (tx1Txid === null || tx1Txid === undefined) {
542
- throw new Error("wallet_anchor_tx1_missing");
543
- }
544
- const provisional = options.allUtxos.find((entry) => entry.txid === tx1Txid
545
- && entry.vout === 1
546
- && entry.scriptPubKey === options.operation.targetIdentity.scriptPubKeyHex
547
- && entry.spendable !== false
548
- && entry.safe !== false);
549
- if (provisional === undefined) {
550
- throw new Error("wallet_anchor_provisional_anchor_missing");
551
- }
552
- const fundingUtxos = sortUtxos(options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
553
- && isSpendableConfirmedUtxo(entry)));
554
- const tx1FundingChangeInputs = findSpendableFundingInputsFromTransaction({
555
- allUtxos: options.allUtxos,
556
- txid: tx1Txid,
557
- fundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
558
- minConf: 0,
559
- });
560
- const foundingPayload = options.operation.foundingMessagePayloadHex === null
561
- ? undefined
562
- : Buffer.from(options.operation.foundingMessagePayloadHex, "hex");
563
- const opReturnData = serializeDomainAnchor(options.operation.chainDomain.domainId, foundingPayload).opReturnData;
564
- return {
565
- sender: {
566
- localIndex: options.operation.targetIdentity.localIndex,
567
- scriptPubKeyHex: options.operation.targetIdentity.scriptPubKeyHex,
568
- address: options.operation.targetIdentity.address,
569
- },
570
- changeAddress: options.state.funding.address,
571
- fixedInputs: [
572
- { txid: provisional.txid, vout: provisional.vout },
573
- ...tx1FundingChangeInputs,
574
- ],
575
- outputs: [
576
- { data: Buffer.from(opReturnData).toString("hex") },
577
- { [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
578
- ],
579
- changePosition: 2,
580
- expectedOpReturnScriptHex: encodeOpReturnScript(opReturnData),
581
- expectedProvisionalAnchorScriptHex: options.operation.targetIdentity.scriptPubKeyHex,
582
- expectedProvisionalAnchorValueSats: BigInt(options.state.anchorValueSats),
583
- expectedReplacementAnchorScriptHex: null,
584
- expectedReplacementAnchorValueSats: null,
585
- allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
586
- eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
587
- requiredSenderOutpoint: null,
588
- requiredProvisionalOutpoint: {
589
- txid: provisional.txid,
590
- vout: provisional.vout,
591
- },
592
- errorPrefix: "wallet_anchor_tx2",
226
+ async function saveState(options) {
227
+ const nextState = {
228
+ ...options.state,
229
+ stateRevision: options.state.stateRevision + 1,
230
+ lastWrittenAtUnixMs: options.nowUnixMs,
593
231
  };
594
- }
595
- function validateTx1Draft(decoded, funded, plan) {
596
- const inputs = decoded.tx.vin;
597
- const outputs = decoded.tx.vout;
598
- if (inputs.length === 0) {
599
- throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
600
- }
601
- assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
602
- const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(decoded, 0);
603
- if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
604
- throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
605
- }
606
- if (plan.requiredSenderOutpoint !== null) {
607
- if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
608
- throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
609
- }
610
- }
611
- assertFundingInputsAfterFixedPrefix({
612
- decoded,
613
- fixedInputs: plan.fixedInputs,
614
- allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
615
- eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
616
- errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
232
+ await saveWalletStatePreservingUnlock({
233
+ state: nextState,
234
+ provider: options.provider,
235
+ nowUnixMs: options.nowUnixMs,
236
+ paths: options.paths,
617
237
  });
618
- if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
619
- throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
620
- }
621
- if (outputs[1]?.scriptPubKey?.hex !== plan.expectedProvisionalAnchorScriptHex) {
622
- throw new Error(`${plan.errorPrefix}_provisional_anchor_output_mismatch`);
623
- }
624
- if (valueToSats(outputs[1]?.value ?? 0) !== plan.expectedProvisionalAnchorValueSats) {
625
- throw new Error(`${plan.errorPrefix}_provisional_anchor_value_mismatch`);
626
- }
627
- const expectedWithoutChange = plan.expectedReplacementAnchorScriptHex === null ? 2 : 3;
628
- if (plan.expectedReplacementAnchorScriptHex !== null) {
629
- if (outputs[2]?.scriptPubKey?.hex !== plan.expectedReplacementAnchorScriptHex) {
630
- throw new Error(`${plan.errorPrefix}_replacement_anchor_output_mismatch`);
631
- }
632
- if (valueToSats(outputs[2]?.value ?? 0) !== (plan.expectedReplacementAnchorValueSats ?? 0n)) {
633
- throw new Error(`${plan.errorPrefix}_replacement_anchor_value_mismatch`);
634
- }
635
- }
636
- if (funded.changepos === -1) {
637
- if (outputs.length !== expectedWithoutChange) {
638
- throw new Error(`${plan.errorPrefix}_unexpected_output_count`);
639
- }
640
- return;
641
- }
642
- if (funded.changepos !== plan.changePosition || outputs.length !== expectedWithoutChange + 1) {
643
- throw new Error(`${plan.errorPrefix}_change_position_mismatch`);
644
- }
645
- if (outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
646
- throw new Error(`${plan.errorPrefix}_change_output_mismatch`);
647
- }
238
+ return nextState;
648
239
  }
649
- function validateTx2Draft(decoded, funded, plan) {
650
- const inputs = decoded.tx.vin;
651
- const outputs = decoded.tx.vout;
652
- if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
653
- throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
654
- }
655
- assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
656
- const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(decoded, 0);
657
- if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
658
- || !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
659
- throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
660
- }
661
- assertFundingInputsAfterFixedPrefix({
662
- decoded,
663
- fixedInputs: plan.fixedInputs,
664
- allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
665
- eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
666
- errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
667
- });
668
- if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
669
- throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
670
- }
671
- if (outputs[1]?.scriptPubKey?.hex !== plan.expectedProvisionalAnchorScriptHex) {
672
- throw new Error(`${plan.errorPrefix}_canonical_anchor_output_mismatch`);
673
- }
674
- if (valueToSats(outputs[1]?.value ?? 0) !== plan.expectedProvisionalAnchorValueSats) {
675
- throw new Error(`${plan.errorPrefix}_canonical_anchor_value_mismatch`);
676
- }
677
- const expectedWithoutChange = 2;
678
- if (funded.changepos === -1) {
679
- if (outputs.length !== expectedWithoutChange) {
680
- throw new Error(`${plan.errorPrefix}_unexpected_output_count`);
681
- }
682
- return;
683
- }
684
- if (funded.changepos !== plan.changePosition || outputs.length !== expectedWithoutChange + 1) {
685
- throw new Error(`${plan.errorPrefix}_change_position_mismatch`);
686
- }
687
- if (outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
688
- throw new Error(`${plan.errorPrefix}_change_output_mismatch`);
240
+ async function reconcilePendingAnchorMutation(options) {
241
+ if (options.mutation.status === "repair-required") {
242
+ return {
243
+ state: options.state,
244
+ mutation: options.mutation,
245
+ resolution: "repair-required",
246
+ };
689
247
  }
690
- }
691
- async function buildTx1(options) {
692
- return buildWalletMutationTransactionWithReserveFallback({
693
- rpc: options.rpc,
694
- walletName: options.walletName,
248
+ if (options.context.snapshot !== null && anchorConfirmedOnSnapshot({
249
+ snapshot: options.context.snapshot,
695
250
  state: options.state,
696
- plan: options.plan,
697
- validateFundedDraft: validateTx1Draft,
698
- finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
699
- mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
700
- reserveCandidates: options.state.proactiveReserveOutpoints,
701
- });
702
- }
703
- async function buildTx2(options) {
704
- return buildWalletMutationTransactionWithReserveFallback({
705
- rpc: options.rpc,
706
- walletName: options.walletName,
707
- state: options.state,
708
- plan: options.plan,
709
- validateFundedDraft: validateTx2Draft,
710
- finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
711
- mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
712
- availableFundingMinConf: 0,
713
- reserveCandidates: options.state.proactiveReserveOutpoints,
714
- });
715
- }
716
- async function relockAnchorOutpoint(rpc, walletName, outpoint) {
717
- if (outpoint === null) {
718
- return;
719
- }
720
- await rpc.lockUnspent(walletName, false, [outpoint]).catch(() => undefined);
721
- }
722
- function resolveAcceptedFamilyStatus(options) {
723
- const chainDomain = options.snapshot === null || options.family.domainName == null
724
- ? null
725
- : lookupDomain(options.snapshot.state, options.family.domainName);
726
- if (chainDomain === null) {
727
- return "live";
728
- }
729
- const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
730
- return chainDomain.anchored && ownerHex === options.target.scriptPubKeyHex
731
- ? "confirmed"
732
- : "live";
733
- }
734
- async function reconcileAnchorFamily(options) {
735
- const chainDomain = lookupDomain(options.operation.readContext.snapshot.state, options.operation.chainDomain.name);
736
- const targetScript = options.operation.targetIdentity.scriptPubKeyHex;
737
- if (chainDomain !== null) {
738
- const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
739
- if (chainDomain.anchored && ownerHex === targetScript) {
740
- const nextState = updateAnchorFamilyState({
741
- state: options.state,
742
- family: options.family,
743
- target: options.operation.targetIdentity,
744
- status: "confirmed",
745
- localAnchorIntent: "none",
746
- currentStep: "tx2",
747
- nowUnixMs: options.nowUnixMs,
748
- tx1: options.family.tx1 == null ? undefined : { ...options.family.tx1, status: "confirmed", temporaryBuilderLockedOutpoints: [] },
749
- tx2: options.family.tx2 == null ? undefined : { ...options.family.tx2, status: "confirmed", temporaryBuilderLockedOutpoints: [] },
750
- moveOwnershipToTarget: true,
751
- canonicalChainStatus: "anchored",
752
- currentCanonicalAnchorOutpoint: options.family.tx2?.attemptedTxid == null
753
- ? options.state.domains.find((domain) => domain.name === options.family.domainName)?.currentCanonicalAnchorOutpoint ?? null
754
- : {
755
- txid: options.family.tx2.attemptedTxid,
756
- vout: 1,
757
- valueSats: options.state.anchorValueSats,
758
- },
759
- });
760
- await saveWalletStatePreservingUnlock({
761
- state: {
762
- ...nextState,
763
- stateRevision: nextState.stateRevision + 1,
764
- lastWrittenAtUnixMs: options.nowUnixMs,
765
- },
766
- provider: options.provider,
767
- unlockUntilUnixMs: options.unlockUntilUnixMs,
768
- nowUnixMs: options.nowUnixMs,
769
- paths: options.paths,
770
- });
771
- return {
251
+ domainName: options.mutation.domainName,
252
+ })) {
253
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
254
+ const confirmedMutation = updateMutationRecord(options.mutation, "confirmed", options.nowUnixMs, {
255
+ temporaryBuilderLockedOutpoints: [],
256
+ });
257
+ const chainDomain = lookupDomain(options.context.snapshot.state, options.mutation.domainName);
258
+ const nextState = upsertAnchoredDomainRecord({
259
+ state: upsertPendingMutation(options.state, confirmedMutation),
260
+ domainName: options.mutation.domainName,
261
+ domainId: chainDomain?.domainId ?? 0,
262
+ foundingMessageText: options.foundingMessageText,
263
+ });
264
+ return {
265
+ state: await saveState({
772
266
  state: nextState,
773
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
774
- ...options.family,
775
- status: "confirmed",
776
- },
777
- resolution: "confirmed",
778
- };
779
- }
780
- if (ownerHex === targetScript && !chainDomain.anchored) {
781
- const nextState = updateAnchorFamilyState({
782
- state: options.state,
783
- family: options.family,
784
- target: options.operation.targetIdentity,
785
- status: "repair-required",
786
- localAnchorIntent: "repair-required",
787
- currentStep: "tx2",
788
- nowUnixMs: options.nowUnixMs,
789
- listingCancelCommitted: true,
790
- moveOwnershipToTarget: true,
791
- });
792
- await saveWalletStatePreservingUnlock({
793
- state: {
794
- ...nextState,
795
- stateRevision: nextState.stateRevision + 1,
796
- lastWrittenAtUnixMs: options.nowUnixMs,
797
- },
798
267
  provider: options.provider,
799
- unlockUntilUnixMs: options.unlockUntilUnixMs,
800
268
  nowUnixMs: options.nowUnixMs,
801
269
  paths: options.paths,
802
- });
803
- return {
804
- state: nextState,
805
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
806
- ...options.family,
807
- status: "repair-required",
808
- },
809
- resolution: "repair-required",
810
- };
811
- }
812
- }
813
- const mempool = await options.rpc.getRawMempool().catch(() => []);
814
- if (options.family.tx2?.attemptedTxid != null && mempool.includes(options.family.tx2.attemptedTxid)) {
815
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.family.tx2.temporaryBuilderLockedOutpoints);
816
- const nextState = updateAnchorFamilyState({
817
- state: options.state,
818
- family: options.family,
819
- target: options.operation.targetIdentity,
820
- status: "live",
821
- localAnchorIntent: "tx2-live",
822
- currentStep: "tx2",
823
- nowUnixMs: options.nowUnixMs,
824
- tx2: {
825
- ...options.family.tx2,
826
- status: "live",
827
- temporaryBuilderLockedOutpoints: [],
828
- },
829
- listingCancelCommitted: true,
830
- moveOwnershipToTarget: true,
831
- currentCanonicalAnchorOutpoint: {
832
- txid: options.family.tx2.attemptedTxid,
833
- vout: 1,
834
- valueSats: options.state.anchorValueSats,
835
- },
836
- });
837
- await relockAnchorOutpoint(options.rpc, options.walletName, {
838
- txid: options.family.tx2.attemptedTxid,
839
- vout: 1,
840
- });
841
- await saveWalletStatePreservingUnlock({
842
- state: {
843
- ...nextState,
844
- stateRevision: nextState.stateRevision + 1,
845
- lastWrittenAtUnixMs: options.nowUnixMs,
846
- },
847
- provider: options.provider,
848
- unlockUntilUnixMs: options.unlockUntilUnixMs,
849
- nowUnixMs: options.nowUnixMs,
850
- paths: options.paths,
851
- });
852
- return {
853
- state: nextState,
854
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
855
- ...options.family,
856
- status: "live",
857
- },
858
- resolution: "live",
270
+ }),
271
+ mutation: confirmedMutation,
272
+ resolution: "confirmed",
859
273
  };
860
274
  }
861
- if (options.family.tx1?.attemptedTxid != null && mempool.includes(options.family.tx1.attemptedTxid)) {
862
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.family.tx1.temporaryBuilderLockedOutpoints);
863
- const nextState = updateAnchorFamilyState({
864
- state: options.state,
865
- family: options.family,
866
- target: options.operation.targetIdentity,
867
- status: "live",
868
- localAnchorIntent: "tx1-live",
869
- currentStep: "tx1",
870
- nowUnixMs: options.nowUnixMs,
871
- tx1: {
872
- ...options.family.tx1,
873
- status: "live",
275
+ if (options.mutation.attemptedTxid !== null) {
276
+ const mempool = await options.rpc.getRawMempool().catch(() => []);
277
+ if (mempool.includes(options.mutation.attemptedTxid)) {
278
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
279
+ const liveMutation = updateMutationRecord(options.mutation, "live", options.nowUnixMs, {
874
280
  temporaryBuilderLockedOutpoints: [],
875
- },
876
- listingCancelCommitted: options.operation.hadListing,
877
- moveOwnershipToTarget: true,
878
- });
879
- if (options.operation.sourceAnchorOutpoint !== null) {
880
- await relockAnchorOutpoint(options.rpc, options.walletName, {
881
- txid: options.family.tx1.attemptedTxid,
882
- vout: 2,
883
281
  });
282
+ const domainId = (options.context.snapshot === null
283
+ ? null
284
+ : lookupDomain(options.context.snapshot.state, options.mutation.domainName)?.domainId)
285
+ ?? options.state.domains.find((domain) => domain.name === options.mutation.domainName)?.domainId
286
+ ?? 0;
287
+ const nextState = upsertAnchoredDomainRecord({
288
+ state: upsertPendingMutation(options.state, liveMutation),
289
+ domainName: options.mutation.domainName,
290
+ domainId,
291
+ foundingMessageText: options.foundingMessageText,
292
+ });
293
+ return {
294
+ state: await saveState({
295
+ state: nextState,
296
+ provider: options.provider,
297
+ nowUnixMs: options.nowUnixMs,
298
+ paths: options.paths,
299
+ }),
300
+ mutation: liveMutation,
301
+ resolution: "live",
302
+ };
884
303
  }
885
- await saveWalletStatePreservingUnlock({
886
- state: {
887
- ...nextState,
888
- stateRevision: nextState.stateRevision + 1,
889
- lastWrittenAtUnixMs: options.nowUnixMs,
890
- },
891
- provider: options.provider,
892
- unlockUntilUnixMs: options.unlockUntilUnixMs,
893
- nowUnixMs: options.nowUnixMs,
894
- paths: options.paths,
895
- });
896
- return {
897
- state: nextState,
898
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
899
- ...options.family,
900
- status: "live",
901
- },
902
- resolution: "ready-for-tx2",
903
- };
904
- }
905
- if (options.family.currentStep === "tx2" || options.family.tx2?.attemptedTxid != null) {
906
- const nextState = updateAnchorFamilyState({
907
- state: options.state,
908
- family: options.family,
909
- target: options.operation.targetIdentity,
910
- status: "repair-required",
911
- localAnchorIntent: "repair-required",
912
- currentStep: "tx2",
913
- nowUnixMs: options.nowUnixMs,
914
- listingCancelCommitted: true,
915
- moveOwnershipToTarget: true,
916
- });
917
- await saveWalletStatePreservingUnlock({
918
- state: {
919
- ...nextState,
920
- stateRevision: nextState.stateRevision + 1,
921
- lastWrittenAtUnixMs: options.nowUnixMs,
922
- },
923
- provider: options.provider,
924
- unlockUntilUnixMs: options.unlockUntilUnixMs,
925
- nowUnixMs: options.nowUnixMs,
926
- paths: options.paths,
927
- });
928
- return {
929
- state: nextState,
930
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
931
- ...options.family,
932
- status: "repair-required",
933
- },
934
- resolution: "repair-required",
935
- };
936
304
  }
937
- if (ACTIVE_FAMILY_STATUSES.has(options.family.status)) {
938
- const nextState = updateAnchorFamilyState({
939
- state: options.state,
940
- family: options.family,
941
- target: options.operation.targetIdentity,
942
- status: "canceled",
943
- localAnchorIntent: "none",
944
- currentStep: options.family.currentStep,
945
- nowUnixMs: options.nowUnixMs,
946
- tx1: options.family.tx1 == null ? undefined : {
947
- ...options.family.tx1,
948
- status: "canceled",
949
- temporaryBuilderLockedOutpoints: [],
950
- },
951
- tx2: options.family.tx2 == null ? undefined : {
952
- ...options.family.tx2,
953
- status: "canceled",
954
- temporaryBuilderLockedOutpoints: [],
955
- },
956
- });
957
- await saveWalletStatePreservingUnlock({
958
- state: {
959
- ...nextState,
960
- stateRevision: nextState.stateRevision + 1,
961
- lastWrittenAtUnixMs: options.nowUnixMs,
962
- },
963
- provider: options.provider,
964
- unlockUntilUnixMs: options.unlockUntilUnixMs,
965
- nowUnixMs: options.nowUnixMs,
966
- paths: options.paths,
305
+ if (options.mutation.status === "broadcast-unknown"
306
+ || options.mutation.status === "live"
307
+ || options.mutation.status === "draft"
308
+ || options.mutation.status === "broadcasting") {
309
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
310
+ const canceledMutation = updateMutationRecord(options.mutation, "canceled", options.nowUnixMs, {
311
+ temporaryBuilderLockedOutpoints: [],
967
312
  });
313
+ const nextState = upsertPendingMutation(options.state, canceledMutation);
968
314
  return {
969
- state: nextState,
970
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
971
- ...options.family,
972
- status: "canceled",
973
- },
315
+ state: await saveState({
316
+ state: nextState,
317
+ provider: options.provider,
318
+ nowUnixMs: options.nowUnixMs,
319
+ paths: options.paths,
320
+ }),
321
+ mutation: canceledMutation,
974
322
  resolution: "not-seen",
975
323
  };
976
324
  }
977
325
  return {
978
326
  state: options.state,
979
- family: options.family,
327
+ mutation: options.mutation,
980
328
  resolution: "continue",
981
329
  };
982
330
  }
983
- function createBroadcastingTxRecord(built) {
984
- return {
985
- status: "broadcasting",
986
- attemptedTxid: built.txid,
987
- attemptedWtxid: built.wtxid,
988
- temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
989
- rawHex: built.rawHex,
990
- };
991
- }
992
- async function saveState(state, provider, unlockUntilUnixMs, nowUnixMs, paths) {
993
- const nextState = {
994
- ...state,
995
- stateRevision: state.stateRevision + 1,
996
- lastWrittenAtUnixMs: nowUnixMs,
997
- };
998
- await saveWalletStatePreservingUnlock({
999
- state: nextState,
1000
- provider,
1001
- unlockUntilUnixMs,
1002
- nowUnixMs,
1003
- paths,
1004
- });
1005
- return nextState;
1006
- }
1007
- async function submitTx2(options) {
1008
- let nextState = options.state;
1009
- let family = options.family;
1010
- const tx2Plan = buildTx2Plan({
1011
- state: nextState,
1012
- allUtxos: await options.rpc.listUnspent(options.walletName, 0),
1013
- operation: options.operation,
1014
- family,
1015
- });
1016
- const builtTx2 = await buildTx2({
1017
- rpc: options.rpc,
1018
- walletName: options.walletName,
1019
- state: nextState,
1020
- plan: tx2Plan,
1021
- });
1022
- const broadcastingTx2 = createBroadcastingTxRecord(builtTx2);
1023
- family = {
1024
- ...family,
1025
- status: "broadcasting",
1026
- currentStep: "tx2",
1027
- tx2: broadcastingTx2,
1028
- };
1029
- nextState = updateAnchorFamilyState({
1030
- state: nextState,
1031
- family,
1032
- target: options.operation.targetIdentity,
1033
- status: "broadcasting",
1034
- localAnchorIntent: "tx1-live",
1035
- currentStep: "tx2",
1036
- nowUnixMs: options.nowUnixMs,
1037
- tx2: broadcastingTx2,
1038
- listingCancelCommitted: true,
1039
- moveOwnershipToTarget: true,
1040
- });
1041
- nextState = await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1042
- ensureSameTipHeight(options.readContext, (await options.rpc.getBlockchainInfo()).blocks, "wallet_anchor_tip_mismatch");
1043
- try {
1044
- await options.rpc.sendRawTransaction(builtTx2.rawHex);
1045
- }
1046
- catch (error) {
1047
- if (!isAlreadyAcceptedError(error)) {
1048
- if (isBroadcastUnknownError(error)) {
1049
- family = {
1050
- ...family,
1051
- status: "broadcast-unknown",
1052
- tx2: {
1053
- ...broadcastingTx2,
1054
- status: "broadcast-unknown",
1055
- },
1056
- };
1057
- nextState = updateAnchorFamilyState({
1058
- state: nextState,
1059
- family,
1060
- target: options.operation.targetIdentity,
1061
- status: "broadcast-unknown",
1062
- localAnchorIntent: "tx1-live",
1063
- currentStep: "tx2",
1064
- nowUnixMs: options.nowUnixMs,
1065
- tx2: family.tx2,
1066
- listingCancelCommitted: true,
1067
- moveOwnershipToTarget: true,
1068
- });
1069
- await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1070
- throw new Error("wallet_anchor_tx2_broadcast_unknown");
1071
- }
1072
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, builtTx2.temporaryBuilderLockedOutpoints);
1073
- family = {
1074
- ...family,
1075
- status: "repair-required",
1076
- tx2: {
1077
- ...broadcastingTx2,
1078
- status: "repair-required",
1079
- temporaryBuilderLockedOutpoints: [],
1080
- },
1081
- };
1082
- nextState = updateAnchorFamilyState({
1083
- state: nextState,
1084
- family,
1085
- target: options.operation.targetIdentity,
1086
- status: "repair-required",
1087
- localAnchorIntent: "repair-required",
1088
- currentStep: "tx2",
1089
- nowUnixMs: options.nowUnixMs,
1090
- tx2: family.tx2,
1091
- listingCancelCommitted: true,
1092
- moveOwnershipToTarget: true,
1093
- });
1094
- await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1095
- throw error;
1096
- }
1097
- }
1098
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, builtTx2.temporaryBuilderLockedOutpoints);
1099
- const finalStatus = resolveAcceptedFamilyStatus({
1100
- snapshot: options.readContext.snapshot,
1101
- family,
1102
- target: options.operation.targetIdentity,
1103
- });
1104
- family = {
1105
- ...family,
1106
- status: finalStatus,
1107
- currentStep: "tx2",
1108
- tx2: {
1109
- ...broadcastingTx2,
1110
- status: finalStatus,
1111
- temporaryBuilderLockedOutpoints: [],
1112
- },
1113
- };
1114
- nextState = updateAnchorFamilyState({
1115
- state: nextState,
1116
- family,
1117
- target: options.operation.targetIdentity,
1118
- status: finalStatus,
1119
- localAnchorIntent: finalStatus === "confirmed" ? "none" : "tx2-live",
1120
- currentStep: "tx2",
1121
- nowUnixMs: options.nowUnixMs,
1122
- tx2: family.tx2,
1123
- listingCancelCommitted: true,
1124
- moveOwnershipToTarget: true,
1125
- canonicalChainStatus: finalStatus === "confirmed" ? "anchored" : undefined,
1126
- currentCanonicalAnchorOutpoint: {
1127
- txid: builtTx2.txid,
1128
- vout: 1,
1129
- valueSats: nextState.anchorValueSats,
1130
- },
1131
- });
1132
- nextState = await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1133
- await relockAnchorOutpoint(options.rpc, options.walletName, {
1134
- txid: builtTx2.txid,
1135
- vout: 1,
1136
- });
1137
- return {
1138
- domainName: options.operation.chainDomain.name,
1139
- txid: builtTx2.txid,
1140
- tx1Txid: family.tx1?.attemptedTxid ?? "unknown",
1141
- tx2Txid: builtTx2.txid,
1142
- dedicatedIndex: options.operation.targetIdentity.localIndex,
1143
- status: finalStatus,
1144
- reusedExisting: false,
1145
- foundingMessageText: options.operation.foundingMessageText,
1146
- };
1147
- }
1148
331
  function ensureSameTipHeight(context, bestHeight, errorCode) {
1149
332
  if (context.snapshot?.tip?.height !== bestHeight) {
1150
333
  throw new Error(errorCode);
@@ -1157,21 +340,16 @@ export async function anchorDomain(options) {
1157
340
  const provider = options.provider ?? createDefaultWalletSecretProvider();
1158
341
  const nowUnixMs = options.nowUnixMs ?? Date.now();
1159
342
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
343
+ const normalizedDomainName = normalizeDomainName(options.domainName);
1160
344
  const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1161
345
  purpose: "wallet-anchor",
1162
346
  walletRootId: null,
1163
347
  });
1164
- const normalizedDomainName = normalizeDomainName(options.domainName);
1165
348
  try {
1166
349
  const miningPreemption = await pauseMiningForWalletMutation({
1167
350
  paths,
1168
351
  reason: "wallet-anchor",
1169
352
  });
1170
- const message = await resolveFoundingMessage({
1171
- foundingMessageText: options.foundingMessageText,
1172
- promptForFoundingMessageWhenMissing: options.promptForFoundingMessageWhenMissing,
1173
- prompter: options.prompter,
1174
- });
1175
353
  const readContext = await (options.openReadContext ?? openWalletReadContext)({
1176
354
  dataDir: options.dataDir,
1177
355
  databasePath: options.databasePath,
@@ -1180,301 +358,222 @@ export async function anchorDomain(options) {
1180
358
  paths,
1181
359
  });
1182
360
  try {
1183
- let operation = resolveAnchorOperation(readContext, normalizedDomainName, message.text, message.payloadHex);
1184
- const initialFamily = createDraftAnchorFamily(operation, nowUnixMs);
1185
- const existingFamily = findAnchorFamilyByIntent(operation.state, initialFamily.intentFingerprintHex);
1186
- const conflictingFamily = findActiveAnchorFamilyByDomain(operation.state, normalizedDomainName);
1187
- if (existingFamily === null && isClearableReservedAnchorFamily(conflictingFamily)) {
1188
- throw new Error(`wallet_anchor_clear_pending_first_${conflictingFamily.domainName}`);
361
+ assertWalletMutationContextReady(readContext, "wallet_anchor");
362
+ const message = await resolveFoundingMessage({
363
+ foundingMessageText: options.foundingMessageText,
364
+ promptForFoundingMessageWhenMissing: options.promptForFoundingMessageWhenMissing,
365
+ prompter: options.prompter,
366
+ });
367
+ const state = readContext.localState.state;
368
+ const chainDomain = lookupDomain(readContext.snapshot.state, normalizedDomainName);
369
+ if (chainDomain === null) {
370
+ throw new Error("wallet_anchor_domain_not_found");
371
+ }
372
+ if (chainDomain.anchored) {
373
+ throw new Error("wallet_anchor_domain_already_anchored");
374
+ }
375
+ const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
376
+ const localScriptHexes = new Set([
377
+ state.funding.scriptPubKeyHex,
378
+ ...(state.localScriptPubKeyHexes ?? []),
379
+ ]);
380
+ if (!localScriptHexes.has(ownerHex)) {
381
+ throw new Error("wallet_anchor_owner_not_locally_controlled");
1189
382
  }
1190
- if (existingFamily === null && conflictingFamily !== null) {
1191
- throw new Error("wallet_anchor_prior_family_unresolved");
383
+ if (state.funding.address.trim() === "") {
384
+ throw new Error("wallet_anchor_owner_identity_not_supported");
1192
385
  }
386
+ const intentFingerprintHex = createIntentFingerprint([
387
+ "anchor",
388
+ state.walletRootId,
389
+ normalizedDomainName,
390
+ state.funding.scriptPubKeyHex,
391
+ message.payloadHex ?? "",
392
+ ]);
1193
393
  const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
1194
394
  dataDir: options.dataDir,
1195
395
  chain: "main",
1196
396
  startHeight: 0,
1197
- walletRootId: operation.state.walletRootId,
397
+ walletRootId: state.walletRootId,
1198
398
  });
1199
399
  const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1200
- const walletName = operation.state.managedCoreWallet.walletName;
1201
- let resumedFamily = null;
1202
- let resumedExisting = false;
1203
- let workingState = operation.state;
1204
- if (existingFamily !== null) {
1205
- const existingReservedIndex = existingFamily.reservedDedicatedIndex ?? operation.targetIdentity.localIndex;
1206
- const existingTargetIdentity = deriveAnchorTargetIdentityForIndex(operation.state, existingReservedIndex);
1207
- const reconciled = await reconcileAnchorFamily({
1208
- state: operation.state,
1209
- family: existingFamily,
1210
- operation: {
1211
- ...operation,
1212
- targetIdentity: existingTargetIdentity,
1213
- },
400
+ const walletName = state.managedCoreWallet.walletName;
401
+ const feeSelection = await resolveWalletMutationFeeSelection({
402
+ rpc,
403
+ feeRateSatVb: options.feeRateSatVb ?? null,
404
+ });
405
+ const existingMutation = findPendingMutationByIntent(state, intentFingerprintHex);
406
+ let workingState = state;
407
+ let replacementFixedInputs = null;
408
+ if (existingMutation !== null) {
409
+ const reconciled = await reconcilePendingAnchorMutation({
410
+ state,
411
+ mutation: existingMutation,
1214
412
  provider,
1215
413
  nowUnixMs,
1216
414
  paths,
1217
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1218
415
  rpc,
1219
416
  walletName,
417
+ context: readContext,
418
+ foundingMessageText: message.text,
1220
419
  });
1221
420
  workingState = reconciled.state;
1222
421
  if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
1223
- return {
1224
- domainName: normalizedDomainName,
1225
- txid: reconciled.family.tx2?.attemptedTxid ?? reconciled.family.tx1?.attemptedTxid ?? "unknown",
1226
- tx1Txid: reconciled.family.tx1?.attemptedTxid ?? "unknown",
1227
- tx2Txid: reconciled.family.tx2?.attemptedTxid ?? "unknown",
1228
- dedicatedIndex: reconciled.family.reservedDedicatedIndex ?? existingTargetIdentity.localIndex,
1229
- status: reconciled.resolution,
1230
- reusedExisting: true,
1231
- foundingMessageText: reconciled.family.foundingMessageText,
1232
- };
1233
- }
1234
- if (reconciled.resolution === "repair-required") {
1235
- throw new Error("wallet_anchor_repair_required");
1236
- }
1237
- if (reconciled.resolution === "ready-for-tx2") {
1238
- operation = {
1239
- ...operation,
1240
- targetIdentity: existingTargetIdentity,
1241
- };
1242
- resumedFamily = reconciled.family;
1243
- resumedExisting = true;
1244
- }
1245
- }
1246
- let nextState = workingState;
1247
- let family;
1248
- if (resumedFamily !== null) {
1249
- family = resumedFamily;
1250
- }
1251
- else {
1252
- await confirmAnchor(options.prompter, operation);
1253
- nextState = reserveAnchorFamilyState(nextState, initialFamily, operation.targetIdentity, operation.foundingMessageText);
1254
- nextState = await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1255
- const tx1Plan = buildTx1Plan({
1256
- state: nextState,
1257
- allUtxos: await rpc.listUnspent(walletName, 1),
1258
- operation,
1259
- });
1260
- const builtTx1 = await buildTx1({
1261
- rpc,
1262
- walletName,
1263
- state: nextState,
1264
- plan: tx1Plan,
1265
- });
1266
- const broadcastingTx1 = createBroadcastingTxRecord(builtTx1);
1267
- family = {
1268
- ...(findAnchorFamilyByIntent(nextState, initialFamily.intentFingerprintHex) ?? initialFamily),
1269
- status: "broadcasting",
1270
- currentStep: "tx1",
1271
- lastUpdatedAtUnixMs: nowUnixMs,
1272
- tx1: broadcastingTx1,
1273
- };
1274
- nextState = updateAnchorFamilyState({
1275
- state: nextState,
1276
- family,
1277
- target: operation.targetIdentity,
1278
- status: "broadcasting",
1279
- localAnchorIntent: "reserved",
1280
- currentStep: "tx1",
1281
- nowUnixMs,
1282
- tx1: broadcastingTx1,
1283
- });
1284
- nextState = await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1285
- ensureSameTipHeight(readContext, (await rpc.getBlockchainInfo()).blocks, "wallet_anchor_tip_mismatch");
1286
- try {
1287
- await rpc.sendRawTransaction(builtTx1.rawHex);
1288
- }
1289
- catch (error) {
1290
- if (!isAlreadyAcceptedError(error)) {
1291
- if (isBroadcastUnknownError(error)) {
1292
- family = {
1293
- ...family,
1294
- status: "broadcast-unknown",
1295
- tx1: {
1296
- ...broadcastingTx1,
1297
- status: "broadcast-unknown",
1298
- },
1299
- };
1300
- nextState = updateAnchorFamilyState({
1301
- state: nextState,
1302
- family,
1303
- target: operation.targetIdentity,
1304
- status: "broadcast-unknown",
1305
- localAnchorIntent: "reserved",
1306
- currentStep: "tx1",
1307
- nowUnixMs,
1308
- tx1: family.tx1,
1309
- });
1310
- await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1311
- throw new Error("wallet_anchor_tx1_broadcast_unknown");
1312
- }
1313
- await unlockTemporaryBuilderLocks(rpc, walletName, builtTx1.temporaryBuilderLockedOutpoints);
1314
- family = {
1315
- ...family,
1316
- status: "canceled",
1317
- tx1: {
1318
- ...broadcastingTx1,
1319
- status: "canceled",
1320
- temporaryBuilderLockedOutpoints: [],
1321
- },
422
+ const reuse = await resolvePendingMutationReuseDecision({
423
+ rpc,
424
+ walletName,
425
+ mutation: reconciled.mutation,
426
+ nextFeeSelection: feeSelection,
427
+ });
428
+ if (reuse.reuseExisting) {
429
+ return {
430
+ domainName: normalizedDomainName,
431
+ txid: reconciled.mutation.attemptedTxid ?? "unknown",
432
+ status: reconciled.resolution,
433
+ reusedExisting: true,
434
+ foundingMessageText: message.text,
435
+ fees: reuse.fees,
1322
436
  };
1323
- nextState = updateAnchorFamilyState({
1324
- state: nextState,
1325
- family,
1326
- target: operation.targetIdentity,
1327
- status: "canceled",
1328
- localAnchorIntent: "none",
1329
- currentStep: "tx1",
1330
- nowUnixMs,
1331
- tx1: family.tx1,
1332
- });
1333
- await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1334
- throw error;
1335
437
  }
438
+ replacementFixedInputs = reuse.replacementFixedInputs;
1336
439
  }
1337
- await unlockTemporaryBuilderLocks(rpc, walletName, builtTx1.temporaryBuilderLockedOutpoints);
1338
- family = {
1339
- ...family,
1340
- status: "live",
1341
- currentStep: "tx1",
1342
- tx1: {
1343
- ...broadcastingTx1,
1344
- status: "live",
1345
- temporaryBuilderLockedOutpoints: [],
1346
- },
1347
- };
1348
- nextState = updateAnchorFamilyState({
1349
- state: nextState,
1350
- family,
1351
- target: operation.targetIdentity,
1352
- status: "live",
1353
- localAnchorIntent: "tx1-live",
1354
- currentStep: "tx1",
1355
- nowUnixMs,
1356
- tx1: family.tx1,
1357
- listingCancelCommitted: operation.hadListing,
1358
- moveOwnershipToTarget: true,
1359
- });
1360
- nextState = await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1361
- if (operation.sourceAnchorOutpoint !== null) {
1362
- await relockAnchorOutpoint(rpc, walletName, {
1363
- txid: builtTx1.txid,
1364
- vout: 2,
1365
- });
440
+ if (reconciled.resolution === "repair-required") {
441
+ throw new Error("wallet_anchor_repair_required");
1366
442
  }
1367
443
  }
1368
- const result = await submitTx2({
444
+ await confirmDirectAnchor(options.prompter, {
445
+ domainName: normalizedDomainName,
446
+ walletAddress: state.funding.address,
447
+ foundingMessageText: message.text,
448
+ });
449
+ let nextState = upsertPendingMutation(workingState, createDraftAnchorMutation({
450
+ state: workingState,
451
+ domainName: normalizedDomainName,
452
+ intentFingerprintHex,
453
+ nowUnixMs,
454
+ feeSelection,
455
+ existing: existingMutation ?? null,
456
+ }));
457
+ nextState = await saveState({
1369
458
  state: nextState,
1370
- family,
1371
- operation,
1372
- readContext: operation.readContext,
1373
459
  provider,
460
+ nowUnixMs,
461
+ paths,
462
+ });
463
+ const directAnchorPlan = buildDirectAnchorPlan({
464
+ state: nextState,
465
+ allUtxos: await rpc.listUnspent(walletName, 1),
466
+ domainId: chainDomain.domainId,
467
+ foundingMessagePayloadHex: message.payloadHex,
468
+ });
469
+ const built = await buildWalletMutationTransactionWithReserveFallback({
1374
470
  rpc,
1375
471
  walletName,
472
+ state: nextState,
473
+ plan: {
474
+ ...directAnchorPlan,
475
+ fixedInputs: mergeFixedWalletInputs(directAnchorPlan.fixedInputs, replacementFixedInputs),
476
+ },
477
+ validateFundedDraft: validateDirectAnchorDraft,
478
+ finalizeErrorCode: "wallet_anchor_finalize_failed",
479
+ mempoolRejectPrefix: "wallet_anchor_mempool_rejected",
480
+ feeRate: feeSelection.feeRateSatVb,
481
+ });
482
+ const currentMutation = nextState.pendingMutations?.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex)
483
+ ?? createDraftAnchorMutation({
484
+ state: nextState,
485
+ domainName: normalizedDomainName,
486
+ intentFingerprintHex,
487
+ nowUnixMs,
488
+ feeSelection,
489
+ });
490
+ const broadcastingMutation = updateMutationRecord(currentMutation, "broadcasting", nowUnixMs, {
491
+ attemptedTxid: built.txid,
492
+ attemptedWtxid: built.wtxid,
493
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
494
+ });
495
+ nextState = await saveState({
496
+ state: upsertPendingMutation(nextState, broadcastingMutation),
497
+ provider,
1376
498
  nowUnixMs,
1377
499
  paths,
1378
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1379
500
  });
1380
- return {
1381
- ...result,
1382
- reusedExisting: resumedExisting,
1383
- foundingMessageText: result.foundingMessageText ?? operation.foundingMessageText,
1384
- };
1385
- }
1386
- finally {
1387
- await readContext.close();
1388
- await miningPreemption.release();
1389
- }
1390
- }
1391
- finally {
1392
- await controlLock.release();
1393
- }
1394
- }
1395
- export async function clearPendingAnchor(options) {
1396
- const provider = options.provider ?? createDefaultWalletSecretProvider();
1397
- const nowUnixMs = options.nowUnixMs ?? Date.now();
1398
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1399
- const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1400
- purpose: "wallet-anchor-clear",
1401
- walletRootId: null,
1402
- });
1403
- const normalizedDomainName = normalizeDomainName(options.domainName);
1404
- try {
1405
- const miningPreemption = await pauseMiningForWalletMutation({
1406
- paths,
1407
- reason: "wallet-anchor-clear",
1408
- });
1409
- const readContext = await (options.openReadContext ?? openWalletReadContext)({
1410
- dataDir: options.dataDir,
1411
- databasePath: options.databasePath,
1412
- secretProvider: provider,
1413
- walletControlLockHeld: true,
1414
- paths,
1415
- });
1416
- try {
1417
- assertWalletMutationContextReady(readContext, "wallet_anchor_clear");
1418
- const family = findActiveAnchorFamilyByDomain(readContext.localState.state, normalizedDomainName);
1419
- const domain = readContext.localState.state.domains.find((entry) => entry.name === normalizedDomainName) ?? null;
1420
- if (domain === null && family === null) {
1421
- throw new Error("wallet_anchor_clear_domain_not_found");
501
+ ensureSameTipHeight(readContext, (await rpc.getBlockchainInfo()).blocks, "wallet_anchor_tip_mismatch");
502
+ let accepted = false;
503
+ try {
504
+ await rpc.sendRawTransaction(built.rawHex);
505
+ accepted = true;
1422
506
  }
1423
- if (family === null) {
1424
- if (domain === null) {
1425
- throw new Error("wallet_anchor_clear_domain_not_found");
507
+ catch (error) {
508
+ if (isAlreadyAcceptedError(error)) {
509
+ accepted = true;
1426
510
  }
1427
- if (domain.localAnchorIntent !== "none") {
1428
- throw new Error("wallet_anchor_clear_inconsistent_state");
511
+ else if (isBroadcastUnknownError(error)) {
512
+ const unknownMutation = updateMutationRecord(broadcastingMutation, "broadcast-unknown", nowUnixMs, {
513
+ attemptedTxid: built.txid,
514
+ attemptedWtxid: built.wtxid,
515
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
516
+ });
517
+ await saveState({
518
+ state: upsertPendingMutation(nextState, unknownMutation),
519
+ provider,
520
+ nowUnixMs,
521
+ paths,
522
+ });
523
+ throw new Error("wallet_anchor_broadcast_unknown");
524
+ }
525
+ else {
526
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
527
+ const canceledMutation = updateMutationRecord(broadcastingMutation, "canceled", nowUnixMs, {
528
+ attemptedTxid: built.txid,
529
+ attemptedWtxid: built.wtxid,
530
+ temporaryBuilderLockedOutpoints: [],
531
+ });
532
+ await saveState({
533
+ state: upsertPendingMutation(nextState, canceledMutation),
534
+ provider,
535
+ nowUnixMs,
536
+ paths,
537
+ });
538
+ throw error;
1429
539
  }
1430
- return {
1431
- domainName: normalizedDomainName,
1432
- cleared: false,
1433
- previousFamilyStatus: null,
1434
- previousFamilyStep: null,
1435
- releasedDedicatedIndex: null,
1436
- };
1437
- }
1438
- if (family.type !== "anchor") {
1439
- throw new Error("wallet_anchor_clear_inconsistent_state");
1440
- }
1441
- if (family.status !== "draft" || family.currentStep !== "reserved") {
1442
- throw new Error(`wallet_anchor_clear_not_clearable_${family.status}`);
1443
540
  }
1444
- const reservedDedicatedIndex = family.reservedDedicatedIndex ?? null;
1445
- if (reservedDedicatedIndex === null
1446
- || family.tx1?.attemptedTxid !== null
1447
- || family.tx2?.attemptedTxid !== null
1448
- || (domain !== null
1449
- && (domain.localAnchorIntent !== "reserved"
1450
- || domain.dedicatedIndex === null
1451
- || domain.dedicatedIndex !== reservedDedicatedIndex))) {
1452
- throw new Error("wallet_anchor_clear_inconsistent_state");
541
+ if (!accepted) {
542
+ throw new Error("wallet_anchor_broadcast_failed");
1453
543
  }
1454
- await confirmAnchorClear(options.prompter, normalizedDomainName, reservedDedicatedIndex, options.assumeYes ?? false);
1455
- const releasedState = releaseClearedAnchorReservationState({
1456
- state: readContext.localState.state,
1457
- familyId: family.familyId,
544
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
545
+ const finalStatus = anchorConfirmedOnSnapshot({
546
+ snapshot: readContext.snapshot,
547
+ state: nextState,
1458
548
  domainName: normalizedDomainName,
1459
- nowUnixMs,
549
+ }) ? "confirmed" : "live";
550
+ const finalMutation = updateMutationRecord(broadcastingMutation, finalStatus, nowUnixMs, {
551
+ attemptedTxid: built.txid,
552
+ attemptedWtxid: built.wtxid,
553
+ temporaryBuilderLockedOutpoints: [],
1460
554
  });
1461
- await saveWalletStatePreservingUnlock({
1462
- state: {
1463
- ...releasedState,
1464
- stateRevision: releasedState.stateRevision + 1,
1465
- lastWrittenAtUnixMs: nowUnixMs,
1466
- },
555
+ nextState = upsertAnchoredDomainRecord({
556
+ state: upsertPendingMutation(nextState, finalMutation),
557
+ domainName: normalizedDomainName,
558
+ domainId: chainDomain.domainId,
559
+ foundingMessageText: message.text,
560
+ });
561
+ nextState = await saveState({
562
+ state: nextState,
1467
563
  provider,
1468
- unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1469
564
  nowUnixMs,
1470
565
  paths,
1471
566
  });
1472
567
  return {
1473
568
  domainName: normalizedDomainName,
1474
- cleared: true,
1475
- previousFamilyStatus: family.status,
1476
- previousFamilyStep: family.currentStep ?? null,
1477
- releasedDedicatedIndex: reservedDedicatedIndex,
569
+ txid: built.txid,
570
+ status: finalStatus,
571
+ reusedExisting: false,
572
+ foundingMessageText: message.text,
573
+ fees: createBuiltWalletMutationFeeSummary({
574
+ selection: feeSelection,
575
+ built,
576
+ }),
1478
577
  };
1479
578
  }
1480
579
  finally {