@arkade-os/sdk 0.4.27 → 0.4.29

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 (532) hide show
  1. package/README.md +45 -116
  2. package/dist/adapters/asyncStorage.cjs +48 -0
  3. package/dist/adapters/asyncStorage.cjs.map +1 -0
  4. package/dist/adapters/asyncStorage.d.cts +16 -0
  5. package/dist/{types/storage → adapters}/asyncStorage.d.ts +5 -2
  6. package/dist/adapters/asyncStorage.js +46 -0
  7. package/dist/adapters/asyncStorage.js.map +1 -0
  8. package/dist/adapters/expo.cjs +19 -0
  9. package/dist/adapters/expo.cjs.map +1 -0
  10. package/dist/adapters/expo.d.cts +48 -0
  11. package/dist/adapters/expo.d.ts +48 -0
  12. package/dist/adapters/expo.js +6 -0
  13. package/dist/adapters/expo.js.map +1 -0
  14. package/dist/adapters/fileSystem.cjs +116 -0
  15. package/dist/adapters/fileSystem.cjs.map +1 -0
  16. package/dist/adapters/fileSystem.d.cts +17 -0
  17. package/dist/{types/storage → adapters}/fileSystem.d.ts +5 -2
  18. package/dist/adapters/fileSystem.js +93 -0
  19. package/dist/adapters/fileSystem.js.map +1 -0
  20. package/dist/adapters/indexedDB.cjs +103 -0
  21. package/dist/adapters/indexedDB.cjs.map +1 -0
  22. package/dist/adapters/indexedDB.d.cts +18 -0
  23. package/dist/{types/storage → adapters}/indexedDB.d.ts +5 -2
  24. package/dist/adapters/indexedDB.js +101 -0
  25. package/dist/adapters/indexedDB.js.map +1 -0
  26. package/dist/adapters/localStorage.cjs +50 -0
  27. package/dist/adapters/localStorage.cjs.map +1 -0
  28. package/dist/{types/storage/inMemory.d.ts → adapters/localStorage.d.cts} +6 -3
  29. package/dist/{types/storage → adapters}/localStorage.d.ts +5 -2
  30. package/dist/adapters/localStorage.js +48 -0
  31. package/dist/adapters/localStorage.js.map +1 -0
  32. package/dist/ark-ChhTwpLf.d.cts +3892 -0
  33. package/dist/ark-ChhTwpLf.d.ts +3892 -0
  34. package/dist/asyncStorageTaskQueue-DW1-BpI7.d.cts +49 -0
  35. package/dist/{types/worker/expo/asyncStorageTaskQueue.d.ts → asyncStorageTaskQueue-DZ0nUuEJ.d.ts} +6 -3
  36. package/dist/chunk-5BLDMQED.cjs +18 -0
  37. package/dist/chunk-5BLDMQED.cjs.map +1 -0
  38. package/dist/chunk-6FLL2Q36.cjs +2701 -0
  39. package/dist/chunk-6FLL2Q36.cjs.map +1 -0
  40. package/dist/chunk-6NWNOLL3.js +2671 -0
  41. package/dist/chunk-6NWNOLL3.js.map +1 -0
  42. package/dist/chunk-ABWRLTX5.js +210 -0
  43. package/dist/chunk-ABWRLTX5.js.map +1 -0
  44. package/dist/chunk-BVP2U66Q.js +13943 -0
  45. package/dist/chunk-BVP2U66Q.js.map +1 -0
  46. package/dist/chunk-GDCTOSMV.cjs +14058 -0
  47. package/dist/chunk-GDCTOSMV.cjs.map +1 -0
  48. package/dist/chunk-GIGILVVP.cjs +213 -0
  49. package/dist/chunk-GIGILVVP.cjs.map +1 -0
  50. package/dist/chunk-IEO3XDKI.cjs +838 -0
  51. package/dist/chunk-IEO3XDKI.cjs.map +1 -0
  52. package/dist/chunk-NSBPE2FW.js +15 -0
  53. package/dist/chunk-NSBPE2FW.js.map +1 -0
  54. package/dist/chunk-PJUFOJ2L.cjs +100 -0
  55. package/dist/chunk-PJUFOJ2L.cjs.map +1 -0
  56. package/dist/chunk-TH6T23XG.js +95 -0
  57. package/dist/chunk-TH6T23XG.js.map +1 -0
  58. package/dist/chunk-TU3LVAPX.js +769 -0
  59. package/dist/chunk-TU3LVAPX.js.map +1 -0
  60. package/dist/chunk-WMIPYZSB.cjs +803 -0
  61. package/dist/chunk-WMIPYZSB.cjs.map +1 -0
  62. package/dist/chunk-YA4G7RFB.js +829 -0
  63. package/dist/chunk-YA4G7RFB.js.map +1 -0
  64. package/dist/contracts/handlers/index.cjs +26 -0
  65. package/dist/contracts/handlers/index.cjs.map +1 -0
  66. package/dist/contracts/handlers/index.d.cts +7 -0
  67. package/dist/contracts/handlers/index.d.ts +7 -0
  68. package/dist/contracts/handlers/index.js +5 -0
  69. package/dist/contracts/handlers/index.js.map +1 -0
  70. package/dist/delegate-4JBUkUhR.d.cts +84 -0
  71. package/dist/delegate-DN7RELL1.d.ts +84 -0
  72. package/dist/{types/storage/index.d.ts → index-C0IanN1m.d.cts} +3 -1
  73. package/dist/index-C0IanN1m.d.ts +11 -0
  74. package/dist/index-Cn82bBUu.d.ts +199 -0
  75. package/dist/index-DfT5xzgY.d.cts +199 -0
  76. package/dist/index.cjs +504 -0
  77. package/dist/index.cjs.map +1 -0
  78. package/dist/index.d.cts +3469 -0
  79. package/dist/index.d.ts +3469 -0
  80. package/dist/index.js +7 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/repositories/realm/index.cjs +513 -0
  83. package/dist/repositories/realm/index.cjs.map +1 -0
  84. package/dist/repositories/realm/index.d.cts +217 -0
  85. package/dist/{types/repositories/realm/schemas.d.ts → repositories/realm/index.d.ts} +80 -112
  86. package/dist/repositories/realm/index.js +507 -0
  87. package/dist/repositories/realm/index.js.map +1 -0
  88. package/dist/repositories/sqlite/index.cjs +588 -0
  89. package/dist/repositories/sqlite/index.cjs.map +1 -0
  90. package/dist/repositories/sqlite/index.d.cts +118 -0
  91. package/dist/{types/repositories/sqlite/walletRepository.d.ts → repositories/sqlite/index.d.ts} +58 -5
  92. package/dist/repositories/sqlite/index.js +585 -0
  93. package/dist/repositories/sqlite/index.js.map +1 -0
  94. package/dist/taskRunner-B-aPfHhK.d.cts +114 -0
  95. package/dist/taskRunner-B-vG08pX.d.ts +114 -0
  96. package/dist/wallet/expo/background.cjs +93 -0
  97. package/dist/wallet/expo/background.cjs.map +1 -0
  98. package/dist/wallet/expo/background.d.cts +84 -0
  99. package/dist/wallet/expo/background.d.ts +84 -0
  100. package/dist/wallet/expo/background.js +68 -0
  101. package/dist/wallet/expo/background.js.map +1 -0
  102. package/dist/wallet/expo/index.cjs +175 -0
  103. package/dist/wallet/expo/index.cjs.map +1 -0
  104. package/dist/wallet/expo/index.d.cts +124 -0
  105. package/dist/wallet/expo/index.d.ts +124 -0
  106. package/dist/wallet/expo/index.js +173 -0
  107. package/dist/wallet/expo/index.js.map +1 -0
  108. package/dist/wallet-CCtqT2Wb.d.ts +778 -0
  109. package/dist/wallet-DjgFb_4T.d.cts +778 -0
  110. package/dist/worker/expo/index.cjs +140 -0
  111. package/dist/worker/expo/index.cjs.map +1 -0
  112. package/dist/worker/expo/index.d.cts +29 -0
  113. package/dist/worker/expo/index.d.ts +29 -0
  114. package/dist/worker/expo/index.js +121 -0
  115. package/dist/worker/expo/index.js.map +1 -0
  116. package/package.json +110 -76
  117. package/dist/cjs/adapters/asyncStorage.js +0 -5
  118. package/dist/cjs/adapters/expo.js +0 -8
  119. package/dist/cjs/adapters/fileSystem.js +0 -5
  120. package/dist/cjs/adapters/indexedDB.js +0 -5
  121. package/dist/cjs/adapters/localStorage.js +0 -5
  122. package/dist/cjs/arkfee/celenv.js +0 -43
  123. package/dist/cjs/arkfee/estimator.js +0 -143
  124. package/dist/cjs/arkfee/index.js +0 -5
  125. package/dist/cjs/arkfee/types.js +0 -26
  126. package/dist/cjs/arknote/index.js +0 -128
  127. package/dist/cjs/bip322/index.js +0 -270
  128. package/dist/cjs/contracts/arkcontract.js +0 -147
  129. package/dist/cjs/contracts/contractManager.js +0 -649
  130. package/dist/cjs/contracts/contractWatcher.js +0 -598
  131. package/dist/cjs/contracts/handlers/default.js +0 -93
  132. package/dist/cjs/contracts/handlers/delegate.js +0 -90
  133. package/dist/cjs/contracts/handlers/helpers.js +0 -115
  134. package/dist/cjs/contracts/handlers/index.js +0 -19
  135. package/dist/cjs/contracts/handlers/registry.js +0 -89
  136. package/dist/cjs/contracts/handlers/vhtlc.js +0 -194
  137. package/dist/cjs/contracts/index.js +0 -41
  138. package/dist/cjs/contracts/types.js +0 -2
  139. package/dist/cjs/contracts/vtxoOwnership.js +0 -78
  140. package/dist/cjs/extension/asset/assetGroup.js +0 -228
  141. package/dist/cjs/extension/asset/assetId.js +0 -152
  142. package/dist/cjs/extension/asset/assetInput.js +0 -222
  143. package/dist/cjs/extension/asset/assetOutput.js +0 -174
  144. package/dist/cjs/extension/asset/assetRef.js +0 -148
  145. package/dist/cjs/extension/asset/index.js +0 -23
  146. package/dist/cjs/extension/asset/metadata.js +0 -187
  147. package/dist/cjs/extension/asset/packet.js +0 -114
  148. package/dist/cjs/extension/asset/types.js +0 -22
  149. package/dist/cjs/extension/asset/utils.js +0 -105
  150. package/dist/cjs/extension/index.js +0 -254
  151. package/dist/cjs/extension/packet.js +0 -20
  152. package/dist/cjs/forfeit.js +0 -45
  153. package/dist/cjs/identity/descriptor.js +0 -169
  154. package/dist/cjs/identity/descriptorProvider.js +0 -2
  155. package/dist/cjs/identity/hdCapableIdentity.js +0 -20
  156. package/dist/cjs/identity/index.js +0 -40
  157. package/dist/cjs/identity/seedIdentity.js +0 -477
  158. package/dist/cjs/identity/serialize.js +0 -171
  159. package/dist/cjs/identity/singleKey.js +0 -126
  160. package/dist/cjs/identity/staticDescriptorProvider.js +0 -65
  161. package/dist/cjs/index.js +0 -202
  162. package/dist/cjs/intent/index.js +0 -259
  163. package/dist/cjs/musig2/index.js +0 -11
  164. package/dist/cjs/musig2/keys.js +0 -57
  165. package/dist/cjs/musig2/nonces.js +0 -48
  166. package/dist/cjs/musig2/sign.js +0 -102
  167. package/dist/cjs/networks.js +0 -26
  168. package/dist/cjs/package.json +0 -3
  169. package/dist/cjs/providers/ark.js +0 -577
  170. package/dist/cjs/providers/delegator.js +0 -85
  171. package/dist/cjs/providers/electrum.js +0 -869
  172. package/dist/cjs/providers/errors.js +0 -59
  173. package/dist/cjs/providers/expoArk.js +0 -82
  174. package/dist/cjs/providers/expoIndexer.js +0 -111
  175. package/dist/cjs/providers/expoUtils.js +0 -124
  176. package/dist/cjs/providers/indexer.js +0 -630
  177. package/dist/cjs/providers/onchain.js +0 -262
  178. package/dist/cjs/providers/utils.js +0 -121
  179. package/dist/cjs/repositories/contractRepository.js +0 -2
  180. package/dist/cjs/repositories/inMemory/contractRepository.js +0 -55
  181. package/dist/cjs/repositories/inMemory/walletRepository.js +0 -115
  182. package/dist/cjs/repositories/index.js +0 -34
  183. package/dist/cjs/repositories/indexedDB/contractRepository.js +0 -187
  184. package/dist/cjs/repositories/indexedDB/db.js +0 -19
  185. package/dist/cjs/repositories/indexedDB/manager.js +0 -100
  186. package/dist/cjs/repositories/indexedDB/schema.js +0 -204
  187. package/dist/cjs/repositories/indexedDB/walletRepository.js +0 -474
  188. package/dist/cjs/repositories/indexedDB/websqlAdapter.js +0 -144
  189. package/dist/cjs/repositories/migrations/contractRepositoryImpl.js +0 -127
  190. package/dist/cjs/repositories/migrations/fromStorageAdapter.js +0 -66
  191. package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +0 -184
  192. package/dist/cjs/repositories/realm/contractRepository.js +0 -116
  193. package/dist/cjs/repositories/realm/index.js +0 -11
  194. package/dist/cjs/repositories/realm/schemas.js +0 -157
  195. package/dist/cjs/repositories/realm/types.js +0 -7
  196. package/dist/cjs/repositories/realm/walletRepository.js +0 -305
  197. package/dist/cjs/repositories/scriptFromAddress.js +0 -16
  198. package/dist/cjs/repositories/serialization.js +0 -82
  199. package/dist/cjs/repositories/sqlite/contractRepository.js +0 -135
  200. package/dist/cjs/repositories/sqlite/index.js +0 -7
  201. package/dist/cjs/repositories/sqlite/types.js +0 -2
  202. package/dist/cjs/repositories/sqlite/walletRepository.js +0 -441
  203. package/dist/cjs/repositories/walletRepository.js +0 -2
  204. package/dist/cjs/script/address.js +0 -108
  205. package/dist/cjs/script/base.js +0 -185
  206. package/dist/cjs/script/default.js +0 -57
  207. package/dist/cjs/script/delegate.js +0 -53
  208. package/dist/cjs/script/tapscript.js +0 -619
  209. package/dist/cjs/script/vhtlc.js +0 -170
  210. package/dist/cjs/storage/asyncStorage.js +0 -50
  211. package/dist/cjs/storage/fileSystem.js +0 -141
  212. package/dist/cjs/storage/inMemory.js +0 -24
  213. package/dist/cjs/storage/index.js +0 -2
  214. package/dist/cjs/storage/indexedDB.js +0 -101
  215. package/dist/cjs/storage/localStorage.js +0 -51
  216. package/dist/cjs/tree/signingSession.js +0 -229
  217. package/dist/cjs/tree/txTree.js +0 -192
  218. package/dist/cjs/tree/validation.js +0 -107
  219. package/dist/cjs/utils/anchor.js +0 -35
  220. package/dist/cjs/utils/arkTransaction.js +0 -271
  221. package/dist/cjs/utils/bip21.js +0 -127
  222. package/dist/cjs/utils/syncCursors.js +0 -128
  223. package/dist/cjs/utils/timelock.js +0 -59
  224. package/dist/cjs/utils/transaction.js +0 -28
  225. package/dist/cjs/utils/transactionHistory.js +0 -183
  226. package/dist/cjs/utils/txSizeEstimator.js +0 -132
  227. package/dist/cjs/utils/unknownFields.js +0 -174
  228. package/dist/cjs/wallet/asset-manager.js +0 -330
  229. package/dist/cjs/wallet/asset.js +0 -119
  230. package/dist/cjs/wallet/batch.js +0 -183
  231. package/dist/cjs/wallet/delegator.js +0 -308
  232. package/dist/cjs/wallet/expo/background.js +0 -116
  233. package/dist/cjs/wallet/expo/index.js +0 -9
  234. package/dist/cjs/wallet/expo/wallet.js +0 -230
  235. package/dist/cjs/wallet/hdDescriptorProvider.js +0 -188
  236. package/dist/cjs/wallet/index.js +0 -82
  237. package/dist/cjs/wallet/inputSignerRouter.js +0 -98
  238. package/dist/cjs/wallet/onchain.js +0 -290
  239. package/dist/cjs/wallet/ramps.js +0 -216
  240. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +0 -953
  241. package/dist/cjs/wallet/serviceWorker/wallet.js +0 -1174
  242. package/dist/cjs/wallet/signingErrors.js +0 -32
  243. package/dist/cjs/wallet/unroll.js +0 -293
  244. package/dist/cjs/wallet/utils.js +0 -111
  245. package/dist/cjs/wallet/validation.js +0 -154
  246. package/dist/cjs/wallet/vtxo-manager.js +0 -1142
  247. package/dist/cjs/wallet/wallet.js +0 -2195
  248. package/dist/cjs/wallet/walletReceiveRotator.js +0 -547
  249. package/dist/cjs/worker/browser/service-worker-manager.js +0 -183
  250. package/dist/cjs/worker/browser/utils.js +0 -67
  251. package/dist/cjs/worker/errors.js +0 -16
  252. package/dist/cjs/worker/expo/asyncStorageTaskQueue.js +0 -78
  253. package/dist/cjs/worker/expo/index.js +0 -13
  254. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +0 -62
  255. package/dist/cjs/worker/expo/processors/index.js +0 -6
  256. package/dist/cjs/worker/expo/taskQueue.js +0 -41
  257. package/dist/cjs/worker/expo/taskRunner.js +0 -73
  258. package/dist/cjs/worker/messageBus.js +0 -474
  259. package/dist/esm/adapters/asyncStorage.js +0 -1
  260. package/dist/esm/adapters/expo.js +0 -3
  261. package/dist/esm/adapters/fileSystem.js +0 -1
  262. package/dist/esm/adapters/indexedDB.js +0 -1
  263. package/dist/esm/adapters/localStorage.js +0 -1
  264. package/dist/esm/arkfee/celenv.js +0 -40
  265. package/dist/esm/arkfee/estimator.js +0 -139
  266. package/dist/esm/arkfee/index.js +0 -1
  267. package/dist/esm/arkfee/types.js +0 -22
  268. package/dist/esm/arknote/index.js +0 -124
  269. package/dist/esm/bip322/index.js +0 -267
  270. package/dist/esm/contracts/arkcontract.js +0 -140
  271. package/dist/esm/contracts/contractManager.js +0 -645
  272. package/dist/esm/contracts/contractWatcher.js +0 -594
  273. package/dist/esm/contracts/handlers/default.js +0 -90
  274. package/dist/esm/contracts/handlers/delegate.js +0 -87
  275. package/dist/esm/contracts/handlers/helpers.js +0 -110
  276. package/dist/esm/contracts/handlers/index.js +0 -12
  277. package/dist/esm/contracts/handlers/registry.js +0 -86
  278. package/dist/esm/contracts/handlers/vhtlc.js +0 -191
  279. package/dist/esm/contracts/index.js +0 -13
  280. package/dist/esm/contracts/types.js +0 -1
  281. package/dist/esm/contracts/vtxoOwnership.js +0 -69
  282. package/dist/esm/extension/asset/assetGroup.js +0 -224
  283. package/dist/esm/extension/asset/assetId.js +0 -148
  284. package/dist/esm/extension/asset/assetInput.js +0 -217
  285. package/dist/esm/extension/asset/assetOutput.js +0 -169
  286. package/dist/esm/extension/asset/assetRef.js +0 -144
  287. package/dist/esm/extension/asset/index.js +0 -8
  288. package/dist/esm/extension/asset/metadata.js +0 -182
  289. package/dist/esm/extension/asset/packet.js +0 -110
  290. package/dist/esm/extension/asset/types.js +0 -19
  291. package/dist/esm/extension/asset/utils.js +0 -99
  292. package/dist/esm/extension/index.js +0 -248
  293. package/dist/esm/extension/packet.js +0 -16
  294. package/dist/esm/forfeit.js +0 -41
  295. package/dist/esm/identity/descriptor.js +0 -161
  296. package/dist/esm/identity/descriptorProvider.js +0 -1
  297. package/dist/esm/identity/hdCapableIdentity.js +0 -17
  298. package/dist/esm/identity/index.js +0 -13
  299. package/dist/esm/identity/seedIdentity.js +0 -469
  300. package/dist/esm/identity/serialize.js +0 -164
  301. package/dist/esm/identity/singleKey.js +0 -121
  302. package/dist/esm/identity/staticDescriptorProvider.js +0 -61
  303. package/dist/esm/index.js +0 -87
  304. package/dist/esm/intent/index.js +0 -255
  305. package/dist/esm/musig2/index.js +0 -3
  306. package/dist/esm/musig2/keys.js +0 -21
  307. package/dist/esm/musig2/nonces.js +0 -11
  308. package/dist/esm/musig2/sign.js +0 -63
  309. package/dist/esm/networks.js +0 -22
  310. package/dist/esm/package.json +0 -3
  311. package/dist/esm/providers/ark.js +0 -572
  312. package/dist/esm/providers/delegator.js +0 -81
  313. package/dist/esm/providers/electrum.js +0 -864
  314. package/dist/esm/providers/errors.js +0 -54
  315. package/dist/esm/providers/expoArk.js +0 -78
  316. package/dist/esm/providers/expoIndexer.js +0 -107
  317. package/dist/esm/providers/expoUtils.js +0 -87
  318. package/dist/esm/providers/indexer.js +0 -626
  319. package/dist/esm/providers/onchain.js +0 -258
  320. package/dist/esm/providers/utils.js +0 -117
  321. package/dist/esm/repositories/contractRepository.js +0 -1
  322. package/dist/esm/repositories/inMemory/contractRepository.js +0 -51
  323. package/dist/esm/repositories/inMemory/walletRepository.js +0 -111
  324. package/dist/esm/repositories/index.js +0 -10
  325. package/dist/esm/repositories/indexedDB/contractRepository.js +0 -183
  326. package/dist/esm/repositories/indexedDB/db.js +0 -4
  327. package/dist/esm/repositories/indexedDB/manager.js +0 -95
  328. package/dist/esm/repositories/indexedDB/schema.js +0 -199
  329. package/dist/esm/repositories/indexedDB/walletRepository.js +0 -470
  330. package/dist/esm/repositories/indexedDB/websqlAdapter.js +0 -138
  331. package/dist/esm/repositories/migrations/contractRepositoryImpl.js +0 -121
  332. package/dist/esm/repositories/migrations/fromStorageAdapter.js +0 -58
  333. package/dist/esm/repositories/migrations/walletRepositoryImpl.js +0 -180
  334. package/dist/esm/repositories/realm/contractRepository.js +0 -112
  335. package/dist/esm/repositories/realm/index.js +0 -3
  336. package/dist/esm/repositories/realm/schemas.js +0 -153
  337. package/dist/esm/repositories/realm/types.js +0 -6
  338. package/dist/esm/repositories/realm/walletRepository.js +0 -301
  339. package/dist/esm/repositories/scriptFromAddress.js +0 -13
  340. package/dist/esm/repositories/serialization.js +0 -67
  341. package/dist/esm/repositories/sqlite/contractRepository.js +0 -131
  342. package/dist/esm/repositories/sqlite/index.js +0 -2
  343. package/dist/esm/repositories/sqlite/types.js +0 -1
  344. package/dist/esm/repositories/sqlite/walletRepository.js +0 -437
  345. package/dist/esm/repositories/walletRepository.js +0 -1
  346. package/dist/esm/script/address.js +0 -104
  347. package/dist/esm/script/base.js +0 -179
  348. package/dist/esm/script/default.js +0 -54
  349. package/dist/esm/script/delegate.js +0 -50
  350. package/dist/esm/script/tapscript.js +0 -615
  351. package/dist/esm/script/vhtlc.js +0 -167
  352. package/dist/esm/storage/asyncStorage.js +0 -46
  353. package/dist/esm/storage/fileSystem.js +0 -104
  354. package/dist/esm/storage/inMemory.js +0 -20
  355. package/dist/esm/storage/index.js +0 -1
  356. package/dist/esm/storage/indexedDB.js +0 -97
  357. package/dist/esm/storage/localStorage.js +0 -47
  358. package/dist/esm/tree/signingSession.js +0 -191
  359. package/dist/esm/tree/txTree.js +0 -188
  360. package/dist/esm/tree/validation.js +0 -101
  361. package/dist/esm/utils/anchor.js +0 -31
  362. package/dist/esm/utils/arkTransaction.js +0 -264
  363. package/dist/esm/utils/bip21.js +0 -123
  364. package/dist/esm/utils/syncCursors.js +0 -119
  365. package/dist/esm/utils/timelock.js +0 -22
  366. package/dist/esm/utils/transaction.js +0 -24
  367. package/dist/esm/utils/transactionHistory.js +0 -180
  368. package/dist/esm/utils/txSizeEstimator.js +0 -128
  369. package/dist/esm/utils/unknownFields.js +0 -169
  370. package/dist/esm/wallet/asset-manager.js +0 -325
  371. package/dist/esm/wallet/asset.js +0 -113
  372. package/dist/esm/wallet/batch.js +0 -180
  373. package/dist/esm/wallet/delegator.js +0 -303
  374. package/dist/esm/wallet/expo/background.js +0 -111
  375. package/dist/esm/wallet/expo/index.js +0 -2
  376. package/dist/esm/wallet/expo/wallet.js +0 -193
  377. package/dist/esm/wallet/hdDescriptorProvider.js +0 -184
  378. package/dist/esm/wallet/index.js +0 -75
  379. package/dist/esm/wallet/inputSignerRouter.js +0 -94
  380. package/dist/esm/wallet/onchain.js +0 -285
  381. package/dist/esm/wallet/ramps.js +0 -212
  382. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +0 -946
  383. package/dist/esm/wallet/serviceWorker/wallet.js +0 -1169
  384. package/dist/esm/wallet/signingErrors.js +0 -27
  385. package/dist/esm/wallet/unroll.js +0 -289
  386. package/dist/esm/wallet/utils.js +0 -103
  387. package/dist/esm/wallet/validation.js +0 -142
  388. package/dist/esm/wallet/vtxo-manager.js +0 -1136
  389. package/dist/esm/wallet/wallet.js +0 -2186
  390. package/dist/esm/wallet/walletReceiveRotator.js +0 -540
  391. package/dist/esm/worker/browser/service-worker-manager.js +0 -177
  392. package/dist/esm/worker/browser/utils.js +0 -63
  393. package/dist/esm/worker/errors.js +0 -11
  394. package/dist/esm/worker/expo/asyncStorageTaskQueue.js +0 -74
  395. package/dist/esm/worker/expo/index.js +0 -4
  396. package/dist/esm/worker/expo/processors/contractPollProcessor.js +0 -59
  397. package/dist/esm/worker/expo/processors/index.js +0 -1
  398. package/dist/esm/worker/expo/taskQueue.js +0 -37
  399. package/dist/esm/worker/expo/taskRunner.js +0 -69
  400. package/dist/esm/worker/messageBus.js +0 -470
  401. package/dist/types/adapters/asyncStorage.d.ts +0 -2
  402. package/dist/types/adapters/expo.d.ts +0 -4
  403. package/dist/types/adapters/fileSystem.d.ts +0 -2
  404. package/dist/types/adapters/indexedDB.d.ts +0 -2
  405. package/dist/types/adapters/localStorage.d.ts +0 -2
  406. package/dist/types/arkfee/celenv.d.ts +0 -25
  407. package/dist/types/arkfee/estimator.d.ts +0 -49
  408. package/dist/types/arkfee/index.d.ts +0 -2
  409. package/dist/types/arkfee/types.d.ts +0 -38
  410. package/dist/types/arknote/index.d.ts +0 -84
  411. package/dist/types/bip322/index.d.ts +0 -55
  412. package/dist/types/contracts/arkcontract.d.ts +0 -99
  413. package/dist/types/contracts/contractManager.d.ts +0 -411
  414. package/dist/types/contracts/contractWatcher.d.ts +0 -217
  415. package/dist/types/contracts/handlers/default.d.ts +0 -19
  416. package/dist/types/contracts/handlers/delegate.d.ts +0 -21
  417. package/dist/types/contracts/handlers/helpers.d.ts +0 -19
  418. package/dist/types/contracts/handlers/index.d.ts +0 -7
  419. package/dist/types/contracts/handlers/registry.d.ts +0 -65
  420. package/dist/types/contracts/handlers/vhtlc.d.ts +0 -32
  421. package/dist/types/contracts/index.d.ts +0 -14
  422. package/dist/types/contracts/types.d.ts +0 -250
  423. package/dist/types/contracts/vtxoOwnership.d.ts +0 -33
  424. package/dist/types/extension/asset/assetGroup.d.ts +0 -119
  425. package/dist/types/extension/asset/assetId.d.ts +0 -83
  426. package/dist/types/extension/asset/assetInput.d.ts +0 -64
  427. package/dist/types/extension/asset/assetOutput.d.ts +0 -54
  428. package/dist/types/extension/asset/assetRef.d.ts +0 -91
  429. package/dist/types/extension/asset/index.d.ts +0 -8
  430. package/dist/types/extension/asset/metadata.d.ts +0 -52
  431. package/dist/types/extension/asset/packet.d.ts +0 -41
  432. package/dist/types/extension/asset/types.d.ts +0 -16
  433. package/dist/types/extension/asset/utils.d.ts +0 -21
  434. package/dist/types/extension/index.d.ts +0 -56
  435. package/dist/types/extension/packet.d.ts +0 -21
  436. package/dist/types/forfeit.d.ts +0 -18
  437. package/dist/types/identity/descriptor.d.ts +0 -61
  438. package/dist/types/identity/descriptorProvider.d.ts +0 -42
  439. package/dist/types/identity/hdCapableIdentity.d.ts +0 -71
  440. package/dist/types/identity/index.d.ts +0 -57
  441. package/dist/types/identity/seedIdentity.d.ts +0 -270
  442. package/dist/types/identity/serialize.d.ts +0 -96
  443. package/dist/types/identity/singleKey.d.ts +0 -62
  444. package/dist/types/identity/staticDescriptorProvider.d.ts +0 -18
  445. package/dist/types/index.d.ts +0 -59
  446. package/dist/types/intent/index.d.ts +0 -86
  447. package/dist/types/musig2/index.d.ts +0 -4
  448. package/dist/types/musig2/keys.d.ts +0 -9
  449. package/dist/types/musig2/nonces.d.ts +0 -14
  450. package/dist/types/musig2/sign.d.ts +0 -27
  451. package/dist/types/networks.d.ts +0 -16
  452. package/dist/types/providers/ark.d.ts +0 -369
  453. package/dist/types/providers/delegator.d.ts +0 -82
  454. package/dist/types/providers/electrum.d.ts +0 -312
  455. package/dist/types/providers/errors.d.ts +0 -13
  456. package/dist/types/providers/expoArk.d.ts +0 -22
  457. package/dist/types/providers/expoIndexer.d.ts +0 -18
  458. package/dist/types/providers/expoUtils.d.ts +0 -18
  459. package/dist/types/providers/indexer.d.ts +0 -301
  460. package/dist/types/providers/onchain.d.ts +0 -148
  461. package/dist/types/providers/utils.d.ts +0 -12
  462. package/dist/types/repositories/contractRepository.d.ts +0 -32
  463. package/dist/types/repositories/inMemory/contractRepository.d.ts +0 -17
  464. package/dist/types/repositories/inMemory/walletRepository.d.ts +0 -29
  465. package/dist/types/repositories/index.d.ts +0 -9
  466. package/dist/types/repositories/indexedDB/contractRepository.d.ts +0 -21
  467. package/dist/types/repositories/indexedDB/db.d.ts +0 -4
  468. package/dist/types/repositories/indexedDB/manager.d.ts +0 -25
  469. package/dist/types/repositories/indexedDB/schema.d.ts +0 -9
  470. package/dist/types/repositories/indexedDB/walletRepository.d.ts +0 -28
  471. package/dist/types/repositories/indexedDB/websqlAdapter.d.ts +0 -49
  472. package/dist/types/repositories/migrations/contractRepositoryImpl.d.ts +0 -24
  473. package/dist/types/repositories/migrations/fromStorageAdapter.d.ts +0 -19
  474. package/dist/types/repositories/migrations/walletRepositoryImpl.d.ts +0 -27
  475. package/dist/types/repositories/realm/contractRepository.d.ts +0 -24
  476. package/dist/types/repositories/realm/index.d.ts +0 -4
  477. package/dist/types/repositories/realm/types.d.ts +0 -16
  478. package/dist/types/repositories/realm/walletRepository.d.ts +0 -34
  479. package/dist/types/repositories/scriptFromAddress.d.ts +0 -9
  480. package/dist/types/repositories/serialization.d.ts +0 -65
  481. package/dist/types/repositories/sqlite/contractRepository.d.ts +0 -33
  482. package/dist/types/repositories/sqlite/index.d.ts +0 -3
  483. package/dist/types/repositories/sqlite/types.d.ts +0 -18
  484. package/dist/types/repositories/walletRepository.d.ts +0 -72
  485. package/dist/types/script/address.d.ts +0 -67
  486. package/dist/types/script/base.d.ts +0 -105
  487. package/dist/types/script/default.d.ts +0 -44
  488. package/dist/types/script/delegate.d.ts +0 -40
  489. package/dist/types/script/tapscript.d.ts +0 -169
  490. package/dist/types/script/vhtlc.d.ts +0 -66
  491. package/dist/types/tree/signingSession.d.ts +0 -37
  492. package/dist/types/tree/txTree.d.ts +0 -28
  493. package/dist/types/tree/validation.d.ts +0 -15
  494. package/dist/types/utils/anchor.d.ts +0 -19
  495. package/dist/types/utils/arkTransaction.d.ts +0 -49
  496. package/dist/types/utils/bip21.d.ts +0 -38
  497. package/dist/types/utils/syncCursors.d.ts +0 -60
  498. package/dist/types/utils/timelock.d.ts +0 -9
  499. package/dist/types/utils/transaction.d.ts +0 -13
  500. package/dist/types/utils/transactionHistory.d.ts +0 -15
  501. package/dist/types/utils/txSizeEstimator.d.ts +0 -40
  502. package/dist/types/utils/unknownFields.d.ts +0 -83
  503. package/dist/types/wallet/asset-manager.d.ts +0 -69
  504. package/dist/types/wallet/asset.d.ts +0 -21
  505. package/dist/types/wallet/batch.d.ts +0 -107
  506. package/dist/types/wallet/delegator.d.ts +0 -48
  507. package/dist/types/wallet/expo/background.d.ts +0 -66
  508. package/dist/types/wallet/expo/index.d.ts +0 -4
  509. package/dist/types/wallet/expo/wallet.d.ts +0 -99
  510. package/dist/types/wallet/hdDescriptorProvider.d.ts +0 -114
  511. package/dist/types/wallet/index.d.ts +0 -789
  512. package/dist/types/wallet/inputSignerRouter.d.ts +0 -35
  513. package/dist/types/wallet/onchain.d.ts +0 -109
  514. package/dist/types/wallet/ramps.d.ts +0 -64
  515. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +0 -543
  516. package/dist/types/wallet/serviceWorker/wallet.d.ts +0 -248
  517. package/dist/types/wallet/signingErrors.d.ts +0 -19
  518. package/dist/types/wallet/unroll.d.ts +0 -114
  519. package/dist/types/wallet/utils.d.ts +0 -36
  520. package/dist/types/wallet/validation.d.ts +0 -24
  521. package/dist/types/wallet/vtxo-manager.d.ts +0 -476
  522. package/dist/types/wallet/wallet.d.ts +0 -409
  523. package/dist/types/wallet/walletReceiveRotator.d.ts +0 -306
  524. package/dist/types/worker/browser/service-worker-manager.d.ts +0 -32
  525. package/dist/types/worker/browser/utils.d.ts +0 -17
  526. package/dist/types/worker/errors.d.ts +0 -7
  527. package/dist/types/worker/expo/index.d.ts +0 -7
  528. package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +0 -19
  529. package/dist/types/worker/expo/processors/index.d.ts +0 -1
  530. package/dist/types/worker/expo/taskQueue.d.ts +0 -50
  531. package/dist/types/worker/expo/taskRunner.d.ts +0 -66
  532. package/dist/types/worker/messageBus.d.ts +0 -189
@@ -1,2186 +0,0 @@
1
- import { base64, hex } from "@scure/base";
2
- import { tapLeafHash } from "@scure/btc-signer/payment.js";
3
- import { Address, OutScript, SigHash, Transaction } from "@scure/btc-signer";
4
- import { sha256 } from "@scure/btc-signer/utils.js";
5
- import { ArkAddress } from "../script/address.js";
6
- import { DefaultVtxo } from "../script/default.js";
7
- import { getNetwork } from "../networks.js";
8
- import { ESPLORA_URL, EsploraProvider, } from "../providers/onchain.js";
9
- import { RestArkProvider, } from "../providers/ark.js";
10
- import { buildForfeitTx } from "../forfeit.js";
11
- import { validateConnectorsTxGraph, validateVtxoTxGraph, } from "../tree/validation.js";
12
- import { validateBatchRecipients } from "./validation.js";
13
- import { DEFAULT_ARKADE_SERVER_URL, isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from "./index.js";
14
- import { createAssetPacket, selectCoinsWithAsset, selectedCoinsToAssetInputs, } from "./asset.js";
15
- import { VtxoScript } from "../script/base.js";
16
- import { CSVMultisigTapscript } from "../script/tapscript.js";
17
- import { buildOffchainTx, hasBoardingTxExpired, isValidArkAddress, } from "../utils/arkTransaction.js";
18
- import { DEFAULT_RENEWAL_CONFIG, DEFAULT_SETTLEMENT_CONFIG, VtxoManager, } from "./vtxo-manager.js";
19
- import { ArkNote } from "../arknote/index.js";
20
- import { Intent } from "../intent/index.js";
21
- import { RestIndexerProvider } from "../providers/indexer.js";
22
- import { extendCoin, validateRecipients } from "./utils.js";
23
- import { ArkError } from "../providers/errors.js";
24
- import { Batch } from "./batch.js";
25
- import { Estimator } from "../arkfee/index.js";
26
- import { buildTransactionHistory } from "../utils/transactionHistory.js";
27
- import { AssetManager, ReadonlyAssetManager } from "./asset-manager.js";
28
- import { Extension } from "../extension/index.js";
29
- import { DelegateVtxo } from "../script/delegate.js";
30
- import { DelegatorManagerImpl, findDestinationOutputIndex, } from "./delegator.js";
31
- import { IndexedDBContractRepository, IndexedDBWalletRepository, } from "../repositories/index.js";
32
- import { ContractManager } from "../contracts/contractManager.js";
33
- import { contractHandlers } from "../contracts/handlers/index.js";
34
- import { timelockToSequence } from "../utils/timelock.js";
35
- import { clearSyncCursor, updateWalletState } from "../utils/syncCursors.js";
36
- import { validateVtxosForScript, saveVtxosForContract, } from "../contracts/vtxoOwnership.js";
37
- import { WalletReceiveRotator } from "./walletReceiveRotator.js";
38
- import { InputSignerRouter } from "./inputSignerRouter.js";
39
- import { DescriptorSigningProviderMissingError, MissingSigningDescriptorError, } from "./signingErrors.js";
40
- export const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || DEFAULT_ARKADE_SERVER_URL;
41
- // Build per-input jobs for an intent proof. Index 0 of the proof is a
42
- // synthetic BIP-322 toSpend reference whose witnessUtxo.script mirrors
43
- // coin[0]'s pkScript, so we map it to the same source contract as
44
- // coin[0]; coins 0..N-1 then map to proof inputs 1..N.
45
- function intentProofJobs(coins) {
46
- if (coins.length === 0)
47
- return [];
48
- const coinJobs = coins.map((coin, i) => ({
49
- index: i + 1,
50
- lookupScript: VtxoScript.decode(coin.tapTree).pkScript,
51
- }));
52
- return [{ index: 0, lookupScript: coinJobs[0].lookupScript }, ...coinJobs];
53
- }
54
- // Built-in ArkProvider implementations (Rest/Expo) expose `serverUrl`,
55
- // but the interface itself does not declare a URL accessor — so this is a
56
- // structural read that returns undefined for custom implementations.
57
- function extractArkProviderUrl(provider) {
58
- const serverUrl = provider.serverUrl;
59
- return typeof serverUrl === "string" && serverUrl.length > 0
60
- ? serverUrl
61
- : undefined;
62
- }
63
- // Historical unilateral exit delay for mainnet (~7 days in seconds).
64
- // Kept so existing wallets can still discover and spend VTXOs sent to the
65
- // legacy address after arkd starts advertising a different delay.
66
- const MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
67
- function delayToTimelock(delay) {
68
- return {
69
- value: delay,
70
- type: delay < 512n ? "blocks" : "seconds",
71
- };
72
- }
73
- function dedupeTimelocks(timelocks) {
74
- const seen = new Set();
75
- const deduped = [];
76
- for (const timelock of timelocks) {
77
- const sequence = timelockToSequence(timelock).toString();
78
- if (seen.has(sequence))
79
- continue;
80
- seen.add(sequence);
81
- deduped.push(timelock);
82
- }
83
- return deduped;
84
- }
85
- /**
86
- * Type guard function to check if an identity has a toReadonly method.
87
- */
88
- function hasToReadonly(identity) {
89
- return (typeof identity === "object" &&
90
- identity !== null &&
91
- "toReadonly" in identity &&
92
- typeof identity.toReadonly === "function");
93
- }
94
- export { DescriptorSigningProviderMissingError, MissingSigningDescriptorError };
95
- export class ReadonlyWallet {
96
- get assetManager() {
97
- return this._assetManager;
98
- }
99
- constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks) {
100
- this.identity = identity;
101
- this.network = network;
102
- this.onchainProvider = onchainProvider;
103
- this.indexerProvider = indexerProvider;
104
- this.arkServerPublicKey = arkServerPublicKey;
105
- this.boardingTapscript = boardingTapscript;
106
- this.dustAmount = dustAmount;
107
- this.walletRepository = walletRepository;
108
- this.contractRepository = contractRepository;
109
- this.delegatorProvider = delegatorProvider;
110
- // Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
111
- // from getVtxos() so concurrent callers (UI, VtxoManager auto-renewal,
112
- // another send/settle racing the _txLock) can't reselect coins that are
113
- // already on their way out. The set is in-memory only: a process crash
114
- // clears it, and a stale entry only hides a VTXO (never spends one).
115
- this._pendingSpendOutpoints = new Set();
116
- // Guard: detect identity/server network mismatch for descriptor-based identities.
117
- // This duplicates the check in setupWalletConfig() so that subclasses
118
- // bypassing the factory still get the safety net.
119
- if ("descriptor" in identity) {
120
- const descriptor = identity.descriptor;
121
- const identityIsMainnet = !descriptor.includes("tpub");
122
- const serverIsMainnet = network.bech32 === "bc";
123
- if (identityIsMainnet !== serverIsMainnet) {
124
- throw new Error(`Network mismatch: identity uses ${identityIsMainnet ? "mainnet" : "testnet"} derivation ` +
125
- `but wallet network is ${serverIsMainnet ? "mainnet" : "testnet"}. ` +
126
- `Create identity with { isMainnet: ${serverIsMainnet} } to match.`);
127
- }
128
- }
129
- this._offchainTapscript = offchainTapscript;
130
- this.watcherConfig = watcherConfig;
131
- this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
132
- // Defensive for direct-construction callers; setupWalletConfig already
133
- // passes a deduped list through the public create() factories.
134
- this.walletContractTimelocks =
135
- walletContractTimelocks && walletContractTimelocks.length > 0
136
- ? dedupeTimelocks(walletContractTimelocks)
137
- : [
138
- this.offchainTapscript.options.csvTimelock ??
139
- DefaultVtxo.Script.DEFAULT_TIMELOCK,
140
- ];
141
- }
142
- /**
143
- * Currently-active receive tapscript. Read-only from the outside;
144
- * mutated only via {@link Wallet.setOffchainTapscriptForRotation}
145
- * by {@link WalletReceiveRotator.rotate}.
146
- */
147
- get offchainTapscript() {
148
- return this._offchainTapscript;
149
- }
150
- /**
151
- * Protected helper to set up shared wallet configuration.
152
- * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
153
- */
154
- static async setupWalletConfig(config, pubKey) {
155
- const arkadeServerUrl = getArkadeServerUrl(config);
156
- // Use provided arkProvider instance or create a new one from arkServerUrl
157
- const arkProvider = config.arkProvider ?? new RestArkProvider(arkadeServerUrl);
158
- // Resolve the indexer provider. If a full instance is supplied, use it
159
- // directly. Otherwise pick a URL with priority:
160
- // 1. explicit config.indexerUrl
161
- // 2. URL derived from the injected arkProvider (so a custom
162
- // arkProvider does not silently pair with the public default)
163
- // 3. arkadeServerUrl (only when no custom arkProvider was injected)
164
- let indexerProvider = config.indexerProvider;
165
- if (!indexerProvider) {
166
- let indexerUrl = config.indexerUrl;
167
- if (!indexerUrl) {
168
- if (config.arkProvider) {
169
- const derived = extractArkProviderUrl(config.arkProvider);
170
- if (!derived) {
171
- throw new Error("indexerUrl is required when arkProvider is provided without a discoverable serverUrl");
172
- }
173
- indexerUrl = derived;
174
- }
175
- else {
176
- indexerUrl = arkadeServerUrl;
177
- }
178
- }
179
- indexerProvider = new RestIndexerProvider(indexerUrl);
180
- }
181
- const info = await arkProvider.getInfo();
182
- const network = getNetwork(info.network);
183
- // Guard: detect identity/server network mismatch for seed-based identities.
184
- // A mainnet descriptor (xpub, coin type 0) connected to a testnet server
185
- // (or vice versa) means wrong derivation path → wrong keys → potential fund loss.
186
- if ("descriptor" in config.identity) {
187
- const descriptor = config.identity.descriptor;
188
- const identityIsMainnet = !descriptor.includes("tpub");
189
- const serverIsMainnet = info.network === "bitcoin";
190
- if (identityIsMainnet && !serverIsMainnet) {
191
- throw new Error(`Network mismatch: identity uses mainnet derivation (coin type 0) ` +
192
- `but the Arkade server is on ${info.network}. ` +
193
- `Create identity with { isMainnet: false } to use testnet derivation.`);
194
- }
195
- if (!identityIsMainnet && serverIsMainnet) {
196
- throw new Error(`Network mismatch: identity uses testnet derivation (coin type 1) ` +
197
- `but the Arkade server is on mainnet. ` +
198
- `Create identity with { isMainnet: true } or omit isMainnet (defaults to mainnet).`);
199
- }
200
- }
201
- // Extract esploraUrl from provider if not explicitly provided
202
- const esploraUrl = config.esploraUrl || ESPLORA_URL[info.network];
203
- // Use provided onchainProvider instance or create a new one
204
- const onchainProvider = config.onchainProvider || new EsploraProvider(esploraUrl);
205
- // validate unilateral exit timelock passed in config if any
206
- if (config.exitTimelock) {
207
- const { value, type } = config.exitTimelock;
208
- if ((value < 512n && type !== "blocks") ||
209
- (value >= 512n && type !== "seconds")) {
210
- throw new Error("invalid exitTimelock");
211
- }
212
- }
213
- const arkdExitTimelock = delayToTimelock(info.unilateralExitDelay);
214
- // create unilateral exit timelock
215
- const exitTimelock = config.exitTimelock ?? arkdExitTimelock;
216
- const walletContractTimelocks = config.exitTimelock
217
- ? [exitTimelock]
218
- : dedupeTimelocks([
219
- arkdExitTimelock,
220
- ...(info.network === "bitcoin"
221
- ? [delayToTimelock(MAINNET_UNILATERAL_EXIT_DELAY)]
222
- : []),
223
- ]);
224
- // validate boarding timelock passed in config if any
225
- if (config.boardingTimelock) {
226
- const { value, type } = config.boardingTimelock;
227
- if ((value < 512n && type !== "blocks") ||
228
- (value >= 512n && type !== "seconds")) {
229
- throw new Error("invalid boardingTimelock");
230
- }
231
- }
232
- // create boarding timelock
233
- const boardingTimelock = config.boardingTimelock ?? {
234
- value: info.boardingExitDelay,
235
- type: info.boardingExitDelay < 512n ? "blocks" : "seconds",
236
- };
237
- // Generate tapscripts for offchain and boarding address
238
- const serverPubKey = hex.decode(info.signerPubkey).slice(1);
239
- const delegatePubKey = config.delegatorProvider
240
- ? await config.delegatorProvider
241
- .getDelegateInfo()
242
- .then((info) => hex.decode(info.pubkey).slice(1))
243
- : undefined;
244
- const offchainOptions = {
245
- pubKey,
246
- serverPubKey,
247
- csvTimelock: exitTimelock,
248
- };
249
- const offchainTapscript = !delegatePubKey
250
- ? new DefaultVtxo.Script(offchainOptions)
251
- : new DelegateVtxo.Script({ ...offchainOptions, delegatePubKey });
252
- const boardingTapscript = new DefaultVtxo.Script({
253
- ...offchainOptions,
254
- csvTimelock: boardingTimelock,
255
- });
256
- const walletRepository = config.storage?.walletRepository ?? new IndexedDBWalletRepository();
257
- const contractRepository = config.storage?.contractRepository ??
258
- new IndexedDBContractRepository();
259
- return {
260
- arkProvider,
261
- indexerProvider,
262
- onchainProvider,
263
- network,
264
- networkName: info.network,
265
- serverPubKey,
266
- offchainTapscript,
267
- boardingTapscript,
268
- dustAmount: info.dust,
269
- walletRepository,
270
- contractRepository,
271
- info,
272
- delegatorProvider: config.delegatorProvider,
273
- walletContractTimelocks,
274
- };
275
- }
276
- /**
277
- * Create a readonly wallet for querying balances, addresses, and history.
278
- *
279
- * @param config - Readonly wallet configuration
280
- * @returns A readonly wallet instance
281
- */
282
- static async create(config) {
283
- const pubkey = await config.identity.xOnlyPublicKey();
284
- if (!pubkey) {
285
- throw new Error("Invalid configured public key");
286
- }
287
- const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
288
- return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository, setup.delegatorProvider, config.watcherConfig, setup.walletContractTimelocks);
289
- }
290
- get arkAddress() {
291
- return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
292
- }
293
- /**
294
- * Get the pkScript hex for the wallet's primary offchain address.
295
- * For the full wallet-owned script set registered in ContractManager, use getWalletScripts().
296
- */
297
- get defaultContractScript() {
298
- return hex.encode(this.offchainTapscript.pkScript);
299
- }
300
- /** Returns the wallet's Arkade address. */
301
- async getAddress() {
302
- return this.arkAddress.encode();
303
- }
304
- /** Returns the onchain boarding address used to move funds into Arkade. */
305
- async getBoardingAddress() {
306
- return this.boardingTapscript.onchainAddress(this.network);
307
- }
308
- /**
309
- * Return the wallet's combined onchain and offchain balances.
310
- */
311
- async getBalance() {
312
- const [boardingUtxos, vtxos] = await Promise.all([
313
- this.getBoardingUtxos(),
314
- this.getVtxos(),
315
- ]);
316
- // boarding
317
- let confirmed = 0;
318
- let unconfirmed = 0;
319
- for (const utxo of boardingUtxos) {
320
- if (utxo.status.confirmed) {
321
- confirmed += utxo.value;
322
- }
323
- else {
324
- unconfirmed += utxo.value;
325
- }
326
- }
327
- // offchain
328
- let settled = 0;
329
- let preconfirmed = 0;
330
- let recoverable = 0;
331
- settled = vtxos
332
- .filter((coin) => coin.virtualStatus.state === "settled")
333
- .reduce((sum, coin) => sum + coin.value, 0);
334
- preconfirmed = vtxos
335
- .filter((coin) => coin.virtualStatus.state === "preconfirmed")
336
- .reduce((sum, coin) => sum + coin.value, 0);
337
- recoverable = vtxos
338
- .filter((coin) => isSpendable(coin) && coin.virtualStatus.state === "swept")
339
- .reduce((sum, coin) => sum + coin.value, 0);
340
- const totalBoarding = confirmed + unconfirmed;
341
- const totalOffchain = settled + preconfirmed + recoverable;
342
- // aggregate asset balances from spendable virtual outputs
343
- const assetBalances = new Map();
344
- for (const vtxo of vtxos) {
345
- if (!isSpendable(vtxo))
346
- continue;
347
- if (vtxo.assets) {
348
- for (const a of vtxo.assets) {
349
- const current = assetBalances.get(a.assetId) ?? 0n;
350
- assetBalances.set(a.assetId, current + a.amount);
351
- }
352
- }
353
- }
354
- const assets = Array.from(assetBalances.entries()).map(([assetId, amount]) => ({
355
- assetId,
356
- amount,
357
- }));
358
- return {
359
- boarding: {
360
- confirmed,
361
- unconfirmed,
362
- total: totalBoarding,
363
- },
364
- settled,
365
- preconfirmed,
366
- available: settled + preconfirmed,
367
- recoverable,
368
- total: totalBoarding + totalOffchain,
369
- assets,
370
- };
371
- }
372
- /**
373
- * Return virtual outputs tracked by the wallet.
374
- *
375
- * @param filter - Optional flags controlling whether recoverable or unrolled VTXOs are included
376
- */
377
- async getVtxos(filter) {
378
- const f = filter ?? { withRecoverable: true, withUnrolled: false };
379
- const contractManager = await this.getContractManager();
380
- const vtxos = await contractManager.getContractsWithVtxos();
381
- return vtxos
382
- .flatMap((_) => _.vtxos)
383
- .filter((vtxo) => {
384
- if (this._pendingSpendOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
385
- return false;
386
- }
387
- if (isSpendable(vtxo)) {
388
- if (!f.withRecoverable &&
389
- (isRecoverable(vtxo) || isExpired(vtxo))) {
390
- return false;
391
- }
392
- return true;
393
- }
394
- return !!(f.withUnrolled && vtxo.isUnrolled);
395
- });
396
- }
397
- /**
398
- * Return wallet transaction history derived from Arkade state and boarding transactions.
399
- */
400
- async getTransactionHistory() {
401
- const contractManager = await this.getContractManager();
402
- const response = await contractManager.getContractsWithVtxos();
403
- const allVtxos = response.flatMap((_) => _.vtxos);
404
- const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
405
- const getTxCreatedAt = (txid) => this.indexerProvider
406
- .getVtxos({ outpoints: [{ txid, vout: 0 }] })
407
- .then((res) => res.vtxos[0]?.createdAt.getTime());
408
- return buildTransactionHistory(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
409
- }
410
- /**
411
- * Clear the global VTXO sync cursor, forcing a full re-bootstrap on next sync.
412
- * Useful for recovery after indexer reprocessing or debugging.
413
- */
414
- async clearSyncCursor() {
415
- await clearSyncCursor(this.walletRepository);
416
- }
417
- /**
418
- * Build a transaction history view for the wallet's boarding address.
419
- */
420
- async getBoardingTxs() {
421
- const utxos = [];
422
- const commitmentsToIgnore = new Set();
423
- const boardingAddress = await this.getBoardingAddress();
424
- const txs = await this.onchainProvider.getTransactions(boardingAddress);
425
- const outspendCache = new Map();
426
- for (const tx of txs) {
427
- for (let i = 0; i < tx.vout.length; i++) {
428
- const vout = tx.vout[i];
429
- if (vout.scriptpubkey_address === boardingAddress) {
430
- let spentStatuses = outspendCache.get(tx.txid);
431
- if (!spentStatuses) {
432
- spentStatuses =
433
- await this.onchainProvider.getTxOutspends(tx.txid);
434
- outspendCache.set(tx.txid, spentStatuses);
435
- }
436
- const spentStatus = spentStatuses[i];
437
- if (spentStatus?.spent) {
438
- commitmentsToIgnore.add(spentStatus.txid);
439
- }
440
- utxos.push({
441
- txid: tx.txid,
442
- vout: i,
443
- value: Number(vout.value),
444
- status: {
445
- confirmed: tx.status.confirmed,
446
- block_time: tx.status.block_time,
447
- },
448
- isUnrolled: true,
449
- virtualStatus: {
450
- state: spentStatus?.spent ? "spent" : "settled",
451
- commitmentTxIds: spentStatus?.spent
452
- ? [spentStatus.txid]
453
- : undefined,
454
- },
455
- createdAt: tx.status.confirmed
456
- ? new Date(tx.status.block_time * 1000)
457
- : new Date(0),
458
- script: hex.encode(this.boardingTapscript.pkScript),
459
- });
460
- }
461
- }
462
- }
463
- const unconfirmedTxs = [];
464
- const confirmedTxs = [];
465
- for (const utxo of utxos) {
466
- const tx = {
467
- key: {
468
- boardingTxid: utxo.txid,
469
- commitmentTxid: "",
470
- arkTxid: "",
471
- },
472
- amount: utxo.value,
473
- type: TxType.TxReceived,
474
- settled: utxo.virtualStatus.state === "spent",
475
- createdAt: utxo.status.block_time
476
- ? new Date(utxo.status.block_time * 1000).getTime()
477
- : 0,
478
- };
479
- if (!utxo.status.block_time) {
480
- unconfirmedTxs.push(tx);
481
- }
482
- else {
483
- confirmedTxs.push(tx);
484
- }
485
- }
486
- return {
487
- boardingTxs: [...unconfirmedTxs, ...confirmedTxs],
488
- commitmentsToIgnore,
489
- };
490
- }
491
- /**
492
- * Fetch and cache onchain inputs (UTXOs) received at the boarding address.
493
- */
494
- async getBoardingUtxos() {
495
- const boardingAddress = await this.getBoardingAddress();
496
- const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
497
- const utxos = boardingUtxos.map((utxo) => {
498
- return extendCoin(this, utxo);
499
- });
500
- // Save boarding inputs using unified repository
501
- await this.walletRepository.saveUtxos(boardingAddress, utxos);
502
- return utxos;
503
- }
504
- /**
505
- * Subscribe to onchain and offchain notifications for newly received funds.
506
- *
507
- * @param eventCallback - Callback invoked when matching funds are detected
508
- * @returns A function that stops the subscriptions
509
- */
510
- async notifyIncomingFunds(eventCallback) {
511
- const arkAddress = await this.getAddress();
512
- const boardingAddress = await this.getBoardingAddress();
513
- let onchainStopFunc;
514
- let indexerStopFunc;
515
- if (this.onchainProvider && boardingAddress) {
516
- const findVoutOnTx = (tx) => {
517
- return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
518
- };
519
- onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
520
- // find all onchain outputs belonging to our boarding address
521
- const coins = txs
522
- // filter txs where address is in output
523
- .filter((tx) => findVoutOnTx(tx) !== -1)
524
- // return boarding input as Coin
525
- .map((tx) => {
526
- const { txid, status } = tx;
527
- const vout = findVoutOnTx(tx);
528
- const value = Number(tx.vout[vout].value);
529
- return { txid, vout, value, status };
530
- });
531
- // and notify via callback
532
- eventCallback({
533
- type: "utxo",
534
- coins,
535
- });
536
- });
537
- }
538
- if (this.indexerProvider && arkAddress) {
539
- // Share the ContractWatcher's single subscription instead of
540
- // opening a second SSE stream.
541
- const cm = await this.getContractManager();
542
- // Serialize annotation+notification: parallel `annotateVtxos`
543
- // awaits could resolve out of order and deliver eventCallback
544
- // calls in the wrong sequence (e.g. `vtxo_spent` before its
545
- // matching `vtxo_received`).
546
- let annotationQueue = Promise.resolve();
547
- indexerStopFunc = cm.onContractEvent((event) => {
548
- if (event.type !== "vtxo_received" &&
549
- event.type !== "vtxo_spent") {
550
- return;
551
- }
552
- if (event.contract.type !== "default" &&
553
- event.contract.type !== "delegate") {
554
- return;
555
- }
556
- // `event.vtxos` carries placeholder tapscript fields from
557
- // the watcher; `annotateVtxos` fills them in.
558
- annotationQueue = annotationQueue.then(async () => {
559
- try {
560
- const annotated = await cm.annotateVtxos(event.vtxos);
561
- eventCallback({
562
- type: "vtxo",
563
- newVtxos: event.type === "vtxo_received" ? annotated : [],
564
- spentVtxos: event.type === "vtxo_spent" ? annotated : [],
565
- });
566
- }
567
- catch (error) {
568
- console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
569
- }
570
- });
571
- });
572
- }
573
- const stopFunc = () => {
574
- onchainStopFunc?.();
575
- indexerStopFunc?.();
576
- };
577
- return stopFunc;
578
- }
579
- /** Fetch Arkade transaction ids that are still pending final settlement. */
580
- async fetchPendingTxs() {
581
- // get non-swept virtual outputs, rely on the indexer only in case DB doesn't have the right state
582
- const scripts = await this.getWalletScripts();
583
- let { vtxos } = await this.indexerProvider.getVtxos({
584
- scripts,
585
- });
586
- return vtxos
587
- .filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
588
- vtxo.virtualStatus.state !== "settled" &&
589
- vtxo.arkTxId !== undefined)
590
- .map((_) => _.arkTxId);
591
- }
592
- // ========================================================================
593
- // Multi-script support (default + delegate addresses)
594
- // ========================================================================
595
- /**
596
- * Get all pkScript hex strings for the wallet's own addresses
597
- * (both delegate and non-delegate, current and historical).
598
- */
599
- async getWalletScripts() {
600
- const manager = await this.getContractManager();
601
- const contracts = await manager.getContracts({
602
- type: ["default", "delegate"],
603
- });
604
- return contracts.map((c) => c.script);
605
- }
606
- /**
607
- * Build a map of scriptHex → VtxoScript for all wallet contracts,
608
- * so virtual outputs can be extended with the correct tapscript per contract.
609
- */
610
- async getScriptMap() {
611
- const map = new Map();
612
- const manager = await this.getContractManager();
613
- const contracts = await manager.getContracts({
614
- type: ["default", "delegate"],
615
- });
616
- for (const contract of contracts) {
617
- if (map.has(contract.script))
618
- continue;
619
- const handler = contractHandlers.get(contract.type);
620
- if (handler) {
621
- const script = handler.createScript(contract.params);
622
- map.set(contract.script, script);
623
- }
624
- }
625
- return map;
626
- }
627
- // ========================================================================
628
- // Contract Management
629
- // ========================================================================
630
- /**
631
- * Get the ContractManager for managing contracts including the wallet's default address.
632
- *
633
- * The ContractManager handles:
634
- * - The wallet's default receiving address (as a "default" contract)
635
- * - External contracts (Boltz swaps, HTLCs, etc.)
636
- * - Multi-contract watching with resilient connections
637
- *
638
- * @example
639
- * ```typescript
640
- * const manager = await wallet.getContractManager();
641
- *
642
- * // Create a contract for a Boltz swap
643
- * const contract = await manager.createContract({
644
- * label: "Boltz Swap",
645
- * type: "vhtlc",
646
- * params: { ... },
647
- * script: swapScript,
648
- * address: swapAddress,
649
- * });
650
- *
651
- * // Start watching for events (includes wallet's default address)
652
- * const stop = await manager.onContractEvent((event) => {
653
- * console.log(`${event.type} on ${event.contractScript}`);
654
- * });
655
- * ```
656
- */
657
- async getContractManager() {
658
- // Return existing manager if already initialized
659
- if (this._contractManager) {
660
- return this._contractManager;
661
- }
662
- // If initialization is in progress, wait for it
663
- if (this._contractManagerInitializing) {
664
- return this._contractManagerInitializing;
665
- }
666
- // Start initialization and store the promise
667
- this._contractManagerInitializing = this.initializeContractManager();
668
- try {
669
- const manager = await this._contractManagerInitializing;
670
- this._contractManager = manager;
671
- return manager;
672
- }
673
- catch (error) {
674
- // Clear the initializing promise so subsequent calls can retry
675
- this._contractManagerInitializing = undefined;
676
- throw error;
677
- }
678
- finally {
679
- // Clear the initializing promise after completion
680
- this._contractManagerInitializing = undefined;
681
- }
682
- }
683
- async initializeContractManager() {
684
- const manager = await ContractManager.create({
685
- indexerProvider: this.indexerProvider,
686
- contractRepository: this.contractRepository,
687
- walletRepository: this.walletRepository,
688
- watcherConfig: this.watcherConfig,
689
- });
690
- // Register the wallet's baseline always-active contracts: every
691
- // `walletContractTimelocks` entry × {default, delegate-if-enabled}.
692
- // This matrix is bound to INDEX 0 — the identity's x-only pubkey
693
- // — by design: it's the permanent fallback set the wallet wants
694
- // active forever, independent of any HD rotation. Rotated
695
- // display contracts (registered separately by
696
- // {@link WalletReceiveRotator.rotate}) are intentionally
697
- // single-timelock-single-pubkey at the CURRENT arkd delay, and
698
- // get the `metadata.source = WALLET_RECEIVE_SOURCE` tag so the
699
- // next boot can find them. We deliberately do NOT re-register
700
- // the matrix at a rotated pubkey: doing so would dilute the
701
- // "index-0 baseline" guarantee and turn every rotation into a
702
- // multi-timelock matrix expansion on every boot.
703
- const baselinePubkey = await this.identity.xOnlyPublicKey();
704
- for (const csvTimelock of this.walletContractTimelocks) {
705
- const csvTimelockStr = timelockToSequence(csvTimelock).toString();
706
- const defaultScript = new DefaultVtxo.Script({
707
- pubKey: baselinePubkey,
708
- serverPubKey: this.offchainTapscript.options.serverPubKey,
709
- csvTimelock,
710
- });
711
- const defaultScriptHex = hex.encode(defaultScript.pkScript);
712
- await manager.createContract({
713
- type: "default",
714
- params: {
715
- pubKey: hex.encode(defaultScript.options.pubKey),
716
- serverPubKey: hex.encode(defaultScript.options.serverPubKey),
717
- csvTimelock: csvTimelockStr,
718
- },
719
- script: defaultScriptHex,
720
- address: defaultScript
721
- .address(this.network.hrp, this.arkServerPublicKey)
722
- .encode(),
723
- state: "active",
724
- });
725
- if (this.offchainTapscript instanceof DelegateVtxo.Script) {
726
- const delegateScript = new DelegateVtxo.Script({
727
- pubKey: baselinePubkey,
728
- serverPubKey: this.offchainTapscript.options.serverPubKey,
729
- delegatePubKey: this.offchainTapscript.options.delegatePubKey,
730
- csvTimelock,
731
- });
732
- const delegateScriptHex = hex.encode(delegateScript.pkScript);
733
- await manager.createContract({
734
- type: "delegate",
735
- params: {
736
- pubKey: hex.encode(delegateScript.options.pubKey),
737
- serverPubKey: hex.encode(delegateScript.options.serverPubKey),
738
- delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
739
- csvTimelock: csvTimelockStr,
740
- },
741
- script: delegateScriptHex,
742
- address: delegateScript
743
- .address(this.network.hrp, this.arkServerPublicKey)
744
- .encode(),
745
- state: "active",
746
- });
747
- }
748
- }
749
- return manager;
750
- }
751
- /** Dispose wallet-owned managers and release background resources. */
752
- async dispose() {
753
- const manager = this._contractManager ??
754
- (this._contractManagerInitializing
755
- ? await this._contractManagerInitializing.catch(() => undefined)
756
- : undefined);
757
- manager?.dispose();
758
- this._contractManager = undefined;
759
- this._contractManagerInitializing = undefined;
760
- }
761
- /** Async-dispose hook that forwards to `dispose()`. */
762
- async [Symbol.asyncDispose]() {
763
- await this.dispose();
764
- }
765
- }
766
- /**
767
- * Main wallet implementation for Bitcoin transactions with Arkade protocol support.
768
- * The wallet does not store any data locally and relies on Arkade and onchain
769
- * providers to fetch onchain and virtual outputs.
770
- *
771
- * @example
772
- * ```typescript
773
- * // Create a wallet with URL configuration
774
- * const wallet = await Wallet.create({
775
- * identity: MnemonicIdentity.fromMnemonic('abandon abandon...'),
776
- * arkServerUrl: 'https://arkade.computer',
777
- * esploraUrl: 'https://mempool.space/api'
778
- * });
779
- *
780
- * // Or with custom provider instances (e.g., for Expo/React Native)
781
- * const wallet = await Wallet.create({
782
- * identity: MnemonicIdentity.fromMnemonic('abandon abandon...'),
783
- * arkProvider: new ExpoArkProvider('https://arkade.computer'),
784
- * indexerProvider: new ExpoIndexerProvider('https://arkade.computer'),
785
- * esploraUrl: 'https://mempool.space/api'
786
- * });
787
- *
788
- * // Get addresses
789
- * const arkAddress = await wallet.getAddress();
790
- * const boardingAddress = await wallet.getBoardingAddress();
791
- *
792
- * // Send bitcoin
793
- * const txid = await wallet.send({
794
- * address: 'ark1q...',
795
- * amount: 50000,
796
- * });
797
- * ```
798
- */
799
- export class Wallet extends ReadonlyWallet {
800
- /**
801
- * @internal Sole write path for `offchainTapscript` after construction.
802
- * Called by {@link WalletReceiveRotator.rotate} once the rotated
803
- * display contract has been persisted. External code must treat
804
- * `offchainTapscript` as read-only.
805
- */
806
- setOffchainTapscriptForRotation(tapscript) {
807
- this._offchainTapscript = tapscript;
808
- }
809
- _addPendingSpends(inputs) {
810
- for (const input of inputs) {
811
- if ("virtualStatus" in input) {
812
- this._pendingSpendOutpoints.add(`${input.txid}:${input.vout}`);
813
- }
814
- }
815
- }
816
- _removePendingSpends(inputs) {
817
- for (const input of inputs) {
818
- if ("virtualStatus" in input) {
819
- this._pendingSpendOutpoints.delete(`${input.txid}:${input.vout}`);
820
- }
821
- }
822
- }
823
- _withTxLock(fn) {
824
- let release;
825
- const lock = new Promise((r) => (release = r));
826
- const prev = this._txLock;
827
- this._txLock = lock;
828
- return prev.then(async () => {
829
- try {
830
- return await fn();
831
- }
832
- finally {
833
- release();
834
- }
835
- });
836
- }
837
- constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
838
- /** @deprecated Use settlementConfig */
839
- renewalConfig, delegatorProvider, watcherConfig, settlementConfig, walletContractTimelocks, receiveRotator, descriptorProvider) {
840
- super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks);
841
- this.arkProvider = arkProvider;
842
- this.serverUnrollScript = serverUnrollScript;
843
- this.forfeitOutputScript = forfeitOutputScript;
844
- this.forfeitPubkey = forfeitPubkey;
845
- this._receiveRotatorInstalled = false;
846
- /**
847
- * Async mutex that serializes all operations submitting VTXOs to the Arkade
848
- * server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
849
- * background renewal from racing with user-initiated transactions for the
850
- * same VTXO inputs.
851
- */
852
- this._txLock = Promise.resolve();
853
- this.identity = identity;
854
- // Backwards-compatible: keep renewalConfig populated for any code reading it
855
- this.renewalConfig = {
856
- enabled: renewalConfig?.enabled ?? false,
857
- ...DEFAULT_RENEWAL_CONFIG,
858
- ...renewalConfig,
859
- };
860
- // Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
861
- if (settlementConfig !== undefined) {
862
- this.settlementConfig = settlementConfig;
863
- }
864
- else if (renewalConfig && this.renewalConfig.enabled) {
865
- this.settlementConfig = {
866
- vtxoThreshold: renewalConfig.thresholdMs
867
- ? renewalConfig.thresholdMs / 1000
868
- : undefined,
869
- };
870
- }
871
- else if (renewalConfig) {
872
- // renewalConfig provided but not enabled → disabled
873
- this.settlementConfig = false;
874
- }
875
- else {
876
- // No config at all → enabled by default
877
- this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
878
- }
879
- this._delegatorManager = delegatorProvider
880
- ? new DelegatorManagerImpl(delegatorProvider, arkProvider, identity)
881
- : undefined;
882
- this._receiveRotator = receiveRotator;
883
- this._descriptorProvider = descriptorProvider;
884
- this._signerRouter = new InputSignerRouter({
885
- identity,
886
- contractRepository,
887
- descriptorProvider,
888
- boardingPkScript: boardingTapscript.pkScript,
889
- });
890
- }
891
- get assetManager() {
892
- this._walletAssetManager ?? (this._walletAssetManager = new AssetManager(this));
893
- return this._walletAssetManager;
894
- }
895
- async getVtxoManager() {
896
- if (this._vtxoManager) {
897
- return this._vtxoManager;
898
- }
899
- if (this._vtxoManagerInitializing) {
900
- return this._vtxoManagerInitializing;
901
- }
902
- this._vtxoManagerInitializing = Promise.resolve(new VtxoManager(this, this.renewalConfig, this.settlementConfig));
903
- try {
904
- const manager = await this._vtxoManagerInitializing;
905
- // First-time hookup of the HD rotator: subscribe to
906
- // `vtxo_received` AFTER the contract manager (which is
907
- // initialised inside the VtxoManager construction path) has
908
- // registered the wallet's baseline contracts. The flag
909
- // makes this idempotent across repeated `getVtxoManager`
910
- // calls — install runs at most once per wallet instance.
911
- // Cache the manager and flip the install flag only after
912
- // `install()` resolves; otherwise a failing install would
913
- // leave the manager cached and silently disable HD
914
- // rotation for the lifetime of this wallet.
915
- if (this._receiveRotator && !this._receiveRotatorInstalled) {
916
- try {
917
- await this._receiveRotator.install(this);
918
- }
919
- catch (installErr) {
920
- await manager.dispose();
921
- throw installErr;
922
- }
923
- this._receiveRotatorInstalled = true;
924
- }
925
- this._vtxoManager = manager;
926
- return manager;
927
- }
928
- finally {
929
- this._vtxoManagerInitializing = undefined;
930
- }
931
- }
932
- async dispose() {
933
- // Tear down the rotation subscription + drain in-flight rotations
934
- // first so no late `vtxo_received` event can queue work on a
935
- // disposing wallet, and so any in-flight `createContract` call
936
- // finishes before we dispose the contract manager underneath it.
937
- // A rotator-disposal failure must not abort the rest of
938
- // teardown — the contract manager / super still need to run on
939
- // best-effort, so we capture and rethrow at the end.
940
- let rotatorError;
941
- try {
942
- await this._receiveRotator?.dispose();
943
- }
944
- catch (error) {
945
- rotatorError = error;
946
- }
947
- const manager = this._vtxoManager ??
948
- (this._vtxoManagerInitializing
949
- ? await this._vtxoManagerInitializing.catch(() => undefined)
950
- : undefined);
951
- try {
952
- if (manager) {
953
- await manager.dispose();
954
- }
955
- }
956
- catch {
957
- // best-effort teardown; ensure super.dispose() still runs
958
- }
959
- finally {
960
- this._vtxoManager = undefined;
961
- this._vtxoManagerInitializing = undefined;
962
- await super.dispose();
963
- }
964
- if (rotatorError) {
965
- throw rotatorError;
966
- }
967
- }
968
- /**
969
- * Create a full wallet and initialize its background managers.
970
- *
971
- * @param config - Wallet configuration
972
- * @returns A wallet ready to query balances and send transactions
973
- * @example
974
- * ```typescript
975
- * const wallet = await Wallet.create({
976
- * identity,
977
- * arkServerUrl: 'https://arkade.computer',
978
- * });
979
- * ```
980
- */
981
- static async create(config) {
982
- const pubkey = await config.identity.xOnlyPublicKey();
983
- if (!pubkey) {
984
- throw new Error("Invalid configured public key");
985
- }
986
- const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
987
- // Compute Wallet-specific forfeit and unroll scripts
988
- // the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
989
- let serverUnrollScript;
990
- try {
991
- const raw = hex.decode(setup.info.checkpointTapscript);
992
- serverUnrollScript = CSVMultisigTapscript.decode(raw);
993
- }
994
- catch (e) {
995
- throw new Error("Invalid checkpointTapscript from server");
996
- }
997
- // parse the server forfeit address
998
- // server is expecting funds to be sent to this address
999
- const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
1000
- const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
1001
- const forfeitOutputScript = OutScript.encode(forfeitAddress);
1002
- // HD wiring (boot path) — resolved via the descriptor provider.
1003
- // The rotator (when present) is handed to the constructor as
1004
- // the last positional arg and `getVtxoManager()` lazily
1005
- // installs its `vtxo_received` subscription on first call,
1006
- // after the contract manager has registered the wallet's
1007
- // baseline contracts.
1008
- const boot = await WalletReceiveRotator.resolveBoot(config, setup);
1009
- const wallet = new Wallet(config.identity, setup.network, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, boot?.offchainTapscript ?? setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig, config.settlementConfig, setup.walletContractTimelocks, boot?.rotator, boot?.provider);
1010
- await wallet.getVtxoManager();
1011
- return wallet;
1012
- }
1013
- /**
1014
- * Convert this wallet to a readonly wallet.
1015
- *
1016
- * @returns A readonly wallet with the same configuration but readonly identity
1017
- * @example
1018
- * ```typescript
1019
- * const wallet = await Wallet.create({ identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), ... });
1020
- * const readonlyWallet = await wallet.toReadonly();
1021
- *
1022
- * // Can query balance and addresses
1023
- * const balance = await readonlyWallet.getBalance();
1024
- * const address = await readonlyWallet.getAddress();
1025
- *
1026
- * // But cannot send transactions (type error)
1027
- * // readonlyWallet.send(...); // TypeScript error
1028
- * ```
1029
- */
1030
- async toReadonly() {
1031
- // Check if the identity has a toReadonly method using type guard
1032
- const readonlyIdentity = hasToReadonly(this.identity)
1033
- ? await this.identity.toReadonly()
1034
- : this.identity; // Identity extends ReadonlyIdentity, so this is safe
1035
- return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository, this.delegatorProvider, this.watcherConfig, this.walletContractTimelocks);
1036
- }
1037
- /** Returns the delegator manager when delegation support is configured. */
1038
- async getDelegatorManager() {
1039
- return this._delegatorManager;
1040
- }
1041
- /**
1042
- * Send bitcoin to an Arkade address.
1043
- *
1044
- * @deprecated Use `send`.
1045
- * @param params - Send parameters
1046
- */
1047
- async sendBitcoin(params) {
1048
- if (params.amount <= 0) {
1049
- throw new Error("Amount must be positive");
1050
- }
1051
- if (!isValidArkAddress(params.address)) {
1052
- throw new Error("Invalid Arkade address " + params.address);
1053
- }
1054
- if (params.selectedVtxos && params.selectedVtxos.length > 0) {
1055
- return this._withTxLock(async () => {
1056
- // Snapshot the active receive tapscript synchronously
1057
- // before any `await` so the change output's pkScript and
1058
- // the change-VTXO metadata written later by
1059
- // `updateDbAfterOffchainTx` are bound to the same
1060
- // tapscript even if `WalletReceiveRotator.rotate` fires
1061
- // during the offchain round-trip.
1062
- const offchainTapscript = this.offchainTapscript;
1063
- const arkAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
1064
- const selectedVtxoSum = params
1065
- .selectedVtxos.map((v) => v.value)
1066
- .reduce((a, b) => a + b, 0);
1067
- if (selectedVtxoSum < params.amount) {
1068
- throw new Error("Selected VTXOs do not cover specified amount");
1069
- }
1070
- const changeAmount = selectedVtxoSum - params.amount;
1071
- const selected = {
1072
- inputs: params.selectedVtxos,
1073
- changeAmount: BigInt(changeAmount),
1074
- };
1075
- const outputAddress = ArkAddress.decode(params.address);
1076
- const outputScript = BigInt(params.amount) < this.dustAmount
1077
- ? outputAddress.subdustPkScript
1078
- : outputAddress.pkScript;
1079
- const outputs = [
1080
- {
1081
- script: outputScript,
1082
- amount: BigInt(params.amount),
1083
- },
1084
- ];
1085
- // add change output if needed
1086
- if (selected.changeAmount > 0n) {
1087
- const changeOutputScript = selected.changeAmount < this.dustAmount
1088
- ? arkAddress.subdustPkScript
1089
- : arkAddress.pkScript;
1090
- outputs.push({
1091
- script: changeOutputScript,
1092
- amount: BigInt(selected.changeAmount),
1093
- });
1094
- }
1095
- this._addPendingSpends(selected.inputs);
1096
- try {
1097
- const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
1098
- await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0, offchainTapscript);
1099
- return arkTxid;
1100
- }
1101
- finally {
1102
- this._removePendingSpends(selected.inputs);
1103
- }
1104
- });
1105
- }
1106
- return this.send({
1107
- address: params.address,
1108
- amount: params.amount,
1109
- });
1110
- }
1111
- /**
1112
- * Settle boarding inputs and/or virtual outputs into a finalized mainnet transaction.
1113
- *
1114
- * @param params - Optional settlement inputs and outputs. When omitted, the wallet settles all eligible funds.
1115
- * @param eventCallback - Optional callback invoked for settlement stream events.
1116
- * @returns The finalized Arkade transaction id
1117
- */
1118
- async settle(params, eventCallback) {
1119
- return this._withTxLock(() => this._settleImpl(params, eventCallback));
1120
- }
1121
- async _settleImpl(params, eventCallback) {
1122
- if (params?.inputs) {
1123
- for (const input of params.inputs) {
1124
- // validate arknotes inputs
1125
- if (typeof input === "string") {
1126
- try {
1127
- ArkNote.fromString(input);
1128
- }
1129
- catch (e) {
1130
- throw new Error(`Invalid arknote "${input}"`);
1131
- }
1132
- }
1133
- }
1134
- }
1135
- // if no params are provided, use all non-expired boarding inputs and offchain virtual outputs as inputs
1136
- // and send all to the offchain address
1137
- if (!params) {
1138
- const { fees } = await this.arkProvider.getInfo();
1139
- const estimator = new Estimator(fees.intentFee);
1140
- let amount = 0;
1141
- const exitScript = CSVMultisigTapscript.decode(hex.decode(this.boardingTapscript.exitScript));
1142
- const boardingTimelock = exitScript.params.timelock;
1143
- // For block-based timelocks, fetch the chain tip height
1144
- let chainTipHeight;
1145
- if (boardingTimelock.type === "blocks") {
1146
- const tip = await this.onchainProvider.getChainTip();
1147
- chainTipHeight = tip.height;
1148
- }
1149
- const boardingUtxos = (await this.getBoardingUtxos()).filter((utxo) => utxo.status.confirmed &&
1150
- !hasBoardingTxExpired(utxo, boardingTimelock, chainTipHeight));
1151
- const filteredBoardingUtxos = [];
1152
- for (const utxo of boardingUtxos) {
1153
- const inputFee = estimator.evalOnchainInput({
1154
- amount: BigInt(utxo.value),
1155
- });
1156
- if (inputFee.value >= utxo.value) {
1157
- // skip if fees are greater than the boarding input value
1158
- continue;
1159
- }
1160
- filteredBoardingUtxos.push(utxo);
1161
- amount += utxo.value - inputFee.satoshis;
1162
- }
1163
- const vtxos = await this.getVtxos({ withRecoverable: true });
1164
- const filteredVtxos = [];
1165
- for (const vtxo of vtxos) {
1166
- const inputFee = estimator.evalOffchainInput({
1167
- amount: BigInt(vtxo.value),
1168
- type: vtxo.virtualStatus.state === "swept"
1169
- ? "recoverable"
1170
- : "vtxo",
1171
- weight: 0,
1172
- birth: vtxo.createdAt,
1173
- expiry: vtxo.virtualStatus.batchExpiry
1174
- ? new Date(vtxo.virtualStatus.batchExpiry)
1175
- : undefined,
1176
- });
1177
- if (inputFee.satoshis >= vtxo.value) {
1178
- // skip if fees are greater than the virtual output value
1179
- continue;
1180
- }
1181
- filteredVtxos.push(vtxo);
1182
- amount += vtxo.value - inputFee.satoshis;
1183
- }
1184
- const inputs = [...filteredBoardingUtxos, ...filteredVtxos];
1185
- if (inputs.length === 0) {
1186
- throw new Error("No inputs found");
1187
- }
1188
- const output = {
1189
- address: await this.getAddress(),
1190
- amount: BigInt(amount),
1191
- };
1192
- const outputFee = estimator.evalOffchainOutput({
1193
- amount: output.amount,
1194
- script: hex.encode(ArkAddress.decode(output.address).pkScript),
1195
- });
1196
- output.amount -= BigInt(outputFee.satoshis);
1197
- if (output.amount <= this.dustAmount) {
1198
- throw new Error("Output amount is below dust limit");
1199
- }
1200
- params = {
1201
- inputs,
1202
- outputs: [output],
1203
- };
1204
- }
1205
- const onchainOutputIndexes = [];
1206
- const outputs = [];
1207
- let hasOffchainOutputs = false;
1208
- for (const [index, output] of params.outputs.entries()) {
1209
- let script;
1210
- try {
1211
- // offchain
1212
- const addr = ArkAddress.decode(output.address);
1213
- script = addr.pkScript;
1214
- hasOffchainOutputs = true;
1215
- }
1216
- catch {
1217
- // onchain
1218
- const addr = Address(this.network).decode(output.address);
1219
- script = OutScript.encode(addr);
1220
- onchainOutputIndexes.push(index);
1221
- }
1222
- outputs.push({
1223
- amount: output.amount,
1224
- script,
1225
- });
1226
- }
1227
- // if some of the inputs hold assets, build the asset packet and append as output
1228
- // in the intent proof tx, there is a "fake" input at index 0
1229
- // so the real coin indices are offset by +1
1230
- const assetInputs = new Map();
1231
- for (let i = 0; i < params.inputs.length; i++) {
1232
- if ("assets" in params.inputs[i]) {
1233
- const assets = params.inputs[i]
1234
- .assets;
1235
- if (assets && assets.length > 0) {
1236
- assetInputs.set(i + 1, assets);
1237
- }
1238
- }
1239
- }
1240
- let outputAssets;
1241
- const destinationScript = ArkAddress.decode(await this.getAddress()).pkScript;
1242
- const assetOutputIndex = findDestinationOutputIndex(outputs, destinationScript);
1243
- if (assetInputs.size > 0) {
1244
- if (assetOutputIndex === -1) {
1245
- throw new Error("Cannot assign assets: no output matches the destination address");
1246
- }
1247
- // collect all input assets and assign them to the destination output
1248
- const allAssets = new Map();
1249
- for (const [, assets] of assetInputs) {
1250
- for (const asset of assets) {
1251
- const existing = allAssets.get(asset.assetId) ?? 0n;
1252
- allAssets.set(asset.assetId, existing + asset.amount);
1253
- }
1254
- }
1255
- outputAssets = [];
1256
- for (const [assetId, amount] of allAssets) {
1257
- outputAssets.push({ assetId, amount });
1258
- }
1259
- }
1260
- const recipients = params.outputs.map((output, i) => ({
1261
- address: output.address,
1262
- amount: Number(output.amount),
1263
- assets: i === assetOutputIndex ? outputAssets : undefined,
1264
- }));
1265
- if (outputAssets && outputAssets.length > 0) {
1266
- const assetPacket = createAssetPacket(assetInputs, recipients);
1267
- outputs.push(Extension.create([assetPacket]).txOut());
1268
- }
1269
- // session holds the state of the musig2 signing process of the virtual output tree
1270
- let session;
1271
- const signingPublicKeys = [];
1272
- if (hasOffchainOutputs) {
1273
- session = this.identity.signerSession();
1274
- signingPublicKeys.push(hex.encode(await session.getPublicKey()));
1275
- }
1276
- const [intent, deleteIntent] = await Promise.all([
1277
- this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
1278
- this.makeDeleteIntentSignature(params.inputs),
1279
- ]);
1280
- const topics = [
1281
- ...signingPublicKeys,
1282
- ...params.inputs.map((input) => `${input.txid}:${input.vout}`),
1283
- ];
1284
- const abortController = new AbortController();
1285
- let stream;
1286
- // Optimistically hide these inputs from concurrent getVtxos() callers
1287
- // while the settlement is in flight. Set before safeRegisterIntent so
1288
- // there's no window between intent registration and coin-visibility.
1289
- this._addPendingSpends(params.inputs);
1290
- try {
1291
- stream = this.arkProvider.getEventStream(abortController.signal, topics);
1292
- // Prime the iterator so the provider opens the SSE subscription
1293
- // before safeRegisterIntent can trigger server-side batch events.
1294
- const firstNext = stream.next();
1295
- // If settle exits before Batch.join consumes the primed result,
1296
- // keep the orphaned promise from surfacing as an unhandled rejection.
1297
- void firstNext.catch(() => { });
1298
- const primedStream = (async function* () {
1299
- const first = await firstNext;
1300
- if (!first.done) {
1301
- yield first.value;
1302
- }
1303
- yield* stream;
1304
- })();
1305
- const intentId = await this.safeRegisterIntent(intent, params.inputs);
1306
- const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
1307
- const commitmentTxid = await Batch.join(primedStream, handler, {
1308
- abortController,
1309
- skipVtxoTreeSigning: !hasOffchainOutputs,
1310
- eventCallback: eventCallback
1311
- ? (event) => Promise.resolve(eventCallback(event))
1312
- : undefined,
1313
- });
1314
- await this.updateDbAfterSettle(params.inputs, commitmentTxid);
1315
- return commitmentTxid;
1316
- }
1317
- catch (error) {
1318
- // delete the intent to not be stuck in the queue. If deletion fails
1319
- // the intent stays on the server and the next settle will hit
1320
- // "duplicated input" in safeRegisterIntent — surface the failure
1321
- // rather than silently swallowing it.
1322
- const inputIds = params.inputs
1323
- .map((i) => `${i.txid}:${i.vout}`)
1324
- .join(",");
1325
- await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
1326
- console.warn(`Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`, e);
1327
- });
1328
- throw error;
1329
- }
1330
- finally {
1331
- // Clear state first so a synchronous handler firing from abort()
1332
- // never observes a stale pending-spend set.
1333
- this._removePendingSpends(params.inputs);
1334
- // close the stream — abort() fires the in-body handler if the
1335
- // generator has started iterating; return() also releases the
1336
- // eager resource if the body is still suspended or never ran
1337
- // (e.g. safeRegisterIntent threw before Batch.join was called).
1338
- abortController.abort();
1339
- await stream?.return?.().catch(() => { });
1340
- }
1341
- }
1342
- async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
1343
- // the signed forfeits transactions to submit
1344
- const signedForfeits = [];
1345
- const isVtxo = (input) => "virtualStatus" in input;
1346
- let settlementPsbt = Transaction.fromPSBT(base64.decode(event.commitmentTx));
1347
- let hasBoardingUtxos = false;
1348
- let connectorIndex = 0;
1349
- const connectorsLeaves = connectorsGraph?.leaves() || [];
1350
- for (const input of inputs) {
1351
- // boarding input, we need to sign the settlement tx
1352
- if (!isVtxo(input)) {
1353
- for (let i = 0; i < settlementPsbt.inputsLength; i++) {
1354
- const settlementInput = settlementPsbt.getInput(i);
1355
- if (!settlementInput.txid ||
1356
- settlementInput.index === undefined) {
1357
- throw new Error("The server returned incomplete data. No settlement input found in the PSBT");
1358
- }
1359
- const inputTxId = hex.encode(settlementInput.txid);
1360
- if (inputTxId !== input.txid)
1361
- continue;
1362
- if (settlementInput.index !== input.vout)
1363
- continue;
1364
- // input found in the settlement tx, sign it
1365
- settlementPsbt.updateInput(i, {
1366
- tapLeafScript: [input.forfeitTapLeafScript],
1367
- });
1368
- const script = settlementPsbt.getInput(i).witnessUtxo?.script;
1369
- if (!script) {
1370
- throw new Error("The server returned incomplete data. Settlement input is missing witnessUtxo.script");
1371
- }
1372
- settlementPsbt = await this._signerRouter.sign(settlementPsbt, [{ index: i, lookupScript: script }]);
1373
- hasBoardingUtxos = true;
1374
- break;
1375
- }
1376
- continue;
1377
- }
1378
- if (isRecoverable(input) || isSubdust(input, this.dustAmount)) {
1379
- // recoverable or subdust coin, we don't need to create a forfeit tx
1380
- continue;
1381
- }
1382
- if (connectorsLeaves.length === 0) {
1383
- throw new Error("connectors not received");
1384
- }
1385
- if (connectorIndex >= connectorsLeaves.length) {
1386
- throw new Error("not enough connectors received");
1387
- }
1388
- const connectorLeaf = connectorsLeaves[connectorIndex];
1389
- const connectorTxId = connectorLeaf.id;
1390
- const connectorOutput = connectorLeaf.getOutput(0);
1391
- if (!connectorOutput) {
1392
- throw new Error("connector output not found");
1393
- }
1394
- const connectorAmount = connectorOutput.amount;
1395
- const connectorPkScript = connectorOutput.script;
1396
- if (!connectorAmount || !connectorPkScript) {
1397
- throw new Error("invalid connector output");
1398
- }
1399
- connectorIndex++;
1400
- let forfeitTx = buildForfeitTx([
1401
- {
1402
- txid: input.txid,
1403
- index: input.vout,
1404
- witnessUtxo: {
1405
- amount: BigInt(input.value),
1406
- script: VtxoScript.decode(input.tapTree).pkScript,
1407
- },
1408
- sighashType: SigHash.DEFAULT,
1409
- tapLeafScript: [input.forfeitTapLeafScript],
1410
- },
1411
- {
1412
- txid: connectorTxId,
1413
- index: 0,
1414
- witnessUtxo: {
1415
- amount: connectorAmount,
1416
- script: connectorPkScript,
1417
- },
1418
- },
1419
- ], forfeitOutputScript);
1420
- // do not sign the connector input
1421
- forfeitTx = await this._signerRouter.sign(forfeitTx, [
1422
- {
1423
- index: 0,
1424
- lookupScript: VtxoScript.decode(input.tapTree).pkScript,
1425
- },
1426
- ]);
1427
- signedForfeits.push(base64.encode(forfeitTx.toPSBT()));
1428
- }
1429
- if (signedForfeits.length > 0 || hasBoardingUtxos) {
1430
- await this.arkProvider.submitSignedForfeitTxs(signedForfeits, hasBoardingUtxos
1431
- ? base64.encode(settlementPsbt.toPSBT())
1432
- : undefined);
1433
- }
1434
- }
1435
- /**
1436
- * Create a batch event handler for settlement flows.
1437
- *
1438
- * @param intentId - The intent ID.
1439
- * @param inputs - Inputs used by the intent.
1440
- * @param expectedRecipients - Expected recipients to validate in the virtual output tree.
1441
- * @param session - Optional musig2 signing session. When omitted, signing steps are skipped.
1442
- */
1443
- createBatchHandler(intentId, inputs, expectedRecipients, session) {
1444
- let sweepTapTreeRoot;
1445
- return {
1446
- onBatchStarted: async (event) => {
1447
- const utf8IntentId = new TextEncoder().encode(intentId);
1448
- const intentIdHash = sha256(utf8IntentId);
1449
- const intentIdHashStr = hex.encode(intentIdHash);
1450
- let skip = true;
1451
- // check if our intent ID hash matches any in the event
1452
- for (const idHash of event.intentIdHashes) {
1453
- if (idHash === intentIdHashStr) {
1454
- if (!this.arkProvider) {
1455
- throw new Error("Arkade provider not configured");
1456
- }
1457
- await this.arkProvider.confirmRegistration(intentId);
1458
- skip = false;
1459
- }
1460
- }
1461
- if (skip) {
1462
- return { skip };
1463
- }
1464
- const sweepTapscript = CSVMultisigTapscript.encode({
1465
- timelock: {
1466
- value: event.batchExpiry,
1467
- type: event.batchExpiry >= 512n ? "seconds" : "blocks",
1468
- },
1469
- pubkeys: [this.forfeitPubkey],
1470
- }).script;
1471
- sweepTapTreeRoot = tapLeafHash(sweepTapscript);
1472
- return { skip: false };
1473
- },
1474
- onTreeSigningStarted: async (event, vtxoTree) => {
1475
- if (!session) {
1476
- return { skip: true };
1477
- }
1478
- if (!sweepTapTreeRoot) {
1479
- throw new Error("Sweep tap tree root not set");
1480
- }
1481
- const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
1482
- const signerPublicKey = await session.getPublicKey();
1483
- const xonlySignerPublicKey = signerPublicKey.subarray(1);
1484
- if (!xOnlyPublicKeys.includes(hex.encode(xonlySignerPublicKey))) {
1485
- // not a cosigner, skip the signing
1486
- return { skip: true };
1487
- }
1488
- // validate the unsigned virtual output tree
1489
- const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
1490
- validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
1491
- // validate that all expected receivers are in the virtual output tree with correct amounts and assets
1492
- if (expectedRecipients && expectedRecipients.length > 0) {
1493
- validateBatchRecipients(commitmentTx, vtxoTree.leaves(), expectedRecipients, this.network);
1494
- }
1495
- const sharedOutput = commitmentTx.getOutput(0);
1496
- if (!sharedOutput?.amount) {
1497
- throw new Error("Shared output not found");
1498
- }
1499
- await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
1500
- const pubkey = hex.encode(await session.getPublicKey());
1501
- const nonces = await session.getNonces();
1502
- await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
1503
- return { skip: false };
1504
- },
1505
- onTreeNonces: async (event) => {
1506
- if (!session) {
1507
- return { fullySigned: true }; // Signing complete (no signing needed)
1508
- }
1509
- const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
1510
- // wait to receive and aggregate all nonces before sending signatures
1511
- if (!hasAllNonces)
1512
- return { fullySigned: false };
1513
- const signatures = await session.sign();
1514
- const pubkey = hex.encode(await session.getPublicKey());
1515
- await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
1516
- return { fullySigned: true };
1517
- },
1518
- onBatchFinalization: async (event, _, connectorTree) => {
1519
- if (!this.forfeitOutputScript) {
1520
- throw new Error("Forfeit output script not set");
1521
- }
1522
- if (connectorTree) {
1523
- validateConnectorsTxGraph(event.commitmentTx, connectorTree);
1524
- }
1525
- await this.handleSettlementFinalizationEvent(event, inputs, this.forfeitOutputScript, connectorTree);
1526
- },
1527
- };
1528
- }
1529
- /**
1530
- * Build {@link InputSigningJob}s for a tx whose signable inputs can be
1531
- * resolved from their own `witnessUtxo.script`. Inputs without a
1532
- * `witnessUtxo` are silently omitted, mirroring the wallet's
1533
- * historical silent-skip behaviour for cosigner/connector inputs.
1534
- */
1535
- inputSigningJobsFromWitnessUtxos(tx, indexes) {
1536
- const candidateIndexes = indexes ?? Array.from({ length: tx.inputsLength }, (_, i) => i);
1537
- const jobs = [];
1538
- for (const index of candidateIndexes) {
1539
- const script = tx.getInput(index).witnessUtxo?.script;
1540
- if (script)
1541
- jobs.push({ index, lookupScript: script });
1542
- }
1543
- return jobs;
1544
- }
1545
- async safeRegisterIntent(intent, inputs) {
1546
- try {
1547
- return await this.arkProvider.registerIntent(intent);
1548
- }
1549
- catch (error) {
1550
- // catch the "already registered by another intent" error
1551
- if (error instanceof ArkError &&
1552
- error.code === 0 &&
1553
- error.message.includes("duplicated input")) {
1554
- // Clear any queued intent spending these exact inputs. The
1555
- // previous implementation signed a proof over getVtxos() only,
1556
- // which misses boarding UTXOs — the most common trigger for
1557
- // "duplicated input" on the auto-settle path. Signing the
1558
- // caller's own inputs keeps the proof surgical and correct
1559
- // regardless of whether the stuck input is a VTXO or boarding.
1560
- const deleteIntent = await this.makeDeleteIntentSignature(inputs);
1561
- await this.arkProvider.deleteIntent(deleteIntent);
1562
- // try again
1563
- return this.arkProvider.registerIntent(intent);
1564
- }
1565
- throw error;
1566
- }
1567
- }
1568
- async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
1569
- const message = {
1570
- type: "register",
1571
- onchain_output_indexes: onchainOutputsIndexes,
1572
- valid_at: validAt ? Math.floor(validAt) : 0,
1573
- expire_at: 0,
1574
- cosigners_public_keys: cosignerPubKeys,
1575
- };
1576
- const proof = Intent.create(message, coins, outputs);
1577
- const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
1578
- return {
1579
- proof: base64.encode(signedProof.toPSBT()),
1580
- message,
1581
- };
1582
- }
1583
- async makeDeleteIntentSignature(coins) {
1584
- const message = {
1585
- type: "delete",
1586
- expire_at: 0,
1587
- };
1588
- const proof = Intent.create(message, coins, []);
1589
- const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
1590
- return {
1591
- proof: base64.encode(signedProof.toPSBT()),
1592
- message,
1593
- };
1594
- }
1595
- async makeGetPendingTxIntentSignature(coins) {
1596
- const message = {
1597
- type: "get-pending-tx",
1598
- expire_at: 0,
1599
- };
1600
- const proof = Intent.create(message, coins, []);
1601
- const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
1602
- return {
1603
- proof: base64.encode(signedProof.toPSBT()),
1604
- message,
1605
- };
1606
- }
1607
- /**
1608
- * Finalizes pending transactions by retrieving them from the server and finalizing each one.
1609
- * Skips the server check entirely when no send was interrupted (no pending tx flag set).
1610
- * @param vtxos - Optional list of virtual outputs to use instead of retrieving them from the server
1611
- * @returns Array of transaction IDs that were finalized
1612
- */
1613
- async finalizePendingTxs(vtxos) {
1614
- const hasPending = await this.hasPendingTxFlag();
1615
- if (!hasPending) {
1616
- return { finalized: [], pending: [] };
1617
- }
1618
- const MAX_INPUTS_PER_INTENT = 20;
1619
- if (!vtxos || vtxos.length === 0) {
1620
- // Batch all scripts into a single indexer call
1621
- const scriptMap = await this.getScriptMap();
1622
- const allExtended = [];
1623
- const allScripts = [...scriptMap.keys()];
1624
- const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1625
- scripts: allScripts,
1626
- });
1627
- for (const vtxo of fetchedVtxos) {
1628
- const vtxoScript = scriptMap.get(vtxo.script);
1629
- if (!vtxoScript)
1630
- continue;
1631
- if (vtxo.virtualStatus.state === "swept" ||
1632
- vtxo.virtualStatus.state === "settled") {
1633
- continue;
1634
- }
1635
- allExtended.push({
1636
- ...vtxo,
1637
- forfeitTapLeafScript: vtxoScript.forfeit(),
1638
- intentTapLeafScript: vtxoScript.forfeit(),
1639
- tapTree: vtxoScript.encode(),
1640
- });
1641
- }
1642
- if (allExtended.length === 0) {
1643
- return { finalized: [], pending: [] };
1644
- }
1645
- vtxos = allExtended;
1646
- }
1647
- const batches = [];
1648
- for (let i = 0; i < vtxos.length; i += MAX_INPUTS_PER_INTENT) {
1649
- batches.push(vtxos.slice(i, i + MAX_INPUTS_PER_INTENT));
1650
- }
1651
- // Track seen arkTxids so parallel batches don't finalize the same tx twice
1652
- const seen = new Set();
1653
- const results = await Promise.all(batches.map(async (batch) => {
1654
- const batchFinalized = [];
1655
- const batchPending = [];
1656
- const intent = await this.makeGetPendingTxIntentSignature(batch);
1657
- const pendingTxs = await this.arkProvider.getPendingTxs(intent);
1658
- for (const pendingTx of pendingTxs) {
1659
- if (seen.has(pendingTx.arkTxid))
1660
- continue;
1661
- seen.add(pendingTx.arkTxid);
1662
- batchPending.push(pendingTx.arkTxid);
1663
- try {
1664
- const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
1665
- const tx = Transaction.fromPSBT(base64.decode(c));
1666
- const signedCheckpoint = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
1667
- return base64.encode(signedCheckpoint.toPSBT());
1668
- }));
1669
- await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
1670
- batchFinalized.push(pendingTx.arkTxid);
1671
- }
1672
- catch (error) {
1673
- console.error(`Failed to finalize transaction ${pendingTx.arkTxid}:`, error);
1674
- }
1675
- }
1676
- return {
1677
- finalized: batchFinalized,
1678
- pending: batchPending,
1679
- };
1680
- }));
1681
- const finalized = [];
1682
- const pending = [];
1683
- for (const result of results) {
1684
- finalized.push(...result.finalized);
1685
- pending.push(...result.pending);
1686
- }
1687
- // Only clear the flag if every discovered pending tx was finalized;
1688
- // if any failed, keep it so recovery retries on next startup.
1689
- if (finalized.length === pending.length) {
1690
- await this.setPendingTxFlag(false);
1691
- }
1692
- return { finalized, pending };
1693
- }
1694
- async hasPendingTxFlag() {
1695
- const state = await this.walletRepository.getWalletState();
1696
- return state?.settings?.hasPendingTx === true;
1697
- }
1698
- async setPendingTxFlag(value) {
1699
- await updateWalletState(this.walletRepository, (state) => ({
1700
- ...state,
1701
- settings: { ...state.settings, hasPendingTx: value },
1702
- }));
1703
- }
1704
- /**
1705
- * Send BTC and/or assets to one or more recipients.
1706
- *
1707
- * @param args - Recipients with their addresses, BTC amounts, and assets
1708
- * @returns Promise resolving to the Arkade transaction ID
1709
- *
1710
- * @example
1711
- * ```typescript
1712
- * const txid = await wallet.send({
1713
- * address: 'ark1q...',
1714
- * amount: 1000, // (optional, default to dust) btc amount to send to the output
1715
- * assets: [{ assetId: 'abc123...', amount: 50n }] // (optional) list of assets to send
1716
- * });
1717
- * ```
1718
- */
1719
- async send(...args) {
1720
- return this._withTxLock(() => this._sendImpl(...args));
1721
- }
1722
- async _sendImpl(...args) {
1723
- if (args.length === 0) {
1724
- throw new Error("At least one receiver is required");
1725
- }
1726
- // Snapshot the active receive tapscript synchronously before any
1727
- // `await`. `WalletReceiveRotator.rotate` mutates
1728
- // `this.offchainTapscript` without acquiring `_txLock`, so any
1729
- // yield between here and `updateDbAfterOffchainTx` opens a window
1730
- // where the change-output pkScript (built from `outputAddress`
1731
- // below) and the change-VTXO metadata (built from the snapshot
1732
- // inside `updateDbAfterOffchainTx`) could come from different
1733
- // tapscripts. Threading the snapshot pins both reads.
1734
- const offchainTapscript = this.offchainTapscript;
1735
- const outputAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
1736
- const address = outputAddress.encode();
1737
- // validate recipients and populate undefined amount with dust amount
1738
- const recipients = validateRecipients(args, Number(this.dustAmount));
1739
- const virtualCoins = await this.getVtxos({
1740
- withRecoverable: false,
1741
- });
1742
- // keep track of asset changes
1743
- const assetChanges = new Map();
1744
- let selectedCoins = [];
1745
- let btcAmountToSelect = 0;
1746
- for (const recipient of recipients) {
1747
- btcAmountToSelect += Math.max(recipient.amount, Number(this.dustAmount));
1748
- }
1749
- // select assets
1750
- for (const recipient of recipients) {
1751
- if (!recipient.assets) {
1752
- continue;
1753
- }
1754
- for (const receiverAsset of recipient.assets) {
1755
- let amountToSelect = receiverAsset.amount;
1756
- // check if existing change covers the needed amount
1757
- const existingChange = assetChanges.get(receiverAsset.assetId) ?? 0n;
1758
- if (existingChange >= amountToSelect) {
1759
- assetChanges.set(receiverAsset.assetId, existingChange - amountToSelect);
1760
- if (assetChanges.get(receiverAsset.assetId) === 0n) {
1761
- assetChanges.delete(receiverAsset.assetId);
1762
- }
1763
- continue;
1764
- }
1765
- if (existingChange > 0n) {
1766
- amountToSelect -= existingChange;
1767
- assetChanges.delete(receiverAsset.assetId);
1768
- }
1769
- const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1770
- const { selected, totalAssetAmount } = selectCoinsWithAsset(availableCoins, receiverAsset.assetId, amountToSelect);
1771
- for (const coin of selected) {
1772
- selectedCoins.push(coin);
1773
- // asset coins contain btc, subtract from total amount to select
1774
- btcAmountToSelect -= coin.value;
1775
- // coin may contain other assets, add them to asset changes
1776
- if (coin.assets) {
1777
- for (const a of coin.assets) {
1778
- if (a.assetId === receiverAsset.assetId) {
1779
- continue;
1780
- }
1781
- const existing = assetChanges.get(a.assetId) ?? 0n;
1782
- assetChanges.set(a.assetId, existing + a.amount);
1783
- }
1784
- }
1785
- }
1786
- const assetChangeAmount = totalAssetAmount - amountToSelect;
1787
- if (assetChangeAmount > 0n) {
1788
- const existing = assetChanges.get(receiverAsset.assetId) ?? 0n;
1789
- assetChanges.set(receiverAsset.assetId, existing + assetChangeAmount);
1790
- }
1791
- }
1792
- }
1793
- // select remaining btc
1794
- if (btcAmountToSelect > 0) {
1795
- const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1796
- const { inputs: btcCoins } = selectVirtualCoins(availableCoins, btcAmountToSelect);
1797
- // some coins may contain assets, add them to asset changes
1798
- for (const coin of btcCoins) {
1799
- if (coin.assets) {
1800
- for (const asset of coin.assets) {
1801
- const existing = assetChanges.get(asset.assetId) ?? 0n;
1802
- assetChanges.set(asset.assetId, existing + asset.amount);
1803
- }
1804
- }
1805
- }
1806
- selectedCoins = [...selectedCoins, ...btcCoins];
1807
- }
1808
- let totalBtcSelected = selectedCoins.reduce((sum, c) => sum + c.value, 0);
1809
- // build tx outputs
1810
- const outputs = recipients.map((recipient) => ({
1811
- script: recipient.script,
1812
- amount: BigInt(recipient.amount),
1813
- }));
1814
- const totalBtcOutput = outputs.reduce((sum, o) => sum + Number(o.amount), 0);
1815
- let changeAmount = totalBtcSelected - totalBtcOutput;
1816
- // enforce minimum change amount when there are asset changes
1817
- if (assetChanges.size > 0 && changeAmount < Number(this.dustAmount)) {
1818
- const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1819
- const { inputs: extraCoins } = selectVirtualCoins(availableCoins, Number(this.dustAmount) - changeAmount);
1820
- for (const coin of extraCoins) {
1821
- if (coin.assets) {
1822
- for (const asset of coin.assets) {
1823
- const existing = assetChanges.get(asset.assetId) ?? 0n;
1824
- assetChanges.set(asset.assetId, existing + asset.amount);
1825
- }
1826
- }
1827
- }
1828
- selectedCoins = [...selectedCoins, ...extraCoins];
1829
- totalBtcSelected += extraCoins.reduce((sum, c) => sum + c.value, 0);
1830
- changeAmount = totalBtcSelected - totalBtcOutput;
1831
- }
1832
- // build change receiver with BTC change and all asset changes
1833
- let changeReceiver;
1834
- let changeIndex = 0;
1835
- if (changeAmount > 0) {
1836
- const changeAssets = [];
1837
- for (const [assetId, amount] of assetChanges) {
1838
- if (amount > 0n) {
1839
- changeAssets.push({ assetId, amount });
1840
- }
1841
- }
1842
- changeIndex = outputs.length;
1843
- outputs.push({
1844
- script: BigInt(changeAmount) < this.dustAmount
1845
- ? outputAddress.subdustPkScript
1846
- : outputAddress.pkScript,
1847
- amount: BigInt(changeAmount),
1848
- });
1849
- changeReceiver = {
1850
- address: address,
1851
- amount: changeAmount,
1852
- assets: changeAssets.length > 0 ? changeAssets : undefined,
1853
- };
1854
- }
1855
- // create asset packet only if there are assets involved
1856
- const assetInputs = selectedCoinsToAssetInputs(selectedCoins);
1857
- const hasAssets = assetInputs.size > 0 ||
1858
- recipients.some((r) => r.assets && r.assets.length > 0);
1859
- if (hasAssets) {
1860
- const assetPacket = createAssetPacket(assetInputs, recipients, changeReceiver);
1861
- outputs.push(Extension.create([assetPacket]).txOut());
1862
- }
1863
- const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
1864
- // Optimistically hide selected coins from concurrent getVtxos() while
1865
- // the offchain tx is in flight.
1866
- this._addPendingSpends(selectedCoins);
1867
- try {
1868
- const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
1869
- await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, offchainTapscript, changeReceiver?.assets);
1870
- return arkTxid;
1871
- }
1872
- finally {
1873
- this._removePendingSpends(selectedCoins);
1874
- }
1875
- }
1876
- /**
1877
- * Build an offchain transaction from the given inputs and outputs,
1878
- * sign it, submit to the Arkade provider, and finalize.
1879
- * @returns The Arkade transaction id and server-signed checkpoint PSBTs (for bookkeeping)
1880
- */
1881
- async buildAndSubmitOffchainTx(inputs, outputs) {
1882
- const offchainTx = buildOffchainTx(inputs.map((input) => {
1883
- return {
1884
- ...input,
1885
- tapLeafScript: input.forfeitTapLeafScript,
1886
- };
1887
- }), outputs, this.serverUnrollScript);
1888
- // arkTx inputs spend checkpoint outputs, so each input's
1889
- // `witnessUtxo.script` is the checkpoint pkScript — not the
1890
- // source VTXO contract's pkScript. Build the routing jobs from
1891
- // the source VTXO scripts (positionally aligned to `inputs[i]`)
1892
- // so the router can resolve each input's owning contract.
1893
- const arkTxJobs = inputs.map((input, index) => ({
1894
- index,
1895
- lookupScript: VtxoScript.decode(input.tapTree).pkScript,
1896
- }));
1897
- const signedVirtualTx = await this._signerRouter.sign(offchainTx.arkTx, arkTxJobs);
1898
- // Mark pending before submitting — if we crash between submit and
1899
- // finalize, the next init will recover via finalizePendingTxs.
1900
- await this.setPendingTxFlag(true);
1901
- const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
1902
- const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
1903
- const tx = Transaction.fromPSBT(base64.decode(c));
1904
- const signedCheckpoint = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
1905
- return base64.encode(signedCheckpoint.toPSBT());
1906
- }));
1907
- await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
1908
- try {
1909
- await this.setPendingTxFlag(false);
1910
- }
1911
- catch (error) {
1912
- console.error("Failed to clear pending tx flag:", error);
1913
- }
1914
- return { arkTxid, signedCheckpointTxs };
1915
- }
1916
- // mark virtual outputs as spent, save change outputs if any.
1917
- // `offchainTapscript` is the snapshot the caller captured under
1918
- // `_txLock` before any `await`; deriving both the change-VTXO
1919
- // metadata and `primaryAddress` from it here guarantees the local
1920
- // record matches the pkScript the server saw on the inbound
1921
- // transaction, even if `WalletReceiveRotator.rotate` swaps
1922
- // `this.offchainTapscript` mid-flight.
1923
- async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, changeAssets) {
1924
- const primaryAddress = offchainTapscript
1925
- .address(this.network.hrp, this.arkServerPublicKey)
1926
- .encode();
1927
- try {
1928
- const spentVtxos = [];
1929
- const commitmentTxIds = new Set();
1930
- let batchExpiry = Number.MAX_SAFE_INTEGER;
1931
- if (inputs.length !== signedCheckpointTxs.length) {
1932
- console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
1933
- }
1934
- const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
1935
- const cm = await this.getContractManager();
1936
- const annotatedInputs = await cm.annotateVtxos(inputs);
1937
- for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
1938
- if (inputIndex < safeLength &&
1939
- signedCheckpointTxs[inputIndex]) {
1940
- const checkpoint = Transaction.fromPSBT(base64.decode(signedCheckpointTxs[inputIndex]));
1941
- spentVtxos.push({
1942
- ...vtxo,
1943
- virtualStatus: {
1944
- ...vtxo.virtualStatus,
1945
- state: "spent",
1946
- },
1947
- spentBy: checkpoint.id,
1948
- arkTxId: arkTxid,
1949
- isSpent: true,
1950
- });
1951
- }
1952
- else {
1953
- spentVtxos.push({
1954
- ...vtxo,
1955
- virtualStatus: {
1956
- ...vtxo.virtualStatus,
1957
- state: "spent",
1958
- },
1959
- arkTxId: arkTxid,
1960
- isSpent: true,
1961
- });
1962
- }
1963
- if (vtxo.virtualStatus.commitmentTxIds) {
1964
- for (const id of vtxo.virtualStatus.commitmentTxIds) {
1965
- commitmentTxIds.add(id);
1966
- }
1967
- }
1968
- if (vtxo.virtualStatus.batchExpiry) {
1969
- batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
1970
- }
1971
- }
1972
- const createdAt = Date.now();
1973
- // Only save a change virtual output for preconfirmed coins (those with a batchExpiry).
1974
- // Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
1975
- let changeVtxo;
1976
- if (changeAmount > 0n && batchExpiry !== Number.MAX_SAFE_INTEGER) {
1977
- changeVtxo = {
1978
- txid: arkTxid,
1979
- vout: changeVout,
1980
- createdAt: new Date(createdAt),
1981
- forfeitTapLeafScript: offchainTapscript.forfeit(),
1982
- intentTapLeafScript: offchainTapscript.forfeit(),
1983
- isUnrolled: false,
1984
- isSpent: false,
1985
- tapTree: offchainTapscript.encode(),
1986
- value: Number(changeAmount),
1987
- virtualStatus: {
1988
- state: "preconfirmed",
1989
- commitmentTxIds: Array.from(commitmentTxIds),
1990
- batchExpiry,
1991
- },
1992
- status: {
1993
- confirmed: false,
1994
- },
1995
- assets: changeAssets,
1996
- script: hex.encode(offchainTapscript.pkScript),
1997
- };
1998
- }
1999
- // Route spent rows to their owning contract bucket. The wallet's
2000
- // primary contract is registered with the manager at boot, so
2001
- // `addrByScript` already includes it; in a multi-contract spend
2002
- // each input may belong to a different contract.
2003
- const contracts = await cm.getContracts();
2004
- const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
2005
- const spentByScript = new Map();
2006
- for (const v of spentVtxos) {
2007
- if (!v.script) {
2008
- throw new Error(`Wallet.updateDbAfterOffchainTx: spent VTXO ${v.txid}:${v.vout} has no script`);
2009
- }
2010
- const arr = spentByScript.get(v.script) ?? [];
2011
- arr.push(v);
2012
- spentByScript.set(v.script, arr);
2013
- }
2014
- for (const [script, vtxos] of spentByScript) {
2015
- // User-initiated send path: a wrong-script row here means the
2016
- // wallet is about to record ownership against the wrong
2017
- // contract — fail loudly rather than persist inconsistent state.
2018
- validateVtxosForScript(vtxos, script, "Wallet.updateDbAfterOffchainTx");
2019
- const targetAddr = addrByScript.get(script);
2020
- if (!targetAddr) {
2021
- throw new Error(`Wallet.updateDbAfterOffchainTx: no contract owns script ${script}`);
2022
- }
2023
- await saveVtxosForContract(this.walletRepository, { script, address: targetAddr }, vtxos);
2024
- }
2025
- // Change is always primary-script by construction.
2026
- if (changeVtxo) {
2027
- await saveVtxosForContract(this.walletRepository, { script: changeVtxo.script, address: primaryAddress }, [changeVtxo]);
2028
- }
2029
- await this.walletRepository.saveTransactions(primaryAddress, [
2030
- {
2031
- key: {
2032
- boardingTxid: "",
2033
- commitmentTxid: "",
2034
- arkTxid: arkTxid,
2035
- },
2036
- amount: sentAmount,
2037
- type: TxType.TxSent,
2038
- settled: false,
2039
- createdAt,
2040
- },
2041
- ]);
2042
- }
2043
- catch (e) {
2044
- console.warn("error saving offchain tx to repository", e);
2045
- throw e;
2046
- }
2047
- }
2048
- // mark virtual outputs as spent/settled, remove boarding inputs
2049
- async updateDbAfterSettle(inputs, commitmentTxid) {
2050
- try {
2051
- const boardingAddress = await this.getBoardingAddress();
2052
- const spentVtxos = [];
2053
- const inputArkTxIds = new Set();
2054
- const boardingUtxoToRemove = new Set();
2055
- const isVtxo = (input) => "virtualStatus" in input;
2056
- const vtxoInputs = inputs.filter(isVtxo);
2057
- const cm = await this.getContractManager();
2058
- const annotatedVtxos = await cm.annotateVtxos(vtxoInputs);
2059
- const annotatedByKey = new Map(annotatedVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
2060
- for (const input of inputs) {
2061
- if (isVtxo(input)) {
2062
- // virtual output = mark it settled
2063
- const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
2064
- if (vtxo.arkTxId) {
2065
- inputArkTxIds.add(vtxo.arkTxId);
2066
- }
2067
- spentVtxos.push({
2068
- ...vtxo,
2069
- virtualStatus: {
2070
- ...vtxo.virtualStatus,
2071
- state: "settled",
2072
- },
2073
- settledBy: commitmentTxid,
2074
- isSpent: true,
2075
- });
2076
- }
2077
- else {
2078
- // boarding input = remove it
2079
- boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
2080
- }
2081
- }
2082
- if (spentVtxos.length > 0) {
2083
- // Route settled rows to their owning contract bucket. In a
2084
- // multi-contract settle the inputs may belong to several
2085
- // contracts; the wallet's primary contract is registered with
2086
- // the manager at boot, so its address is in `addrByScript`
2087
- // alongside the rest.
2088
- const contracts = await cm.getContracts();
2089
- const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
2090
- const byScript = new Map();
2091
- for (const v of spentVtxos) {
2092
- if (!v.script) {
2093
- throw new Error(`Wallet.updateDbAfterSettle: spent VTXO ${v.txid}:${v.vout} has no script`);
2094
- }
2095
- const arr = byScript.get(v.script) ?? [];
2096
- arr.push(v);
2097
- byScript.set(v.script, arr);
2098
- }
2099
- for (const [script, vtxos] of byScript) {
2100
- // User-initiated settle path: refuse to record a settle
2101
- // against the wrong script.
2102
- validateVtxosForScript(vtxos, script, "Wallet.updateDbAfterSettle");
2103
- const targetAddr = addrByScript.get(script);
2104
- if (!targetAddr) {
2105
- throw new Error(`Wallet.updateDbAfterSettle: no contract owns script ${script}`);
2106
- }
2107
- await saveVtxosForContract(this.walletRepository, { script, address: targetAddr }, vtxos);
2108
- }
2109
- }
2110
- if (boardingUtxoToRemove.size > 0) {
2111
- const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
2112
- const filtered = currentUtxos.filter((u) => !boardingUtxoToRemove.has(`${u.txid}:${u.vout}`));
2113
- // Clear and re-save the filtered list
2114
- await this.walletRepository.deleteUtxos(boardingAddress);
2115
- if (filtered.length > 0) {
2116
- await this.walletRepository.saveUtxos(boardingAddress, filtered);
2117
- }
2118
- }
2119
- }
2120
- catch (e) {
2121
- console.warn("error updating repository after settle", e);
2122
- throw e;
2123
- }
2124
- }
2125
- }
2126
- Wallet.MIN_FEE_RATE = 1; // sats/vbyte
2127
- /**
2128
- * Select virtual outputs to reach a target amount, prioritizing those closer to expiry
2129
- * @param coins List of virtual outputs to select from
2130
- * @param targetAmount Target amount to reach in satoshis
2131
- * @returns Selected virtual outputs and change amount
2132
- */
2133
- export function selectVirtualCoins(coins, targetAmount) {
2134
- // Sort virtual outputs by expiry (ascending) and amount (descending)
2135
- const sortedCoins = [...coins].sort((a, b) => {
2136
- // First sort by expiry if available
2137
- const expiryA = a.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
2138
- const expiryB = b.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
2139
- if (expiryA !== expiryB) {
2140
- return expiryA - expiryB; // Earlier expiry first
2141
- }
2142
- // Then sort by amount
2143
- return b.value - a.value; // Larger amount first
2144
- });
2145
- const selectedCoins = [];
2146
- let selectedAmount = 0;
2147
- // Select coins until we have enough
2148
- for (const coin of sortedCoins) {
2149
- selectedCoins.push(coin);
2150
- selectedAmount += coin.value;
2151
- if (selectedAmount >= targetAmount) {
2152
- break;
2153
- }
2154
- }
2155
- if (selectedAmount === targetAmount) {
2156
- return { inputs: selectedCoins, changeAmount: 0n };
2157
- }
2158
- // Check if we have enough
2159
- if (selectedAmount < targetAmount) {
2160
- throw new Error("Insufficient funds");
2161
- }
2162
- const changeAmount = BigInt(selectedAmount - targetAmount);
2163
- return {
2164
- inputs: selectedCoins,
2165
- changeAmount,
2166
- };
2167
- }
2168
- /**
2169
- * Wait for incoming funds to the wallet
2170
- * @param wallet - The wallet to wait for incoming funds
2171
- * @returns A promise that resolves the next new coins received by the wallet's address
2172
- */
2173
- export async function waitForIncomingFunds(wallet) {
2174
- let stopFunc;
2175
- return new Promise((resolve) => {
2176
- wallet
2177
- .notifyIncomingFunds((coins) => {
2178
- resolve(coins);
2179
- if (stopFunc)
2180
- stopFunc();
2181
- })
2182
- .then((stop) => {
2183
- stopFunc = stop;
2184
- });
2185
- });
2186
- }