@cogcoin/client 0.5.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 (289) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/dist/app-paths.d.ts +38 -0
  4. package/dist/app-paths.js +121 -0
  5. package/dist/art/banner.txt +13 -0
  6. package/dist/art/scroll.txt +13 -0
  7. package/dist/art/train-car.txt +6 -0
  8. package/dist/art/train-smoke.txt +6 -0
  9. package/dist/art/train.txt +6 -0
  10. package/dist/bitcoind/bootstrap/chainstate.d.ts +4 -0
  11. package/dist/bitcoind/bootstrap/chainstate.js +13 -0
  12. package/dist/bitcoind/bootstrap/constants.d.ts +7 -0
  13. package/dist/bitcoind/bootstrap/constants.js +12 -0
  14. package/dist/bitcoind/bootstrap/controller.d.ts +29 -0
  15. package/dist/bitcoind/bootstrap/controller.js +101 -0
  16. package/dist/bitcoind/bootstrap/download.d.ts +2 -0
  17. package/dist/bitcoind/bootstrap/download.js +196 -0
  18. package/dist/bitcoind/bootstrap/headers.d.ts +13 -0
  19. package/dist/bitcoind/bootstrap/headers.js +61 -0
  20. package/dist/bitcoind/bootstrap/paths.d.ts +4 -0
  21. package/dist/bitcoind/bootstrap/paths.js +15 -0
  22. package/dist/bitcoind/bootstrap/snapshot-file.d.ts +7 -0
  23. package/dist/bitcoind/bootstrap/snapshot-file.js +42 -0
  24. package/dist/bitcoind/bootstrap/state.d.ts +40 -0
  25. package/dist/bitcoind/bootstrap/state.js +70 -0
  26. package/dist/bitcoind/bootstrap/types.d.ts +28 -0
  27. package/dist/bitcoind/bootstrap/types.js +1 -0
  28. package/dist/bitcoind/bootstrap.d.ts +8 -0
  29. package/dist/bitcoind/bootstrap.js +7 -0
  30. package/dist/bitcoind/client/factory.d.ts +3 -0
  31. package/dist/bitcoind/client/factory.js +57 -0
  32. package/dist/bitcoind/client/follow-block-times.d.ts +8 -0
  33. package/dist/bitcoind/client/follow-block-times.js +25 -0
  34. package/dist/bitcoind/client/follow-loop.d.ts +10 -0
  35. package/dist/bitcoind/client/follow-loop.js +57 -0
  36. package/dist/bitcoind/client/internal-types.d.ts +63 -0
  37. package/dist/bitcoind/client/internal-types.js +18 -0
  38. package/dist/bitcoind/client/managed-client.d.ts +20 -0
  39. package/dist/bitcoind/client/managed-client.js +197 -0
  40. package/dist/bitcoind/client/rate-tracker.d.ts +2 -0
  41. package/dist/bitcoind/client/rate-tracker.js +24 -0
  42. package/dist/bitcoind/client/sync-engine.d.ts +3 -0
  43. package/dist/bitcoind/client/sync-engine.js +143 -0
  44. package/dist/bitcoind/client.d.ts +1 -0
  45. package/dist/bitcoind/client.js +1 -0
  46. package/dist/bitcoind/errors.d.ts +1 -0
  47. package/dist/bitcoind/errors.js +49 -0
  48. package/dist/bitcoind/index.d.ts +2 -0
  49. package/dist/bitcoind/index.js +1 -0
  50. package/dist/bitcoind/indexer-daemon-main.d.ts +1 -0
  51. package/dist/bitcoind/indexer-daemon-main.js +472 -0
  52. package/dist/bitcoind/indexer-daemon.d.ts +107 -0
  53. package/dist/bitcoind/indexer-daemon.js +391 -0
  54. package/dist/bitcoind/node.d.ts +8 -0
  55. package/dist/bitcoind/node.js +219 -0
  56. package/dist/bitcoind/normalize.d.ts +3 -0
  57. package/dist/bitcoind/normalize.js +47 -0
  58. package/dist/bitcoind/progress/assets.d.ts +10 -0
  59. package/dist/bitcoind/progress/assets.js +90 -0
  60. package/dist/bitcoind/progress/constants.d.ts +48 -0
  61. package/dist/bitcoind/progress/constants.js +53 -0
  62. package/dist/bitcoind/progress/controller.d.ts +28 -0
  63. package/dist/bitcoind/progress/controller.js +188 -0
  64. package/dist/bitcoind/progress/follow-scene.d.ts +40 -0
  65. package/dist/bitcoind/progress/follow-scene.js +367 -0
  66. package/dist/bitcoind/progress/formatting.d.ts +23 -0
  67. package/dist/bitcoind/progress/formatting.js +227 -0
  68. package/dist/bitcoind/progress/quote-scene.d.ts +4 -0
  69. package/dist/bitcoind/progress/quote-scene.js +137 -0
  70. package/dist/bitcoind/progress/train-scene.d.ts +9 -0
  71. package/dist/bitcoind/progress/train-scene.js +92 -0
  72. package/dist/bitcoind/progress/tty-renderer.d.ts +18 -0
  73. package/dist/bitcoind/progress/tty-renderer.js +150 -0
  74. package/dist/bitcoind/progress.d.ts +7 -0
  75. package/dist/bitcoind/progress.js +7 -0
  76. package/dist/bitcoind/quotes.d.ts +24 -0
  77. package/dist/bitcoind/quotes.js +195 -0
  78. package/dist/bitcoind/rpc.d.ts +71 -0
  79. package/dist/bitcoind/rpc.js +322 -0
  80. package/dist/bitcoind/service-paths.d.ts +19 -0
  81. package/dist/bitcoind/service-paths.js +49 -0
  82. package/dist/bitcoind/service.d.ts +40 -0
  83. package/dist/bitcoind/service.js +735 -0
  84. package/dist/bitcoind/testing.d.ts +9 -0
  85. package/dist/bitcoind/testing.js +9 -0
  86. package/dist/bitcoind/types.d.ts +396 -0
  87. package/dist/bitcoind/types.js +3 -0
  88. package/dist/bytes.d.ts +9 -0
  89. package/dist/bytes.js +36 -0
  90. package/dist/cli/commands/follow.d.ts +2 -0
  91. package/dist/cli/commands/follow.js +43 -0
  92. package/dist/cli/commands/mining-admin.d.ts +2 -0
  93. package/dist/cli/commands/mining-admin.js +92 -0
  94. package/dist/cli/commands/mining-read.d.ts +2 -0
  95. package/dist/cli/commands/mining-read.js +173 -0
  96. package/dist/cli/commands/mining-runtime.d.ts +2 -0
  97. package/dist/cli/commands/mining-runtime.js +108 -0
  98. package/dist/cli/commands/status.d.ts +2 -0
  99. package/dist/cli/commands/status.js +31 -0
  100. package/dist/cli/commands/sync.d.ts +2 -0
  101. package/dist/cli/commands/sync.js +52 -0
  102. package/dist/cli/commands/wallet-admin.d.ts +2 -0
  103. package/dist/cli/commands/wallet-admin.js +175 -0
  104. package/dist/cli/commands/wallet-mutation.d.ts +2 -0
  105. package/dist/cli/commands/wallet-mutation.js +681 -0
  106. package/dist/cli/commands/wallet-read.d.ts +2 -0
  107. package/dist/cli/commands/wallet-read.js +265 -0
  108. package/dist/cli/context.d.ts +3 -0
  109. package/dist/cli/context.js +75 -0
  110. package/dist/cli/io.d.ts +3 -0
  111. package/dist/cli/io.js +12 -0
  112. package/dist/cli/mining-format.d.ts +5 -0
  113. package/dist/cli/mining-format.js +156 -0
  114. package/dist/cli/mining-json.d.ts +49 -0
  115. package/dist/cli/mining-json.js +89 -0
  116. package/dist/cli/mutation-command-groups.d.ts +15 -0
  117. package/dist/cli/mutation-command-groups.js +71 -0
  118. package/dist/cli/mutation-json.d.ts +430 -0
  119. package/dist/cli/mutation-json.js +311 -0
  120. package/dist/cli/mutation-resolved-json.d.ts +124 -0
  121. package/dist/cli/mutation-resolved-json.js +129 -0
  122. package/dist/cli/mutation-success.d.ts +20 -0
  123. package/dist/cli/mutation-success.js +47 -0
  124. package/dist/cli/mutation-text-format.d.ts +22 -0
  125. package/dist/cli/mutation-text-format.js +171 -0
  126. package/dist/cli/mutation-text-write.d.ts +13 -0
  127. package/dist/cli/mutation-text-write.js +16 -0
  128. package/dist/cli/output.d.ts +185 -0
  129. package/dist/cli/output.js +1085 -0
  130. package/dist/cli/parse.d.ts +3 -0
  131. package/dist/cli/parse.js +971 -0
  132. package/dist/cli/preview-json.d.ts +416 -0
  133. package/dist/cli/preview-json.js +293 -0
  134. package/dist/cli/prompt.d.ts +3 -0
  135. package/dist/cli/prompt.js +33 -0
  136. package/dist/cli/read-json.d.ts +187 -0
  137. package/dist/cli/read-json.js +675 -0
  138. package/dist/cli/runner.d.ts +2 -0
  139. package/dist/cli/runner.js +129 -0
  140. package/dist/cli/signals.d.ts +3 -0
  141. package/dist/cli/signals.js +63 -0
  142. package/dist/cli/status-format.d.ts +2 -0
  143. package/dist/cli/status-format.js +48 -0
  144. package/dist/cli/types.d.ts +148 -0
  145. package/dist/cli/types.js +2 -0
  146. package/dist/cli/wallet-format.d.ts +29 -0
  147. package/dist/cli/wallet-format.js +637 -0
  148. package/dist/cli/workflow-hints.d.ts +13 -0
  149. package/dist/cli/workflow-hints.js +94 -0
  150. package/dist/cli-runner.d.ts +3 -0
  151. package/dist/cli-runner.js +3 -0
  152. package/dist/cli.d.ts +2 -0
  153. package/dist/cli.js +6 -0
  154. package/dist/client/default-client.d.ts +11 -0
  155. package/dist/client/default-client.js +118 -0
  156. package/dist/client/factory.d.ts +2 -0
  157. package/dist/client/factory.js +15 -0
  158. package/dist/client/initialization.d.ts +6 -0
  159. package/dist/client/initialization.js +30 -0
  160. package/dist/client/persistence.d.ts +5 -0
  161. package/dist/client/persistence.js +28 -0
  162. package/dist/client/store-adapter.d.ts +3 -0
  163. package/dist/client/store-adapter.js +20 -0
  164. package/dist/client.d.ts +2 -0
  165. package/dist/client.js +2 -0
  166. package/dist/index.d.ts +2 -0
  167. package/dist/index.js +1 -0
  168. package/dist/passive-status.d.ts +36 -0
  169. package/dist/passive-status.js +100 -0
  170. package/dist/sqlite/better-sqlite3.d.ts +26 -0
  171. package/dist/sqlite/better-sqlite3.js +4 -0
  172. package/dist/sqlite/checkpoints.d.ts +11 -0
  173. package/dist/sqlite/checkpoints.js +27 -0
  174. package/dist/sqlite/driver.d.ts +17 -0
  175. package/dist/sqlite/driver.js +98 -0
  176. package/dist/sqlite/index.d.ts +4 -0
  177. package/dist/sqlite/index.js +9 -0
  178. package/dist/sqlite/migrate.d.ts +2 -0
  179. package/dist/sqlite/migrate.js +37 -0
  180. package/dist/sqlite/store.d.ts +3 -0
  181. package/dist/sqlite/store.js +122 -0
  182. package/dist/sqlite/tip-meta.d.ts +26 -0
  183. package/dist/sqlite/tip-meta.js +97 -0
  184. package/dist/sqlite/types.d.ts +10 -0
  185. package/dist/sqlite/types.js +1 -0
  186. package/dist/types.d.ts +55 -0
  187. package/dist/types.js +1 -0
  188. package/dist/wallet/archive.d.ts +4 -0
  189. package/dist/wallet/archive.js +39 -0
  190. package/dist/wallet/cogop/constants.d.ts +32 -0
  191. package/dist/wallet/cogop/constants.js +32 -0
  192. package/dist/wallet/cogop/index.d.ts +32 -0
  193. package/dist/wallet/cogop/index.js +213 -0
  194. package/dist/wallet/cogop/numeric.d.ts +3 -0
  195. package/dist/wallet/cogop/numeric.js +24 -0
  196. package/dist/wallet/cogop/scriptpubkey.d.ts +2 -0
  197. package/dist/wallet/cogop/scriptpubkey.js +13 -0
  198. package/dist/wallet/cogop/validate-name.d.ts +2 -0
  199. package/dist/wallet/cogop/validate-name.js +18 -0
  200. package/dist/wallet/fs/atomic.d.ts +6 -0
  201. package/dist/wallet/fs/atomic.js +46 -0
  202. package/dist/wallet/fs/lock.d.ts +19 -0
  203. package/dist/wallet/fs/lock.js +61 -0
  204. package/dist/wallet/fs/status-file.d.ts +1 -0
  205. package/dist/wallet/fs/status-file.js +4 -0
  206. package/dist/wallet/lifecycle.d.ts +193 -0
  207. package/dist/wallet/lifecycle.js +1475 -0
  208. package/dist/wallet/material.d.ts +45 -0
  209. package/dist/wallet/material.js +118 -0
  210. package/dist/wallet/mining/config.d.ts +18 -0
  211. package/dist/wallet/mining/config.js +44 -0
  212. package/dist/wallet/mining/constants.d.ts +24 -0
  213. package/dist/wallet/mining/constants.js +24 -0
  214. package/dist/wallet/mining/control.d.ts +53 -0
  215. package/dist/wallet/mining/control.js +758 -0
  216. package/dist/wallet/mining/coordination.d.ts +40 -0
  217. package/dist/wallet/mining/coordination.js +121 -0
  218. package/dist/wallet/mining/hook-protocol.d.ts +47 -0
  219. package/dist/wallet/mining/hook-protocol.js +161 -0
  220. package/dist/wallet/mining/hook-runner.d.ts +1 -0
  221. package/dist/wallet/mining/hook-runner.js +52 -0
  222. package/dist/wallet/mining/hooks.d.ts +38 -0
  223. package/dist/wallet/mining/hooks.js +520 -0
  224. package/dist/wallet/mining/index.d.ts +8 -0
  225. package/dist/wallet/mining/index.js +6 -0
  226. package/dist/wallet/mining/runner.d.ts +155 -0
  227. package/dist/wallet/mining/runner.js +2574 -0
  228. package/dist/wallet/mining/runtime-artifacts.d.ts +17 -0
  229. package/dist/wallet/mining/runtime-artifacts.js +166 -0
  230. package/dist/wallet/mining/sentences.d.ts +23 -0
  231. package/dist/wallet/mining/sentences.js +281 -0
  232. package/dist/wallet/mining/state.d.ts +9 -0
  233. package/dist/wallet/mining/state.js +75 -0
  234. package/dist/wallet/mining/types.d.ts +141 -0
  235. package/dist/wallet/mining/types.js +1 -0
  236. package/dist/wallet/mining/visualizer.d.ts +19 -0
  237. package/dist/wallet/mining/visualizer.js +134 -0
  238. package/dist/wallet/mining/worker-main.d.ts +1 -0
  239. package/dist/wallet/mining/worker-main.js +17 -0
  240. package/dist/wallet/read/context.d.ts +20 -0
  241. package/dist/wallet/read/context.js +532 -0
  242. package/dist/wallet/read/filter.d.ts +9 -0
  243. package/dist/wallet/read/filter.js +42 -0
  244. package/dist/wallet/read/index.d.ts +4 -0
  245. package/dist/wallet/read/index.js +3 -0
  246. package/dist/wallet/read/project.d.ts +11 -0
  247. package/dist/wallet/read/project.js +300 -0
  248. package/dist/wallet/read/types.d.ts +144 -0
  249. package/dist/wallet/read/types.js +1 -0
  250. package/dist/wallet/runtime.d.ts +26 -0
  251. package/dist/wallet/runtime.js +28 -0
  252. package/dist/wallet/state/crypto.d.ts +31 -0
  253. package/dist/wallet/state/crypto.js +127 -0
  254. package/dist/wallet/state/provider.d.ts +37 -0
  255. package/dist/wallet/state/provider.js +312 -0
  256. package/dist/wallet/state/session.d.ts +12 -0
  257. package/dist/wallet/state/session.js +23 -0
  258. package/dist/wallet/state/storage.d.ts +19 -0
  259. package/dist/wallet/state/storage.js +55 -0
  260. package/dist/wallet/tx/anchor.d.ts +40 -0
  261. package/dist/wallet/tx/anchor.js +1210 -0
  262. package/dist/wallet/tx/cog.d.ts +92 -0
  263. package/dist/wallet/tx/cog.js +1055 -0
  264. package/dist/wallet/tx/common.d.ts +89 -0
  265. package/dist/wallet/tx/common.js +156 -0
  266. package/dist/wallet/tx/confirm.d.ts +15 -0
  267. package/dist/wallet/tx/confirm.js +24 -0
  268. package/dist/wallet/tx/domain-admin.d.ts +105 -0
  269. package/dist/wallet/tx/domain-admin.js +869 -0
  270. package/dist/wallet/tx/domain-market.d.ts +112 -0
  271. package/dist/wallet/tx/domain-market.js +1365 -0
  272. package/dist/wallet/tx/field.d.ts +101 -0
  273. package/dist/wallet/tx/field.js +1853 -0
  274. package/dist/wallet/tx/identity-selector.d.ts +12 -0
  275. package/dist/wallet/tx/identity-selector.js +52 -0
  276. package/dist/wallet/tx/index.d.ts +7 -0
  277. package/dist/wallet/tx/index.js +7 -0
  278. package/dist/wallet/tx/journal.d.ts +5 -0
  279. package/dist/wallet/tx/journal.js +31 -0
  280. package/dist/wallet/tx/register.d.ts +68 -0
  281. package/dist/wallet/tx/register.js +952 -0
  282. package/dist/wallet/tx/reputation.d.ts +72 -0
  283. package/dist/wallet/tx/reputation.js +693 -0
  284. package/dist/wallet/tx/targets.d.ts +7 -0
  285. package/dist/wallet/tx/targets.js +122 -0
  286. package/dist/wallet/types.d.ts +249 -0
  287. package/dist/wallet/types.js +1 -0
  288. package/dist/writing_quotes.json +1654 -0
  289. package/package.json +78 -0
@@ -0,0 +1,1365 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { getBalance, getListing, lookupDomain } from "@cogcoin/indexer/queries";
3
+ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
4
+ import { createRpcClient } from "../../bitcoind/node.js";
5
+ import { acquireFileLock } from "../fs/lock.js";
6
+ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
7
+ import { createDefaultWalletSecretProvider, } from "../state/provider.js";
8
+ import { serializeDomainBuy, serializeDomainSell, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
9
+ import { openWalletReadContext } from "../read/index.js";
10
+ import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
+ import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
12
+ import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
13
+ import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
14
+ import { normalizeBtcTarget } from "./targets.js";
15
+ function normalizeDomainName(domainName) {
16
+ const normalized = domainName.trim().toLowerCase();
17
+ if (normalized.length === 0) {
18
+ throw new Error("wallet_domain_missing_domain");
19
+ }
20
+ validateDomainName(normalized);
21
+ return normalized;
22
+ }
23
+ export function parseCogAmountToCogtoshi(raw) {
24
+ const match = /^(-?)(\d+)(?:\.(\d{0,8}))?$/.exec(raw.trim());
25
+ if (match == null) {
26
+ throw new Error(`wallet_sell_invalid_amount_${raw}`);
27
+ }
28
+ const sign = match[1] === "-" ? -1n : 1n;
29
+ const whole = BigInt(match[2] ?? "0");
30
+ const fraction = BigInt((match[3] ?? "").padEnd(8, "0"));
31
+ return sign * ((whole * 100000000n) + fraction);
32
+ }
33
+ function satsToBtcNumber(value) {
34
+ return Number(value) / 100_000_000;
35
+ }
36
+ function valueToSats(value) {
37
+ const text = typeof value === "number" ? value.toFixed(8) : value;
38
+ const match = /^(-?)(\d+)(?:\.(\d{0,8}))?$/.exec(text.trim());
39
+ if (match == null) {
40
+ throw new Error(`wallet_domain_invalid_amount_${text}`);
41
+ }
42
+ const sign = match[1] === "-" ? -1n : 1n;
43
+ const whole = BigInt(match[2] ?? "0");
44
+ const fraction = BigInt((match[3] ?? "").padEnd(8, "0"));
45
+ return sign * ((whole * 100000000n) + fraction);
46
+ }
47
+ function encodeOpReturnScript(payload) {
48
+ if (payload.length <= 75) {
49
+ return Buffer.concat([
50
+ Buffer.from([0x6a, payload.length]),
51
+ Buffer.from(payload),
52
+ ]).toString("hex");
53
+ }
54
+ return Buffer.concat([
55
+ Buffer.from([0x6a, 0x4c, payload.length]),
56
+ Buffer.from(payload),
57
+ ]).toString("hex");
58
+ }
59
+ function createIntentFingerprint(parts) {
60
+ return createHash("sha256")
61
+ .update(parts.join("\n"))
62
+ .digest("hex");
63
+ }
64
+ function replaceAssignedDomainNames(identity, nextAssignedDomainNames) {
65
+ return {
66
+ ...identity,
67
+ assignedDomainNames: nextAssignedDomainNames.slice().sort((left, right) => left.localeCompare(right)),
68
+ };
69
+ }
70
+ function reserveTransferredDomainRecord(options) {
71
+ const existing = options.state.domains.find((domain) => domain.name === options.domainName) ?? null;
72
+ const domains = options.state.domains.some((domain) => domain.name === options.domainName)
73
+ ? options.state.domains.map((domain) => {
74
+ if (domain.name !== options.domainName) {
75
+ return domain;
76
+ }
77
+ return {
78
+ ...domain,
79
+ domainId: options.domainId ?? domain.domainId,
80
+ currentOwnerScriptPubKeyHex: options.currentOwnerScriptPubKeyHex,
81
+ currentOwnerLocalIndex: options.currentOwnerLocalIndex,
82
+ canonicalChainStatus: "registered-unanchored",
83
+ currentCanonicalAnchorOutpoint: null,
84
+ birthTime: domain.birthTime ?? Math.floor(options.nowUnixMs / 1000),
85
+ };
86
+ })
87
+ : [
88
+ ...options.state.domains,
89
+ {
90
+ name: options.domainName,
91
+ domainId: options.domainId,
92
+ dedicatedIndex: null,
93
+ currentOwnerScriptPubKeyHex: options.currentOwnerScriptPubKeyHex,
94
+ currentOwnerLocalIndex: options.currentOwnerLocalIndex,
95
+ canonicalChainStatus: "registered-unanchored",
96
+ localAnchorIntent: "none",
97
+ currentCanonicalAnchorOutpoint: null,
98
+ foundingMessageText: existing?.foundingMessageText ?? null,
99
+ birthTime: Math.floor(options.nowUnixMs / 1000),
100
+ },
101
+ ];
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
+ return {
110
+ ...options.state,
111
+ domains,
112
+ identities,
113
+ };
114
+ }
115
+ function createResolvedDomainMarketSenderSummary(sender, selector) {
116
+ return {
117
+ selector,
118
+ localIndex: sender.localIndex,
119
+ scriptPubKeyHex: sender.scriptPubKeyHex,
120
+ address: sender.address,
121
+ };
122
+ }
123
+ function createResolvedDomainMarketRecipientSummary(recipient) {
124
+ return {
125
+ scriptPubKeyHex: recipient.scriptPubKeyHex,
126
+ address: recipient.address,
127
+ opaque: recipient.opaque,
128
+ };
129
+ }
130
+ function createTransferEconomicEffectSummary(clearsListing) {
131
+ return {
132
+ kind: "ownership-transfer",
133
+ clearsListing,
134
+ };
135
+ }
136
+ function createSellEconomicEffectSummary(listedPriceCogtoshi) {
137
+ if (listedPriceCogtoshi === 0n) {
138
+ return {
139
+ kind: "listing-clear",
140
+ listedPriceCogtoshi: "0",
141
+ };
142
+ }
143
+ return {
144
+ kind: "listing-set",
145
+ listedPriceCogtoshi: listedPriceCogtoshi.toString(),
146
+ };
147
+ }
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
+ function resolveOwnedDomainOperation(context, domainName, errorPrefix) {
164
+ assertWalletMutationContextReady(context, errorPrefix);
165
+ const chainDomain = lookupDomain(context.snapshot.state, domainName);
166
+ if (chainDomain === null) {
167
+ throw new Error(`${errorPrefix}_domain_not_found`);
168
+ }
169
+ if (chainDomain.anchored) {
170
+ throw new Error(`${errorPrefix}_domain_anchored`);
171
+ }
172
+ 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) {
175
+ throw new Error(`${errorPrefix}_owner_not_locally_controlled`);
176
+ }
177
+ if (senderIdentity.readOnly) {
178
+ throw new Error(`${errorPrefix}_owner_read_only`);
179
+ }
180
+ return {
181
+ readContext: context,
182
+ 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),
191
+ chainDomain,
192
+ };
193
+ }
194
+ function resolveBuyOperation(context, domainName, fromIdentity = null) {
195
+ assertWalletMutationContextReady(context, "wallet_buy");
196
+ const chainDomain = lookupDomain(context.snapshot.state, domainName);
197
+ if (chainDomain === null) {
198
+ throw new Error("wallet_buy_domain_not_found");
199
+ }
200
+ if (chainDomain.anchored) {
201
+ throw new Error("wallet_buy_domain_anchored");
202
+ }
203
+ const listing = getListing(context.snapshot.state, chainDomain.domainId);
204
+ if (listing === null) {
205
+ throw new Error("wallet_buy_domain_not_listed");
206
+ }
207
+ const selectedIdentity = fromIdentity === null
208
+ ? context.model.fundingIdentity
209
+ : resolveIdentityBySelector(context, fromIdentity, "wallet_buy");
210
+ if (selectedIdentity === null) {
211
+ throw new Error("wallet_buy_funding_identity_unavailable");
212
+ }
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
+ const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
222
+ if (ownerHex === selectedIdentity.scriptPubKeyHex) {
223
+ throw new Error("wallet_buy_already_owner");
224
+ }
225
+ if (getBalance(context.snapshot.state, selectedIdentity.scriptPubKeyHex) < listing.priceCogtoshi) {
226
+ throw new Error("wallet_buy_insufficient_cog_balance");
227
+ }
228
+ return {
229
+ readContext: context,
230
+ 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"),
239
+ chainDomain,
240
+ listingPriceCogtoshi: listing.priceCogtoshi,
241
+ buyerSelector: getCanonicalIdentitySelector(selectedIdentity),
242
+ };
243
+ }
244
+ function buildPlanForDomainOperation(options) {
245
+ const fundingUtxos = options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
246
+ && entry.confirmations >= 1
247
+ && entry.spendable !== false
248
+ && entry.safe !== false);
249
+ const outputs = [{ data: Buffer.from(options.opReturnData).toString("hex") }];
250
+ if (options.anchorOutpoint === null) {
251
+ const senderUtxo = options.allUtxos.find((entry) => entry.scriptPubKey === options.sender.scriptPubKeyHex
252
+ && entry.confirmations >= 1
253
+ && entry.spendable !== false
254
+ && entry.safe !== false);
255
+ if (senderUtxo === undefined) {
256
+ throw new Error(`${options.errorPrefix}_sender_utxo_unavailable`);
257
+ }
258
+ return {
259
+ sender: options.sender,
260
+ changeAddress: options.state.funding.address,
261
+ inputs: [
262
+ { txid: senderUtxo.txid, vout: senderUtxo.vout },
263
+ ...fundingUtxos
264
+ .filter((entry) => !(entry.txid === senderUtxo.txid && entry.vout === senderUtxo.vout))
265
+ .map((entry) => ({ txid: entry.txid, vout: entry.vout })),
266
+ ],
267
+ outputs,
268
+ changePosition: 1,
269
+ expectedOpReturnScriptHex: encodeOpReturnScript(options.opReturnData),
270
+ expectedAnchorScriptHex: null,
271
+ expectedAnchorValueSats: null,
272
+ allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
273
+ errorPrefix: options.errorPrefix,
274
+ };
275
+ }
276
+ const anchorUtxo = options.allUtxos.find((entry) => entry.txid === options.anchorOutpoint?.txid
277
+ && entry.vout === options.anchorOutpoint.vout
278
+ && entry.scriptPubKey === options.sender.scriptPubKeyHex
279
+ && entry.confirmations >= 1
280
+ && entry.spendable !== false
281
+ && entry.safe !== false);
282
+ if (anchorUtxo === undefined) {
283
+ throw new Error(`${options.errorPrefix}_anchor_utxo_missing`);
284
+ }
285
+ outputs.push({
286
+ [options.sender.address]: satsToBtcNumber(options.anchorValueSats),
287
+ });
288
+ return {
289
+ sender: options.sender,
290
+ changeAddress: options.state.funding.address,
291
+ inputs: [
292
+ { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
293
+ ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
294
+ ],
295
+ outputs,
296
+ changePosition: 2,
297
+ expectedOpReturnScriptHex: encodeOpReturnScript(options.opReturnData),
298
+ expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
299
+ expectedAnchorValueSats: options.anchorValueSats,
300
+ allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
301
+ errorPrefix: options.errorPrefix,
302
+ };
303
+ }
304
+ function validateFundedDraft(decoded, funded, plan) {
305
+ const inputs = decoded.tx.vin;
306
+ const outputs = decoded.tx.vout;
307
+ if (inputs.length === 0) {
308
+ throw new Error(`${plan.errorPrefix}_missing_sender_input`);
309
+ }
310
+ if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
311
+ throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
312
+ }
313
+ for (let index = 1; index < inputs.length; index += 1) {
314
+ if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
315
+ throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
316
+ }
317
+ }
318
+ if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
319
+ throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
320
+ }
321
+ if (plan.expectedAnchorScriptHex !== null) {
322
+ if (outputs[1]?.scriptPubKey?.hex !== plan.expectedAnchorScriptHex) {
323
+ throw new Error(`${plan.errorPrefix}_anchor_output_mismatch`);
324
+ }
325
+ if (valueToSats(outputs[1]?.value ?? 0) !== (plan.expectedAnchorValueSats ?? 0n)) {
326
+ throw new Error(`${plan.errorPrefix}_anchor_value_mismatch`);
327
+ }
328
+ }
329
+ const expectedWithoutChange = plan.expectedAnchorScriptHex === null ? 1 : 2;
330
+ if (funded.changepos === -1) {
331
+ if (outputs.length !== expectedWithoutChange) {
332
+ throw new Error(`${plan.errorPrefix}_unexpected_output_count`);
333
+ }
334
+ return;
335
+ }
336
+ if (funded.changepos !== plan.changePosition || outputs.length !== expectedWithoutChange + 1) {
337
+ throw new Error(`${plan.errorPrefix}_change_position_mismatch`);
338
+ }
339
+ if (outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
340
+ throw new Error(`${plan.errorPrefix}_change_output_mismatch`);
341
+ }
342
+ }
343
+ async function buildTransaction(options) {
344
+ return buildWalletMutationTransaction({
345
+ rpc: options.rpc,
346
+ walletName: options.walletName,
347
+ plan: options.plan,
348
+ validateFundedDraft,
349
+ finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
350
+ mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
351
+ });
352
+ }
353
+ function createDraftMutation(options) {
354
+ if (options.existing !== null && options.existing !== undefined) {
355
+ return {
356
+ ...options.existing,
357
+ kind: options.kind,
358
+ parentDomainName: options.parentDomainName ?? null,
359
+ senderScriptPubKeyHex: options.sender.scriptPubKeyHex,
360
+ senderLocalIndex: options.sender.localIndex,
361
+ recipientScriptPubKeyHex: options.recipientScriptPubKeyHex ?? null,
362
+ priceCogtoshi: options.priceCogtoshi ?? null,
363
+ status: "draft",
364
+ lastUpdatedAtUnixMs: options.nowUnixMs,
365
+ attemptedTxid: null,
366
+ attemptedWtxid: null,
367
+ temporaryBuilderLockedOutpoints: [],
368
+ };
369
+ }
370
+ return {
371
+ mutationId: randomBytes(12).toString("hex"),
372
+ kind: options.kind,
373
+ domainName: options.domainName,
374
+ parentDomainName: options.parentDomainName ?? null,
375
+ senderScriptPubKeyHex: options.sender.scriptPubKeyHex,
376
+ senderLocalIndex: options.sender.localIndex,
377
+ recipientScriptPubKeyHex: options.recipientScriptPubKeyHex ?? null,
378
+ priceCogtoshi: options.priceCogtoshi ?? null,
379
+ intentFingerprintHex: options.intentFingerprintHex,
380
+ status: "draft",
381
+ createdAtUnixMs: options.nowUnixMs,
382
+ lastUpdatedAtUnixMs: options.nowUnixMs,
383
+ attemptedTxid: null,
384
+ attemptedWtxid: null,
385
+ temporaryBuilderLockedOutpoints: [],
386
+ };
387
+ }
388
+ function getTransferStatusAfterAcceptance(options) {
389
+ const chainDomain = options.snapshot === null ? null : lookupDomain(options.snapshot.state, options.domainName);
390
+ if (chainDomain === null) {
391
+ return "live";
392
+ }
393
+ return Buffer.from(chainDomain.ownerScriptPubKey).toString("hex") === options.recipientScriptPubKeyHex
394
+ ? "confirmed"
395
+ : "live";
396
+ }
397
+ function getSellStatusAfterAcceptance(options) {
398
+ const chainDomain = options.snapshot === null ? null : lookupDomain(options.snapshot.state, options.domainName);
399
+ if (chainDomain === null) {
400
+ return "live";
401
+ }
402
+ const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
403
+ const listing = getListing(options.snapshot.state, chainDomain.domainId);
404
+ if (options.listedPriceCogtoshi === 0n) {
405
+ return ownerHex === options.senderScriptPubKeyHex && listing === null ? "confirmed" : "live";
406
+ }
407
+ return ownerHex === options.senderScriptPubKeyHex && listing?.priceCogtoshi === options.listedPriceCogtoshi
408
+ ? "confirmed"
409
+ : "live";
410
+ }
411
+ function getBuyStatusAfterAcceptance(options) {
412
+ const chainDomain = options.snapshot === null ? null : lookupDomain(options.snapshot.state, options.domainName);
413
+ if (chainDomain === null) {
414
+ return "live";
415
+ }
416
+ return Buffer.from(chainDomain.ownerScriptPubKey).toString("hex") === options.buyerScriptPubKeyHex
417
+ ? "confirmed"
418
+ : "live";
419
+ }
420
+ async function reconcilePendingMutation(options) {
421
+ if (options.mutation.status === "repair-required") {
422
+ return {
423
+ state: options.state,
424
+ mutation: options.mutation,
425
+ resolution: "repair-required",
426
+ };
427
+ }
428
+ const chainDomain = options.context.snapshot === null
429
+ ? null
430
+ : lookupDomain(options.context.snapshot.state, options.mutation.domainName);
431
+ if (chainDomain !== null) {
432
+ const ownerHex = Buffer.from(chainDomain.ownerScriptPubKey).toString("hex");
433
+ const listing = getListing(options.context.snapshot.state, chainDomain.domainId);
434
+ if (options.mutation.kind === "transfer") {
435
+ if (ownerHex === options.mutation.recipientScriptPubKeyHex) {
436
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
437
+ const confirmed = updateMutationRecord(options.mutation, "confirmed", options.nowUnixMs, {
438
+ temporaryBuilderLockedOutpoints: [],
439
+ });
440
+ const nextState = reserveTransferredDomainRecord({
441
+ state: upsertPendingMutation(options.state, confirmed),
442
+ domainName: options.mutation.domainName,
443
+ domainId: chainDomain.domainId,
444
+ currentOwnerScriptPubKeyHex: ownerHex,
445
+ currentOwnerLocalIndex: options.context.model?.identities.find((identity) => identity.scriptPubKeyHex === ownerHex)?.index ?? null,
446
+ nowUnixMs: options.nowUnixMs,
447
+ });
448
+ await saveWalletStatePreservingUnlock({
449
+ state: nextState,
450
+ provider: options.provider,
451
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
452
+ nowUnixMs: options.nowUnixMs,
453
+ paths: options.paths,
454
+ });
455
+ return { state: nextState, mutation: confirmed, resolution: "confirmed" };
456
+ }
457
+ if (ownerHex !== options.mutation.senderScriptPubKeyHex) {
458
+ const repair = updateMutationRecord(options.mutation, "repair-required", options.nowUnixMs, {
459
+ temporaryBuilderLockedOutpoints: [],
460
+ });
461
+ const nextState = upsertPendingMutation(options.state, repair);
462
+ await saveWalletStatePreservingUnlock({
463
+ state: nextState,
464
+ provider: options.provider,
465
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
466
+ nowUnixMs: options.nowUnixMs,
467
+ paths: options.paths,
468
+ });
469
+ return { state: nextState, mutation: repair, resolution: "repair-required" };
470
+ }
471
+ }
472
+ if (options.mutation.kind === "sell") {
473
+ const targetPrice = options.mutation.priceCogtoshi ?? 0n;
474
+ if (ownerHex === options.mutation.senderScriptPubKeyHex) {
475
+ if (targetPrice === 0n && listing === null) {
476
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
477
+ const confirmed = updateMutationRecord(options.mutation, "confirmed", options.nowUnixMs, {
478
+ temporaryBuilderLockedOutpoints: [],
479
+ });
480
+ const nextState = upsertPendingMutation(options.state, confirmed);
481
+ await saveWalletStatePreservingUnlock({
482
+ state: nextState,
483
+ provider: options.provider,
484
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
485
+ nowUnixMs: options.nowUnixMs,
486
+ paths: options.paths,
487
+ });
488
+ return { state: nextState, mutation: confirmed, resolution: "confirmed" };
489
+ }
490
+ if (targetPrice > 0n && listing?.priceCogtoshi === targetPrice) {
491
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
492
+ const confirmed = updateMutationRecord(options.mutation, "confirmed", options.nowUnixMs, {
493
+ temporaryBuilderLockedOutpoints: [],
494
+ });
495
+ const nextState = upsertPendingMutation(options.state, confirmed);
496
+ await saveWalletStatePreservingUnlock({
497
+ state: nextState,
498
+ provider: options.provider,
499
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
500
+ nowUnixMs: options.nowUnixMs,
501
+ paths: options.paths,
502
+ });
503
+ return { state: nextState, mutation: confirmed, resolution: "confirmed" };
504
+ }
505
+ }
506
+ else {
507
+ const repair = updateMutationRecord(options.mutation, "repair-required", options.nowUnixMs, {
508
+ temporaryBuilderLockedOutpoints: [],
509
+ });
510
+ const nextState = upsertPendingMutation(options.state, repair);
511
+ await saveWalletStatePreservingUnlock({
512
+ state: nextState,
513
+ provider: options.provider,
514
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
515
+ nowUnixMs: options.nowUnixMs,
516
+ paths: options.paths,
517
+ });
518
+ return { state: nextState, mutation: repair, resolution: "repair-required" };
519
+ }
520
+ }
521
+ if (options.mutation.kind === "buy") {
522
+ if (ownerHex === options.mutation.senderScriptPubKeyHex) {
523
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
524
+ const confirmed = updateMutationRecord(options.mutation, "confirmed", options.nowUnixMs, {
525
+ temporaryBuilderLockedOutpoints: [],
526
+ });
527
+ const nextState = reserveTransferredDomainRecord({
528
+ state: upsertPendingMutation(options.state, confirmed),
529
+ domainName: options.mutation.domainName,
530
+ domainId: chainDomain.domainId,
531
+ currentOwnerScriptPubKeyHex: ownerHex,
532
+ currentOwnerLocalIndex: options.context.model?.identities.find((identity) => identity.scriptPubKeyHex === ownerHex)?.index ?? null,
533
+ nowUnixMs: options.nowUnixMs,
534
+ });
535
+ await saveWalletStatePreservingUnlock({
536
+ state: nextState,
537
+ provider: options.provider,
538
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
539
+ nowUnixMs: options.nowUnixMs,
540
+ paths: options.paths,
541
+ });
542
+ return { state: nextState, mutation: confirmed, resolution: "confirmed" };
543
+ }
544
+ if (listing === null) {
545
+ const repair = updateMutationRecord(options.mutation, "repair-required", options.nowUnixMs, {
546
+ temporaryBuilderLockedOutpoints: [],
547
+ });
548
+ const nextState = upsertPendingMutation(options.state, repair);
549
+ await saveWalletStatePreservingUnlock({
550
+ state: nextState,
551
+ provider: options.provider,
552
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
553
+ nowUnixMs: options.nowUnixMs,
554
+ paths: options.paths,
555
+ });
556
+ return { state: nextState, mutation: repair, resolution: "repair-required" };
557
+ }
558
+ }
559
+ }
560
+ if (options.mutation.attemptedTxid !== null) {
561
+ const mempool = await options.rpc.getRawMempool().catch(() => []);
562
+ if (mempool.includes(options.mutation.attemptedTxid)) {
563
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
564
+ const live = updateMutationRecord(options.mutation, "live", options.nowUnixMs, {
565
+ temporaryBuilderLockedOutpoints: [],
566
+ });
567
+ let nextState = upsertPendingMutation(options.state, live);
568
+ if (live.kind === "transfer" || live.kind === "buy") {
569
+ nextState = reserveTransferredDomainRecord({
570
+ state: nextState,
571
+ domainName: live.domainName,
572
+ domainId: chainDomain?.domainId ?? null,
573
+ currentOwnerScriptPubKeyHex: live.kind === "transfer"
574
+ ? (live.recipientScriptPubKeyHex ?? live.senderScriptPubKeyHex)
575
+ : live.senderScriptPubKeyHex,
576
+ currentOwnerLocalIndex: live.kind === "transfer"
577
+ ? options.context.model?.identities.find((identity) => identity.scriptPubKeyHex === live.recipientScriptPubKeyHex)?.index ?? null
578
+ : live.senderLocalIndex,
579
+ nowUnixMs: options.nowUnixMs,
580
+ });
581
+ }
582
+ await saveWalletStatePreservingUnlock({
583
+ state: nextState,
584
+ provider: options.provider,
585
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
586
+ nowUnixMs: options.nowUnixMs,
587
+ paths: options.paths,
588
+ });
589
+ return { state: nextState, mutation: live, resolution: "live" };
590
+ }
591
+ }
592
+ if (options.mutation.status === "broadcast-unknown"
593
+ || options.mutation.status === "live"
594
+ || options.mutation.status === "draft"
595
+ || options.mutation.status === "broadcasting") {
596
+ await unlockTemporaryBuilderLocks(options.rpc, options.walletName, options.mutation.temporaryBuilderLockedOutpoints);
597
+ const canceled = updateMutationRecord(options.mutation, "canceled", options.nowUnixMs, {
598
+ temporaryBuilderLockedOutpoints: [],
599
+ });
600
+ const nextState = upsertPendingMutation(options.state, canceled);
601
+ await saveWalletStatePreservingUnlock({
602
+ state: nextState,
603
+ provider: options.provider,
604
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
605
+ nowUnixMs: options.nowUnixMs,
606
+ paths: options.paths,
607
+ });
608
+ return { state: nextState, mutation: canceled, resolution: "not-seen" };
609
+ }
610
+ return {
611
+ state: options.state,
612
+ mutation: options.mutation,
613
+ resolution: "continue",
614
+ };
615
+ }
616
+ async function confirmTransfer(prompter, domainName, sender, recipient, economicEffect, assumeYes = false) {
617
+ prompter.writeLine(`You are transferring "${domainName}".`);
618
+ prompter.writeLine(`Resolved sender: ${sender.selector} (${sender.address})`);
619
+ prompter.writeLine(`Resolved recipient: ${recipient.address ?? `spk:${recipient.scriptPubKeyHex}`}`);
620
+ prompter.writeLine(`Economic effect: ${economicEffect.kind === "ownership-transfer" && economicEffect.clearsListing
621
+ ? "transfer domain ownership and clear any active listing."
622
+ : "transfer domain ownership."}`);
623
+ if (recipient.opaque) {
624
+ prompter.writeLine(`Target script length: ${recipient.scriptPubKeyHex.length / 2} bytes`);
625
+ prompter.writeLine("Cogcoin identity is exact raw-script equality. Different script templates are different identities.");
626
+ const acknowledgement = `RAW-SCRIPT:${recipient.scriptPubKeyHex.slice(0, 16)}`;
627
+ await confirmTypedAcknowledgement(prompter, {
628
+ assumeYes,
629
+ expected: acknowledgement,
630
+ prompt: `Type ${acknowledgement} to continue: `,
631
+ errorCode: "wallet_transfer_confirmation_rejected",
632
+ requiresTtyErrorCode: "wallet_transfer_requires_tty",
633
+ typedAckRequiredErrorCode: "wallet_transfer_typed_ack_required",
634
+ });
635
+ return;
636
+ }
637
+ await confirmYesNo(prompter, "This publishes a standalone DOMAIN_TRANSFER.", {
638
+ assumeYes,
639
+ errorCode: "wallet_transfer_confirmation_rejected",
640
+ requiresTtyErrorCode: "wallet_transfer_requires_tty",
641
+ });
642
+ }
643
+ async function confirmSell(prompter, domainName, sender, listedPriceCogtoshi, assumeYes = false) {
644
+ prompter.writeLine(`You are listing "${domainName}".`);
645
+ prompter.writeLine(`Resolved sender: ${sender.selector} (${sender.address})`);
646
+ prompter.writeLine(`Exact listing price: ${listedPriceCogtoshi.toString()} cogtoshi.`);
647
+ prompter.writeLine(`Economic effect: set the listing price to ${listedPriceCogtoshi.toString()} cogtoshi in COG state.`);
648
+ prompter.writeLine("Settlement: entirely in COG state. No BTC payment output will be added.");
649
+ await confirmYesNo(prompter, "This publishes a standalone DOMAIN_SELL mutation.", {
650
+ assumeYes,
651
+ errorCode: "wallet_sell_confirmation_rejected",
652
+ requiresTtyErrorCode: "wallet_sell_requires_tty",
653
+ });
654
+ }
655
+ async function confirmBuy(prompter, domainName, buyerSelector, buyer, sellerScriptPubKeyHex, sellerAddress, listedPriceCogtoshi, assumeYes = false) {
656
+ prompter.writeLine(`You are buying "${domainName}".`);
657
+ prompter.writeLine(`Exact listing price: ${listedPriceCogtoshi.toString()} cogtoshi.`);
658
+ prompter.writeLine(`Resolved buyer: ${buyerSelector} (${buyer.address})`);
659
+ prompter.writeLine(`Resolved seller: ${sellerAddress ?? `spk:${sellerScriptPubKeyHex}`}`);
660
+ prompter.writeLine("Settlement: entirely in COG state. No BTC payment output will be added.");
661
+ await confirmYesNo(prompter, "This publishes a standalone DOMAIN_BUY mutation.", {
662
+ assumeYes,
663
+ errorCode: "wallet_buy_confirmation_rejected",
664
+ requiresTtyErrorCode: "wallet_buy_requires_tty",
665
+ });
666
+ }
667
+ export async function transferDomain(options) {
668
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
669
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
670
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
671
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
672
+ purpose: "wallet-transfer",
673
+ walletRootId: null,
674
+ });
675
+ const normalizedDomainName = normalizeDomainName(options.domainName);
676
+ const recipient = normalizeBtcTarget(options.target);
677
+ try {
678
+ const miningPreemption = await pauseMiningForWalletMutation({
679
+ paths,
680
+ reason: "wallet-transfer",
681
+ });
682
+ const readContext = await (options.openReadContext ?? openWalletReadContext)({
683
+ dataDir: options.dataDir,
684
+ databasePath: options.databasePath,
685
+ secretProvider: provider,
686
+ paths,
687
+ });
688
+ try {
689
+ const operation = resolveOwnedDomainOperation(readContext, normalizedDomainName, "wallet_transfer");
690
+ const snapshot = readContext.snapshot;
691
+ const model = readContext.model;
692
+ const resolvedSender = createResolvedDomainMarketSenderSummary(operation.sender, operation.senderSelector);
693
+ const resolvedRecipient = createResolvedDomainMarketRecipientSummary(recipient);
694
+ const resolvedEconomicEffect = createTransferEconomicEffectSummary(getListing(snapshot.state, operation.chainDomain.domainId) !== null);
695
+ if (operation.sender.scriptPubKeyHex === recipient.scriptPubKeyHex) {
696
+ throw new Error("wallet_transfer_self_transfer");
697
+ }
698
+ const intentFingerprintHex = createIntentFingerprint([
699
+ "transfer",
700
+ operation.state.walletRootId,
701
+ normalizedDomainName,
702
+ operation.sender.scriptPubKeyHex,
703
+ recipient.scriptPubKeyHex,
704
+ ]);
705
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
706
+ dataDir: options.dataDir,
707
+ chain: "main",
708
+ startHeight: 0,
709
+ walletRootId: operation.state.walletRootId,
710
+ });
711
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
712
+ const walletName = operation.state.managedCoreWallet.walletName;
713
+ const existingMutation = findPendingMutationByIntent(operation.state, intentFingerprintHex);
714
+ if (existingMutation !== null) {
715
+ const reconciled = await reconcilePendingMutation({
716
+ state: operation.state,
717
+ mutation: existingMutation,
718
+ provider,
719
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
720
+ nowUnixMs,
721
+ paths,
722
+ rpc,
723
+ walletName,
724
+ context: readContext,
725
+ });
726
+ if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
727
+ return {
728
+ kind: "transfer",
729
+ domainName: normalizedDomainName,
730
+ txid: reconciled.mutation.attemptedTxid ?? "unknown",
731
+ status: reconciled.resolution,
732
+ reusedExisting: true,
733
+ recipientScriptPubKeyHex: recipient.scriptPubKeyHex,
734
+ resolved: {
735
+ sender: resolvedSender,
736
+ recipient: resolvedRecipient,
737
+ economicEffect: resolvedEconomicEffect,
738
+ },
739
+ };
740
+ }
741
+ if (reconciled.resolution === "repair-required") {
742
+ throw new Error("wallet_transfer_repair_required");
743
+ }
744
+ }
745
+ await confirmTransfer(options.prompter, normalizedDomainName, resolvedSender, resolvedRecipient, resolvedEconomicEffect, options.assumeYes);
746
+ let nextState = upsertPendingMutation(operation.state, createDraftMutation({
747
+ kind: "transfer",
748
+ domainName: normalizedDomainName,
749
+ sender: operation.sender,
750
+ recipientScriptPubKeyHex: recipient.scriptPubKeyHex,
751
+ intentFingerprintHex,
752
+ nowUnixMs,
753
+ existing: existingMutation,
754
+ }));
755
+ nextState = {
756
+ ...nextState,
757
+ stateRevision: nextState.stateRevision + 1,
758
+ lastWrittenAtUnixMs: nowUnixMs,
759
+ };
760
+ await saveWalletStatePreservingUnlock({
761
+ state: nextState,
762
+ provider,
763
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
764
+ nowUnixMs,
765
+ paths,
766
+ });
767
+ const built = await buildTransaction({
768
+ rpc,
769
+ walletName,
770
+ plan: buildPlanForDomainOperation({
771
+ state: nextState,
772
+ allUtxos: await rpc.listUnspent(walletName, 1),
773
+ sender: operation.sender,
774
+ anchorOutpoint: operation.anchorOutpoint,
775
+ opReturnData: serializeDomainTransfer(operation.chainDomain.domainId, Buffer.from(recipient.scriptPubKeyHex, "hex")).opReturnData,
776
+ anchorValueSats: BigInt(nextState.anchorValueSats),
777
+ errorPrefix: "wallet_transfer",
778
+ }),
779
+ });
780
+ const broadcasting = updateMutationRecord(nextState.pendingMutations.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex), "broadcasting", nowUnixMs, {
781
+ attemptedTxid: built.txid,
782
+ attemptedWtxid: built.wtxid,
783
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
784
+ });
785
+ nextState = {
786
+ ...upsertPendingMutation(nextState, broadcasting),
787
+ stateRevision: nextState.stateRevision + 1,
788
+ lastWrittenAtUnixMs: nowUnixMs,
789
+ };
790
+ await saveWalletStatePreservingUnlock({
791
+ state: nextState,
792
+ provider,
793
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
794
+ nowUnixMs,
795
+ paths,
796
+ });
797
+ if (snapshot.tip?.height !== (await rpc.getBlockchainInfo()).blocks) {
798
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
799
+ throw new Error("wallet_transfer_tip_mismatch");
800
+ }
801
+ try {
802
+ await rpc.sendRawTransaction(built.rawHex);
803
+ }
804
+ catch (error) {
805
+ if (!isAlreadyAcceptedError(error)) {
806
+ if (isBroadcastUnknownError(error)) {
807
+ const unknown = updateMutationRecord(broadcasting, "broadcast-unknown", nowUnixMs, {
808
+ attemptedTxid: built.txid,
809
+ attemptedWtxid: built.wtxid,
810
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
811
+ });
812
+ nextState = {
813
+ ...upsertPendingMutation(nextState, unknown),
814
+ stateRevision: nextState.stateRevision + 1,
815
+ lastWrittenAtUnixMs: nowUnixMs,
816
+ };
817
+ await saveWalletStatePreservingUnlock({
818
+ state: nextState,
819
+ provider,
820
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
821
+ nowUnixMs,
822
+ paths,
823
+ });
824
+ throw new Error("wallet_transfer_broadcast_unknown");
825
+ }
826
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
827
+ const canceled = updateMutationRecord(broadcasting, "canceled", nowUnixMs, {
828
+ attemptedTxid: built.txid,
829
+ attemptedWtxid: built.wtxid,
830
+ temporaryBuilderLockedOutpoints: [],
831
+ });
832
+ nextState = {
833
+ ...upsertPendingMutation(nextState, canceled),
834
+ stateRevision: nextState.stateRevision + 1,
835
+ lastWrittenAtUnixMs: nowUnixMs,
836
+ };
837
+ await saveWalletStatePreservingUnlock({
838
+ state: nextState,
839
+ provider,
840
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
841
+ nowUnixMs,
842
+ paths,
843
+ });
844
+ throw error;
845
+ }
846
+ }
847
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
848
+ const finalStatus = getTransferStatusAfterAcceptance({
849
+ snapshot: readContext.snapshot,
850
+ domainName: normalizedDomainName,
851
+ recipientScriptPubKeyHex: recipient.scriptPubKeyHex,
852
+ });
853
+ const finalMutation = updateMutationRecord(broadcasting, finalStatus, nowUnixMs, {
854
+ attemptedTxid: built.txid,
855
+ attemptedWtxid: built.wtxid,
856
+ temporaryBuilderLockedOutpoints: [],
857
+ });
858
+ nextState = reserveTransferredDomainRecord({
859
+ state: upsertPendingMutation(nextState, finalMutation),
860
+ domainName: normalizedDomainName,
861
+ domainId: operation.chainDomain.domainId,
862
+ currentOwnerScriptPubKeyHex: recipient.scriptPubKeyHex,
863
+ currentOwnerLocalIndex: model.identities.find((identity) => identity.scriptPubKeyHex === recipient.scriptPubKeyHex)?.index ?? null,
864
+ nowUnixMs,
865
+ });
866
+ nextState = {
867
+ ...nextState,
868
+ stateRevision: nextState.stateRevision + 1,
869
+ lastWrittenAtUnixMs: nowUnixMs,
870
+ };
871
+ await saveWalletStatePreservingUnlock({
872
+ state: nextState,
873
+ provider,
874
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
875
+ nowUnixMs,
876
+ paths,
877
+ });
878
+ return {
879
+ kind: "transfer",
880
+ domainName: normalizedDomainName,
881
+ txid: built.txid,
882
+ status: finalStatus,
883
+ reusedExisting: false,
884
+ recipientScriptPubKeyHex: recipient.scriptPubKeyHex,
885
+ resolved: {
886
+ sender: resolvedSender,
887
+ recipient: resolvedRecipient,
888
+ economicEffect: resolvedEconomicEffect,
889
+ },
890
+ };
891
+ }
892
+ finally {
893
+ await readContext.close();
894
+ await miningPreemption.release();
895
+ }
896
+ }
897
+ finally {
898
+ await controlLock.release();
899
+ }
900
+ }
901
+ async function runSellMutation(options) {
902
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
903
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
904
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
905
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
906
+ purpose: "wallet-sell",
907
+ walletRootId: null,
908
+ });
909
+ const normalizedDomainName = normalizeDomainName(options.domainName);
910
+ try {
911
+ const miningPreemption = await pauseMiningForWalletMutation({
912
+ paths,
913
+ reason: "wallet-sell",
914
+ });
915
+ const readContext = await (options.openReadContext ?? openWalletReadContext)({
916
+ dataDir: options.dataDir,
917
+ databasePath: options.databasePath,
918
+ secretProvider: provider,
919
+ paths,
920
+ });
921
+ try {
922
+ const operation = resolveOwnedDomainOperation(readContext, normalizedDomainName, "wallet_sell");
923
+ const resolvedSender = createResolvedDomainMarketSenderSummary(operation.sender, operation.senderSelector);
924
+ const resolvedEconomicEffect = createSellEconomicEffectSummary(options.listedPriceCogtoshi);
925
+ const snapshot = readContext.snapshot;
926
+ const intentFingerprintHex = createIntentFingerprint([
927
+ "sell",
928
+ operation.state.walletRootId,
929
+ normalizedDomainName,
930
+ operation.sender.scriptPubKeyHex,
931
+ options.listedPriceCogtoshi.toString(),
932
+ ]);
933
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
934
+ dataDir: options.dataDir,
935
+ chain: "main",
936
+ startHeight: 0,
937
+ walletRootId: operation.state.walletRootId,
938
+ });
939
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
940
+ const walletName = operation.state.managedCoreWallet.walletName;
941
+ const existingMutation = findPendingMutationByIntent(operation.state, intentFingerprintHex);
942
+ if (existingMutation !== null) {
943
+ const reconciled = await reconcilePendingMutation({
944
+ state: operation.state,
945
+ mutation: existingMutation,
946
+ provider,
947
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
948
+ nowUnixMs,
949
+ paths,
950
+ rpc,
951
+ walletName,
952
+ context: readContext,
953
+ });
954
+ if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
955
+ return {
956
+ kind: "sell",
957
+ domainName: normalizedDomainName,
958
+ txid: reconciled.mutation.attemptedTxid ?? "unknown",
959
+ status: reconciled.resolution,
960
+ reusedExisting: true,
961
+ listedPriceCogtoshi: options.listedPriceCogtoshi,
962
+ resolved: {
963
+ sender: resolvedSender,
964
+ economicEffect: resolvedEconomicEffect,
965
+ },
966
+ };
967
+ }
968
+ if (reconciled.resolution === "repair-required") {
969
+ throw new Error("wallet_sell_repair_required");
970
+ }
971
+ }
972
+ if (options.listedPriceCogtoshi > 0n) {
973
+ await confirmSell(options.prompter, normalizedDomainName, resolvedSender, options.listedPriceCogtoshi, options.assumeYes);
974
+ }
975
+ let nextState = upsertPendingMutation(operation.state, createDraftMutation({
976
+ kind: "sell",
977
+ domainName: normalizedDomainName,
978
+ sender: operation.sender,
979
+ priceCogtoshi: options.listedPriceCogtoshi,
980
+ intentFingerprintHex,
981
+ nowUnixMs,
982
+ existing: existingMutation,
983
+ }));
984
+ nextState = {
985
+ ...nextState,
986
+ stateRevision: nextState.stateRevision + 1,
987
+ lastWrittenAtUnixMs: nowUnixMs,
988
+ };
989
+ await saveWalletStatePreservingUnlock({
990
+ state: nextState,
991
+ provider,
992
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
993
+ nowUnixMs,
994
+ paths,
995
+ });
996
+ const built = await buildTransaction({
997
+ rpc,
998
+ walletName,
999
+ plan: buildPlanForDomainOperation({
1000
+ state: nextState,
1001
+ allUtxos: await rpc.listUnspent(walletName, 1),
1002
+ sender: operation.sender,
1003
+ anchorOutpoint: operation.anchorOutpoint,
1004
+ opReturnData: serializeDomainSell(operation.chainDomain.domainId, options.listedPriceCogtoshi).opReturnData,
1005
+ anchorValueSats: BigInt(nextState.anchorValueSats),
1006
+ errorPrefix: "wallet_sell",
1007
+ }),
1008
+ });
1009
+ const broadcasting = updateMutationRecord(nextState.pendingMutations.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex), "broadcasting", nowUnixMs, {
1010
+ attemptedTxid: built.txid,
1011
+ attemptedWtxid: built.wtxid,
1012
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
1013
+ });
1014
+ nextState = {
1015
+ ...upsertPendingMutation(nextState, broadcasting),
1016
+ stateRevision: nextState.stateRevision + 1,
1017
+ lastWrittenAtUnixMs: nowUnixMs,
1018
+ };
1019
+ await saveWalletStatePreservingUnlock({
1020
+ state: nextState,
1021
+ provider,
1022
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1023
+ nowUnixMs,
1024
+ paths,
1025
+ });
1026
+ if (snapshot.tip?.height !== (await rpc.getBlockchainInfo()).blocks) {
1027
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
1028
+ throw new Error("wallet_sell_tip_mismatch");
1029
+ }
1030
+ try {
1031
+ await rpc.sendRawTransaction(built.rawHex);
1032
+ }
1033
+ catch (error) {
1034
+ if (!isAlreadyAcceptedError(error)) {
1035
+ if (isBroadcastUnknownError(error)) {
1036
+ const unknown = updateMutationRecord(broadcasting, "broadcast-unknown", nowUnixMs, {
1037
+ attemptedTxid: built.txid,
1038
+ attemptedWtxid: built.wtxid,
1039
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
1040
+ });
1041
+ nextState = {
1042
+ ...upsertPendingMutation(nextState, unknown),
1043
+ stateRevision: nextState.stateRevision + 1,
1044
+ lastWrittenAtUnixMs: nowUnixMs,
1045
+ };
1046
+ await saveWalletStatePreservingUnlock({
1047
+ state: nextState,
1048
+ provider,
1049
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1050
+ nowUnixMs,
1051
+ paths,
1052
+ });
1053
+ throw new Error("wallet_sell_broadcast_unknown");
1054
+ }
1055
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
1056
+ const canceled = updateMutationRecord(broadcasting, "canceled", nowUnixMs, {
1057
+ attemptedTxid: built.txid,
1058
+ attemptedWtxid: built.wtxid,
1059
+ temporaryBuilderLockedOutpoints: [],
1060
+ });
1061
+ nextState = {
1062
+ ...upsertPendingMutation(nextState, canceled),
1063
+ stateRevision: nextState.stateRevision + 1,
1064
+ lastWrittenAtUnixMs: nowUnixMs,
1065
+ };
1066
+ await saveWalletStatePreservingUnlock({
1067
+ state: nextState,
1068
+ provider,
1069
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1070
+ nowUnixMs,
1071
+ paths,
1072
+ });
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
1077
+ const finalStatus = getSellStatusAfterAcceptance({
1078
+ snapshot: readContext.snapshot,
1079
+ domainName: normalizedDomainName,
1080
+ senderScriptPubKeyHex: operation.sender.scriptPubKeyHex,
1081
+ listedPriceCogtoshi: options.listedPriceCogtoshi,
1082
+ });
1083
+ const finalMutation = updateMutationRecord(broadcasting, finalStatus, nowUnixMs, {
1084
+ attemptedTxid: built.txid,
1085
+ attemptedWtxid: built.wtxid,
1086
+ temporaryBuilderLockedOutpoints: [],
1087
+ });
1088
+ nextState = {
1089
+ ...upsertPendingMutation(nextState, finalMutation),
1090
+ stateRevision: nextState.stateRevision + 1,
1091
+ lastWrittenAtUnixMs: nowUnixMs,
1092
+ };
1093
+ await saveWalletStatePreservingUnlock({
1094
+ state: nextState,
1095
+ provider,
1096
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1097
+ nowUnixMs,
1098
+ paths,
1099
+ });
1100
+ return {
1101
+ kind: "sell",
1102
+ domainName: normalizedDomainName,
1103
+ txid: built.txid,
1104
+ status: finalStatus,
1105
+ reusedExisting: false,
1106
+ listedPriceCogtoshi: options.listedPriceCogtoshi,
1107
+ resolved: {
1108
+ sender: resolvedSender,
1109
+ economicEffect: resolvedEconomicEffect,
1110
+ },
1111
+ };
1112
+ }
1113
+ finally {
1114
+ await readContext.close();
1115
+ await miningPreemption.release();
1116
+ }
1117
+ }
1118
+ finally {
1119
+ await controlLock.release();
1120
+ }
1121
+ }
1122
+ export async function sellDomain(options) {
1123
+ if (options.listedPriceCogtoshi < 0n) {
1124
+ throw new Error("wallet_sell_invalid_amount");
1125
+ }
1126
+ return runSellMutation(options);
1127
+ }
1128
+ export async function buyDomain(options) {
1129
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
1130
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
1131
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1132
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1133
+ purpose: "wallet-buy",
1134
+ walletRootId: null,
1135
+ });
1136
+ const normalizedDomainName = normalizeDomainName(options.domainName);
1137
+ try {
1138
+ const miningPreemption = await pauseMiningForWalletMutation({
1139
+ paths,
1140
+ reason: "wallet-buy",
1141
+ });
1142
+ const readContext = await (options.openReadContext ?? openWalletReadContext)({
1143
+ dataDir: options.dataDir,
1144
+ databasePath: options.databasePath,
1145
+ secretProvider: provider,
1146
+ paths,
1147
+ });
1148
+ try {
1149
+ const operation = resolveBuyOperation(readContext, normalizedDomainName, options.fromIdentity ?? null);
1150
+ const snapshot = readContext.snapshot;
1151
+ const model = readContext.model;
1152
+ const sellerScriptPubKeyHex = Buffer.from(operation.chainDomain.ownerScriptPubKey).toString("hex");
1153
+ const sellerAddress = model.identities.find((identity) => identity.scriptPubKeyHex === sellerScriptPubKeyHex)?.address ?? null;
1154
+ const resolvedBuyer = {
1155
+ selector: operation.buyerSelector,
1156
+ localIndex: operation.sender.localIndex,
1157
+ scriptPubKeyHex: operation.sender.scriptPubKeyHex,
1158
+ address: operation.sender.address,
1159
+ };
1160
+ const resolvedSeller = {
1161
+ scriptPubKeyHex: sellerScriptPubKeyHex,
1162
+ address: sellerAddress,
1163
+ };
1164
+ const intentFingerprintHex = createIntentFingerprint([
1165
+ "buy",
1166
+ operation.state.walletRootId,
1167
+ normalizedDomainName,
1168
+ operation.sender.scriptPubKeyHex,
1169
+ operation.listingPriceCogtoshi.toString(),
1170
+ ]);
1171
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
1172
+ dataDir: options.dataDir,
1173
+ chain: "main",
1174
+ startHeight: 0,
1175
+ walletRootId: operation.state.walletRootId,
1176
+ });
1177
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1178
+ const walletName = operation.state.managedCoreWallet.walletName;
1179
+ const existingMutation = findPendingMutationByIntent(operation.state, intentFingerprintHex);
1180
+ if (existingMutation !== null) {
1181
+ const reconciled = await reconcilePendingMutation({
1182
+ state: operation.state,
1183
+ mutation: existingMutation,
1184
+ provider,
1185
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1186
+ nowUnixMs,
1187
+ paths,
1188
+ rpc,
1189
+ walletName,
1190
+ context: readContext,
1191
+ });
1192
+ if (reconciled.resolution === "confirmed" || reconciled.resolution === "live") {
1193
+ return {
1194
+ kind: "buy",
1195
+ domainName: normalizedDomainName,
1196
+ txid: reconciled.mutation.attemptedTxid ?? "unknown",
1197
+ status: reconciled.resolution,
1198
+ reusedExisting: true,
1199
+ listedPriceCogtoshi: operation.listingPriceCogtoshi,
1200
+ resolvedBuyer,
1201
+ resolvedSeller,
1202
+ };
1203
+ }
1204
+ if (reconciled.resolution === "repair-required") {
1205
+ throw new Error("wallet_buy_repair_required");
1206
+ }
1207
+ }
1208
+ await confirmBuy(options.prompter, normalizedDomainName, operation.buyerSelector, operation.sender, sellerScriptPubKeyHex, sellerAddress, operation.listingPriceCogtoshi, options.assumeYes);
1209
+ let nextState = upsertPendingMutation(operation.state, createDraftMutation({
1210
+ kind: "buy",
1211
+ domainName: normalizedDomainName,
1212
+ sender: operation.sender,
1213
+ priceCogtoshi: operation.listingPriceCogtoshi,
1214
+ intentFingerprintHex,
1215
+ nowUnixMs,
1216
+ existing: existingMutation,
1217
+ }));
1218
+ nextState = {
1219
+ ...nextState,
1220
+ stateRevision: nextState.stateRevision + 1,
1221
+ lastWrittenAtUnixMs: nowUnixMs,
1222
+ };
1223
+ await saveWalletStatePreservingUnlock({
1224
+ state: nextState,
1225
+ provider,
1226
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1227
+ nowUnixMs,
1228
+ paths,
1229
+ });
1230
+ const built = await buildTransaction({
1231
+ rpc,
1232
+ walletName,
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
+ }),
1242
+ });
1243
+ const currentSellerHex = Buffer.from(operation.chainDomain.ownerScriptPubKey).toString("hex");
1244
+ if (currentSellerHex !== sellerScriptPubKeyHex) {
1245
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
1246
+ throw new Error("wallet_buy_stale_listing_owner");
1247
+ }
1248
+ const broadcasting = updateMutationRecord(nextState.pendingMutations.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex), "broadcasting", nowUnixMs, {
1249
+ attemptedTxid: built.txid,
1250
+ attemptedWtxid: built.wtxid,
1251
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
1252
+ });
1253
+ nextState = {
1254
+ ...upsertPendingMutation(nextState, broadcasting),
1255
+ stateRevision: nextState.stateRevision + 1,
1256
+ lastWrittenAtUnixMs: nowUnixMs,
1257
+ };
1258
+ await saveWalletStatePreservingUnlock({
1259
+ state: nextState,
1260
+ provider,
1261
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1262
+ nowUnixMs,
1263
+ paths,
1264
+ });
1265
+ if (snapshot.tip?.height !== (await rpc.getBlockchainInfo()).blocks) {
1266
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
1267
+ throw new Error("wallet_buy_tip_mismatch");
1268
+ }
1269
+ try {
1270
+ await rpc.sendRawTransaction(built.rawHex);
1271
+ }
1272
+ catch (error) {
1273
+ if (!isAlreadyAcceptedError(error)) {
1274
+ if (isBroadcastUnknownError(error)) {
1275
+ const unknown = updateMutationRecord(broadcasting, "broadcast-unknown", nowUnixMs, {
1276
+ attemptedTxid: built.txid,
1277
+ attemptedWtxid: built.wtxid,
1278
+ temporaryBuilderLockedOutpoints: built.temporaryBuilderLockedOutpoints,
1279
+ });
1280
+ nextState = {
1281
+ ...upsertPendingMutation(nextState, unknown),
1282
+ stateRevision: nextState.stateRevision + 1,
1283
+ lastWrittenAtUnixMs: nowUnixMs,
1284
+ };
1285
+ await saveWalletStatePreservingUnlock({
1286
+ state: nextState,
1287
+ provider,
1288
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1289
+ nowUnixMs,
1290
+ paths,
1291
+ });
1292
+ throw new Error("wallet_buy_broadcast_unknown");
1293
+ }
1294
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
1295
+ const canceled = updateMutationRecord(broadcasting, "canceled", nowUnixMs, {
1296
+ attemptedTxid: built.txid,
1297
+ attemptedWtxid: built.wtxid,
1298
+ temporaryBuilderLockedOutpoints: [],
1299
+ });
1300
+ nextState = {
1301
+ ...upsertPendingMutation(nextState, canceled),
1302
+ stateRevision: nextState.stateRevision + 1,
1303
+ lastWrittenAtUnixMs: nowUnixMs,
1304
+ };
1305
+ await saveWalletStatePreservingUnlock({
1306
+ state: nextState,
1307
+ provider,
1308
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1309
+ nowUnixMs,
1310
+ paths,
1311
+ });
1312
+ throw error;
1313
+ }
1314
+ }
1315
+ await unlockTemporaryBuilderLocks(rpc, walletName, built.temporaryBuilderLockedOutpoints);
1316
+ const finalStatus = getBuyStatusAfterAcceptance({
1317
+ snapshot: readContext.snapshot,
1318
+ domainName: normalizedDomainName,
1319
+ buyerScriptPubKeyHex: operation.sender.scriptPubKeyHex,
1320
+ });
1321
+ const finalMutation = updateMutationRecord(broadcasting, finalStatus, nowUnixMs, {
1322
+ attemptedTxid: built.txid,
1323
+ attemptedWtxid: built.wtxid,
1324
+ temporaryBuilderLockedOutpoints: [],
1325
+ });
1326
+ nextState = reserveTransferredDomainRecord({
1327
+ state: upsertPendingMutation(nextState, finalMutation),
1328
+ domainName: normalizedDomainName,
1329
+ domainId: operation.chainDomain.domainId,
1330
+ currentOwnerScriptPubKeyHex: operation.sender.scriptPubKeyHex,
1331
+ currentOwnerLocalIndex: operation.sender.localIndex,
1332
+ nowUnixMs,
1333
+ });
1334
+ nextState = {
1335
+ ...nextState,
1336
+ stateRevision: nextState.stateRevision + 1,
1337
+ lastWrittenAtUnixMs: nowUnixMs,
1338
+ };
1339
+ await saveWalletStatePreservingUnlock({
1340
+ state: nextState,
1341
+ provider,
1342
+ unlockUntilUnixMs: operation.unlockUntilUnixMs,
1343
+ nowUnixMs,
1344
+ paths,
1345
+ });
1346
+ return {
1347
+ kind: "buy",
1348
+ domainName: normalizedDomainName,
1349
+ txid: built.txid,
1350
+ status: finalStatus,
1351
+ reusedExisting: false,
1352
+ listedPriceCogtoshi: operation.listingPriceCogtoshi,
1353
+ resolvedBuyer,
1354
+ resolvedSeller,
1355
+ };
1356
+ }
1357
+ finally {
1358
+ await readContext.close();
1359
+ await miningPreemption.release();
1360
+ }
1361
+ }
1362
+ finally {
1363
+ await controlLock.release();
1364
+ }
1365
+ }