@cogcoin/client 0.5.15 → 1.0.1

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 (174) 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 +142 -5
  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 +49 -92
  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 +5 -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/sync-progress.d.ts +6 -0
  75. package/dist/cli/sync-progress.js +91 -0
  76. package/dist/cli/types.d.ts +9 -17
  77. package/dist/cli/types.js +0 -2
  78. package/dist/cli/wallet-format.d.ts +1 -0
  79. package/dist/cli/wallet-format.js +208 -144
  80. package/dist/cli/workflow-hints.d.ts +3 -3
  81. package/dist/cli/workflow-hints.js +11 -8
  82. package/dist/client/default-client.d.ts +3 -1
  83. package/dist/client/default-client.js +45 -2
  84. package/dist/client/factory.js +1 -1
  85. package/dist/client/initialization.js +23 -0
  86. package/dist/client/persistence.js +5 -5
  87. package/dist/client/store-adapter.js +1 -0
  88. package/dist/sqlite/checkpoints.d.ts +1 -0
  89. package/dist/sqlite/checkpoints.js +7 -0
  90. package/dist/sqlite/store.js +14 -1
  91. package/dist/types.d.ts +1 -0
  92. package/dist/wallet/coin-control.d.ts +41 -12
  93. package/dist/wallet/coin-control.js +100 -428
  94. package/dist/wallet/descriptor-normalization.d.ts +1 -3
  95. package/dist/wallet/descriptor-normalization.js +0 -16
  96. package/dist/wallet/lifecycle.d.ts +7 -99
  97. package/dist/wallet/lifecycle.js +513 -968
  98. package/dist/wallet/managed-core-wallet.d.ts +13 -0
  99. package/dist/wallet/managed-core-wallet.js +20 -0
  100. package/dist/wallet/mining/constants.d.ts +5 -12
  101. package/dist/wallet/mining/constants.js +5 -12
  102. package/dist/wallet/mining/control.d.ts +1 -13
  103. package/dist/wallet/mining/control.js +45 -349
  104. package/dist/wallet/mining/index.d.ts +4 -5
  105. package/dist/wallet/mining/index.js +2 -3
  106. package/dist/wallet/mining/runner.d.ts +123 -13
  107. package/dist/wallet/mining/runner.js +899 -511
  108. package/dist/wallet/mining/runtime-artifacts.js +23 -3
  109. package/dist/wallet/mining/sentence-protocol.d.ts +44 -0
  110. package/dist/wallet/mining/sentence-protocol.js +123 -0
  111. package/dist/wallet/mining/sentences.d.ts +4 -8
  112. package/dist/wallet/mining/sentences.js +3 -52
  113. package/dist/wallet/mining/state.d.ts +11 -6
  114. package/dist/wallet/mining/state.js +7 -6
  115. package/dist/wallet/mining/types.d.ts +2 -30
  116. package/dist/wallet/mining/visualizer.d.ts +31 -3
  117. package/dist/wallet/mining/visualizer.js +135 -13
  118. package/dist/wallet/read/context.d.ts +0 -2
  119. package/dist/wallet/read/context.js +119 -140
  120. package/dist/wallet/read/filter.js +2 -11
  121. package/dist/wallet/read/index.d.ts +1 -1
  122. package/dist/wallet/read/project.js +24 -77
  123. package/dist/wallet/read/types.d.ts +10 -25
  124. package/dist/wallet/reset.d.ts +0 -1
  125. package/dist/wallet/reset.js +60 -138
  126. package/dist/wallet/root-resolution.d.ts +1 -5
  127. package/dist/wallet/root-resolution.js +0 -18
  128. package/dist/wallet/runtime.d.ts +0 -6
  129. package/dist/wallet/runtime.js +0 -8
  130. package/dist/wallet/state/client-password-agent.js +208 -0
  131. package/dist/wallet/state/client-password.d.ts +65 -0
  132. package/dist/wallet/state/client-password.js +952 -0
  133. package/dist/wallet/state/crypto.d.ts +1 -20
  134. package/dist/wallet/state/crypto.js +0 -63
  135. package/dist/wallet/state/provider.d.ts +23 -11
  136. package/dist/wallet/state/provider.js +248 -290
  137. package/dist/wallet/state/storage.d.ts +2 -2
  138. package/dist/wallet/state/storage.js +48 -16
  139. package/dist/wallet/tx/anchor.d.ts +3 -28
  140. package/dist/wallet/tx/anchor.js +349 -1250
  141. package/dist/wallet/tx/bitcoin-transfer.d.ts +35 -0
  142. package/dist/wallet/tx/bitcoin-transfer.js +200 -0
  143. package/dist/wallet/tx/cog.d.ts +5 -1
  144. package/dist/wallet/tx/cog.js +149 -185
  145. package/dist/wallet/tx/common.d.ts +61 -8
  146. package/dist/wallet/tx/common.js +266 -146
  147. package/dist/wallet/tx/domain-admin.d.ts +3 -1
  148. package/dist/wallet/tx/domain-admin.js +61 -99
  149. package/dist/wallet/tx/domain-market.d.ts +5 -1
  150. package/dist/wallet/tx/domain-market.js +221 -228
  151. package/dist/wallet/tx/field.d.ts +4 -10
  152. package/dist/wallet/tx/field.js +83 -924
  153. package/dist/wallet/tx/identity-selector.d.ts +9 -3
  154. package/dist/wallet/tx/identity-selector.js +17 -35
  155. package/dist/wallet/tx/index.d.ts +3 -1
  156. package/dist/wallet/tx/index.js +2 -1
  157. package/dist/wallet/tx/register.d.ts +3 -1
  158. package/dist/wallet/tx/register.js +62 -220
  159. package/dist/wallet/tx/reputation.d.ts +3 -1
  160. package/dist/wallet/tx/reputation.js +58 -95
  161. package/dist/wallet/types.d.ts +8 -122
  162. package/package.json +5 -5
  163. package/dist/wallet/archive.d.ts +0 -4
  164. package/dist/wallet/archive.js +0 -41
  165. package/dist/wallet/mining/hook-protocol.d.ts +0 -47
  166. package/dist/wallet/mining/hook-protocol.js +0 -161
  167. package/dist/wallet/mining/hook-runner.js +0 -52
  168. package/dist/wallet/mining/hooks.d.ts +0 -38
  169. package/dist/wallet/mining/hooks.js +0 -520
  170. package/dist/wallet/state/explicit-lock.d.ts +0 -4
  171. package/dist/wallet/state/explicit-lock.js +0 -19
  172. package/dist/wallet/state/session.d.ts +0 -12
  173. package/dist/wallet/state/session.js +0 -23
  174. /package/dist/wallet/{mining/hook-runner.d.ts → state/client-password-agent.d.ts} +0 -0
@@ -4,14 +4,47 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
4
4
  import { createRpcClient } from "../../bitcoind/node.js";
5
5
  import { acquireFileLock } from "../fs/lock.js";
6
6
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
7
+ import { reconcilePersistentPolicyLocks as reconcileWalletCoinControlLocks } from "../coin-control.js";
7
8
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
8
9
  import { serializeDomainBuy, serializeDomainSell, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
9
10
  import { openWalletReadContext } from "../read/index.js";
10
- import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, createBuiltWalletMutationFeeSummary, createFundingMutationSender, createWalletMutationFeeMetadata, getDecodedInputScriptPubKeyHex, isLocalWalletScript, isAlreadyAcceptedError, isBroadcastUnknownError, mergeFixedWalletInputs, outpointKey, pauseMiningForWalletMutation, resolvePendingMutationReuseDecision, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
12
  import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
12
13
  import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
13
14
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
14
15
  import { normalizeBtcTarget } from "./targets.js";
16
+ async function prepareDomainMarketBuildState(options) {
17
+ if (!options.preflightCoinControl) {
18
+ return {
19
+ state: options.state,
20
+ allUtxos: (await options.rpc.listUnspent(options.walletName, 1)).slice(),
21
+ };
22
+ }
23
+ const reconciled = await reconcileWalletCoinControlLocks({
24
+ rpc: options.rpc,
25
+ walletName: options.walletName,
26
+ state: options.state,
27
+ });
28
+ const nextState = reconciled.changed
29
+ ? {
30
+ ...reconciled.state,
31
+ stateRevision: reconciled.state.stateRevision + 1,
32
+ lastWrittenAtUnixMs: options.nowUnixMs,
33
+ }
34
+ : reconciled.state;
35
+ if (reconciled.changed) {
36
+ await saveWalletStatePreservingUnlock({
37
+ state: nextState,
38
+ provider: options.provider,
39
+ nowUnixMs: options.nowUnixMs,
40
+ paths: options.paths,
41
+ });
42
+ }
43
+ return {
44
+ state: nextState,
45
+ allUtxos: (await options.rpc.listUnspent(options.walletName, 1)).slice(),
46
+ };
47
+ }
15
48
  function normalizeDomainName(domainName) {
16
49
  const normalized = domainName.trim().toLowerCase();
17
50
  if (normalized.length === 0) {
@@ -61,12 +94,6 @@ function createIntentFingerprint(parts) {
61
94
  .update(parts.join("\n"))
62
95
  .digest("hex");
63
96
  }
64
- function replaceAssignedDomainNames(identity, nextAssignedDomainNames) {
65
- return {
66
- ...identity,
67
- assignedDomainNames: nextAssignedDomainNames.slice().sort((left, right) => left.localeCompare(right)),
68
- };
69
- }
70
97
  function reserveTransferredDomainRecord(options) {
71
98
  const existing = options.state.domains.find((domain) => domain.name === options.domainName) ?? null;
72
99
  const domains = options.state.domains.some((domain) => domain.name === options.domainName)
@@ -78,9 +105,7 @@ function reserveTransferredDomainRecord(options) {
78
105
  ...domain,
79
106
  domainId: options.domainId ?? domain.domainId,
80
107
  currentOwnerScriptPubKeyHex: options.currentOwnerScriptPubKeyHex,
81
- currentOwnerLocalIndex: options.currentOwnerLocalIndex,
82
108
  canonicalChainStatus: "registered-unanchored",
83
- currentCanonicalAnchorOutpoint: null,
84
109
  birthTime: domain.birthTime ?? Math.floor(options.nowUnixMs / 1000),
85
110
  };
86
111
  })
@@ -89,27 +114,15 @@ function reserveTransferredDomainRecord(options) {
89
114
  {
90
115
  name: options.domainName,
91
116
  domainId: options.domainId,
92
- dedicatedIndex: null,
93
117
  currentOwnerScriptPubKeyHex: options.currentOwnerScriptPubKeyHex,
94
- currentOwnerLocalIndex: options.currentOwnerLocalIndex,
95
118
  canonicalChainStatus: "registered-unanchored",
96
- localAnchorIntent: "none",
97
- currentCanonicalAnchorOutpoint: null,
98
119
  foundingMessageText: existing?.foundingMessageText ?? null,
99
120
  birthTime: Math.floor(options.nowUnixMs / 1000),
100
121
  },
101
122
  ];
102
- const identities = options.state.identities.map((identity) => {
103
- const filtered = identity.assignedDomainNames.filter((domainName) => domainName !== options.domainName);
104
- if (identity.index === options.currentOwnerLocalIndex) {
105
- return replaceAssignedDomainNames(identity, [...filtered, options.domainName]);
106
- }
107
- return replaceAssignedDomainNames(identity, filtered);
108
- });
109
123
  return {
110
124
  ...options.state,
111
125
  domains,
112
- identities,
113
126
  };
114
127
  }
115
128
  function createResolvedDomainMarketSenderSummary(sender, selector) {
@@ -145,21 +158,6 @@ function createSellEconomicEffectSummary(listedPriceCogtoshi) {
145
158
  listedPriceCogtoshi: listedPriceCogtoshi.toString(),
146
159
  };
147
160
  }
148
- function resolveAnchorOutpointForSender(state, sender, errorPrefix) {
149
- const anchoredDomains = state.domains.filter((domain) => domain.currentOwnerLocalIndex === sender.index
150
- && domain.canonicalChainStatus === "anchored");
151
- if (anchoredDomains.length === 0) {
152
- return null;
153
- }
154
- const anchoredDomain = anchoredDomains[0];
155
- if (anchoredDomain.currentCanonicalAnchorOutpoint === null) {
156
- throw new Error(`${errorPrefix}_anchor_outpoint_unavailable`);
157
- }
158
- return {
159
- txid: anchoredDomain.currentCanonicalAnchorOutpoint.txid,
160
- vout: anchoredDomain.currentCanonicalAnchorOutpoint.vout,
161
- };
162
- }
163
161
  function resolveOwnedDomainOperation(context, domainName, errorPrefix) {
164
162
  assertWalletMutationContextReady(context, errorPrefix);
165
163
  const chainDomain = lookupDomain(context.snapshot.state, domainName);
@@ -170,24 +168,14 @@ function resolveOwnedDomainOperation(context, domainName, errorPrefix) {
170
168
  throw new Error(`${errorPrefix}_domain_anchored`);
171
169
  }
172
170
  const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
173
- const senderIdentity = context.model.identities.find((identity) => identity.scriptPubKeyHex === ownerHex) ?? null;
174
- if (senderIdentity === null || senderIdentity.address === null) {
171
+ if (ownerHex !== context.localState.state.funding.scriptPubKeyHex || context.model.walletAddress == null) {
175
172
  throw new Error(`${errorPrefix}_owner_not_locally_controlled`);
176
173
  }
177
- if (senderIdentity.readOnly) {
178
- throw new Error(`${errorPrefix}_owner_read_only`);
179
- }
180
174
  return {
181
175
  readContext: context,
182
176
  state: context.localState.state,
183
- unlockUntilUnixMs: context.localState.unlockUntilUnixMs,
184
- sender: {
185
- localIndex: senderIdentity.index,
186
- scriptPubKeyHex: senderIdentity.scriptPubKeyHex,
187
- address: senderIdentity.address,
188
- },
189
- senderSelector: getCanonicalIdentitySelector(senderIdentity),
190
- anchorOutpoint: resolveAnchorOutpointForSender(context.localState.state, senderIdentity, errorPrefix),
177
+ sender: createFundingMutationSender(context.localState.state),
178
+ senderSelector: context.model.walletAddress,
191
179
  chainDomain,
192
180
  };
193
181
  }
@@ -204,41 +192,24 @@ function resolveBuyOperation(context, domainName, fromIdentity = null) {
204
192
  if (listing === null) {
205
193
  throw new Error("wallet_buy_domain_not_listed");
206
194
  }
207
- const selectedIdentity = fromIdentity === null
208
- ? context.model.fundingIdentity
209
- : resolveIdentityBySelector(context, fromIdentity, "wallet_buy");
210
- if (selectedIdentity === null) {
195
+ if (context.model.walletAddress === null) {
211
196
  throw new Error("wallet_buy_funding_identity_unavailable");
212
197
  }
213
- if (selectedIdentity.address === null) {
214
- throw new Error(fromIdentity === null
215
- ? "wallet_buy_funding_identity_unavailable"
216
- : "wallet_buy_sender_address_unavailable");
217
- }
218
- if (selectedIdentity.readOnly) {
219
- throw new Error("wallet_buy_sender_read_only");
220
- }
221
198
  const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
222
- if (ownerHex === selectedIdentity.scriptPubKeyHex) {
199
+ if (ownerHex === context.localState.state.funding.scriptPubKeyHex) {
223
200
  throw new Error("wallet_buy_already_owner");
224
201
  }
225
- if (getBalance(context.snapshot.state, selectedIdentity.scriptPubKeyHex) < listing.priceCogtoshi) {
202
+ if (getBalance(context.snapshot.state, context.localState.state.funding.scriptPubKeyHex) < listing.priceCogtoshi) {
226
203
  throw new Error("wallet_buy_insufficient_cog_balance");
227
204
  }
228
205
  return {
229
206
  readContext: context,
230
207
  state: context.localState.state,
231
- unlockUntilUnixMs: context.localState.unlockUntilUnixMs,
232
- sender: {
233
- localIndex: selectedIdentity.index,
234
- scriptPubKeyHex: selectedIdentity.scriptPubKeyHex,
235
- address: selectedIdentity.address,
236
- },
237
- senderSelector: getCanonicalIdentitySelector(selectedIdentity),
238
- anchorOutpoint: resolveAnchorOutpointForSender(context.localState.state, selectedIdentity, "wallet_buy"),
208
+ sender: createFundingMutationSender(context.localState.state),
209
+ senderSelector: context.model.walletAddress,
239
210
  chainDomain,
240
211
  listingPriceCogtoshi: listing.priceCogtoshi,
241
- buyerSelector: getCanonicalIdentitySelector(selectedIdentity),
212
+ buyerSelector: context.model.walletAddress,
242
213
  };
243
214
  }
244
215
  function buildPlanForDomainOperation(options) {
@@ -246,45 +217,13 @@ function buildPlanForDomainOperation(options) {
246
217
  && entry.confirmations >= 1
247
218
  && entry.spendable !== false
248
219
  && entry.safe !== false);
249
- const outputs = [{ data: Buffer.from(options.opReturnData).toString("hex") }];
250
- if (options.anchorOutpoint === null) {
251
- return {
252
- sender: options.sender,
253
- changeAddress: options.state.funding.address,
254
- fixedInputs: [],
255
- outputs,
256
- changePosition: 1,
257
- expectedOpReturnScriptHex: encodeOpReturnScript(options.opReturnData),
258
- expectedAnchorScriptHex: null,
259
- expectedAnchorValueSats: null,
260
- allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
261
- eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
262
- errorPrefix: options.errorPrefix,
263
- };
264
- }
265
- const anchorUtxo = options.allUtxos.find((entry) => entry.txid === options.anchorOutpoint?.txid
266
- && entry.vout === options.anchorOutpoint.vout
267
- && entry.scriptPubKey === options.sender.scriptPubKeyHex
268
- && entry.confirmations >= 1
269
- && entry.spendable !== false
270
- && entry.safe !== false);
271
- if (anchorUtxo === undefined) {
272
- throw new Error(`${options.errorPrefix}_anchor_utxo_missing`);
273
- }
274
- outputs.push({
275
- [options.sender.address]: satsToBtcNumber(options.anchorValueSats),
276
- });
277
220
  return {
278
221
  sender: options.sender,
279
222
  changeAddress: options.state.funding.address,
280
- fixedInputs: [
281
- { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
282
- ],
283
- outputs,
284
- changePosition: 2,
223
+ fixedInputs: [],
224
+ outputs: [{ data: Buffer.from(options.opReturnData).toString("hex") }],
225
+ changePosition: 1,
285
226
  expectedOpReturnScriptHex: encodeOpReturnScript(options.opReturnData),
286
- expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
287
- expectedAnchorValueSats: options.anchorValueSats,
288
227
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
289
228
  eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
290
229
  errorPrefix: options.errorPrefix,
@@ -296,29 +235,10 @@ function validateFundedDraft(decoded, funded, plan) {
296
235
  if (inputs.length === 0) {
297
236
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
298
237
  }
299
- assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
300
- if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
301
- throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
302
- }
303
- assertFundingInputsAfterFixedPrefix({
304
- decoded,
305
- fixedInputs: plan.fixedInputs,
306
- allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
307
- eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
308
- errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
309
- });
310
238
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
311
239
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
312
240
  }
313
- if (plan.expectedAnchorScriptHex !== null) {
314
- if (outputs[1]?.scriptPubKey?.hex !== plan.expectedAnchorScriptHex) {
315
- throw new Error(`${plan.errorPrefix}_anchor_output_mismatch`);
316
- }
317
- if (valueToSats(outputs[1]?.value ?? 0) !== (plan.expectedAnchorValueSats ?? 0n)) {
318
- throw new Error(`${plan.errorPrefix}_anchor_value_mismatch`);
319
- }
320
- }
321
- const expectedWithoutChange = plan.expectedAnchorScriptHex === null ? 1 : 2;
241
+ const expectedWithoutChange = 1;
322
242
  if (funded.changepos === -1) {
323
243
  if (outputs.length !== expectedWithoutChange) {
324
244
  throw new Error(`${plan.errorPrefix}_unexpected_output_count`);
@@ -341,7 +261,7 @@ async function buildTransaction(options) {
341
261
  validateFundedDraft,
342
262
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
343
263
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
344
- reserveCandidates: options.state.proactiveReserveOutpoints,
264
+ feeRate: options.feeRateSatVb,
345
265
  });
346
266
  }
347
267
  function createDraftMutation(options) {
@@ -358,6 +278,7 @@ function createDraftMutation(options) {
358
278
  lastUpdatedAtUnixMs: options.nowUnixMs,
359
279
  attemptedTxid: null,
360
280
  attemptedWtxid: null,
281
+ ...createWalletMutationFeeMetadata(options.feeSelection),
361
282
  temporaryBuilderLockedOutpoints: [],
362
283
  };
363
284
  }
@@ -376,6 +297,7 @@ function createDraftMutation(options) {
376
297
  lastUpdatedAtUnixMs: options.nowUnixMs,
377
298
  attemptedTxid: null,
378
299
  attemptedWtxid: null,
300
+ ...createWalletMutationFeeMetadata(options.feeSelection),
379
301
  temporaryBuilderLockedOutpoints: [],
380
302
  };
381
303
  }
@@ -436,13 +358,11 @@ async function reconcilePendingMutation(options) {
436
358
  domainName: options.mutation.domainName,
437
359
  domainId: chainDomain.domainId,
438
360
  currentOwnerScriptPubKeyHex: ownerHex,
439
- currentOwnerLocalIndex: options.context.model?.identities.find((identity) => identity.scriptPubKeyHex === ownerHex)?.index ?? null,
440
361
  nowUnixMs: options.nowUnixMs,
441
362
  });
442
363
  await saveWalletStatePreservingUnlock({
443
364
  state: nextState,
444
365
  provider: options.provider,
445
- unlockUntilUnixMs: options.unlockUntilUnixMs,
446
366
  nowUnixMs: options.nowUnixMs,
447
367
  paths: options.paths,
448
368
  });
@@ -456,7 +376,6 @@ async function reconcilePendingMutation(options) {
456
376
  await saveWalletStatePreservingUnlock({
457
377
  state: nextState,
458
378
  provider: options.provider,
459
- unlockUntilUnixMs: options.unlockUntilUnixMs,
460
379
  nowUnixMs: options.nowUnixMs,
461
380
  paths: options.paths,
462
381
  });
@@ -475,7 +394,6 @@ async function reconcilePendingMutation(options) {
475
394
  await saveWalletStatePreservingUnlock({
476
395
  state: nextState,
477
396
  provider: options.provider,
478
- unlockUntilUnixMs: options.unlockUntilUnixMs,
479
397
  nowUnixMs: options.nowUnixMs,
480
398
  paths: options.paths,
481
399
  });
@@ -490,7 +408,6 @@ async function reconcilePendingMutation(options) {
490
408
  await saveWalletStatePreservingUnlock({
491
409
  state: nextState,
492
410
  provider: options.provider,
493
- unlockUntilUnixMs: options.unlockUntilUnixMs,
494
411
  nowUnixMs: options.nowUnixMs,
495
412
  paths: options.paths,
496
413
  });
@@ -505,7 +422,6 @@ async function reconcilePendingMutation(options) {
505
422
  await saveWalletStatePreservingUnlock({
506
423
  state: nextState,
507
424
  provider: options.provider,
508
- unlockUntilUnixMs: options.unlockUntilUnixMs,
509
425
  nowUnixMs: options.nowUnixMs,
510
426
  paths: options.paths,
511
427
  });
@@ -523,13 +439,11 @@ async function reconcilePendingMutation(options) {
523
439
  domainName: options.mutation.domainName,
524
440
  domainId: chainDomain.domainId,
525
441
  currentOwnerScriptPubKeyHex: ownerHex,
526
- currentOwnerLocalIndex: options.context.model?.identities.find((identity) => identity.scriptPubKeyHex === ownerHex)?.index ?? null,
527
442
  nowUnixMs: options.nowUnixMs,
528
443
  });
529
444
  await saveWalletStatePreservingUnlock({
530
445
  state: nextState,
531
446
  provider: options.provider,
532
- unlockUntilUnixMs: options.unlockUntilUnixMs,
533
447
  nowUnixMs: options.nowUnixMs,
534
448
  paths: options.paths,
535
449
  });
@@ -543,7 +457,6 @@ async function reconcilePendingMutation(options) {
543
457
  await saveWalletStatePreservingUnlock({
544
458
  state: nextState,
545
459
  provider: options.provider,
546
- unlockUntilUnixMs: options.unlockUntilUnixMs,
547
460
  nowUnixMs: options.nowUnixMs,
548
461
  paths: options.paths,
549
462
  });
@@ -567,16 +480,12 @@ async function reconcilePendingMutation(options) {
567
480
  currentOwnerScriptPubKeyHex: live.kind === "transfer"
568
481
  ? (live.recipientScriptPubKeyHex ?? live.senderScriptPubKeyHex)
569
482
  : live.senderScriptPubKeyHex,
570
- currentOwnerLocalIndex: live.kind === "transfer"
571
- ? options.context.model?.identities.find((identity) => identity.scriptPubKeyHex === live.recipientScriptPubKeyHex)?.index ?? null
572
- : live.senderLocalIndex,
573
483
  nowUnixMs: options.nowUnixMs,
574
484
  });
575
485
  }
576
486
  await saveWalletStatePreservingUnlock({
577
487
  state: nextState,
578
488
  provider: options.provider,
579
- unlockUntilUnixMs: options.unlockUntilUnixMs,
580
489
  nowUnixMs: options.nowUnixMs,
581
490
  paths: options.paths,
582
491
  });
@@ -595,7 +504,6 @@ async function reconcilePendingMutation(options) {
595
504
  await saveWalletStatePreservingUnlock({
596
505
  state: nextState,
597
506
  provider: options.provider,
598
- unlockUntilUnixMs: options.unlockUntilUnixMs,
599
507
  nowUnixMs: options.nowUnixMs,
600
508
  paths: options.paths,
601
509
  });
@@ -705,46 +613,63 @@ export async function transferDomain(options) {
705
613
  });
706
614
  const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
707
615
  const walletName = operation.state.managedCoreWallet.walletName;
616
+ const feeSelection = await resolveWalletMutationFeeSelection({
617
+ rpc,
618
+ feeRateSatVb: options.feeRateSatVb ?? null,
619
+ });
708
620
  const existingMutation = findPendingMutationByIntent(operation.state, intentFingerprintHex);
621
+ let workingState = operation.state;
622
+ let replacementFixedInputs = null;
709
623
  if (existingMutation !== null) {
710
624
  const reconciled = await reconcilePendingMutation({
711
625
  state: operation.state,
712
626
  mutation: existingMutation,
713
627
  provider,
714
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
715
628
  nowUnixMs,
716
629
  paths,
717
630
  rpc,
718
631
  walletName,
719
632
  context: readContext,
720
633
  });
634
+ workingState = reconciled.state;
721
635
  if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
722
- return {
723
- kind: "transfer",
724
- domainName: normalizedDomainName,
725
- txid: reconciled.mutation.attemptedTxid ?? "unknown",
726
- status: reconciled.resolution,
727
- reusedExisting: true,
728
- recipientScriptPubKeyHex: recipient.scriptPubKeyHex,
729
- resolved: {
730
- sender: resolvedSender,
731
- recipient: resolvedRecipient,
732
- economicEffect: resolvedEconomicEffect,
733
- },
734
- };
636
+ const reuse = await resolvePendingMutationReuseDecision({
637
+ rpc,
638
+ walletName,
639
+ mutation: reconciled.mutation,
640
+ nextFeeSelection: feeSelection,
641
+ });
642
+ if (reuse.reuseExisting) {
643
+ return {
644
+ kind: "transfer",
645
+ domainName: normalizedDomainName,
646
+ txid: reconciled.mutation.attemptedTxid ?? "unknown",
647
+ status: reconciled.resolution,
648
+ reusedExisting: true,
649
+ recipientScriptPubKeyHex: recipient.scriptPubKeyHex,
650
+ resolved: {
651
+ sender: resolvedSender,
652
+ recipient: resolvedRecipient,
653
+ economicEffect: resolvedEconomicEffect,
654
+ },
655
+ fees: reuse.fees,
656
+ };
657
+ }
658
+ replacementFixedInputs = reuse.replacementFixedInputs;
735
659
  }
736
660
  if (reconciled.resolution === "repair-required") {
737
661
  throw new Error("wallet_transfer_repair_required");
738
662
  }
739
663
  }
740
664
  await confirmTransfer(options.prompter, normalizedDomainName, resolvedSender, resolvedRecipient, resolvedEconomicEffect, options.assumeYes);
741
- let nextState = upsertPendingMutation(operation.state, createDraftMutation({
665
+ let nextState = upsertPendingMutation(workingState, createDraftMutation({
742
666
  kind: "transfer",
743
667
  domainName: normalizedDomainName,
744
668
  sender: operation.sender,
745
669
  recipientScriptPubKeyHex: recipient.scriptPubKeyHex,
746
670
  intentFingerprintHex,
747
671
  nowUnixMs,
672
+ feeSelection,
748
673
  existing: existingMutation,
749
674
  }));
750
675
  nextState = {
@@ -755,23 +680,35 @@ export async function transferDomain(options) {
755
680
  await saveWalletStatePreservingUnlock({
756
681
  state: nextState,
757
682
  provider,
758
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
759
683
  nowUnixMs,
760
684
  paths,
761
685
  });
686
+ const buildPreparation = await prepareDomainMarketBuildState({
687
+ rpc,
688
+ walletName,
689
+ state: nextState,
690
+ provider,
691
+ nowUnixMs,
692
+ paths,
693
+ preflightCoinControl: false,
694
+ });
695
+ nextState = buildPreparation.state;
696
+ const transferPlan = buildPlanForDomainOperation({
697
+ state: nextState,
698
+ allUtxos: buildPreparation.allUtxos,
699
+ sender: operation.sender,
700
+ opReturnData: serializeDomainTransfer(operation.chainDomain.domainId, Buffer.from(recipient.scriptPubKeyHex, "hex")).opReturnData,
701
+ errorPrefix: "wallet_transfer",
702
+ });
762
703
  const built = await buildTransaction({
763
704
  rpc,
764
705
  walletName,
765
706
  state: nextState,
766
- plan: buildPlanForDomainOperation({
767
- state: nextState,
768
- allUtxos: await rpc.listUnspent(walletName, 1),
769
- sender: operation.sender,
770
- anchorOutpoint: operation.anchorOutpoint,
771
- opReturnData: serializeDomainTransfer(operation.chainDomain.domainId, Buffer.from(recipient.scriptPubKeyHex, "hex")).opReturnData,
772
- anchorValueSats: BigInt(nextState.anchorValueSats),
773
- errorPrefix: "wallet_transfer",
774
- }),
707
+ plan: {
708
+ ...transferPlan,
709
+ fixedInputs: mergeFixedWalletInputs(transferPlan.fixedInputs, replacementFixedInputs),
710
+ },
711
+ feeRateSatVb: feeSelection.feeRateSatVb,
775
712
  });
776
713
  const broadcasting = updateMutationRecord(nextState.pendingMutations.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex), "broadcasting", nowUnixMs, {
777
714
  attemptedTxid: built.txid,
@@ -786,7 +723,6 @@ export async function transferDomain(options) {
786
723
  await saveWalletStatePreservingUnlock({
787
724
  state: nextState,
788
725
  provider,
789
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
790
726
  nowUnixMs,
791
727
  paths,
792
728
  });
@@ -813,7 +749,6 @@ export async function transferDomain(options) {
813
749
  await saveWalletStatePreservingUnlock({
814
750
  state: nextState,
815
751
  provider,
816
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
817
752
  nowUnixMs,
818
753
  paths,
819
754
  });
@@ -833,7 +768,6 @@ export async function transferDomain(options) {
833
768
  await saveWalletStatePreservingUnlock({
834
769
  state: nextState,
835
770
  provider,
836
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
837
771
  nowUnixMs,
838
772
  paths,
839
773
  });
@@ -856,7 +790,6 @@ export async function transferDomain(options) {
856
790
  domainName: normalizedDomainName,
857
791
  domainId: operation.chainDomain.domainId,
858
792
  currentOwnerScriptPubKeyHex: recipient.scriptPubKeyHex,
859
- currentOwnerLocalIndex: model.identities.find((identity) => identity.scriptPubKeyHex === recipient.scriptPubKeyHex)?.index ?? null,
860
793
  nowUnixMs,
861
794
  });
862
795
  nextState = {
@@ -867,7 +800,6 @@ export async function transferDomain(options) {
867
800
  await saveWalletStatePreservingUnlock({
868
801
  state: nextState,
869
802
  provider,
870
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
871
803
  nowUnixMs,
872
804
  paths,
873
805
  });
@@ -883,6 +815,10 @@ export async function transferDomain(options) {
883
815
  recipient: resolvedRecipient,
884
816
  economicEffect: resolvedEconomicEffect,
885
817
  },
818
+ fees: createBuiltWalletMutationFeeSummary({
819
+ selection: feeSelection,
820
+ built,
821
+ }),
886
822
  };
887
823
  }
888
824
  finally {
@@ -935,32 +871,48 @@ async function runSellMutation(options) {
935
871
  });
936
872
  const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
937
873
  const walletName = operation.state.managedCoreWallet.walletName;
874
+ const feeSelection = await resolveWalletMutationFeeSelection({
875
+ rpc,
876
+ feeRateSatVb: options.feeRateSatVb ?? null,
877
+ });
938
878
  const existingMutation = findPendingMutationByIntent(operation.state, intentFingerprintHex);
879
+ let workingState = operation.state;
880
+ let replacementFixedInputs = null;
939
881
  if (existingMutation !== null) {
940
882
  const reconciled = await reconcilePendingMutation({
941
883
  state: operation.state,
942
884
  mutation: existingMutation,
943
885
  provider,
944
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
945
886
  nowUnixMs,
946
887
  paths,
947
888
  rpc,
948
889
  walletName,
949
890
  context: readContext,
950
891
  });
892
+ workingState = reconciled.state;
951
893
  if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
952
- return {
953
- kind: "sell",
954
- domainName: normalizedDomainName,
955
- txid: reconciled.mutation.attemptedTxid ?? "unknown",
956
- status: reconciled.resolution,
957
- reusedExisting: true,
958
- listedPriceCogtoshi: options.listedPriceCogtoshi,
959
- resolved: {
960
- sender: resolvedSender,
961
- economicEffect: resolvedEconomicEffect,
962
- },
963
- };
894
+ const reuse = await resolvePendingMutationReuseDecision({
895
+ rpc,
896
+ walletName,
897
+ mutation: reconciled.mutation,
898
+ nextFeeSelection: feeSelection,
899
+ });
900
+ if (reuse.reuseExisting) {
901
+ return {
902
+ kind: "sell",
903
+ domainName: normalizedDomainName,
904
+ txid: reconciled.mutation.attemptedTxid ?? "unknown",
905
+ status: reconciled.resolution,
906
+ reusedExisting: true,
907
+ listedPriceCogtoshi: options.listedPriceCogtoshi,
908
+ resolved: {
909
+ sender: resolvedSender,
910
+ economicEffect: resolvedEconomicEffect,
911
+ },
912
+ fees: reuse.fees,
913
+ };
914
+ }
915
+ replacementFixedInputs = reuse.replacementFixedInputs;
964
916
  }
965
917
  if (reconciled.resolution === "repair-required") {
966
918
  throw new Error("wallet_sell_repair_required");
@@ -969,13 +921,14 @@ async function runSellMutation(options) {
969
921
  if (options.listedPriceCogtoshi > 0n) {
970
922
  await confirmSell(options.prompter, normalizedDomainName, resolvedSender, options.listedPriceCogtoshi, options.assumeYes);
971
923
  }
972
- let nextState = upsertPendingMutation(operation.state, createDraftMutation({
924
+ let nextState = upsertPendingMutation(workingState, createDraftMutation({
973
925
  kind: "sell",
974
926
  domainName: normalizedDomainName,
975
927
  sender: operation.sender,
976
928
  priceCogtoshi: options.listedPriceCogtoshi,
977
929
  intentFingerprintHex,
978
930
  nowUnixMs,
931
+ feeSelection,
979
932
  existing: existingMutation,
980
933
  }));
981
934
  nextState = {
@@ -986,23 +939,35 @@ async function runSellMutation(options) {
986
939
  await saveWalletStatePreservingUnlock({
987
940
  state: nextState,
988
941
  provider,
989
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
990
942
  nowUnixMs,
991
943
  paths,
992
944
  });
945
+ const buildPreparation = await prepareDomainMarketBuildState({
946
+ rpc,
947
+ walletName,
948
+ state: nextState,
949
+ provider,
950
+ nowUnixMs,
951
+ paths,
952
+ preflightCoinControl: false,
953
+ });
954
+ nextState = buildPreparation.state;
955
+ const sellPlan = buildPlanForDomainOperation({
956
+ state: nextState,
957
+ allUtxos: buildPreparation.allUtxos,
958
+ sender: operation.sender,
959
+ opReturnData: serializeDomainSell(operation.chainDomain.domainId, options.listedPriceCogtoshi).opReturnData,
960
+ errorPrefix: "wallet_sell",
961
+ });
993
962
  const built = await buildTransaction({
994
963
  rpc,
995
964
  walletName,
996
965
  state: nextState,
997
- plan: buildPlanForDomainOperation({
998
- state: nextState,
999
- allUtxos: await rpc.listUnspent(walletName, 1),
1000
- sender: operation.sender,
1001
- anchorOutpoint: operation.anchorOutpoint,
1002
- opReturnData: serializeDomainSell(operation.chainDomain.domainId, options.listedPriceCogtoshi).opReturnData,
1003
- anchorValueSats: BigInt(nextState.anchorValueSats),
1004
- errorPrefix: "wallet_sell",
1005
- }),
966
+ plan: {
967
+ ...sellPlan,
968
+ fixedInputs: mergeFixedWalletInputs(sellPlan.fixedInputs, replacementFixedInputs),
969
+ },
970
+ feeRateSatVb: feeSelection.feeRateSatVb,
1006
971
  });
1007
972
  const broadcasting = updateMutationRecord(nextState.pendingMutations.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex), "broadcasting", nowUnixMs, {
1008
973
  attemptedTxid: built.txid,
@@ -1017,7 +982,6 @@ async function runSellMutation(options) {
1017
982
  await saveWalletStatePreservingUnlock({
1018
983
  state: nextState,
1019
984
  provider,
1020
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1021
985
  nowUnixMs,
1022
986
  paths,
1023
987
  });
@@ -1044,7 +1008,6 @@ async function runSellMutation(options) {
1044
1008
  await saveWalletStatePreservingUnlock({
1045
1009
  state: nextState,
1046
1010
  provider,
1047
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1048
1011
  nowUnixMs,
1049
1012
  paths,
1050
1013
  });
@@ -1064,7 +1027,6 @@ async function runSellMutation(options) {
1064
1027
  await saveWalletStatePreservingUnlock({
1065
1028
  state: nextState,
1066
1029
  provider,
1067
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1068
1030
  nowUnixMs,
1069
1031
  paths,
1070
1032
  });
@@ -1091,7 +1053,6 @@ async function runSellMutation(options) {
1091
1053
  await saveWalletStatePreservingUnlock({
1092
1054
  state: nextState,
1093
1055
  provider,
1094
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1095
1056
  nowUnixMs,
1096
1057
  paths,
1097
1058
  });
@@ -1106,6 +1067,10 @@ async function runSellMutation(options) {
1106
1067
  sender: resolvedSender,
1107
1068
  economicEffect: resolvedEconomicEffect,
1108
1069
  },
1070
+ fees: createBuiltWalletMutationFeeSummary({
1071
+ selection: feeSelection,
1072
+ built,
1073
+ }),
1109
1074
  };
1110
1075
  }
1111
1076
  finally {
@@ -1149,7 +1114,7 @@ export async function buyDomain(options) {
1149
1114
  const snapshot = readContext.snapshot;
1150
1115
  const model = readContext.model;
1151
1116
  const sellerScriptPubKeyHex = Buffer.from(operation.chainDomain.ownerScriptPubKey).toString("hex");
1152
- const sellerAddress = model.identities.find((identity) => identity.scriptPubKeyHex === sellerScriptPubKeyHex)?.address ?? null;
1117
+ const sellerAddress = sellerScriptPubKeyHex === model.walletScriptPubKeyHex ? model.walletAddress : null;
1153
1118
  const resolvedBuyer = {
1154
1119
  selector: operation.buyerSelector,
1155
1120
  localIndex: operation.sender.localIndex,
@@ -1175,43 +1140,60 @@ export async function buyDomain(options) {
1175
1140
  });
1176
1141
  const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1177
1142
  const walletName = operation.state.managedCoreWallet.walletName;
1143
+ const feeSelection = await resolveWalletMutationFeeSelection({
1144
+ rpc,
1145
+ feeRateSatVb: options.feeRateSatVb ?? null,
1146
+ });
1178
1147
  const existingMutation = findPendingMutationByIntent(operation.state, intentFingerprintHex);
1148
+ let workingState = operation.state;
1149
+ let replacementFixedInputs = null;
1179
1150
  if (existingMutation !== null) {
1180
1151
  const reconciled = await reconcilePendingMutation({
1181
1152
  state: operation.state,
1182
1153
  mutation: existingMutation,
1183
1154
  provider,
1184
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1185
1155
  nowUnixMs,
1186
1156
  paths,
1187
1157
  rpc,
1188
1158
  walletName,
1189
1159
  context: readContext,
1190
1160
  });
1161
+ workingState = reconciled.state;
1191
1162
  if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
1192
- return {
1193
- kind: "buy",
1194
- domainName: normalizedDomainName,
1195
- txid: reconciled.mutation.attemptedTxid ?? "unknown",
1196
- status: reconciled.resolution,
1197
- reusedExisting: true,
1198
- listedPriceCogtoshi: operation.listingPriceCogtoshi,
1199
- resolvedBuyer,
1200
- resolvedSeller,
1201
- };
1163
+ const reuse = await resolvePendingMutationReuseDecision({
1164
+ rpc,
1165
+ walletName,
1166
+ mutation: reconciled.mutation,
1167
+ nextFeeSelection: feeSelection,
1168
+ });
1169
+ if (reuse.reuseExisting) {
1170
+ return {
1171
+ kind: "buy",
1172
+ domainName: normalizedDomainName,
1173
+ txid: reconciled.mutation.attemptedTxid ?? "unknown",
1174
+ status: reconciled.resolution,
1175
+ reusedExisting: true,
1176
+ listedPriceCogtoshi: operation.listingPriceCogtoshi,
1177
+ resolvedBuyer,
1178
+ resolvedSeller,
1179
+ fees: reuse.fees,
1180
+ };
1181
+ }
1182
+ replacementFixedInputs = reuse.replacementFixedInputs;
1202
1183
  }
1203
1184
  if (reconciled.resolution === "repair-required") {
1204
1185
  throw new Error("wallet_buy_repair_required");
1205
1186
  }
1206
1187
  }
1207
1188
  await confirmBuy(options.prompter, normalizedDomainName, operation.buyerSelector, operation.sender, sellerScriptPubKeyHex, sellerAddress, operation.listingPriceCogtoshi, options.assumeYes);
1208
- let nextState = upsertPendingMutation(operation.state, createDraftMutation({
1189
+ let nextState = upsertPendingMutation(workingState, createDraftMutation({
1209
1190
  kind: "buy",
1210
1191
  domainName: normalizedDomainName,
1211
1192
  sender: operation.sender,
1212
1193
  priceCogtoshi: operation.listingPriceCogtoshi,
1213
1194
  intentFingerprintHex,
1214
1195
  nowUnixMs,
1196
+ feeSelection,
1215
1197
  existing: existingMutation,
1216
1198
  }));
1217
1199
  nextState = {
@@ -1222,23 +1204,35 @@ export async function buyDomain(options) {
1222
1204
  await saveWalletStatePreservingUnlock({
1223
1205
  state: nextState,
1224
1206
  provider,
1225
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1226
1207
  nowUnixMs,
1227
1208
  paths,
1228
1209
  });
1210
+ const buildPreparation = await prepareDomainMarketBuildState({
1211
+ rpc,
1212
+ walletName,
1213
+ state: nextState,
1214
+ provider,
1215
+ nowUnixMs,
1216
+ paths,
1217
+ preflightCoinControl: false,
1218
+ });
1219
+ nextState = buildPreparation.state;
1220
+ const buyPlan = buildPlanForDomainOperation({
1221
+ state: nextState,
1222
+ allUtxos: buildPreparation.allUtxos,
1223
+ sender: operation.sender,
1224
+ opReturnData: serializeDomainBuy(operation.chainDomain.domainId, operation.listingPriceCogtoshi).opReturnData,
1225
+ errorPrefix: "wallet_buy",
1226
+ });
1229
1227
  const built = await buildTransaction({
1230
1228
  rpc,
1231
1229
  walletName,
1232
1230
  state: nextState,
1233
- plan: buildPlanForDomainOperation({
1234
- state: nextState,
1235
- allUtxos: await rpc.listUnspent(walletName, 1),
1236
- sender: operation.sender,
1237
- anchorOutpoint: operation.anchorOutpoint,
1238
- opReturnData: serializeDomainBuy(operation.chainDomain.domainId, operation.listingPriceCogtoshi).opReturnData,
1239
- anchorValueSats: BigInt(nextState.anchorValueSats),
1240
- errorPrefix: "wallet_buy",
1241
- }),
1231
+ plan: {
1232
+ ...buyPlan,
1233
+ fixedInputs: mergeFixedWalletInputs(buyPlan.fixedInputs, replacementFixedInputs),
1234
+ },
1235
+ feeRateSatVb: feeSelection.feeRateSatVb,
1242
1236
  });
1243
1237
  const currentSellerHex = Buffer.from(operation.chainDomain.ownerScriptPubKey).toString("hex");
1244
1238
  if (currentSellerHex !== sellerScriptPubKeyHex) {
@@ -1258,7 +1252,6 @@ export async function buyDomain(options) {
1258
1252
  await saveWalletStatePreservingUnlock({
1259
1253
  state: nextState,
1260
1254
  provider,
1261
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1262
1255
  nowUnixMs,
1263
1256
  paths,
1264
1257
  });
@@ -1285,7 +1278,6 @@ export async function buyDomain(options) {
1285
1278
  await saveWalletStatePreservingUnlock({
1286
1279
  state: nextState,
1287
1280
  provider,
1288
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1289
1281
  nowUnixMs,
1290
1282
  paths,
1291
1283
  });
@@ -1305,7 +1297,6 @@ export async function buyDomain(options) {
1305
1297
  await saveWalletStatePreservingUnlock({
1306
1298
  state: nextState,
1307
1299
  provider,
1308
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1309
1300
  nowUnixMs,
1310
1301
  paths,
1311
1302
  });
@@ -1328,7 +1319,6 @@ export async function buyDomain(options) {
1328
1319
  domainName: normalizedDomainName,
1329
1320
  domainId: operation.chainDomain.domainId,
1330
1321
  currentOwnerScriptPubKeyHex: operation.sender.scriptPubKeyHex,
1331
- currentOwnerLocalIndex: operation.sender.localIndex,
1332
1322
  nowUnixMs,
1333
1323
  });
1334
1324
  nextState = {
@@ -1339,7 +1329,6 @@ export async function buyDomain(options) {
1339
1329
  await saveWalletStatePreservingUnlock({
1340
1330
  state: nextState,
1341
1331
  provider,
1342
- unlockUntilUnixMs: operation.unlockUntilUnixMs,
1343
1332
  nowUnixMs,
1344
1333
  paths,
1345
1334
  });
@@ -1352,6 +1341,10 @@ export async function buyDomain(options) {
1352
1341
  listedPriceCogtoshi: operation.listingPriceCogtoshi,
1353
1342
  resolvedBuyer,
1354
1343
  resolvedSeller,
1344
+ fees: createBuiltWalletMutationFeeSummary({
1345
+ selection: feeSelection,
1346
+ built,
1347
+ }),
1355
1348
  };
1356
1349
  }
1357
1350
  finally {