@cogcoin/client 0.5.14 → 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 +16 -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 -11
  91. package/dist/wallet/coin-control.js +100 -357
  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 +179 -6
  105. package/dist/wallet/mining/runner.js +891 -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 -1240
  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 +74 -10
  144. package/dist/wallet/tx/common.js +315 -138
  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 +84 -914
  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, 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,892 +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 foundingPayload = options.operation.foundingMessagePayloadHex === null
555
- ? undefined
556
- : Buffer.from(options.operation.foundingMessagePayloadHex, "hex");
557
- const opReturnData = serializeDomainAnchor(options.operation.chainDomain.domainId, foundingPayload).opReturnData;
558
- return {
559
- sender: {
560
- localIndex: options.operation.targetIdentity.localIndex,
561
- scriptPubKeyHex: options.operation.targetIdentity.scriptPubKeyHex,
562
- address: options.operation.targetIdentity.address,
563
- },
564
- changeAddress: options.state.funding.address,
565
- fixedInputs: [{ txid: provisional.txid, vout: provisional.vout }],
566
- outputs: [
567
- { data: Buffer.from(opReturnData).toString("hex") },
568
- { [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
569
- ],
570
- changePosition: 2,
571
- expectedOpReturnScriptHex: encodeOpReturnScript(opReturnData),
572
- expectedProvisionalAnchorScriptHex: options.operation.targetIdentity.scriptPubKeyHex,
573
- expectedProvisionalAnchorValueSats: BigInt(options.state.anchorValueSats),
574
- expectedReplacementAnchorScriptHex: null,
575
- expectedReplacementAnchorValueSats: null,
576
- allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
577
- eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
578
- requiredSenderOutpoint: null,
579
- requiredProvisionalOutpoint: {
580
- txid: provisional.txid,
581
- vout: provisional.vout,
582
- },
583
- 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,
584
231
  };
585
- }
586
- function validateTx1Draft(decoded, funded, plan) {
587
- const inputs = decoded.tx.vin;
588
- const outputs = decoded.tx.vout;
589
- if (inputs.length === 0) {
590
- throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
591
- }
592
- assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
593
- const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
594
- if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
595
- throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
596
- }
597
- if (plan.requiredSenderOutpoint !== null) {
598
- if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
599
- throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
600
- }
601
- }
602
- assertFundingInputsAfterFixedPrefix({
603
- inputs,
604
- fixedInputs: plan.fixedInputs,
605
- allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
606
- eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
607
- errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
232
+ await saveWalletStatePreservingUnlock({
233
+ state: nextState,
234
+ provider: options.provider,
235
+ nowUnixMs: options.nowUnixMs,
236
+ paths: options.paths,
608
237
  });
609
- if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
610
- throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
611
- }
612
- if (outputs[1]?.scriptPubKey?.hex !== plan.expectedProvisionalAnchorScriptHex) {
613
- throw new Error(`${plan.errorPrefix}_provisional_anchor_output_mismatch`);
614
- }
615
- if (valueToSats(outputs[1]?.value ?? 0) !== plan.expectedProvisionalAnchorValueSats) {
616
- throw new Error(`${plan.errorPrefix}_provisional_anchor_value_mismatch`);
617
- }
618
- const expectedWithoutChange = plan.expectedReplacementAnchorScriptHex === null ? 2 : 3;
619
- if (plan.expectedReplacementAnchorScriptHex !== null) {
620
- if (outputs[2]?.scriptPubKey?.hex !== plan.expectedReplacementAnchorScriptHex) {
621
- throw new Error(`${plan.errorPrefix}_replacement_anchor_output_mismatch`);
622
- }
623
- if (valueToSats(outputs[2]?.value ?? 0) !== (plan.expectedReplacementAnchorValueSats ?? 0n)) {
624
- throw new Error(`${plan.errorPrefix}_replacement_anchor_value_mismatch`);
625
- }
626
- }
627
- if (funded.changepos === -1) {
628
- if (outputs.length !== expectedWithoutChange) {
629
- throw new Error(`${plan.errorPrefix}_unexpected_output_count`);
630
- }
631
- return;
632
- }
633
- if (funded.changepos !== plan.changePosition || outputs.length !== expectedWithoutChange + 1) {
634
- throw new Error(`${plan.errorPrefix}_change_position_mismatch`);
635
- }
636
- if (outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
637
- throw new Error(`${plan.errorPrefix}_change_output_mismatch`);
638
- }
238
+ return nextState;
639
239
  }
640
- function validateTx2Draft(decoded, funded, plan) {
641
- const inputs = decoded.tx.vin;
642
- const outputs = decoded.tx.vout;
643
- if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
644
- throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
645
- }
646
- assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
647
- const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
648
- if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
649
- || !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
650
- throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
651
- }
652
- assertFundingInputsAfterFixedPrefix({
653
- inputs,
654
- fixedInputs: plan.fixedInputs,
655
- allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
656
- eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
657
- errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
658
- });
659
- if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
660
- throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
661
- }
662
- if (outputs[1]?.scriptPubKey?.hex !== plan.expectedProvisionalAnchorScriptHex) {
663
- throw new Error(`${plan.errorPrefix}_canonical_anchor_output_mismatch`);
664
- }
665
- if (valueToSats(outputs[1]?.value ?? 0) !== plan.expectedProvisionalAnchorValueSats) {
666
- throw new Error(`${plan.errorPrefix}_canonical_anchor_value_mismatch`);
667
- }
668
- const expectedWithoutChange = 2;
669
- if (funded.changepos === -1) {
670
- if (outputs.length !== expectedWithoutChange) {
671
- throw new Error(`${plan.errorPrefix}_unexpected_output_count`);
672
- }
673
- return;
674
- }
675
- if (funded.changepos !== plan.changePosition || outputs.length !== expectedWithoutChange + 1) {
676
- throw new Error(`${plan.errorPrefix}_change_position_mismatch`);
677
- }
678
- if (outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
679
- 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
+ };
680
247
  }
681
- }
682
- async function buildTx1(options) {
683
- return buildWalletMutationTransactionWithReserveFallback({
684
- rpc: options.rpc,
685
- walletName: options.walletName,
686
- state: options.state,
687
- plan: options.plan,
688
- validateFundedDraft: validateTx1Draft,
689
- finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
690
- mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
691
- reserveCandidates: options.state.proactiveReserveOutpoints,
692
- });
693
- }
694
- async function buildTx2(options) {
695
- return buildWalletMutationTransactionWithReserveFallback({
696
- rpc: options.rpc,
697
- walletName: options.walletName,
248
+ if (options.context.snapshot !== null && anchorConfirmedOnSnapshot({
249
+ snapshot: options.context.snapshot,
698
250
  state: options.state,
699
- plan: options.plan,
700
- validateFundedDraft: validateTx2Draft,
701
- finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
702
- mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
703
- reserveCandidates: options.state.proactiveReserveOutpoints,
704
- });
705
- }
706
- async function relockAnchorOutpoint(rpc, walletName, outpoint) {
707
- if (outpoint === null) {
708
- return;
709
- }
710
- await rpc.lockUnspent(walletName, false, [outpoint]).catch(() => undefined);
711
- }
712
- function resolveAcceptedFamilyStatus(options) {
713
- const chainDomain = options.snapshot === null || options.family.domainName == null
714
- ? null
715
- : lookupDomain(options.snapshot.state, options.family.domainName);
716
- if (chainDomain === null) {
717
- return "live";
718
- }
719
- const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
720
- return chainDomain.anchored && ownerHex === options.target.scriptPubKeyHex
721
- ? "confirmed"
722
- : "live";
723
- }
724
- async function reconcileAnchorFamily(options) {
725
- const chainDomain = lookupDomain(options.operation.readContext.snapshot.state, options.operation.chainDomain.name);
726
- const targetScript = options.operation.targetIdentity.scriptPubKeyHex;
727
- if (chainDomain !== null) {
728
- const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
729
- if (chainDomain.anchored && ownerHex === targetScript) {
730
- const nextState = updateAnchorFamilyState({
731
- state: options.state,
732
- family: options.family,
733
- target: options.operation.targetIdentity,
734
- status: "confirmed",
735
- localAnchorIntent: "none",
736
- currentStep: "tx2",
737
- nowUnixMs: options.nowUnixMs,
738
- tx1: options.family.tx1 == null ? undefined : { ...options.family.tx1, status: "confirmed", temporaryBuilderLockedOutpoints: [] },
739
- tx2: options.family.tx2 == null ? undefined : { ...options.family.tx2, status: "confirmed", temporaryBuilderLockedOutpoints: [] },
740
- moveOwnershipToTarget: true,
741
- canonicalChainStatus: "anchored",
742
- currentCanonicalAnchorOutpoint: options.family.tx2?.attemptedTxid == null
743
- ? options.state.domains.find((domain) => domain.name === options.family.domainName)?.currentCanonicalAnchorOutpoint ?? null
744
- : {
745
- txid: options.family.tx2.attemptedTxid,
746
- vout: 1,
747
- valueSats: options.state.anchorValueSats,
748
- },
749
- });
750
- await saveWalletStatePreservingUnlock({
751
- state: {
752
- ...nextState,
753
- stateRevision: nextState.stateRevision + 1,
754
- lastWrittenAtUnixMs: options.nowUnixMs,
755
- },
756
- provider: options.provider,
757
- unlockUntilUnixMs: options.unlockUntilUnixMs,
758
- nowUnixMs: options.nowUnixMs,
759
- paths: options.paths,
760
- });
761
- 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({
762
266
  state: nextState,
763
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
764
- ...options.family,
765
- status: "confirmed",
766
- },
767
- resolution: "confirmed",
768
- };
769
- }
770
- if (ownerHex === targetScript && !chainDomain.anchored) {
771
- const nextState = updateAnchorFamilyState({
772
- state: options.state,
773
- family: options.family,
774
- target: options.operation.targetIdentity,
775
- status: "repair-required",
776
- localAnchorIntent: "repair-required",
777
- currentStep: "tx2",
778
- nowUnixMs: options.nowUnixMs,
779
- listingCancelCommitted: true,
780
- moveOwnershipToTarget: true,
781
- });
782
- await saveWalletStatePreservingUnlock({
783
- state: {
784
- ...nextState,
785
- stateRevision: nextState.stateRevision + 1,
786
- lastWrittenAtUnixMs: options.nowUnixMs,
787
- },
788
267
  provider: options.provider,
789
- unlockUntilUnixMs: options.unlockUntilUnixMs,
790
268
  nowUnixMs: options.nowUnixMs,
791
269
  paths: options.paths,
792
- });
793
- return {
794
- state: nextState,
795
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
796
- ...options.family,
797
- status: "repair-required",
798
- },
799
- resolution: "repair-required",
800
- };
801
- }
802
- }
803
- const mempool = await options.rpc.getRawMempool().catch(() => []);
804
- if (options.family.tx2?.attemptedTxid != null && mempool.includes(options.family.tx2.attemptedTxid)) {
805
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.family.tx2.temporaryBuilderLockedOutpoints);
806
- const nextState = updateAnchorFamilyState({
807
- state: options.state,
808
- family: options.family,
809
- target: options.operation.targetIdentity,
810
- status: "live",
811
- localAnchorIntent: "tx2-live",
812
- currentStep: "tx2",
813
- nowUnixMs: options.nowUnixMs,
814
- tx2: {
815
- ...options.family.tx2,
816
- status: "live",
817
- temporaryBuilderLockedOutpoints: [],
818
- },
819
- listingCancelCommitted: true,
820
- moveOwnershipToTarget: true,
821
- currentCanonicalAnchorOutpoint: {
822
- txid: options.family.tx2.attemptedTxid,
823
- vout: 1,
824
- valueSats: options.state.anchorValueSats,
825
- },
826
- });
827
- await relockAnchorOutpoint(options.rpc, options.walletName, {
828
- txid: options.family.tx2.attemptedTxid,
829
- vout: 1,
830
- });
831
- await saveWalletStatePreservingUnlock({
832
- state: {
833
- ...nextState,
834
- stateRevision: nextState.stateRevision + 1,
835
- lastWrittenAtUnixMs: options.nowUnixMs,
836
- },
837
- provider: options.provider,
838
- unlockUntilUnixMs: options.unlockUntilUnixMs,
839
- nowUnixMs: options.nowUnixMs,
840
- paths: options.paths,
841
- });
842
- return {
843
- state: nextState,
844
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
845
- ...options.family,
846
- status: "live",
847
- },
848
- resolution: "live",
270
+ }),
271
+ mutation: confirmedMutation,
272
+ resolution: "confirmed",
849
273
  };
850
274
  }
851
- if (options.family.tx1?.attemptedTxid != null && mempool.includes(options.family.tx1.attemptedTxid)) {
852
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.family.tx1.temporaryBuilderLockedOutpoints);
853
- const nextState = updateAnchorFamilyState({
854
- state: options.state,
855
- family: options.family,
856
- target: options.operation.targetIdentity,
857
- status: "live",
858
- localAnchorIntent: "tx1-live",
859
- currentStep: "tx1",
860
- nowUnixMs: options.nowUnixMs,
861
- tx1: {
862
- ...options.family.tx1,
863
- 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, {
864
280
  temporaryBuilderLockedOutpoints: [],
865
- },
866
- listingCancelCommitted: options.operation.hadListing,
867
- moveOwnershipToTarget: true,
868
- });
869
- if (options.operation.sourceAnchorOutpoint !== null) {
870
- await relockAnchorOutpoint(options.rpc, options.walletName, {
871
- txid: options.family.tx1.attemptedTxid,
872
- vout: 2,
873
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
+ };
874
303
  }
875
- await saveWalletStatePreservingUnlock({
876
- state: {
877
- ...nextState,
878
- stateRevision: nextState.stateRevision + 1,
879
- lastWrittenAtUnixMs: options.nowUnixMs,
880
- },
881
- provider: options.provider,
882
- unlockUntilUnixMs: options.unlockUntilUnixMs,
883
- nowUnixMs: options.nowUnixMs,
884
- paths: options.paths,
885
- });
886
- return {
887
- state: nextState,
888
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
889
- ...options.family,
890
- status: "live",
891
- },
892
- resolution: "ready-for-tx2",
893
- };
894
- }
895
- if (options.family.currentStep === "tx2" || options.family.tx2?.attemptedTxid != null) {
896
- const nextState = updateAnchorFamilyState({
897
- state: options.state,
898
- family: options.family,
899
- target: options.operation.targetIdentity,
900
- status: "repair-required",
901
- localAnchorIntent: "repair-required",
902
- currentStep: "tx2",
903
- nowUnixMs: options.nowUnixMs,
904
- listingCancelCommitted: true,
905
- moveOwnershipToTarget: true,
906
- });
907
- await saveWalletStatePreservingUnlock({
908
- state: {
909
- ...nextState,
910
- stateRevision: nextState.stateRevision + 1,
911
- lastWrittenAtUnixMs: options.nowUnixMs,
912
- },
913
- provider: options.provider,
914
- unlockUntilUnixMs: options.unlockUntilUnixMs,
915
- nowUnixMs: options.nowUnixMs,
916
- paths: options.paths,
917
- });
918
- return {
919
- state: nextState,
920
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
921
- ...options.family,
922
- status: "repair-required",
923
- },
924
- resolution: "repair-required",
925
- };
926
304
  }
927
- if (ACTIVE_FAMILY_STATUSES.has(options.family.status)) {
928
- const nextState = updateAnchorFamilyState({
929
- state: options.state,
930
- family: options.family,
931
- target: options.operation.targetIdentity,
932
- status: "canceled",
933
- localAnchorIntent: "none",
934
- currentStep: options.family.currentStep,
935
- nowUnixMs: options.nowUnixMs,
936
- tx1: options.family.tx1 == null ? undefined : {
937
- ...options.family.tx1,
938
- status: "canceled",
939
- temporaryBuilderLockedOutpoints: [],
940
- },
941
- tx2: options.family.tx2 == null ? undefined : {
942
- ...options.family.tx2,
943
- status: "canceled",
944
- temporaryBuilderLockedOutpoints: [],
945
- },
946
- });
947
- await saveWalletStatePreservingUnlock({
948
- state: {
949
- ...nextState,
950
- stateRevision: nextState.stateRevision + 1,
951
- lastWrittenAtUnixMs: options.nowUnixMs,
952
- },
953
- provider: options.provider,
954
- unlockUntilUnixMs: options.unlockUntilUnixMs,
955
- nowUnixMs: options.nowUnixMs,
956
- 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: [],
957
312
  });
313
+ const nextState = upsertPendingMutation(options.state, canceledMutation);
958
314
  return {
959
- state: nextState,
960
- family: findAnchorFamilyById(nextState, options.family.familyId) ?? {
961
- ...options.family,
962
- status: "canceled",
963
- },
315
+ state: await saveState({
316
+ state: nextState,
317
+ provider: options.provider,
318
+ nowUnixMs: options.nowUnixMs,
319
+ paths: options.paths,
320
+ }),
321
+ mutation: canceledMutation,
964
322
  resolution: "not-seen",
965
323
  };
966
324
  }
967
325
  return {
968
326
  state: options.state,
969
- family: options.family,
327
+ mutation: options.mutation,
970
328
  resolution: "continue",
971
329
  };
972
330
  }
973
- function createBroadcastingTxRecord(built) {
974
- return {
975
- status: "broadcasting",
976
- attemptedTxid: built.txid,
977
- attemptedWtxid: built.wtxid,
978
- temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
979
- rawHex: built.rawHex,
980
- };
981
- }
982
- async function saveState(state, provider, unlockUntilUnixMs, nowUnixMs, paths) {
983
- const nextState = {
984
- ...state,
985
- stateRevision: state.stateRevision + 1,
986
- lastWrittenAtUnixMs: nowUnixMs,
987
- };
988
- await saveWalletStatePreservingUnlock({
989
- state: nextState,
990
- provider,
991
- unlockUntilUnixMs,
992
- nowUnixMs,
993
- paths,
994
- });
995
- return nextState;
996
- }
997
- async function submitTx2(options) {
998
- let nextState = options.state;
999
- let family = options.family;
1000
- const tx2Plan = buildTx2Plan({
1001
- state: nextState,
1002
- allUtxos: await options.rpc.listUnspent(options.walletName, 0),
1003
- operation: options.operation,
1004
- family,
1005
- });
1006
- const builtTx2 = await buildTx2({
1007
- rpc: options.rpc,
1008
- walletName: options.walletName,
1009
- state: nextState,
1010
- plan: tx2Plan,
1011
- });
1012
- const broadcastingTx2 = createBroadcastingTxRecord(builtTx2);
1013
- family = {
1014
- ...family,
1015
- status: "broadcasting",
1016
- currentStep: "tx2",
1017
- tx2: broadcastingTx2,
1018
- };
1019
- nextState = updateAnchorFamilyState({
1020
- state: nextState,
1021
- family,
1022
- target: options.operation.targetIdentity,
1023
- status: "broadcasting",
1024
- localAnchorIntent: "tx1-live",
1025
- currentStep: "tx2",
1026
- nowUnixMs: options.nowUnixMs,
1027
- tx2: broadcastingTx2,
1028
- listingCancelCommitted: true,
1029
- moveOwnershipToTarget: true,
1030
- });
1031
- nextState = await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1032
- ensureSameTipHeight(options.readContext, (await options.rpc.getBlockchainInfo()).blocks, "wallet_anchor_tip_mismatch");
1033
- try {
1034
- await options.rpc.sendRawTransaction(builtTx2.rawHex);
1035
- }
1036
- catch (error) {
1037
- if (!isAlreadyAcceptedError(error)) {
1038
- if (isBroadcastUnknownError(error)) {
1039
- family = {
1040
- ...family,
1041
- status: "broadcast-unknown",
1042
- tx2: {
1043
- ...broadcastingTx2,
1044
- status: "broadcast-unknown",
1045
- },
1046
- };
1047
- nextState = updateAnchorFamilyState({
1048
- state: nextState,
1049
- family,
1050
- target: options.operation.targetIdentity,
1051
- status: "broadcast-unknown",
1052
- localAnchorIntent: "tx1-live",
1053
- currentStep: "tx2",
1054
- nowUnixMs: options.nowUnixMs,
1055
- tx2: family.tx2,
1056
- listingCancelCommitted: true,
1057
- moveOwnershipToTarget: true,
1058
- });
1059
- await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1060
- throw new Error("wallet_anchor_tx2_broadcast_unknown");
1061
- }
1062
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, builtTx2.temporaryBuilderLockedOutpoints);
1063
- family = {
1064
- ...family,
1065
- status: "repair-required",
1066
- tx2: {
1067
- ...broadcastingTx2,
1068
- status: "repair-required",
1069
- temporaryBuilderLockedOutpoints: [],
1070
- },
1071
- };
1072
- nextState = updateAnchorFamilyState({
1073
- state: nextState,
1074
- family,
1075
- target: options.operation.targetIdentity,
1076
- status: "repair-required",
1077
- localAnchorIntent: "repair-required",
1078
- currentStep: "tx2",
1079
- nowUnixMs: options.nowUnixMs,
1080
- tx2: family.tx2,
1081
- listingCancelCommitted: true,
1082
- moveOwnershipToTarget: true,
1083
- });
1084
- await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1085
- throw error;
1086
- }
1087
- }
1088
- await unlockTemporaryBuilderLocks(options.rpc, options.walletName, builtTx2.temporaryBuilderLockedOutpoints);
1089
- const finalStatus = resolveAcceptedFamilyStatus({
1090
- snapshot: options.readContext.snapshot,
1091
- family,
1092
- target: options.operation.targetIdentity,
1093
- });
1094
- family = {
1095
- ...family,
1096
- status: finalStatus,
1097
- currentStep: "tx2",
1098
- tx2: {
1099
- ...broadcastingTx2,
1100
- status: finalStatus,
1101
- temporaryBuilderLockedOutpoints: [],
1102
- },
1103
- };
1104
- nextState = updateAnchorFamilyState({
1105
- state: nextState,
1106
- family,
1107
- target: options.operation.targetIdentity,
1108
- status: finalStatus,
1109
- localAnchorIntent: finalStatus === "confirmed" ? "none" : "tx2-live",
1110
- currentStep: "tx2",
1111
- nowUnixMs: options.nowUnixMs,
1112
- tx2: family.tx2,
1113
- listingCancelCommitted: true,
1114
- moveOwnershipToTarget: true,
1115
- canonicalChainStatus: finalStatus === "confirmed" ? "anchored" : undefined,
1116
- currentCanonicalAnchorOutpoint: {
1117
- txid: builtTx2.txid,
1118
- vout: 1,
1119
- valueSats: nextState.anchorValueSats,
1120
- },
1121
- });
1122
- nextState = await saveState(nextState, options.provider, options.unlockUntilUnixMs, options.nowUnixMs, options.paths);
1123
- await relockAnchorOutpoint(options.rpc, options.walletName, {
1124
- txid: builtTx2.txid,
1125
- vout: 1,
1126
- });
1127
- return {
1128
- domainName: options.operation.chainDomain.name,
1129
- txid: builtTx2.txid,
1130
- tx1Txid: family.tx1?.attemptedTxid ?? "unknown",
1131
- tx2Txid: builtTx2.txid,
1132
- dedicatedIndex: options.operation.targetIdentity.localIndex,
1133
- status: finalStatus,
1134
- reusedExisting: false,
1135
- foundingMessageText: options.operation.foundingMessageText,
1136
- };
1137
- }
1138
331
  function ensureSameTipHeight(context, bestHeight, errorCode) {
1139
332
  if (context.snapshot?.tip?.height !== bestHeight) {
1140
333
  throw new Error(errorCode);
@@ -1147,21 +340,16 @@ export async function anchorDomain(options) {
1147
340
  const provider = options.provider ?? createDefaultWalletSecretProvider();
1148
341
  const nowUnixMs = options.nowUnixMs ?? Date.now();
1149
342
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
343
+ const normalizedDomainName = normalizeDomainName(options.domainName);
1150
344
  const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1151
345
  purpose: "wallet-anchor",
1152
346
  walletRootId: null,
1153
347
  });
1154
- const normalizedDomainName = normalizeDomainName(options.domainName);
1155
348
  try {
1156
349
  const miningPreemption = await pauseMiningForWalletMutation({
1157
350
  paths,
1158
351
  reason: "wallet-anchor",
1159
352
  });
1160
- const message = await resolveFoundingMessage({
1161
- foundingMessageText: options.foundingMessageText,
1162
- promptForFoundingMessageWhenMissing: options.promptForFoundingMessageWhenMissing,
1163
- prompter: options.prompter,
1164
- });
1165
353
  const readContext = await (options.openReadContext ?? openWalletReadContext)({
1166
354
  dataDir: options.dataDir,
1167
355
  databasePath: options.databasePath,
@@ -1170,301 +358,222 @@ export async function anchorDomain(options) {
1170
358
  paths,
1171
359
  });
1172
360
  try {
1173
- let operation = resolveAnchorOperation(readContext, normalizedDomainName, message.text, message.payloadHex);
1174
- const initialFamily = createDraftAnchorFamily(operation, nowUnixMs);
1175
- const existingFamily = findAnchorFamilyByIntent(operation.state, initialFamily.intentFingerprintHex);
1176
- const conflictingFamily = findActiveAnchorFamilyByDomain(operation.state, normalizedDomainName);
1177
- if (existingFamily === null && isClearableReservedAnchorFamily(conflictingFamily)) {
1178
- 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");
1179
382
  }
1180
- if (existingFamily === null && conflictingFamily !== null) {
1181
- throw new Error("wallet_anchor_prior_family_unresolved");
383
+ if (state.funding.address.trim() === "") {
384
+ throw new Error("wallet_anchor_owner_identity_not_supported");
1182
385
  }
386
+ const intentFingerprintHex = createIntentFingerprint([
387
+ "anchor",
388
+ state.walletRootId,
389
+ normalizedDomainName,
390
+ state.funding.scriptPubKeyHex,
391
+ message.payloadHex ?? "",
392
+ ]);
1183
393
  const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
1184
394
  dataDir: options.dataDir,
1185
395
  chain: "main",
1186
396
  startHeight: 0,
1187
- walletRootId: operation.state.walletRootId,
397
+ walletRootId: state.walletRootId,
1188
398
  });
1189
399
  const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1190
- const walletName = operation.state.managedCoreWallet.walletName;
1191
- let resumedFamily = null;
1192
- let resumedExisting = false;
1193
- let workingState = operation.state;
1194
- if (existingFamily !== null) {
1195
- const existingReservedIndex = existingFamily.reservedDedicatedIndex ?? operation.targetIdentity.localIndex;
1196
- const existingTargetIdentity = deriveAnchorTargetIdentityForIndex(operation.state, existingReservedIndex);
1197
- const reconciled = await reconcileAnchorFamily({
1198
- state: operation.state,
1199
- family: existingFamily,
1200
- operation: {
1201
- ...operation,
1202
- targetIdentity: existingTargetIdentity,
1203
- },
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,
1204
412
  provider,
1205
413
  nowUnixMs,
1206
414
  paths,
1207
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1208
415
  rpc,
1209
416
  walletName,
417
+ context: readContext,
418
+ foundingMessageText: message.text,
1210
419
  });
1211
420
  workingState = reconciled.state;
1212
421
  if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
1213
- return {
1214
- domainName: normalizedDomainName,
1215
- txid: reconciled.family.tx2?.attemptedTxid ?? reconciled.family.tx1?.attemptedTxid ?? "unknown",
1216
- tx1Txid: reconciled.family.tx1?.attemptedTxid ?? "unknown",
1217
- tx2Txid: reconciled.family.tx2?.attemptedTxid ?? "unknown",
1218
- dedicatedIndex: reconciled.family.reservedDedicatedIndex ?? existingTargetIdentity.localIndex,
1219
- status: reconciled.resolution,
1220
- reusedExisting: true,
1221
- foundingMessageText: reconciled.family.foundingMessageText,
1222
- };
1223
- }
1224
- if (reconciled.resolution === "repair-required") {
1225
- throw new Error("wallet_anchor_repair_required");
1226
- }
1227
- if (reconciled.resolution === "ready-for-tx2") {
1228
- operation = {
1229
- ...operation,
1230
- targetIdentity: existingTargetIdentity,
1231
- };
1232
- resumedFamily = reconciled.family;
1233
- resumedExisting = true;
1234
- }
1235
- }
1236
- let nextState = workingState;
1237
- let family;
1238
- if (resumedFamily !== null) {
1239
- family = resumedFamily;
1240
- }
1241
- else {
1242
- await confirmAnchor(options.prompter, operation);
1243
- nextState = reserveAnchorFamilyState(nextState, initialFamily, operation.targetIdentity, operation.foundingMessageText);
1244
- nextState = await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1245
- const tx1Plan = buildTx1Plan({
1246
- state: nextState,
1247
- allUtxos: await rpc.listUnspent(walletName, 1),
1248
- operation,
1249
- });
1250
- const builtTx1 = await buildTx1({
1251
- rpc,
1252
- walletName,
1253
- state: nextState,
1254
- plan: tx1Plan,
1255
- });
1256
- const broadcastingTx1 = createBroadcastingTxRecord(builtTx1);
1257
- family = {
1258
- ...(findAnchorFamilyByIntent(nextState, initialFamily.intentFingerprintHex) ?? initialFamily),
1259
- status: "broadcasting",
1260
- currentStep: "tx1",
1261
- lastUpdatedAtUnixMs: nowUnixMs,
1262
- tx1: broadcastingTx1,
1263
- };
1264
- nextState = updateAnchorFamilyState({
1265
- state: nextState,
1266
- family,
1267
- target: operation.targetIdentity,
1268
- status: "broadcasting",
1269
- localAnchorIntent: "reserved",
1270
- currentStep: "tx1",
1271
- nowUnixMs,
1272
- tx1: broadcastingTx1,
1273
- });
1274
- nextState = await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1275
- ensureSameTipHeight(readContext, (await rpc.getBlockchainInfo()).blocks, "wallet_anchor_tip_mismatch");
1276
- try {
1277
- await rpc.sendRawTransaction(builtTx1.rawHex);
1278
- }
1279
- catch (error) {
1280
- if (!isAlreadyAcceptedError(error)) {
1281
- if (isBroadcastUnknownError(error)) {
1282
- family = {
1283
- ...family,
1284
- status: "broadcast-unknown",
1285
- tx1: {
1286
- ...broadcastingTx1,
1287
- status: "broadcast-unknown",
1288
- },
1289
- };
1290
- nextState = updateAnchorFamilyState({
1291
- state: nextState,
1292
- family,
1293
- target: operation.targetIdentity,
1294
- status: "broadcast-unknown",
1295
- localAnchorIntent: "reserved",
1296
- currentStep: "tx1",
1297
- nowUnixMs,
1298
- tx1: family.tx1,
1299
- });
1300
- await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1301
- throw new Error("wallet_anchor_tx1_broadcast_unknown");
1302
- }
1303
- await unlockTemporaryBuilderLocks(rpc, walletName, builtTx1.temporaryBuilderLockedOutpoints);
1304
- family = {
1305
- ...family,
1306
- status: "canceled",
1307
- tx1: {
1308
- ...broadcastingTx1,
1309
- status: "canceled",
1310
- temporaryBuilderLockedOutpoints: [],
1311
- },
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,
1312
436
  };
1313
- nextState = updateAnchorFamilyState({
1314
- state: nextState,
1315
- family,
1316
- target: operation.targetIdentity,
1317
- status: "canceled",
1318
- localAnchorIntent: "none",
1319
- currentStep: "tx1",
1320
- nowUnixMs,
1321
- tx1: family.tx1,
1322
- });
1323
- await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1324
- throw error;
1325
437
  }
438
+ replacementFixedInputs = reuse.replacementFixedInputs;
1326
439
  }
1327
- await unlockTemporaryBuilderLocks(rpc, walletName, builtTx1.temporaryBuilderLockedOutpoints);
1328
- family = {
1329
- ...family,
1330
- status: "live",
1331
- currentStep: "tx1",
1332
- tx1: {
1333
- ...broadcastingTx1,
1334
- status: "live",
1335
- temporaryBuilderLockedOutpoints: [],
1336
- },
1337
- };
1338
- nextState = updateAnchorFamilyState({
1339
- state: nextState,
1340
- family,
1341
- target: operation.targetIdentity,
1342
- status: "live",
1343
- localAnchorIntent: "tx1-live",
1344
- currentStep: "tx1",
1345
- nowUnixMs,
1346
- tx1: family.tx1,
1347
- listingCancelCommitted: operation.hadListing,
1348
- moveOwnershipToTarget: true,
1349
- });
1350
- nextState = await saveState(nextState, provider, operation.unlockUntilUnixMs, nowUnixMs, paths);
1351
- if (operation.sourceAnchorOutpoint !== null) {
1352
- await relockAnchorOutpoint(rpc, walletName, {
1353
- txid: builtTx1.txid,
1354
- vout: 2,
1355
- });
440
+ if (reconciled.resolution === "repair-required") {
441
+ throw new Error("wallet_anchor_repair_required");
1356
442
  }
1357
443
  }
1358
- 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({
1359
458
  state: nextState,
1360
- family,
1361
- operation,
1362
- readContext: operation.readContext,
1363
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({
1364
470
  rpc,
1365
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,
1366
498
  nowUnixMs,
1367
499
  paths,
1368
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1369
500
  });
1370
- return {
1371
- ...result,
1372
- reusedExisting: resumedExisting,
1373
- foundingMessageText: result.foundingMessageText ?? operation.foundingMessageText,
1374
- };
1375
- }
1376
- finally {
1377
- await readContext.close();
1378
- await miningPreemption.release();
1379
- }
1380
- }
1381
- finally {
1382
- await controlLock.release();
1383
- }
1384
- }
1385
- export async function clearPendingAnchor(options) {
1386
- const provider = options.provider ?? createDefaultWalletSecretProvider();
1387
- const nowUnixMs = options.nowUnixMs ?? Date.now();
1388
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1389
- const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1390
- purpose: "wallet-anchor-clear",
1391
- walletRootId: null,
1392
- });
1393
- const normalizedDomainName = normalizeDomainName(options.domainName);
1394
- try {
1395
- const miningPreemption = await pauseMiningForWalletMutation({
1396
- paths,
1397
- reason: "wallet-anchor-clear",
1398
- });
1399
- const readContext = await (options.openReadContext ?? openWalletReadContext)({
1400
- dataDir: options.dataDir,
1401
- databasePath: options.databasePath,
1402
- secretProvider: provider,
1403
- walletControlLockHeld: true,
1404
- paths,
1405
- });
1406
- try {
1407
- assertWalletMutationContextReady(readContext, "wallet_anchor_clear");
1408
- const family = findActiveAnchorFamilyByDomain(readContext.localState.state, normalizedDomainName);
1409
- const domain = readContext.localState.state.domains.find((entry) => entry.name === normalizedDomainName) ?? null;
1410
- if (domain === null && family === null) {
1411
- 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;
1412
506
  }
1413
- if (family === null) {
1414
- if (domain === null) {
1415
- throw new Error("wallet_anchor_clear_domain_not_found");
507
+ catch (error) {
508
+ if (isAlreadyAcceptedError(error)) {
509
+ accepted = true;
1416
510
  }
1417
- if (domain.localAnchorIntent !== "none") {
1418
- 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;
1419
539
  }
1420
- return {
1421
- domainName: normalizedDomainName,
1422
- cleared: false,
1423
- previousFamilyStatus: null,
1424
- previousFamilyStep: null,
1425
- releasedDedicatedIndex: null,
1426
- };
1427
- }
1428
- if (family.type !== "anchor") {
1429
- throw new Error("wallet_anchor_clear_inconsistent_state");
1430
- }
1431
- if (family.status !== "draft" || family.currentStep !== "reserved") {
1432
- throw new Error(`wallet_anchor_clear_not_clearable_${family.status}`);
1433
540
  }
1434
- const reservedDedicatedIndex = family.reservedDedicatedIndex ?? null;
1435
- if (reservedDedicatedIndex === null
1436
- || family.tx1?.attemptedTxid !== null
1437
- || family.tx2?.attemptedTxid !== null
1438
- || (domain !== null
1439
- && (domain.localAnchorIntent !== "reserved"
1440
- || domain.dedicatedIndex === null
1441
- || domain.dedicatedIndex !== reservedDedicatedIndex))) {
1442
- throw new Error("wallet_anchor_clear_inconsistent_state");
541
+ if (!accepted) {
542
+ throw new Error("wallet_anchor_broadcast_failed");
1443
543
  }
1444
- await confirmAnchorClear(options.prompter, normalizedDomainName, reservedDedicatedIndex, options.assumeYes ?? false);
1445
- const releasedState = releaseClearedAnchorReservationState({
1446
- state: readContext.localState.state,
1447
- familyId: family.familyId,
544
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
545
+ const finalStatus = anchorConfirmedOnSnapshot({
546
+ snapshot: readContext.snapshot,
547
+ state: nextState,
1448
548
  domainName: normalizedDomainName,
1449
- nowUnixMs,
549
+ }) ? "confirmed" : "live";
550
+ const finalMutation = updateMutationRecord(broadcastingMutation, finalStatus, nowUnixMs, {
551
+ attemptedTxid: built.txid,
552
+ attemptedWtxid: built.wtxid,
553
+ temporaryBuilderLockedOutpoints: [],
1450
554
  });
1451
- await saveWalletStatePreservingUnlock({
1452
- state: {
1453
- ...releasedState,
1454
- stateRevision: releasedState.stateRevision + 1,
1455
- lastWrittenAtUnixMs: nowUnixMs,
1456
- },
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,
1457
563
  provider,
1458
- unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1459
564
  nowUnixMs,
1460
565
  paths,
1461
566
  });
1462
567
  return {
1463
568
  domainName: normalizedDomainName,
1464
- cleared: true,
1465
- previousFamilyStatus: family.status,
1466
- previousFamilyStep: family.currentStep ?? null,
1467
- 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
+ }),
1468
577
  };
1469
578
  }
1470
579
  finally {