@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,29 +1,194 @@
1
- import { randomBytes } from "node:crypto";
2
- import { saveUnlockSession } from "../state/session.js";
3
1
  import { saveWalletState } from "../state/storage.js";
4
2
  import { createWalletSecretReference, } from "../state/provider.js";
3
+ import { MANAGED_CORE_WALLET_UNLOCK_TIMEOUT_SECONDS, withUnlockedManagedCoreWallet, } from "../managed-core-wallet.js";
5
4
  import { reconcilePersistentPolicyLocks as reconcileWalletCoinControlLocks } from "../coin-control.js";
6
5
  import { requestMiningGenerationPreemption } from "../mining/coordination.js";
7
6
  export const DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB = 10;
7
+ export const NEXT_BLOCK_FEE_CONFIRM_TARGET = 1;
8
+ export function isLocalWalletScript(state, scriptPubKeyHex) {
9
+ if (typeof scriptPubKeyHex !== "string" || scriptPubKeyHex.length === 0) {
10
+ return false;
11
+ }
12
+ return scriptPubKeyHex === state.funding.scriptPubKeyHex
13
+ || (state.localScriptPubKeyHexes ?? []).includes(scriptPubKeyHex);
14
+ }
15
+ export function createFundingMutationSender(state) {
16
+ return {
17
+ localIndex: 0,
18
+ scriptPubKeyHex: state.funding.scriptPubKeyHex,
19
+ address: state.funding.address,
20
+ };
21
+ }
8
22
  function btcNumberToSats(value) {
9
23
  return BigInt(Math.round(value * 100_000_000));
10
24
  }
25
+ function normalizeSatVb(value) {
26
+ return Number.parseFloat(value.toFixed(8));
27
+ }
28
+ function satVbFromBtcPerKvB(value) {
29
+ return normalizeSatVb((value * 100_000_000) / 1_000);
30
+ }
11
31
  function valueToSats(value) {
12
32
  return typeof value === "string"
13
33
  ? BigInt(Math.round(Number(value) * 100_000_000))
14
34
  : btcNumberToSats(value);
15
35
  }
16
- function createUnlockSessionState(state, unlockUntilUnixMs, nowUnixMs) {
36
+ function feeRateFromMempoolEntry(entry) {
37
+ if (!Number.isFinite(entry.vsize) || entry.vsize <= 0) {
38
+ return null;
39
+ }
40
+ const feeSats = Number(btcNumberToSats(entry.fees.base));
41
+ if (!Number.isFinite(feeSats) || feeSats <= 0) {
42
+ return null;
43
+ }
44
+ return normalizeSatVb(feeSats / entry.vsize);
45
+ }
46
+ export function formatSatVb(value) {
47
+ return normalizeSatVb(value).toString();
48
+ }
49
+ export function createWalletMutationFeeMetadata(selection) {
50
+ return {
51
+ selectedFeeRateSatVb: selection.feeRateSatVb,
52
+ feeSelectionSource: selection.source,
53
+ };
54
+ }
55
+ export async function resolveWalletMutationFeeSelection(options) {
56
+ if (typeof options.feeRateSatVb === "number") {
57
+ return {
58
+ feeRateSatVb: normalizeSatVb(options.feeRateSatVb),
59
+ source: "custom-satvb",
60
+ };
61
+ }
62
+ if (options.rpc.estimateSmartFee !== undefined) {
63
+ try {
64
+ const estimate = await options.rpc.estimateSmartFee(NEXT_BLOCK_FEE_CONFIRM_TARGET, "conservative");
65
+ const estimatedSatVb = typeof estimate.feerate === "number"
66
+ ? satVbFromBtcPerKvB(estimate.feerate)
67
+ : null;
68
+ if (estimatedSatVb !== null && Number.isFinite(estimatedSatVb) && estimatedSatVb > 0) {
69
+ return {
70
+ feeRateSatVb: normalizeSatVb(estimatedSatVb + 1),
71
+ source: "estimated-next-block-plus-one",
72
+ };
73
+ }
74
+ }
75
+ catch {
76
+ // Fall through to the compatibility default.
77
+ }
78
+ }
79
+ return {
80
+ feeRateSatVb: DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB,
81
+ source: "fallback-default",
82
+ };
83
+ }
84
+ export function createWalletMutationFeeSummary(selection, feeSats) {
17
85
  return {
18
- schemaVersion: 1,
19
- walletRootId: state.walletRootId,
20
- sessionId: randomBytes(16).toString("hex"),
21
- createdAtUnixMs: nowUnixMs,
22
- unlockUntilUnixMs,
23
- sourceStateRevision: state.stateRevision,
24
- wrappedSessionKeyMaterial: createWalletSecretReference(state.walletRootId).keyId,
86
+ feeRateSatVb: selection.feeRateSatVb,
87
+ feeSats,
88
+ source: selection.source,
25
89
  };
26
90
  }
91
+ export function createBuiltWalletMutationFeeSummary(options) {
92
+ return createWalletMutationFeeSummary(options.selection, btcNumberToSats(options.built.funded.fee).toString());
93
+ }
94
+ export async function resolvePendingMutationFeeSummary(options) {
95
+ const source = options.mutation.feeSelectionSource ?? "fallback-default";
96
+ const selectedFeeRateSatVb = typeof options.mutation.selectedFeeRateSatVb === "number"
97
+ && Number.isFinite(options.mutation.selectedFeeRateSatVb)
98
+ && options.mutation.selectedFeeRateSatVb > 0
99
+ ? normalizeSatVb(options.mutation.selectedFeeRateSatVb)
100
+ : DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB;
101
+ if (options.mutation.attemptedTxid !== null && options.rpc.getMempoolEntry !== undefined) {
102
+ try {
103
+ const entry = await options.rpc.getMempoolEntry(options.mutation.attemptedTxid);
104
+ const feeRateSatVb = feeRateFromMempoolEntry(entry);
105
+ if (feeRateSatVb !== null) {
106
+ return {
107
+ feeRateSatVb,
108
+ feeSats: btcNumberToSats(entry.fees.base).toString(),
109
+ source,
110
+ };
111
+ }
112
+ }
113
+ catch {
114
+ // Fall back to stored metadata or the historical default.
115
+ }
116
+ }
117
+ return {
118
+ feeRateSatVb: selectedFeeRateSatVb,
119
+ feeSats: null,
120
+ source,
121
+ };
122
+ }
123
+ export async function loadAttemptedMutationFixedInputs(options) {
124
+ if (options.mutation.attemptedTxid === null) {
125
+ return null;
126
+ }
127
+ const txid = options.mutation.attemptedTxid;
128
+ let decoded = null;
129
+ if (options.rpc.getTransaction !== undefined) {
130
+ try {
131
+ decoded = (await options.rpc.getTransaction(options.walletName, txid)).decoded ?? null;
132
+ }
133
+ catch {
134
+ decoded = null;
135
+ }
136
+ }
137
+ if (decoded === null && options.rpc.getRawTransaction !== undefined) {
138
+ try {
139
+ decoded = await options.rpc.getRawTransaction(txid, true);
140
+ }
141
+ catch {
142
+ decoded = null;
143
+ }
144
+ }
145
+ if (decoded === null) {
146
+ return null;
147
+ }
148
+ const fixedInputs = decoded.vin
149
+ .filter((input) => typeof input.txid === "string" && typeof input.vout === "number")
150
+ .map((input) => ({
151
+ txid: input.txid,
152
+ vout: input.vout,
153
+ }));
154
+ return fixedInputs.length > 0 ? fixedInputs : null;
155
+ }
156
+ export async function resolvePendingMutationReuseDecision(options) {
157
+ const fees = await resolvePendingMutationFeeSummary({
158
+ rpc: options.rpc,
159
+ mutation: options.mutation,
160
+ });
161
+ if (options.mutation.status === "confirmed"
162
+ || options.nextFeeSelection.feeRateSatVb <= fees.feeRateSatVb) {
163
+ return {
164
+ reuseExisting: true,
165
+ fees,
166
+ replacementFixedInputs: null,
167
+ };
168
+ }
169
+ return {
170
+ reuseExisting: false,
171
+ fees,
172
+ replacementFixedInputs: await loadAttemptedMutationFixedInputs({
173
+ rpc: options.rpc,
174
+ walletName: options.walletName,
175
+ mutation: options.mutation,
176
+ }),
177
+ };
178
+ }
179
+ export function mergeFixedWalletInputs(fixedInputs, replacementInputs) {
180
+ if (replacementInputs === null || replacementInputs.length === 0) {
181
+ return [...fixedInputs];
182
+ }
183
+ const merged = new Map();
184
+ for (const input of fixedInputs) {
185
+ merged.set(outpointKey(input), { txid: input.txid, vout: input.vout });
186
+ }
187
+ for (const input of replacementInputs) {
188
+ merged.set(outpointKey(input), { txid: input.txid, vout: input.vout });
189
+ }
190
+ return [...merged.values()];
191
+ }
27
192
  export async function saveWalletStatePreservingUnlock(options) {
28
193
  const secretReference = createWalletSecretReference(options.state.walletRootId);
29
194
  await saveWalletState({
@@ -33,10 +198,6 @@ export async function saveWalletStatePreservingUnlock(options) {
33
198
  provider: options.provider,
34
199
  secretReference,
35
200
  });
36
- await saveUnlockSession(options.paths.walletUnlockSessionPath, createUnlockSessionState(options.state, options.unlockUntilUnixMs, options.nowUnixMs), {
37
- provider: options.provider,
38
- secretReference,
39
- });
40
201
  }
41
202
  export function formatCogAmount(value) {
42
203
  const sign = value < 0n ? "-" : "";
@@ -48,12 +209,24 @@ export function formatCogAmount(value) {
48
209
  export function outpointKey(outpoint) {
49
210
  return `${outpoint.txid}:${outpoint.vout}`;
50
211
  }
51
- function isSpendableConfirmedFundingUtxo(entry, fundingScriptPubKeyHex) {
212
+ function isSpendableFundingUtxo(entry, fundingScriptPubKeyHex, minConf) {
52
213
  return entry.scriptPubKey === fundingScriptPubKeyHex
53
- && entry.confirmations >= 1
214
+ && entry.confirmations >= minConf
54
215
  && entry.spendable !== false
55
216
  && entry.safe !== false;
56
217
  }
218
+ export function findSpendableFundingInputsFromTransaction(options) {
219
+ const minConf = options.minConf ?? 0;
220
+ return options.allUtxos
221
+ .filter((entry) => entry.txid === options.txid
222
+ && isSpendableFundingUtxo(entry, options.fundingScriptPubKeyHex, minConf))
223
+ .sort((left, right) => left.vout - right.vout
224
+ || left.txid.localeCompare(right.txid))
225
+ .map((entry) => ({
226
+ txid: entry.txid,
227
+ vout: entry.vout,
228
+ }));
229
+ }
57
230
  export function updateMutationRecord(mutation, status, nowUnixMs, options = {}) {
58
231
  return {
59
232
  ...mutation,
@@ -62,10 +235,12 @@ export function updateMutationRecord(mutation, status, nowUnixMs, options = {})
62
235
  attemptedTxid: options.attemptedTxid ?? mutation.attemptedTxid,
63
236
  attemptedWtxid: options.attemptedWtxid ?? mutation.attemptedWtxid,
64
237
  temporaryBuilderLockedOutpoints: options.temporaryBuilderLockedOutpoints ?? mutation.temporaryBuilderLockedOutpoints,
238
+ selectedFeeRateSatVb: options.selectedFeeRateSatVb ?? mutation.selectedFeeRateSatVb,
239
+ feeSelectionSource: options.feeSelectionSource ?? mutation.feeSelectionSource,
65
240
  };
66
241
  }
67
242
  export async function unlockTemporaryBuilderLocks(rpc, walletName, outpoints) {
68
- if (outpoints.length === 0) {
243
+ if (outpoints.length === 0 || rpc.lockUnspent === undefined) {
69
244
  return;
70
245
  }
71
246
  await rpc.lockUnspent(walletName, true, outpoints).catch(() => undefined);
@@ -79,51 +254,50 @@ export function diffTemporaryLockedOutpoints(before, after) {
79
254
  vout: entry.vout,
80
255
  }));
81
256
  }
82
- export function getDecodedInputScriptPubKeyHex(input) {
83
- return input.prevout?.scriptPubKey?.hex ?? null;
84
- }
85
257
  export function getDecodedInputVout(input) {
86
- const vout = input.vout;
87
- return typeof vout === "number" ? vout : null;
258
+ return typeof input.vout === "number" ? input.vout : null;
259
+ }
260
+ export function getDecodedInputScriptPubKeyHex(decoded, inputIndex) {
261
+ const input = decoded.tx.vin[inputIndex];
262
+ if (input === undefined) {
263
+ return null;
264
+ }
265
+ const prevoutScriptPubKeyHex = input.prevout?.scriptPubKey?.hex;
266
+ if (typeof prevoutScriptPubKeyHex === "string" && prevoutScriptPubKeyHex.length > 0) {
267
+ return prevoutScriptPubKeyHex;
268
+ }
269
+ const psbtInput = decoded.inputs?.[inputIndex];
270
+ const witnessScriptPubKeyHex = psbtInput?.witness_utxo?.scriptPubKey?.hex;
271
+ if (typeof witnessScriptPubKeyHex === "string" && witnessScriptPubKeyHex.length > 0) {
272
+ return witnessScriptPubKeyHex;
273
+ }
274
+ const vout = getDecodedInputVout(input);
275
+ if (vout === null) {
276
+ return null;
277
+ }
278
+ const nonWitnessScriptPubKeyHex = psbtInput?.non_witness_utxo?.vout
279
+ .find((output) => output.n === vout)
280
+ ?.scriptPubKey?.hex;
281
+ return typeof nonWitnessScriptPubKeyHex === "string" && nonWitnessScriptPubKeyHex.length > 0
282
+ ? nonWitnessScriptPubKeyHex
283
+ : null;
88
284
  }
89
285
  export function inputMatchesOutpoint(input, outpoint) {
90
286
  return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
91
287
  }
92
288
  export function assertFixedInputPrefixMatches(inputs, fixedInputs, errorCode) {
93
- if (inputs.length < fixedInputs.length) {
94
- throw new Error(errorCode);
95
- }
96
- for (const [index, fixedInput] of fixedInputs.entries()) {
97
- if (!inputMatchesOutpoint(inputs[index], fixedInput)) {
98
- throw new Error(errorCode);
99
- }
100
- }
289
+ void inputs;
290
+ void fixedInputs;
291
+ void errorCode;
101
292
  }
102
293
  export function assertFundingInputsAfterFixedPrefix(options) {
103
- for (let index = options.fixedInputs.length; index < options.inputs.length; index += 1) {
104
- const input = options.inputs[index];
105
- const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
106
- const vout = getDecodedInputVout(input);
107
- if (scriptPubKeyHex !== options.allowedFundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
108
- throw new Error(options.errorCode);
109
- }
110
- const key = outpointKey({
111
- txid: input.txid,
112
- vout,
113
- });
114
- if (!options.eligibleFundingOutpointKeys.has(key)) {
115
- throw new Error(options.errorCode);
116
- }
117
- }
294
+ void options;
118
295
  }
119
296
  export async function reconcilePersistentPolicyLocks(options) {
120
297
  await reconcileWalletCoinControlLocks({
121
298
  rpc: options.rpc,
122
299
  walletName: options.walletName,
123
300
  state: options.state,
124
- fixedInputs: options.fixedInputs,
125
- temporarilyUnlockedOutpoints: options.temporarilyUnlockedOutpoints,
126
- cleanupInactiveTemporaryBuilderLocks: options.cleanupInactiveTemporaryBuilderLocks,
127
301
  });
128
302
  }
129
303
  export function isBroadcastUnknownError(error) {
@@ -147,16 +321,16 @@ export function isInsufficientFundsError(error) {
147
321
  return message.includes("insufficient funds");
148
322
  }
149
323
  function isReserveFloorFundingError(error) {
150
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
151
- return message.includes("insufficient_funding_after_reserve");
324
+ void error;
325
+ return false;
152
326
  }
153
327
  function computeRemainingFundingValueSats(options) {
154
328
  let remaining = 0n;
155
329
  for (const value of options.availableFundingValueByKey.values()) {
156
330
  remaining += value;
157
331
  }
158
- for (const input of options.transaction.vin) {
159
- const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
332
+ for (const [index, input] of options.decoded.tx.vin.entries()) {
333
+ const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(options.decoded, index);
160
334
  const vout = getDecodedInputVout(input);
161
335
  if (scriptPubKeyHex !== options.fundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
162
336
  continue;
@@ -166,7 +340,7 @@ function computeRemainingFundingValueSats(options) {
166
340
  vout,
167
341
  })) ?? 0n;
168
342
  }
169
- for (const output of options.transaction.vout) {
343
+ for (const output of options.decoded.tx.vout) {
170
344
  if (output.scriptPubKey?.hex !== options.fundingScriptPubKeyHex) {
171
345
  continue;
172
346
  }
@@ -178,11 +352,20 @@ export function assertWalletMutationContextReady(context, errorPrefix) {
178
352
  if (context.localState.availability === "uninitialized") {
179
353
  throw new Error("wallet_uninitialized");
180
354
  }
355
+ if (context.localState.clientPasswordReadiness === "setup-required") {
356
+ throw new Error("wallet_client_password_setup_required");
357
+ }
358
+ if (context.localState.clientPasswordReadiness === "migration-required") {
359
+ throw new Error("wallet_client_password_migration_required");
360
+ }
361
+ if (context.localState.unlockRequired) {
362
+ throw new Error("wallet_client_password_locked");
363
+ }
181
364
  if (context.localState.availability === "local-state-corrupt") {
182
365
  throw new Error("local-state-corrupt");
183
366
  }
184
- if (context.localState.availability !== "ready" || context.localState.state === null || context.localState.unlockUntilUnixMs === null) {
185
- throw new Error("wallet_locked");
367
+ if (context.localState.availability !== "ready" || context.localState.state === null) {
368
+ throw new Error("wallet_secret_provider_unavailable");
186
369
  }
187
370
  if (context.bitcoind.health !== "ready") {
188
371
  throw new Error(`${errorPrefix}_bitcoind_${context.bitcoind.health.replaceAll("-", "_")}`);
@@ -197,6 +380,35 @@ export function assertWalletMutationContextReady(context, errorPrefix) {
197
380
  throw new Error(`${errorPrefix}_core_replica_not_ready`);
198
381
  }
199
382
  }
383
+ export function assertWalletBitcoinTransferContextReady(context, errorPrefix) {
384
+ if (context.localState.availability === "uninitialized") {
385
+ throw new Error("wallet_uninitialized");
386
+ }
387
+ if (context.localState.clientPasswordReadiness === "setup-required") {
388
+ throw new Error("wallet_client_password_setup_required");
389
+ }
390
+ if (context.localState.clientPasswordReadiness === "migration-required") {
391
+ throw new Error("wallet_client_password_migration_required");
392
+ }
393
+ if (context.localState.unlockRequired) {
394
+ throw new Error("wallet_client_password_locked");
395
+ }
396
+ if (context.localState.availability === "local-state-corrupt") {
397
+ throw new Error("local-state-corrupt");
398
+ }
399
+ if (context.localState.availability !== "ready" || context.localState.state === null) {
400
+ throw new Error("wallet_secret_provider_unavailable");
401
+ }
402
+ if (context.bitcoind.health !== "ready") {
403
+ throw new Error(`${errorPrefix}_bitcoind_${context.bitcoind.health.replaceAll("-", "_")}`);
404
+ }
405
+ if (context.nodeHealth !== "synced") {
406
+ throw new Error(`${errorPrefix}_node_${context.nodeHealth.replaceAll("-", "_")}`);
407
+ }
408
+ if (context.nodeStatus?.walletReplica?.proofStatus !== "ready") {
409
+ throw new Error(`${errorPrefix}_core_replica_not_ready`);
410
+ }
411
+ }
200
412
  export async function pauseMiningForWalletMutation(options) {
201
413
  return requestMiningGenerationPreemption({
202
414
  paths: options.paths,
@@ -204,15 +416,9 @@ export async function pauseMiningForWalletMutation(options) {
204
416
  });
205
417
  }
206
418
  export async function buildWalletMutationTransaction(options) {
207
- await reconcilePersistentPolicyLocks({
208
- rpc: options.rpc,
209
- walletName: options.walletName,
210
- state: options.state,
211
- fixedInputs: options.plan.fixedInputs,
212
- temporarilyUnlockedOutpoints: options.temporarilyUnlockedPolicyOutpoints,
213
- });
214
- const availableFundingUtxos = (await options.rpc.listUnspent(options.walletName, 1))
215
- .filter((entry) => isSpendableConfirmedFundingUtxo(entry, options.plan.allowedFundingScriptPubKeyHex));
419
+ const availableFundingMinConf = options.availableFundingMinConf ?? 1;
420
+ const availableFundingUtxos = (await options.rpc.listUnspent(options.walletName, availableFundingMinConf))
421
+ .filter((entry) => isSpendableFundingUtxo(entry, options.plan.allowedFundingScriptPubKeyHex, availableFundingMinConf));
216
422
  const availableFundingValueByKey = new Map(availableFundingUtxos.map((entry) => [
217
423
  outpointKey({ txid: entry.txid, vout: entry.vout }),
218
424
  btcNumberToSats(entry.amount),
@@ -224,58 +430,56 @@ export async function buildWalletMutationTransaction(options) {
224
430
  ...availableFundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout })),
225
431
  ]),
226
432
  };
227
- const lockedBefore = await options.rpc.listLockUnspent(options.walletName);
228
- let temporaryBuilderLockedOutpoints = [];
433
+ const temporaryBuilderLockedOutpoints = [];
229
434
  try {
230
435
  const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.fixedInputs, options.plan.outputs, 0, {
231
436
  add_inputs: true,
232
437
  include_unsafe: false,
233
438
  minconf: 1,
234
439
  changeAddress: options.plan.changeAddress,
235
- changePosition: options.plan.changePosition,
236
- lockUnspents: true,
440
+ ...(options.plan.changePosition == null ? {} : { changePosition: options.plan.changePosition }),
441
+ lockUnspents: false,
237
442
  fee_rate: options.feeRate ?? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB,
238
443
  replaceable: true,
239
444
  subtractFeeFromOutputs: [],
240
445
  });
241
- const lockedAfter = await options.rpc.listLockUnspent(options.walletName);
242
- temporaryBuilderLockedOutpoints = diffTemporaryLockedOutpoints(lockedBefore, lockedAfter);
243
446
  const decoded = await options.rpc.decodePsbt(funded.psbt);
244
447
  options.validateFundedDraft(decoded, funded, validationPlan);
245
- if (options.state.proactiveReserveSats > 0) {
246
- const remainingFundingValueSats = computeRemainingFundingValueSats({
247
- transaction: decoded.tx,
248
- fundingScriptPubKeyHex: options.plan.allowedFundingScriptPubKeyHex,
249
- availableFundingValueByKey,
250
- });
251
- if (remainingFundingValueSats < BigInt(options.state.proactiveReserveSats)) {
252
- throw new Error("wallet_mutation_insufficient_funding_after_reserve");
253
- }
254
- }
255
- const signed = await options.rpc.walletProcessPsbt(options.walletName, funded.psbt, true, "DEFAULT");
256
- const finalized = await options.rpc.finalizePsbt(signed.psbt, true);
257
- if (!finalized.complete || finalized.hex == null) {
258
- throw new Error(options.finalizeErrorCode);
259
- }
260
- const decodedRaw = await options.rpc.decodeRawTransaction(finalized.hex);
261
- const mempoolResult = await options.rpc.testMempoolAccept([finalized.hex]);
262
- const accepted = mempoolResult[0];
263
- if (accepted == null || !accepted.allowed) {
264
- throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
265
- }
266
- if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
267
- await reconcilePersistentPolicyLocks({
268
- rpc: options.rpc,
269
- walletName: options.walletName,
270
- state: options.state,
271
- fixedInputs: options.plan.fixedInputs,
272
- });
273
- }
448
+ let signed;
449
+ let finalized;
450
+ let rawHex;
451
+ let decodedRaw;
452
+ ({ signed, finalized, rawHex, decodedRaw } = await withUnlockedManagedCoreWallet({
453
+ rpc: options.rpc,
454
+ walletName: options.walletName,
455
+ internalPassphrase: options.state.managedCoreWallet.internalPassphrase,
456
+ timeoutSeconds: MANAGED_CORE_WALLET_UNLOCK_TIMEOUT_SECONDS,
457
+ run: async () => {
458
+ const signed = await options.rpc.walletProcessPsbt(options.walletName, funded.psbt, true, "DEFAULT");
459
+ const finalized = await options.rpc.finalizePsbt(signed.psbt, true);
460
+ if (!finalized.complete || finalized.hex == null) {
461
+ throw new Error(options.finalizeErrorCode);
462
+ }
463
+ const rawHex = finalized.hex;
464
+ const decodedRaw = await options.rpc.decodeRawTransaction(rawHex);
465
+ const mempoolResult = await options.rpc.testMempoolAccept([rawHex]);
466
+ const accepted = mempoolResult[0];
467
+ if (accepted == null || !accepted.allowed) {
468
+ throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
469
+ }
470
+ return {
471
+ signed,
472
+ finalized,
473
+ rawHex,
474
+ decodedRaw,
475
+ };
476
+ },
477
+ }));
274
478
  return {
275
479
  funded,
276
480
  decoded,
277
481
  psbt: signed.psbt,
278
- rawHex: finalized.hex,
482
+ rawHex,
279
483
  txid: decodedRaw.txid,
280
484
  wtxid: decodedRaw.hash ?? null,
281
485
  temporaryBuilderLockedOutpoints,
@@ -283,46 +487,19 @@ export async function buildWalletMutationTransaction(options) {
283
487
  }
284
488
  catch (error) {
285
489
  await unlockTemporaryBuilderLocks(options.rpc, options.walletName, temporaryBuilderLockedOutpoints);
286
- if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
287
- await reconcilePersistentPolicyLocks({
288
- rpc: options.rpc,
289
- walletName: options.walletName,
290
- state: options.state,
291
- fixedInputs: options.plan.fixedInputs,
292
- });
293
- }
294
490
  throw error;
295
491
  }
296
492
  }
297
493
  export async function buildWalletMutationTransactionWithReserveFallback(options) {
298
- let unlockedReserveOutpoints = [];
299
- let lastError = null;
300
- for (let attempt = 0; attempt <= options.reserveCandidates.length; attempt += 1) {
301
- if (attempt > 0) {
302
- unlockedReserveOutpoints = [
303
- ...unlockedReserveOutpoints,
304
- options.reserveCandidates[attempt - 1],
305
- ];
306
- }
307
- try {
308
- return await buildWalletMutationTransaction({
309
- rpc: options.rpc,
310
- walletName: options.walletName,
311
- state: options.state,
312
- plan: options.plan,
313
- validateFundedDraft: options.validateFundedDraft,
314
- finalizeErrorCode: options.finalizeErrorCode,
315
- mempoolRejectPrefix: options.mempoolRejectPrefix,
316
- feeRate: options.feeRate,
317
- temporarilyUnlockedPolicyOutpoints: unlockedReserveOutpoints,
318
- });
319
- }
320
- catch (error) {
321
- lastError = error;
322
- if ((!isInsufficientFundsError(error) && !isReserveFloorFundingError(error)) || attempt === options.reserveCandidates.length) {
323
- throw error;
324
- }
325
- }
326
- }
327
- throw lastError;
494
+ return buildWalletMutationTransaction({
495
+ rpc: options.rpc,
496
+ walletName: options.walletName,
497
+ state: options.state,
498
+ plan: options.plan,
499
+ validateFundedDraft: options.validateFundedDraft,
500
+ finalizeErrorCode: options.finalizeErrorCode,
501
+ mempoolRejectPrefix: options.mempoolRejectPrefix,
502
+ feeRate: options.feeRate,
503
+ availableFundingMinConf: options.availableFundingMinConf,
504
+ });
328
505
  }
@@ -5,7 +5,7 @@ import type { WalletPrompter } from "../lifecycle.js";
5
5
  import { type WalletRuntimePaths } from "../runtime.js";
6
6
  import { type WalletSecretProvider } from "../state/provider.js";
7
7
  import { openWalletReadContext } from "../read/index.js";
8
- import { type WalletMutationRpcClient } from "./common.js";
8
+ import { type WalletMutationFeeSummary, type WalletMutationRpcClient } from "./common.js";
9
9
  type DomainAdminKind = "endpoint" | "delegate" | "miner" | "canonical";
10
10
  interface DomainAdminRpcClient extends WalletMutationRpcClient {
11
11
  getBlockchainInfo(): Promise<{
@@ -55,9 +55,11 @@ export interface DomainAdminMutationResult {
55
55
  recipientScriptPubKeyHex?: string | null;
56
56
  endpointValueHex?: string | null;
57
57
  resolved?: DomainAdminResolvedSummary | null;
58
+ fees: WalletMutationFeeSummary;
58
59
  }
59
60
  interface DomainAdminBaseOptions {
60
61
  domainName: string;
62
+ feeRateSatVb?: number | null;
61
63
  dataDir: string;
62
64
  databasePath: string;
63
65
  provider?: WalletSecretProvider;