@arkade-os/sdk 0.3.12 → 0.4.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +483 -54
  2. package/dist/cjs/adapters/expo-db.js +35 -0
  3. package/dist/cjs/asset/assetGroup.js +141 -0
  4. package/dist/cjs/asset/assetId.js +88 -0
  5. package/dist/cjs/asset/assetInput.js +204 -0
  6. package/dist/cjs/asset/assetOutput.js +159 -0
  7. package/dist/cjs/asset/assetRef.js +82 -0
  8. package/dist/cjs/asset/index.js +24 -0
  9. package/dist/cjs/asset/metadata.js +172 -0
  10. package/dist/cjs/asset/packet.js +164 -0
  11. package/dist/cjs/asset/types.js +25 -0
  12. package/dist/cjs/asset/utils.js +105 -0
  13. package/dist/cjs/contracts/arkcontract.js +148 -0
  14. package/dist/cjs/contracts/contractManager.js +436 -0
  15. package/dist/cjs/contracts/contractWatcher.js +567 -0
  16. package/dist/cjs/contracts/handlers/default.js +85 -0
  17. package/dist/cjs/contracts/handlers/delegate.js +89 -0
  18. package/dist/cjs/contracts/handlers/helpers.js +105 -0
  19. package/dist/cjs/contracts/handlers/index.js +19 -0
  20. package/dist/cjs/contracts/handlers/registry.js +89 -0
  21. package/dist/cjs/contracts/handlers/vhtlc.js +193 -0
  22. package/dist/cjs/contracts/index.js +41 -0
  23. package/dist/cjs/contracts/types.js +2 -0
  24. package/dist/cjs/db/manager.js +97 -0
  25. package/dist/cjs/forfeit.js +12 -8
  26. package/dist/cjs/identity/index.js +1 -0
  27. package/dist/cjs/identity/seedIdentity.js +255 -0
  28. package/dist/cjs/index.js +70 -14
  29. package/dist/cjs/intent/index.js +28 -2
  30. package/dist/cjs/providers/ark.js +7 -0
  31. package/dist/cjs/providers/delegator.js +66 -0
  32. package/dist/cjs/providers/expoIndexer.js +5 -0
  33. package/dist/cjs/providers/indexer.js +68 -1
  34. package/dist/cjs/providers/onchain.js +2 -2
  35. package/dist/cjs/providers/utils.js +1 -0
  36. package/dist/cjs/repositories/contractRepository.js +0 -103
  37. package/dist/cjs/repositories/inMemory/contractRepository.js +55 -0
  38. package/dist/cjs/repositories/inMemory/walletRepository.js +80 -0
  39. package/dist/cjs/repositories/index.js +16 -0
  40. package/dist/cjs/repositories/indexedDB/contractRepository.js +187 -0
  41. package/dist/cjs/repositories/indexedDB/db.js +57 -0
  42. package/dist/cjs/repositories/indexedDB/schema.js +159 -0
  43. package/dist/cjs/repositories/indexedDB/walletRepository.js +338 -0
  44. package/dist/cjs/repositories/indexedDB/websqlAdapter.js +144 -0
  45. package/dist/cjs/repositories/migrations/contractRepositoryImpl.js +127 -0
  46. package/dist/cjs/repositories/migrations/fromStorageAdapter.js +66 -0
  47. package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +180 -0
  48. package/dist/cjs/repositories/walletRepository.js +0 -169
  49. package/dist/cjs/script/base.js +54 -0
  50. package/dist/cjs/script/delegate.js +49 -0
  51. package/dist/cjs/storage/asyncStorage.js +4 -1
  52. package/dist/cjs/storage/fileSystem.js +3 -0
  53. package/dist/cjs/storage/inMemory.js +3 -0
  54. package/dist/cjs/storage/indexedDB.js +5 -1
  55. package/dist/cjs/storage/localStorage.js +3 -0
  56. package/dist/cjs/utils/arkTransaction.js +16 -0
  57. package/dist/cjs/utils/transactionHistory.js +50 -0
  58. package/dist/cjs/utils/txSizeEstimator.js +39 -14
  59. package/dist/cjs/wallet/asset-manager.js +338 -0
  60. package/dist/cjs/wallet/asset.js +117 -0
  61. package/dist/cjs/wallet/batch.js +1 -1
  62. package/dist/cjs/wallet/delegator.js +235 -0
  63. package/dist/cjs/wallet/expo/background.js +133 -0
  64. package/dist/cjs/wallet/expo/index.js +9 -0
  65. package/dist/cjs/wallet/expo/wallet.js +231 -0
  66. package/dist/cjs/wallet/onchain.js +57 -12
  67. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +568 -0
  68. package/dist/cjs/wallet/serviceWorker/wallet.js +383 -102
  69. package/dist/cjs/wallet/unroll.js +7 -2
  70. package/dist/cjs/wallet/utils.js +60 -0
  71. package/dist/cjs/wallet/validation.js +151 -0
  72. package/dist/cjs/wallet/vtxo-manager.js +1 -1
  73. package/dist/cjs/wallet/wallet.js +702 -260
  74. package/dist/cjs/worker/browser/service-worker-manager.js +82 -0
  75. package/dist/cjs/{wallet/serviceWorker → worker/browser}/utils.js +2 -1
  76. package/dist/cjs/worker/expo/asyncStorageTaskQueue.js +78 -0
  77. package/dist/cjs/worker/expo/index.js +12 -0
  78. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +61 -0
  79. package/dist/cjs/worker/expo/processors/index.js +6 -0
  80. package/dist/cjs/worker/expo/taskQueue.js +41 -0
  81. package/dist/cjs/worker/expo/taskRunner.js +57 -0
  82. package/dist/cjs/worker/messageBus.js +252 -0
  83. package/dist/esm/adapters/expo-db.js +27 -0
  84. package/dist/esm/asset/assetGroup.js +137 -0
  85. package/dist/esm/asset/assetId.js +84 -0
  86. package/dist/esm/asset/assetInput.js +199 -0
  87. package/dist/esm/asset/assetOutput.js +154 -0
  88. package/dist/esm/asset/assetRef.js +78 -0
  89. package/dist/esm/asset/index.js +8 -0
  90. package/dist/esm/asset/metadata.js +167 -0
  91. package/dist/esm/asset/packet.js +159 -0
  92. package/dist/esm/asset/types.js +22 -0
  93. package/dist/esm/asset/utils.js +99 -0
  94. package/dist/esm/contracts/arkcontract.js +141 -0
  95. package/dist/esm/contracts/contractManager.js +432 -0
  96. package/dist/esm/contracts/contractWatcher.js +563 -0
  97. package/dist/esm/contracts/handlers/default.js +82 -0
  98. package/dist/esm/contracts/handlers/delegate.js +86 -0
  99. package/dist/esm/contracts/handlers/helpers.js +66 -0
  100. package/dist/esm/contracts/handlers/index.js +12 -0
  101. package/dist/esm/contracts/handlers/registry.js +86 -0
  102. package/dist/esm/contracts/handlers/vhtlc.js +190 -0
  103. package/dist/esm/contracts/index.js +13 -0
  104. package/dist/esm/contracts/types.js +1 -0
  105. package/dist/esm/db/manager.js +92 -0
  106. package/dist/esm/forfeit.js +11 -8
  107. package/dist/esm/identity/index.js +1 -0
  108. package/dist/esm/identity/seedIdentity.js +249 -0
  109. package/dist/esm/index.js +25 -15
  110. package/dist/esm/intent/index.js +28 -2
  111. package/dist/esm/providers/ark.js +7 -0
  112. package/dist/esm/providers/delegator.js +62 -0
  113. package/dist/esm/providers/expoIndexer.js +5 -0
  114. package/dist/esm/providers/indexer.js +68 -1
  115. package/dist/esm/providers/onchain.js +2 -2
  116. package/dist/esm/providers/utils.js +1 -0
  117. package/dist/esm/repositories/contractRepository.js +1 -101
  118. package/dist/esm/repositories/inMemory/contractRepository.js +51 -0
  119. package/dist/esm/repositories/inMemory/walletRepository.js +76 -0
  120. package/dist/esm/repositories/index.js +8 -0
  121. package/dist/esm/repositories/indexedDB/contractRepository.js +183 -0
  122. package/dist/esm/repositories/indexedDB/db.js +42 -0
  123. package/dist/esm/repositories/indexedDB/schema.js +155 -0
  124. package/dist/esm/repositories/indexedDB/walletRepository.js +334 -0
  125. package/dist/esm/repositories/indexedDB/websqlAdapter.js +138 -0
  126. package/dist/esm/repositories/migrations/contractRepositoryImpl.js +121 -0
  127. package/dist/esm/repositories/migrations/fromStorageAdapter.js +58 -0
  128. package/dist/esm/repositories/migrations/walletRepositoryImpl.js +176 -0
  129. package/dist/esm/repositories/walletRepository.js +1 -167
  130. package/dist/esm/script/base.js +21 -1
  131. package/dist/esm/script/delegate.js +46 -0
  132. package/dist/esm/storage/asyncStorage.js +4 -1
  133. package/dist/esm/storage/fileSystem.js +3 -0
  134. package/dist/esm/storage/inMemory.js +3 -0
  135. package/dist/esm/storage/indexedDB.js +5 -1
  136. package/dist/esm/storage/localStorage.js +3 -0
  137. package/dist/esm/utils/arkTransaction.js +15 -0
  138. package/dist/esm/utils/transactionHistory.js +50 -0
  139. package/dist/esm/utils/txSizeEstimator.js +39 -14
  140. package/dist/esm/wallet/asset-manager.js +333 -0
  141. package/dist/esm/wallet/asset.js +111 -0
  142. package/dist/esm/wallet/batch.js +1 -1
  143. package/dist/esm/wallet/delegator.js +231 -0
  144. package/dist/esm/wallet/expo/background.js +128 -0
  145. package/dist/esm/wallet/expo/index.js +2 -0
  146. package/dist/esm/wallet/expo/wallet.js +194 -0
  147. package/dist/esm/wallet/onchain.js +57 -12
  148. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +564 -0
  149. package/dist/esm/wallet/serviceWorker/wallet.js +382 -101
  150. package/dist/esm/wallet/unroll.js +7 -2
  151. package/dist/esm/wallet/utils.js +55 -0
  152. package/dist/esm/wallet/validation.js +139 -0
  153. package/dist/esm/wallet/vtxo-manager.js +1 -1
  154. package/dist/esm/wallet/wallet.js +704 -229
  155. package/dist/esm/worker/browser/service-worker-manager.js +76 -0
  156. package/dist/esm/{wallet/serviceWorker → worker/browser}/utils.js +2 -1
  157. package/dist/esm/worker/expo/asyncStorageTaskQueue.js +74 -0
  158. package/dist/esm/worker/expo/index.js +4 -0
  159. package/dist/esm/worker/expo/processors/contractPollProcessor.js +58 -0
  160. package/dist/esm/worker/expo/processors/index.js +1 -0
  161. package/dist/esm/worker/expo/taskQueue.js +37 -0
  162. package/dist/esm/worker/expo/taskRunner.js +54 -0
  163. package/dist/esm/worker/messageBus.js +248 -0
  164. package/dist/types/adapters/expo-db.d.ts +7 -0
  165. package/dist/types/asset/assetGroup.d.ts +28 -0
  166. package/dist/types/asset/assetId.d.ts +19 -0
  167. package/dist/types/asset/assetInput.d.ts +46 -0
  168. package/dist/types/asset/assetOutput.d.ts +39 -0
  169. package/dist/types/asset/assetRef.d.ts +25 -0
  170. package/dist/types/asset/index.d.ts +8 -0
  171. package/dist/types/asset/metadata.d.ts +37 -0
  172. package/dist/types/asset/packet.d.ts +27 -0
  173. package/dist/types/asset/types.d.ts +18 -0
  174. package/dist/types/asset/utils.d.ts +21 -0
  175. package/dist/types/contracts/arkcontract.d.ts +101 -0
  176. package/dist/types/contracts/contractManager.d.ts +331 -0
  177. package/dist/types/contracts/contractWatcher.d.ts +192 -0
  178. package/dist/types/contracts/handlers/default.d.ts +19 -0
  179. package/dist/types/contracts/handlers/delegate.d.ts +21 -0
  180. package/dist/types/contracts/handlers/helpers.d.ts +18 -0
  181. package/dist/types/contracts/handlers/index.d.ts +7 -0
  182. package/dist/types/contracts/handlers/registry.d.ts +65 -0
  183. package/dist/types/contracts/handlers/vhtlc.d.ts +32 -0
  184. package/dist/types/contracts/index.d.ts +14 -0
  185. package/dist/types/contracts/types.d.ts +222 -0
  186. package/dist/types/db/manager.d.ts +22 -0
  187. package/dist/types/forfeit.d.ts +2 -1
  188. package/dist/types/identity/index.d.ts +1 -0
  189. package/dist/types/identity/seedIdentity.d.ts +128 -0
  190. package/dist/types/index.d.ts +21 -12
  191. package/dist/types/intent/index.d.ts +2 -1
  192. package/dist/types/providers/ark.d.ts +11 -2
  193. package/dist/types/providers/delegator.d.ts +29 -0
  194. package/dist/types/providers/indexer.d.ts +11 -1
  195. package/dist/types/repositories/contractRepository.d.ts +30 -19
  196. package/dist/types/repositories/inMemory/contractRepository.d.ts +17 -0
  197. package/dist/types/repositories/inMemory/walletRepository.d.ts +26 -0
  198. package/dist/types/repositories/index.d.ts +7 -0
  199. package/dist/types/repositories/indexedDB/contractRepository.d.ts +21 -0
  200. package/dist/types/repositories/indexedDB/db.d.ts +56 -0
  201. package/dist/types/repositories/indexedDB/schema.d.ts +8 -0
  202. package/dist/types/repositories/indexedDB/walletRepository.d.ts +25 -0
  203. package/dist/types/repositories/indexedDB/websqlAdapter.d.ts +49 -0
  204. package/dist/types/repositories/migrations/contractRepositoryImpl.d.ts +24 -0
  205. package/dist/types/repositories/migrations/fromStorageAdapter.d.ts +19 -0
  206. package/dist/types/repositories/migrations/walletRepositoryImpl.d.ts +27 -0
  207. package/dist/types/repositories/walletRepository.d.ts +13 -24
  208. package/dist/types/script/base.d.ts +1 -0
  209. package/dist/types/script/delegate.d.ts +36 -0
  210. package/dist/types/storage/asyncStorage.d.ts +4 -0
  211. package/dist/types/storage/fileSystem.d.ts +3 -0
  212. package/dist/types/storage/inMemory.d.ts +3 -0
  213. package/dist/types/storage/index.d.ts +3 -0
  214. package/dist/types/storage/indexedDB.d.ts +3 -0
  215. package/dist/types/storage/localStorage.d.ts +3 -0
  216. package/dist/types/utils/arkTransaction.d.ts +6 -0
  217. package/dist/types/utils/txSizeEstimator.d.ts +12 -2
  218. package/dist/types/wallet/asset-manager.d.ts +78 -0
  219. package/dist/types/wallet/asset.d.ts +21 -0
  220. package/dist/types/wallet/batch.d.ts +1 -1
  221. package/dist/types/wallet/delegator.d.ts +24 -0
  222. package/dist/types/wallet/expo/background.d.ts +66 -0
  223. package/dist/types/wallet/expo/index.d.ts +4 -0
  224. package/dist/types/wallet/expo/wallet.d.ts +97 -0
  225. package/dist/types/wallet/index.d.ts +75 -2
  226. package/dist/types/wallet/onchain.d.ts +22 -1
  227. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +366 -0
  228. package/dist/types/wallet/serviceWorker/wallet.d.ts +20 -11
  229. package/dist/types/wallet/utils.d.ts +13 -1
  230. package/dist/types/wallet/validation.d.ts +24 -0
  231. package/dist/types/wallet/wallet.d.ts +111 -17
  232. package/dist/types/worker/browser/service-worker-manager.d.ts +21 -0
  233. package/dist/types/{wallet/serviceWorker → worker/browser}/utils.d.ts +2 -1
  234. package/dist/types/worker/expo/asyncStorageTaskQueue.d.ts +46 -0
  235. package/dist/types/worker/expo/index.d.ts +7 -0
  236. package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +14 -0
  237. package/dist/types/worker/expo/processors/index.d.ts +1 -0
  238. package/dist/types/worker/expo/taskQueue.d.ts +50 -0
  239. package/dist/types/worker/expo/taskRunner.d.ts +42 -0
  240. package/dist/types/worker/messageBus.d.ts +109 -0
  241. package/package.json +71 -17
  242. package/dist/cjs/wallet/serviceWorker/request.js +0 -78
  243. package/dist/cjs/wallet/serviceWorker/response.js +0 -222
  244. package/dist/cjs/wallet/serviceWorker/worker.js +0 -655
  245. package/dist/esm/wallet/serviceWorker/request.js +0 -75
  246. package/dist/esm/wallet/serviceWorker/response.js +0 -219
  247. package/dist/esm/wallet/serviceWorker/worker.js +0 -651
  248. package/dist/types/wallet/serviceWorker/request.d.ts +0 -74
  249. package/dist/types/wallet/serviceWorker/response.d.ts +0 -123
  250. package/dist/types/wallet/serviceWorker/worker.d.ts +0 -53
@@ -1,43 +1,9 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.Wallet = exports.ReadonlyWallet = void 0;
37
- exports.getSequence = getSequence;
4
+ exports.selectVirtualCoins = selectVirtualCoins;
38
5
  exports.waitForIncomingFunds = waitForIncomingFunds;
39
6
  const base_1 = require("@scure/base");
40
- const bip68 = __importStar(require("bip68"));
41
7
  const payment_js_1 = require("@scure/btc-signer/payment.js");
42
8
  const btc_signer_1 = require("@scure/btc-signer");
43
9
  const utils_js_1 = require("@scure/btc-signer/utils.js");
@@ -48,7 +14,9 @@ const onchain_1 = require("../providers/onchain");
48
14
  const ark_1 = require("../providers/ark");
49
15
  const forfeit_1 = require("../forfeit");
50
16
  const validation_1 = require("../tree/validation");
17
+ const validation_2 = require("./validation");
51
18
  const _1 = require(".");
19
+ const asset_1 = require("./asset");
52
20
  const base_2 = require("../script/base");
53
21
  const tapscript_1 = require("../script/tapscript");
54
22
  const arkTransaction_1 = require("../utils/arkTransaction");
@@ -56,15 +24,18 @@ const vtxo_manager_1 = require("./vtxo-manager");
56
24
  const arknote_1 = require("../arknote");
57
25
  const intent_1 = require("../intent");
58
26
  const indexer_1 = require("../providers/indexer");
59
- const unknownFields_1 = require("../utils/unknownFields");
60
- const inMemory_1 = require("../storage/inMemory");
61
- const walletRepository_1 = require("../repositories/walletRepository");
62
- const contractRepository_1 = require("../repositories/contractRepository");
63
27
  const utils_1 = require("./utils");
64
28
  const errors_1 = require("../providers/errors");
65
29
  const batch_1 = require("./batch");
66
30
  const arkfee_1 = require("../arkfee");
67
31
  const transactionHistory_1 = require("../utils/transactionHistory");
32
+ const asset_manager_1 = require("./asset-manager");
33
+ const delegate_1 = require("../script/delegate");
34
+ const delegator_1 = require("./delegator");
35
+ const repositories_1 = require("../repositories");
36
+ const contractManager_1 = require("../contracts/contractManager");
37
+ const handlers_1 = require("../contracts/handlers");
38
+ const helpers_1 = require("../contracts/handlers/helpers");
68
39
  /**
69
40
  * Type guard function to check if an identity has a toReadonly method.
70
41
  */
@@ -75,7 +46,10 @@ function hasToReadonly(identity) {
75
46
  typeof identity.toReadonly === "function");
76
47
  }
77
48
  class ReadonlyWallet {
78
- constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository) {
49
+ get assetManager() {
50
+ return this._assetManager;
51
+ }
52
+ constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig) {
79
53
  this.identity = identity;
80
54
  this.network = network;
81
55
  this.onchainProvider = onchainProvider;
@@ -86,12 +60,15 @@ class ReadonlyWallet {
86
60
  this.dustAmount = dustAmount;
87
61
  this.walletRepository = walletRepository;
88
62
  this.contractRepository = contractRepository;
63
+ this.delegatorProvider = delegatorProvider;
64
+ this.watcherConfig = watcherConfig;
65
+ this._assetManager = new asset_manager_1.ReadonlyAssetManager(this.indexerProvider);
89
66
  }
90
67
  /**
91
68
  * Protected helper to set up shared wallet configuration.
92
69
  * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
93
70
  */
94
- static async setupWalletConfig(config, pubkey) {
71
+ static async setupWalletConfig(config, pubKey) {
95
72
  // Use provided arkProvider instance or create a new one from arkServerUrl
96
73
  const arkProvider = config.arkProvider ||
97
74
  (() => {
@@ -143,22 +120,26 @@ class ReadonlyWallet {
143
120
  };
144
121
  // Generate tapscripts for offchain and boarding address
145
122
  const serverPubKey = base_1.hex.decode(info.signerPubkey).slice(1);
146
- const bareVtxoTapscript = new default_1.DefaultVtxo.Script({
147
- pubKey: pubkey,
123
+ const delegatePubKey = config.delegatorProvider
124
+ ? await config.delegatorProvider
125
+ .getDelegateInfo()
126
+ .then((info) => base_1.hex.decode(info.pubkey).slice(1))
127
+ : undefined;
128
+ const offchainOptions = {
129
+ pubKey,
148
130
  serverPubKey,
149
131
  csvTimelock: exitTimelock,
150
- });
132
+ };
133
+ const offchainTapscript = !delegatePubKey
134
+ ? new default_1.DefaultVtxo.Script(offchainOptions)
135
+ : new delegate_1.DelegateVtxo.Script({ ...offchainOptions, delegatePubKey });
151
136
  const boardingTapscript = new default_1.DefaultVtxo.Script({
152
- pubKey: pubkey,
153
- serverPubKey,
137
+ ...offchainOptions,
154
138
  csvTimelock: boardingTimelock,
155
139
  });
156
- // Save tapscripts
157
- const offchainTapscript = bareVtxoTapscript;
158
- // Set up storage and repositories
159
- const storage = config.storage || new inMemory_1.InMemoryStorageAdapter();
160
- const walletRepository = new walletRepository_1.WalletRepositoryImpl(storage);
161
- const contractRepository = new contractRepository_1.ContractRepositoryImpl(storage);
140
+ const walletRepository = config.storage?.walletRepository ?? new repositories_1.IndexedDBWalletRepository();
141
+ const contractRepository = config.storage?.contractRepository ??
142
+ new repositories_1.IndexedDBContractRepository();
162
143
  return {
163
144
  arkProvider,
164
145
  indexerProvider,
@@ -172,6 +153,7 @@ class ReadonlyWallet {
172
153
  walletRepository,
173
154
  contractRepository,
174
155
  info,
156
+ delegatorProvider: config.delegatorProvider,
175
157
  };
176
158
  }
177
159
  static async create(config) {
@@ -180,11 +162,18 @@ class ReadonlyWallet {
180
162
  throw new Error("Invalid configured public key");
181
163
  }
182
164
  const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
183
- return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository);
165
+ 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);
184
166
  }
185
167
  get arkAddress() {
186
168
  return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
187
169
  }
170
+ /**
171
+ * Get the contract script for the wallet's default address.
172
+ * This is the pkScript hex, used to identify the wallet in ContractManager.
173
+ */
174
+ get defaultContractScript() {
175
+ return base_1.hex.encode(this.offchainTapscript.pkScript);
176
+ }
188
177
  async getAddress() {
189
178
  return this.arkAddress.encode();
190
179
  }
@@ -222,6 +211,22 @@ class ReadonlyWallet {
222
211
  .reduce((sum, coin) => sum + coin.value, 0);
223
212
  const totalBoarding = confirmed + unconfirmed;
224
213
  const totalOffchain = settled + preconfirmed + recoverable;
214
+ // aggregate asset balances from spendable vtxos
215
+ const assetBalances = new Map();
216
+ for (const vtxo of vtxos) {
217
+ if (!(0, _1.isSpendable)(vtxo))
218
+ continue;
219
+ if (vtxo.assets) {
220
+ for (const a of vtxo.assets) {
221
+ const current = assetBalances.get(a.assetId) ?? 0;
222
+ assetBalances.set(a.assetId, current + a.amount);
223
+ }
224
+ }
225
+ }
226
+ const assets = Array.from(assetBalances.entries()).map(([assetId, amount]) => ({
227
+ assetId,
228
+ amount,
229
+ }));
225
230
  return {
226
231
  boarding: {
227
232
  confirmed,
@@ -233,20 +238,39 @@ class ReadonlyWallet {
233
238
  available: settled + preconfirmed,
234
239
  recoverable,
235
240
  total: totalBoarding + totalOffchain,
241
+ assets,
236
242
  };
237
243
  }
238
244
  async getVtxos(filter) {
239
245
  const address = await this.getAddress();
240
- // Try to get from cache first first (optional fast path)
241
- // const cachedVtxos = await this.walletRepository.getVtxos(address);
242
- // if (cachedVtxos.length) return cachedVtxos;
243
- // For now, always fetch fresh data from provider and update cache
244
- // In future, we can add cache invalidation logic based on timestamps
245
- const vtxos = await this.getVirtualCoins(filter);
246
- const extendedVtxos = vtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo));
246
+ const scriptMap = await this.getScriptMap();
247
+ const f = filter ?? { withRecoverable: true, withUnrolled: false };
248
+ const allExtended = [];
249
+ // Query each script separately so we can extend VTXOs with the correct tapscript
250
+ for (const [scriptHex, vtxoScript] of scriptMap) {
251
+ const response = await this.indexerProvider.getVtxos({
252
+ scripts: [scriptHex],
253
+ });
254
+ let vtxos = response.vtxos.filter(_1.isSpendable);
255
+ if (!f.withRecoverable) {
256
+ vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo) && !(0, _1.isExpired)(vtxo));
257
+ }
258
+ if (f.withUnrolled) {
259
+ const spentVtxos = response.vtxos.filter((vtxo) => !(0, _1.isSpendable)(vtxo));
260
+ vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
261
+ }
262
+ for (const vtxo of vtxos) {
263
+ allExtended.push({
264
+ ...vtxo,
265
+ forfeitTapLeafScript: vtxoScript.forfeit(),
266
+ intentTapLeafScript: vtxoScript.forfeit(),
267
+ tapTree: vtxoScript.encode(),
268
+ });
269
+ }
270
+ }
247
271
  // Update cache with fresh data
248
- await this.walletRepository.saveVtxos(address, extendedVtxos);
249
- return extendedVtxos;
272
+ await this.walletRepository.saveVtxos(address, allExtended);
273
+ return allExtended;
250
274
  }
251
275
  async getVirtualCoins(filter = { withRecoverable: true, withUnrolled: false }) {
252
276
  const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
@@ -264,9 +288,8 @@ class ReadonlyWallet {
264
288
  return vtxos;
265
289
  }
266
290
  async getTransactionHistory() {
267
- const response = await this.indexerProvider.getVtxos({
268
- scripts: [base_1.hex.encode(this.offchainTapscript.pkScript)],
269
- });
291
+ const scripts = await this.getWalletScripts();
292
+ const response = await this.indexerProvider.getVtxos({ scripts });
270
293
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
271
294
  const getTxCreatedAt = (txid) => this.indexerProvider
272
295
  .getVtxos({ outpoints: [{ txid, vout: 0 }] })
@@ -376,17 +399,20 @@ class ReadonlyWallet {
376
399
  });
377
400
  }
378
401
  if (this.indexerProvider && arkAddress) {
379
- const offchainScript = this.offchainTapscript;
380
- const subscriptionId = await this.indexerProvider.subscribeForScripts([
381
- base_1.hex.encode(offchainScript.pkScript),
382
- ]);
402
+ const walletScripts = await this.getWalletScripts();
403
+ const subscriptionId = await this.indexerProvider.subscribeForScripts(walletScripts);
383
404
  const abortController = new AbortController();
384
405
  const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
385
406
  indexerStopFunc = async () => {
386
407
  abortController.abort();
387
408
  await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
388
409
  };
389
- // Handle subscription updates asynchronously without blocking
410
+ // Handle subscription updates asynchronously without blocking.
411
+ // Note: subscription covers all wallet scripts (default + delegate),
412
+ // but we can't determine which script each VTXO belongs to from the
413
+ // subscription event. VTXOs are extended with the current offchainTapscript;
414
+ // this is for notification/display only — not for spending.
415
+ // For correct extension metadata, use getVtxos() which queries per-script.
390
416
  (async () => {
391
417
  try {
392
418
  for await (const update of subscription) {
@@ -413,7 +439,7 @@ class ReadonlyWallet {
413
439
  }
414
440
  async fetchPendingTxs() {
415
441
  // get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
416
- const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
442
+ const scripts = await this.getWalletScripts();
417
443
  let { vtxos } = await this.indexerProvider.getVtxos({
418
444
  scripts,
419
445
  });
@@ -423,6 +449,183 @@ class ReadonlyWallet {
423
449
  vtxo.arkTxId !== undefined)
424
450
  .map((_) => _.arkTxId);
425
451
  }
452
+ // ========================================================================
453
+ // Multi-script support (default + delegate addresses)
454
+ // ========================================================================
455
+ /**
456
+ * Get all pkScript hex strings for the wallet's own addresses
457
+ * (both delegate and non-delegate, current and historical).
458
+ * Falls back to only the current script if ContractManager is not yet initialized.
459
+ */
460
+ async getWalletScripts() {
461
+ if (this._contractManager) {
462
+ try {
463
+ const contracts = await this._contractManager.getContracts({
464
+ type: ["default", "delegate"],
465
+ });
466
+ if (contracts.length > 0) {
467
+ return contracts.map((c) => c.script);
468
+ }
469
+ }
470
+ catch {
471
+ // fall through to current script only
472
+ }
473
+ }
474
+ return [base_1.hex.encode(this.offchainTapscript.pkScript)];
475
+ }
476
+ /**
477
+ * Build a map of scriptHex → VtxoScript for all wallet contracts,
478
+ * so VTXOs can be extended with the correct tapscript per contract.
479
+ */
480
+ async getScriptMap() {
481
+ const map = new Map();
482
+ // Always include the current script
483
+ const currentScriptHex = base_1.hex.encode(this.offchainTapscript.pkScript);
484
+ map.set(currentScriptHex, this.offchainTapscript);
485
+ if (this._contractManager) {
486
+ try {
487
+ const contracts = await this._contractManager.getContracts({
488
+ type: ["default", "delegate"],
489
+ });
490
+ for (const contract of contracts) {
491
+ if (map.has(contract.script))
492
+ continue;
493
+ const handler = handlers_1.contractHandlers.get(contract.type);
494
+ if (handler) {
495
+ const script = handler.createScript(contract.params);
496
+ map.set(contract.script, script);
497
+ }
498
+ }
499
+ }
500
+ catch {
501
+ // ContractManager error — only current script in map
502
+ }
503
+ }
504
+ return map;
505
+ }
506
+ // ========================================================================
507
+ // Contract Management
508
+ // ========================================================================
509
+ /**
510
+ * Get the ContractManager for managing contracts including the wallet's default address.
511
+ *
512
+ * The ContractManager handles:
513
+ * - The wallet's default receiving address (as a "default" contract)
514
+ * - External contracts (Boltz swaps, HTLCs, etc.)
515
+ * - Multi-contract watching with resilient connections
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * const manager = await wallet.getContractManager();
520
+ *
521
+ * // Create a contract for a Boltz swap
522
+ * const contract = await manager.createContract({
523
+ * label: "Boltz Swap",
524
+ * type: "vhtlc",
525
+ * params: { ... },
526
+ * script: swapScript,
527
+ * address: swapAddress,
528
+ * });
529
+ *
530
+ * // Start watching for events (includes wallet's default address)
531
+ * const stop = await manager.onContractEvent((event) => {
532
+ * console.log(`${event.type} on ${event.contractScript}`);
533
+ * });
534
+ * ```
535
+ */
536
+ async getContractManager() {
537
+ // Return existing manager if already initialized
538
+ if (this._contractManager) {
539
+ return this._contractManager;
540
+ }
541
+ // If initialization is in progress, wait for it
542
+ if (this._contractManagerInitializing) {
543
+ return this._contractManagerInitializing;
544
+ }
545
+ // Start initialization and store the promise
546
+ this._contractManagerInitializing = this.initializeContractManager();
547
+ try {
548
+ const manager = await this._contractManagerInitializing;
549
+ this._contractManager = manager;
550
+ return manager;
551
+ }
552
+ catch (error) {
553
+ // Clear the initializing promise so subsequent calls can retry
554
+ this._contractManagerInitializing = undefined;
555
+ throw error;
556
+ }
557
+ finally {
558
+ // Clear the initializing promise after completion
559
+ this._contractManagerInitializing = undefined;
560
+ }
561
+ }
562
+ async initializeContractManager() {
563
+ const manager = await contractManager_1.ContractManager.create({
564
+ indexerProvider: this.indexerProvider,
565
+ contractRepository: this.contractRepository,
566
+ walletRepository: this.walletRepository,
567
+ getDefaultAddress: () => this.getAddress(),
568
+ watcherConfig: this.watcherConfig,
569
+ });
570
+ // Register the wallet's current address as a contract
571
+ const csvTimelock = this.offchainTapscript.options.csvTimelock ??
572
+ default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK;
573
+ const csvTimelockStr = (0, helpers_1.timelockToSequence)(csvTimelock).toString();
574
+ const isDelegateScript = this.offchainTapscript instanceof delegate_1.DelegateVtxo.Script;
575
+ if (isDelegateScript) {
576
+ const delegateScript = this
577
+ .offchainTapscript;
578
+ // Register the delegate contract (current address)
579
+ await manager.createContract({
580
+ type: "delegate",
581
+ params: {
582
+ pubKey: base_1.hex.encode(delegateScript.options.pubKey),
583
+ serverPubKey: base_1.hex.encode(delegateScript.options.serverPubKey),
584
+ delegatePubKey: base_1.hex.encode(delegateScript.options.delegatePubKey),
585
+ csvTimelock: csvTimelockStr,
586
+ },
587
+ script: this.defaultContractScript,
588
+ address: await this.getAddress(),
589
+ state: "active",
590
+ });
591
+ // Also register the non-delegate version so old VTXOs remain visible
592
+ const nonDelegateScript = new default_1.DefaultVtxo.Script({
593
+ pubKey: delegateScript.options.pubKey,
594
+ serverPubKey: delegateScript.options.serverPubKey,
595
+ csvTimelock,
596
+ });
597
+ await manager.createContract({
598
+ type: "default",
599
+ params: {
600
+ pubKey: base_1.hex.encode(delegateScript.options.pubKey),
601
+ serverPubKey: base_1.hex.encode(delegateScript.options.serverPubKey),
602
+ csvTimelock: csvTimelockStr,
603
+ },
604
+ script: base_1.hex.encode(nonDelegateScript.pkScript),
605
+ address: nonDelegateScript
606
+ .address(this.network.hrp, this.arkServerPublicKey)
607
+ .encode(),
608
+ state: "active",
609
+ });
610
+ }
611
+ else {
612
+ // Register the default contract (current address)
613
+ await manager.createContract({
614
+ type: "default",
615
+ params: {
616
+ pubKey: base_1.hex.encode(this.offchainTapscript.options.pubKey),
617
+ serverPubKey: base_1.hex.encode(this.offchainTapscript.options.serverPubKey),
618
+ csvTimelock: csvTimelockStr,
619
+ },
620
+ script: this.defaultContractScript,
621
+ address: await this.getAddress(),
622
+ state: "active",
623
+ });
624
+ // Any old "delegate" contract from a prior wallet incarnation
625
+ // is already loaded by ContractManager.initialize() from ContractRepository
626
+ }
627
+ return manager;
628
+ }
426
629
  }
427
630
  exports.ReadonlyWallet = ReadonlyWallet;
428
631
  /**
@@ -459,8 +662,8 @@ exports.ReadonlyWallet = ReadonlyWallet;
459
662
  * ```
460
663
  */
461
664
  class Wallet extends ReadonlyWallet {
462
- constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
463
- super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository);
665
+ constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig, delegatorProvider, watcherConfig) {
666
+ super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
464
667
  this.networkName = networkName;
465
668
  this.arkProvider = arkProvider;
466
669
  this.serverUnrollScript = serverUnrollScript;
@@ -472,6 +675,13 @@ class Wallet extends ReadonlyWallet {
472
675
  ...vtxo_manager_1.DEFAULT_RENEWAL_CONFIG,
473
676
  ...renewalConfig,
474
677
  };
678
+ this.delegatorManager = delegatorProvider
679
+ ? new delegator_1.DelegatorManagerImpl(delegatorProvider, arkProvider, identity)
680
+ : undefined;
681
+ }
682
+ get assetManager() {
683
+ this._walletAssetManager ?? (this._walletAssetManager = new asset_manager_1.AssetManager(this));
684
+ return this._walletAssetManager;
475
685
  }
476
686
  static async create(config) {
477
687
  const pubkey = await config.identity.xOnlyPublicKey();
@@ -494,7 +704,7 @@ class Wallet extends ReadonlyWallet {
494
704
  const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
495
705
  const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
496
706
  const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
497
- return new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig);
707
+ return new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig);
498
708
  }
499
709
  /**
500
710
  * Convert this wallet to a readonly wallet.
@@ -518,21 +728,16 @@ class Wallet extends ReadonlyWallet {
518
728
  const readonlyIdentity = hasToReadonly(this.identity)
519
729
  ? await this.identity.toReadonly()
520
730
  : this.identity; // Identity extends ReadonlyIdentity, so this is safe
521
- return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository);
731
+ 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);
522
732
  }
523
733
  async sendBitcoin(params) {
524
734
  if (params.amount <= 0) {
525
735
  throw new Error("Amount must be positive");
526
736
  }
527
- if (!isValidArkAddress(params.address)) {
737
+ if (!(0, arkTransaction_1.isValidArkAddress)(params.address)) {
528
738
  throw new Error("Invalid Ark address " + params.address);
529
739
  }
530
- // recoverable and subdust coins can't be spent in offchain tx
531
- const virtualCoins = await this.getVirtualCoins({
532
- withRecoverable: false,
533
- });
534
- let selected;
535
- if (params.selectedVtxos) {
740
+ if (params.selectedVtxos && params.selectedVtxos.length > 0) {
536
741
  const selectedVtxoSum = params.selectedVtxos
537
742
  .map((v) => v.value)
538
743
  .reduce((a, b) => a + b, 0);
@@ -540,125 +745,38 @@ class Wallet extends ReadonlyWallet {
540
745
  throw new Error("Selected VTXOs do not cover specified amount");
541
746
  }
542
747
  const changeAmount = selectedVtxoSum - params.amount;
543
- selected = {
748
+ const selected = {
544
749
  inputs: params.selectedVtxos,
545
750
  changeAmount: BigInt(changeAmount),
546
751
  };
547
- }
548
- else {
549
- selected = selectVirtualCoins(virtualCoins, params.amount);
550
- }
551
- const selectedLeaf = this.offchainTapscript.forfeit();
552
- if (!selectedLeaf) {
553
- throw new Error("Selected leaf not found");
554
- }
555
- const outputAddress = address_1.ArkAddress.decode(params.address);
556
- const outputScript = BigInt(params.amount) < this.dustAmount
557
- ? outputAddress.subdustPkScript
558
- : outputAddress.pkScript;
559
- const outputs = [
560
- {
561
- script: outputScript,
562
- amount: BigInt(params.amount),
563
- },
564
- ];
565
- // add change output if needed
566
- if (selected.changeAmount > 0n) {
567
- const changeOutputScript = selected.changeAmount < this.dustAmount
568
- ? this.arkAddress.subdustPkScript
569
- : this.arkAddress.pkScript;
570
- outputs.push({
571
- script: changeOutputScript,
572
- amount: BigInt(selected.changeAmount),
573
- });
574
- }
575
- const tapTree = this.offchainTapscript.encode();
576
- const offchainTx = (0, arkTransaction_1.buildOffchainTx)(selected.inputs.map((input) => ({
577
- ...input,
578
- tapLeafScript: selectedLeaf,
579
- tapTree,
580
- })), outputs, this.serverUnrollScript);
581
- const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
582
- const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base_1.base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base_1.base64.encode(c.toPSBT())));
583
- // sign the checkpoints
584
- const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
585
- const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
586
- const signedCheckpoint = await this.identity.sign(tx);
587
- return base_1.base64.encode(signedCheckpoint.toPSBT());
588
- }));
589
- await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
590
- try {
591
- // mark VTXOs as spent and optionally add the change VTXO
592
- const spentVtxos = [];
593
- const commitmentTxIds = new Set();
594
- let batchExpiry = Number.MAX_SAFE_INTEGER;
595
- for (const [inputIndex, input] of selected.inputs.entries()) {
596
- const vtxo = (0, utils_1.extendVirtualCoin)(this, input);
597
- const checkpointB64 = signedCheckpointTxs[inputIndex];
598
- const checkpoint = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(checkpointB64));
599
- spentVtxos.push({
600
- ...vtxo,
601
- virtualStatus: { ...vtxo.virtualStatus, state: "spent" },
602
- spentBy: checkpoint.id,
603
- arkTxId: arkTxid,
604
- isSpent: true,
605
- });
606
- if (vtxo.virtualStatus.commitmentTxIds) {
607
- for (const commitmentTxId of vtxo.virtualStatus
608
- .commitmentTxIds) {
609
- commitmentTxIds.add(commitmentTxId);
610
- }
611
- }
612
- if (vtxo.virtualStatus.batchExpiry) {
613
- batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
614
- }
615
- }
616
- const createdAt = Date.now();
617
- const addr = this.arkAddress.encode();
618
- if (selected.changeAmount > 0n &&
619
- batchExpiry !== Number.MAX_SAFE_INTEGER) {
620
- const changeVtxo = {
621
- txid: arkTxid,
622
- vout: outputs.length - 1,
623
- createdAt: new Date(createdAt),
624
- forfeitTapLeafScript: this.offchainTapscript.forfeit(),
625
- intentTapLeafScript: this.offchainTapscript.forfeit(),
626
- isUnrolled: false,
627
- isSpent: false,
628
- tapTree: this.offchainTapscript.encode(),
629
- value: Number(selected.changeAmount),
630
- virtualStatus: {
631
- state: "preconfirmed",
632
- commitmentTxIds: Array.from(commitmentTxIds),
633
- batchExpiry,
634
- },
635
- status: {
636
- confirmed: false,
637
- },
638
- };
639
- await this.walletRepository.saveVtxos(addr, [changeVtxo]);
640
- }
641
- await this.walletRepository.saveVtxos(addr, spentVtxos);
642
- await this.walletRepository.saveTransactions(addr, [
752
+ const outputAddress = address_1.ArkAddress.decode(params.address);
753
+ const outputScript = BigInt(params.amount) < this.dustAmount
754
+ ? outputAddress.subdustPkScript
755
+ : outputAddress.pkScript;
756
+ const outputs = [
643
757
  {
644
- key: {
645
- boardingTxid: "",
646
- commitmentTxid: "",
647
- arkTxid: arkTxid,
648
- },
649
- amount: params.amount,
650
- type: _1.TxType.TxSent,
651
- settled: false,
652
- createdAt: Date.now(),
758
+ script: outputScript,
759
+ amount: BigInt(params.amount),
653
760
  },
654
- ]);
655
- }
656
- catch (e) {
657
- console.warn("error saving offchain tx to repository", e);
658
- }
659
- finally {
761
+ ];
762
+ // add change output if needed
763
+ if (selected.changeAmount > 0n) {
764
+ const changeOutputScript = selected.changeAmount < this.dustAmount
765
+ ? this.arkAddress.subdustPkScript
766
+ : this.arkAddress.pkScript;
767
+ outputs.push({
768
+ script: changeOutputScript,
769
+ amount: BigInt(selected.changeAmount),
770
+ });
771
+ }
772
+ const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
773
+ await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
660
774
  return arkTxid;
661
775
  }
776
+ return this.send({
777
+ address: params.address,
778
+ amount: params.amount,
779
+ });
662
780
  }
663
781
  async settle(params, eventCallback) {
664
782
  if (params?.inputs) {
@@ -759,6 +877,49 @@ class Wallet extends ReadonlyWallet {
759
877
  script,
760
878
  });
761
879
  }
880
+ // if some of the inputs hold assets, build the asset packet and append as output
881
+ // in the intent proof tx, there is a "fake" input at index 0
882
+ // so the real coin indices are offset by +1
883
+ const assetInputs = new Map();
884
+ for (let i = 0; i < params.inputs.length; i++) {
885
+ if ("assets" in params.inputs[i]) {
886
+ const assets = params.inputs[i]
887
+ .assets;
888
+ if (assets && assets.length > 0) {
889
+ assetInputs.set(i + 1, assets);
890
+ }
891
+ }
892
+ }
893
+ let outputAssets;
894
+ let assetOutputIndex; // where to send the assets
895
+ if (assetInputs.size > 0) {
896
+ // collect all input assets and assign them to the first offchain output
897
+ const allAssets = new Map();
898
+ for (const [, assets] of assetInputs) {
899
+ for (const asset of assets) {
900
+ const existing = allAssets.get(asset.assetId) ?? 0n;
901
+ allAssets.set(asset.assetId, existing + BigInt(asset.amount));
902
+ }
903
+ }
904
+ outputAssets = [];
905
+ for (const [assetId, amount] of allAssets) {
906
+ outputAssets.push({ assetId, amount: Number(amount) });
907
+ }
908
+ const firstOffchainIndex = params.outputs.findIndex((_, i) => !onchainOutputIndexes.includes(i));
909
+ if (firstOffchainIndex === -1) {
910
+ throw new Error("Cannot settle assets without an offchain output");
911
+ }
912
+ assetOutputIndex = firstOffchainIndex;
913
+ }
914
+ const recipients = params.outputs.map((output, i) => ({
915
+ address: output.address,
916
+ amount: Number(output.amount),
917
+ assets: i === assetOutputIndex ? outputAssets : undefined,
918
+ }));
919
+ if (outputAssets && outputAssets.length > 0) {
920
+ const assetPacket = (0, asset_1.createAssetPacket)(assetInputs, recipients);
921
+ outputs.push(assetPacket.txOut());
922
+ }
762
923
  // session holds the state of the musig2 signing process of the vtxo tree
763
924
  let session;
764
925
  const signingPublicKeys = [];
@@ -775,17 +936,19 @@ class Wallet extends ReadonlyWallet {
775
936
  ...signingPublicKeys,
776
937
  ...params.inputs.map((input) => `${input.txid}:${input.vout}`),
777
938
  ];
778
- const handler = this.createBatchHandler(intentId, params.inputs, session);
939
+ const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
779
940
  const abortController = new AbortController();
780
941
  try {
781
942
  const stream = this.arkProvider.getEventStream(abortController.signal, topics);
782
- return await batch_1.Batch.join(stream, handler, {
943
+ const commitmentTxid = await batch_1.Batch.join(stream, handler, {
783
944
  abortController,
784
945
  skipVtxoTreeSigning: !hasOffchainOutputs,
785
946
  eventCallback: eventCallback
786
947
  ? (event) => Promise.resolve(eventCallback(event))
787
948
  : undefined,
788
949
  });
950
+ await this.updateDbAfterSettle(params.inputs, commitmentTxid);
951
+ return commitmentTxid;
789
952
  }
790
953
  catch (error) {
791
954
  // delete the intent to not be stuck in the queue
@@ -890,8 +1053,9 @@ class Wallet extends ReadonlyWallet {
890
1053
  * @param intentId - The intent ID.
891
1054
  * @param inputs - The inputs of the intent.
892
1055
  * @param session - The musig2 signing session, if not provided, the signing will be skipped.
1056
+ * @param expectedRecipients - Expected recipients to validate in the vtxo tree.
893
1057
  */
894
- createBatchHandler(intentId, inputs, session) {
1058
+ createBatchHandler(intentId, inputs, expectedRecipients, session) {
895
1059
  let sweepTapTreeRoot;
896
1060
  return {
897
1061
  onBatchStarted: async (event) => {
@@ -939,7 +1103,10 @@ class Wallet extends ReadonlyWallet {
939
1103
  // validate the unsigned vtxo tree
940
1104
  const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
941
1105
  (0, validation_1.validateVtxoTxGraph)(vtxoTree, commitmentTx, sweepTapTreeRoot);
942
- // TODO check if our registered outputs are in the vtxo tree
1106
+ // validate that all expected receivers are in the vtxo tree with correct amounts and assets
1107
+ if (expectedRecipients && expectedRecipients.length > 0) {
1108
+ (0, validation_2.validateBatchRecipients)(commitmentTx, vtxoTree.leaves(), expectedRecipients, this.network);
1109
+ }
943
1110
  const sharedOutput = commitmentTx.getOutput(0);
944
1111
  if (!sharedOutput?.amount) {
945
1112
  throw new Error("Shared output not found");
@@ -995,16 +1162,15 @@ class Wallet extends ReadonlyWallet {
995
1162
  throw error;
996
1163
  }
997
1164
  }
998
- async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
999
- const inputs = this.prepareIntentProofInputs(coins);
1165
+ async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
1000
1166
  const message = {
1001
1167
  type: "register",
1002
1168
  onchain_output_indexes: onchainOutputsIndexes,
1003
- valid_at: 0,
1169
+ valid_at: validAt ? Math.floor(validAt) : 0,
1004
1170
  expire_at: 0,
1005
1171
  cosigners_public_keys: cosignerPubKeys,
1006
1172
  };
1007
- const proof = intent_1.Intent.create(message, inputs, outputs);
1173
+ const proof = intent_1.Intent.create(message, coins, outputs);
1008
1174
  const signedProof = await this.identity.sign(proof);
1009
1175
  return {
1010
1176
  proof: base_1.base64.encode(signedProof.toPSBT()),
@@ -1012,25 +1178,23 @@ class Wallet extends ReadonlyWallet {
1012
1178
  };
1013
1179
  }
1014
1180
  async makeDeleteIntentSignature(coins) {
1015
- const inputs = this.prepareIntentProofInputs(coins);
1016
1181
  const message = {
1017
1182
  type: "delete",
1018
1183
  expire_at: 0,
1019
1184
  };
1020
- const proof = intent_1.Intent.create(message, inputs, []);
1185
+ const proof = intent_1.Intent.create(message, coins, []);
1021
1186
  const signedProof = await this.identity.sign(proof);
1022
1187
  return {
1023
1188
  proof: base_1.base64.encode(signedProof.toPSBT()),
1024
1189
  message,
1025
1190
  };
1026
1191
  }
1027
- async makeGetPendingTxIntentSignature(vtxos) {
1028
- const inputs = this.prepareIntentProofInputs(vtxos);
1192
+ async makeGetPendingTxIntentSignature(coins) {
1029
1193
  const message = {
1030
1194
  type: "get-pending-tx",
1031
1195
  expire_at: 0,
1032
1196
  };
1033
- const proof = intent_1.Intent.create(message, inputs, []);
1197
+ const proof = intent_1.Intent.create(message, coins, []);
1034
1198
  const signedProof = await this.identity.sign(proof);
1035
1199
  return {
1036
1200
  proof: base_1.base64.encode(signedProof.toPSBT()),
@@ -1045,17 +1209,28 @@ class Wallet extends ReadonlyWallet {
1045
1209
  async finalizePendingTxs(vtxos) {
1046
1210
  const MAX_INPUTS_PER_INTENT = 20;
1047
1211
  if (!vtxos || vtxos.length === 0) {
1048
- // get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
1049
- const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
1050
- let { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1051
- scripts,
1052
- });
1053
- fetchedVtxos = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1054
- vtxo.virtualStatus.state !== "settled");
1055
- if (fetchedVtxos.length === 0) {
1212
+ // Query per-script so each VTXO is extended with the correct tapscript
1213
+ const scriptMap = await this.getScriptMap();
1214
+ const allExtended = [];
1215
+ for (const [scriptHex, vtxoScript] of scriptMap) {
1216
+ const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1217
+ scripts: [scriptHex],
1218
+ });
1219
+ const pending = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1220
+ vtxo.virtualStatus.state !== "settled");
1221
+ for (const vtxo of pending) {
1222
+ allExtended.push({
1223
+ ...vtxo,
1224
+ forfeitTapLeafScript: vtxoScript.forfeit(),
1225
+ intentTapLeafScript: vtxoScript.forfeit(),
1226
+ tapTree: vtxoScript.encode(),
1227
+ });
1228
+ }
1229
+ }
1230
+ if (allExtended.length === 0) {
1056
1231
  return { finalized: [], pending: [] };
1057
1232
  }
1058
- vtxos = fetchedVtxos.map((v) => (0, utils_1.extendVirtualCoin)(this, v));
1233
+ vtxos = allExtended;
1059
1234
  }
1060
1235
  const finalized = [];
1061
1236
  const pending = [];
@@ -1084,60 +1259,327 @@ class Wallet extends ReadonlyWallet {
1084
1259
  }
1085
1260
  return { finalized, pending };
1086
1261
  }
1087
- prepareIntentProofInputs(coins) {
1088
- const inputs = [];
1089
- for (const input of coins) {
1090
- const vtxoScript = base_2.VtxoScript.decode(input.tapTree);
1091
- const sequence = getSequence(input.intentTapLeafScript);
1092
- const unknown = [unknownFields_1.VtxoTaprootTree.encode(input.tapTree)];
1093
- if (input.extraWitness) {
1094
- unknown.push(unknownFields_1.ConditionWitness.encode(input.extraWitness));
1262
+ /**
1263
+ * Send BTC and/or assets to one or more recipients.
1264
+ *
1265
+ * @param recipients - Array of recipients with their addresses, BTC amounts, and assets
1266
+ * @returns Promise resolving to the ark transaction ID
1267
+ *
1268
+ * @example
1269
+ * ```typescript
1270
+ * const txid = await wallet.send({
1271
+ * address: 'ark1...',
1272
+ * amount: 1000, // (optional, default to dust) btc amount to send to the output
1273
+ * assets: [{ assetId: 'abc123...', amount: 50 }] // (optional) list of assets to send
1274
+ * });
1275
+ * ```
1276
+ */
1277
+ async send(...args) {
1278
+ if (args.length === 0) {
1279
+ throw new Error("At least one receiver is required");
1280
+ }
1281
+ // validate recipients and populate undefined amount with dust amount
1282
+ const recipients = (0, utils_1.validateRecipients)(args, Number(this.dustAmount));
1283
+ const address = await this.getAddress();
1284
+ const outputAddress = address_1.ArkAddress.decode(address);
1285
+ const virtualCoins = await this.getVirtualCoins({
1286
+ withRecoverable: false,
1287
+ });
1288
+ // keep track of asset changes
1289
+ const assetChanges = new Map();
1290
+ let selectedCoins = [];
1291
+ let btcAmountToSelect = 0;
1292
+ for (const recipient of recipients) {
1293
+ btcAmountToSelect += Math.max(recipient.amount, Number(this.dustAmount));
1294
+ }
1295
+ // select assets
1296
+ for (const recipient of recipients) {
1297
+ if (!recipient.assets) {
1298
+ continue;
1095
1299
  }
1096
- inputs.push({
1097
- txid: base_1.hex.decode(input.txid),
1098
- index: input.vout,
1099
- witnessUtxo: {
1100
- amount: BigInt(input.value),
1101
- script: vtxoScript.pkScript,
1102
- },
1103
- sequence,
1104
- tapLeafScript: [input.intentTapLeafScript],
1105
- unknown,
1300
+ for (const receiverAsset of recipient.assets) {
1301
+ let amountToSelect = BigInt(receiverAsset.amount);
1302
+ // check if existing change covers the needed amount
1303
+ const existingChange = assetChanges.get(receiverAsset.assetId) ?? 0n;
1304
+ if (existingChange >= amountToSelect) {
1305
+ assetChanges.set(receiverAsset.assetId, existingChange - amountToSelect);
1306
+ if (assetChanges.get(receiverAsset.assetId) === 0n) {
1307
+ assetChanges.delete(receiverAsset.assetId);
1308
+ }
1309
+ continue;
1310
+ }
1311
+ if (existingChange > 0n) {
1312
+ amountToSelect -= existingChange;
1313
+ assetChanges.delete(receiverAsset.assetId);
1314
+ }
1315
+ const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1316
+ const { selected, totalAssetAmount } = (0, asset_1.selectCoinsWithAsset)(availableCoins, receiverAsset.assetId, amountToSelect);
1317
+ for (const coin of selected) {
1318
+ selectedCoins.push(coin);
1319
+ // asset coins contain btc, subtract from total amount to select
1320
+ btcAmountToSelect -= coin.value;
1321
+ // coin may contain other assets, add them to asset changes
1322
+ if (coin.assets) {
1323
+ for (const a of coin.assets) {
1324
+ if (a.assetId === receiverAsset.assetId) {
1325
+ continue;
1326
+ }
1327
+ const existing = assetChanges.get(a.assetId) ?? 0n;
1328
+ assetChanges.set(a.assetId, existing + BigInt(a.amount));
1329
+ }
1330
+ }
1331
+ }
1332
+ const assetChangeAmount = totalAssetAmount - amountToSelect;
1333
+ if (assetChangeAmount > 0n) {
1334
+ const existing = assetChanges.get(receiverAsset.assetId) ?? 0n;
1335
+ assetChanges.set(receiverAsset.assetId, existing + assetChangeAmount);
1336
+ }
1337
+ }
1338
+ }
1339
+ // select remaining btc
1340
+ if (btcAmountToSelect > 0) {
1341
+ const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1342
+ const { inputs: btcCoins } = selectVirtualCoins(availableCoins, btcAmountToSelect);
1343
+ // some coins may contain assets, add them to asset changes
1344
+ for (const coin of btcCoins) {
1345
+ if (coin.assets) {
1346
+ for (const asset of coin.assets) {
1347
+ const existing = assetChanges.get(asset.assetId) ?? 0n;
1348
+ assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
1349
+ }
1350
+ }
1351
+ }
1352
+ selectedCoins = [...selectedCoins, ...btcCoins];
1353
+ }
1354
+ let totalBtcSelected = selectedCoins.reduce((sum, c) => sum + c.value, 0);
1355
+ // build tx outputs
1356
+ const outputs = recipients.map((recipient) => ({
1357
+ script: recipient.script,
1358
+ amount: BigInt(recipient.amount),
1359
+ }));
1360
+ const totalBtcOutput = outputs.reduce((sum, o) => sum + Number(o.amount), 0);
1361
+ let changeAmount = totalBtcSelected - totalBtcOutput;
1362
+ // enforce minimum change amount when there are asset changes
1363
+ if (assetChanges.size > 0 && changeAmount < Number(this.dustAmount)) {
1364
+ const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1365
+ const { inputs: extraCoins } = selectVirtualCoins(availableCoins, Number(this.dustAmount) - changeAmount);
1366
+ for (const coin of extraCoins) {
1367
+ if (coin.assets) {
1368
+ for (const asset of coin.assets) {
1369
+ const existing = assetChanges.get(asset.assetId) ?? 0n;
1370
+ assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
1371
+ }
1372
+ }
1373
+ }
1374
+ selectedCoins = [...selectedCoins, ...extraCoins];
1375
+ totalBtcSelected += extraCoins.reduce((sum, c) => sum + c.value, 0);
1376
+ changeAmount = totalBtcSelected - totalBtcOutput;
1377
+ }
1378
+ // build change receiver with BTC change and all asset changes
1379
+ let changeReceiver;
1380
+ let changeIndex = 0;
1381
+ if (changeAmount > 0) {
1382
+ const changeAssets = [];
1383
+ for (const [assetId, amount] of assetChanges) {
1384
+ if (amount > 0n) {
1385
+ changeAssets.push({ assetId, amount: Number(amount) });
1386
+ }
1387
+ }
1388
+ changeIndex = outputs.length;
1389
+ outputs.push({
1390
+ script: BigInt(changeAmount) < this.dustAmount
1391
+ ? outputAddress.subdustPkScript
1392
+ : outputAddress.pkScript,
1393
+ amount: BigInt(changeAmount),
1106
1394
  });
1395
+ changeReceiver = {
1396
+ address: address,
1397
+ amount: changeAmount,
1398
+ assets: changeAssets.length > 0 ? changeAssets : undefined,
1399
+ };
1400
+ }
1401
+ // create asset packet only if there are assets involved
1402
+ const assetInputs = (0, asset_1.selectedCoinsToAssetInputs)(selectedCoins);
1403
+ const hasAssets = assetInputs.size > 0 ||
1404
+ recipients.some((r) => r.assets && r.assets.length > 0);
1405
+ if (hasAssets) {
1406
+ const assetPacket = (0, asset_1.createAssetPacket)(assetInputs, recipients, changeReceiver);
1407
+ outputs.push(assetPacket.txOut());
1107
1408
  }
1108
- return inputs;
1409
+ const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
1410
+ const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
1411
+ await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, changeReceiver?.assets);
1412
+ return arkTxid;
1109
1413
  }
1110
- }
1111
- exports.Wallet = Wallet;
1112
- Wallet.MIN_FEE_RATE = 1; // sats/vbyte
1113
- function getSequence(tapLeafScript) {
1114
- let sequence = undefined;
1115
- try {
1116
- const scriptWithLeafVersion = tapLeafScript[1];
1117
- const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
1414
+ /**
1415
+ * Build an offchain transaction from the given inputs and outputs,
1416
+ * sign it, submit to the ark provider, and finalize.
1417
+ * @returns The ark transaction id and server-signed checkpoint PSBTs (for bookkeeping)
1418
+ */
1419
+ async buildAndSubmitOffchainTx(inputs, outputs) {
1420
+ const tapLeafScript = this.offchainTapscript.forfeit();
1421
+ if (!tapLeafScript) {
1422
+ throw new Error("Selected leaf not found");
1423
+ }
1424
+ const tapTree = this.offchainTapscript.encode();
1425
+ const offchainTx = (0, arkTransaction_1.buildOffchainTx)(inputs.map((input) => ({
1426
+ ...input,
1427
+ tapLeafScript,
1428
+ tapTree,
1429
+ })), outputs, this.serverUnrollScript);
1430
+ const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
1431
+ const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base_1.base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base_1.base64.encode(c.toPSBT())));
1432
+ const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
1433
+ const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
1434
+ const signedCheckpoint = await this.identity.sign(tx);
1435
+ return base_1.base64.encode(signedCheckpoint.toPSBT());
1436
+ }));
1437
+ await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
1438
+ return { arkTxid, signedCheckpointTxs };
1439
+ }
1440
+ // mark vtxo spent and save change vtxo if any
1441
+ async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, changeAssets) {
1118
1442
  try {
1119
- const params = tapscript_1.CSVMultisigTapscript.decode(script).params;
1120
- sequence = bip68.encode(params.timelock.type === "blocks"
1121
- ? { blocks: Number(params.timelock.value) }
1122
- : { seconds: Number(params.timelock.value) });
1443
+ const spentVtxos = [];
1444
+ const commitmentTxIds = new Set();
1445
+ let batchExpiry = Number.MAX_SAFE_INTEGER;
1446
+ if (inputs.length !== signedCheckpointTxs.length) {
1447
+ console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
1448
+ }
1449
+ const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
1450
+ for (const [inputIndex, input] of inputs.entries()) {
1451
+ const vtxo = (0, utils_1.extendVirtualCoin)(this, input);
1452
+ if (inputIndex < safeLength &&
1453
+ signedCheckpointTxs[inputIndex]) {
1454
+ const checkpoint = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(signedCheckpointTxs[inputIndex]));
1455
+ spentVtxos.push({
1456
+ ...vtxo,
1457
+ virtualStatus: {
1458
+ ...vtxo.virtualStatus,
1459
+ state: "spent",
1460
+ },
1461
+ spentBy: checkpoint.id,
1462
+ arkTxId: arkTxid,
1463
+ isSpent: true,
1464
+ });
1465
+ }
1466
+ else {
1467
+ spentVtxos.push({
1468
+ ...vtxo,
1469
+ virtualStatus: {
1470
+ ...vtxo.virtualStatus,
1471
+ state: "spent",
1472
+ },
1473
+ arkTxId: arkTxid,
1474
+ isSpent: true,
1475
+ });
1476
+ }
1477
+ if (vtxo.virtualStatus.commitmentTxIds) {
1478
+ for (const id of vtxo.virtualStatus.commitmentTxIds) {
1479
+ commitmentTxIds.add(id);
1480
+ }
1481
+ }
1482
+ if (vtxo.virtualStatus.batchExpiry) {
1483
+ batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
1484
+ }
1485
+ }
1486
+ const createdAt = Date.now();
1487
+ const addr = this.arkAddress.encode();
1488
+ // Only save a change VTXO for preconfirmed coins (those with a batchExpiry).
1489
+ // Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
1490
+ let changeVtxo;
1491
+ if (changeAmount > 0n && batchExpiry !== Number.MAX_SAFE_INTEGER) {
1492
+ changeVtxo = {
1493
+ txid: arkTxid,
1494
+ vout: changeVout,
1495
+ createdAt: new Date(createdAt),
1496
+ forfeitTapLeafScript: this.offchainTapscript.forfeit(),
1497
+ intentTapLeafScript: this.offchainTapscript.forfeit(),
1498
+ isUnrolled: false,
1499
+ isSpent: false,
1500
+ tapTree: this.offchainTapscript.encode(),
1501
+ value: Number(changeAmount),
1502
+ virtualStatus: {
1503
+ state: "preconfirmed",
1504
+ commitmentTxIds: Array.from(commitmentTxIds),
1505
+ batchExpiry,
1506
+ },
1507
+ status: {
1508
+ confirmed: false,
1509
+ },
1510
+ assets: changeAssets,
1511
+ };
1512
+ }
1513
+ await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
1514
+ await this.walletRepository.saveTransactions(addr, [
1515
+ {
1516
+ key: {
1517
+ boardingTxid: "",
1518
+ commitmentTxid: "",
1519
+ arkTxid: arkTxid,
1520
+ },
1521
+ amount: sentAmount,
1522
+ type: _1.TxType.TxSent,
1523
+ settled: false,
1524
+ createdAt,
1525
+ },
1526
+ ]);
1123
1527
  }
1124
- catch {
1125
- const params = tapscript_1.CLTVMultisigTapscript.decode(script).params;
1126
- sequence = Number(params.absoluteTimelock);
1528
+ catch (e) {
1529
+ console.warn("error saving offchain tx to repository", e);
1127
1530
  }
1128
1531
  }
1129
- catch { }
1130
- return sequence;
1131
- }
1132
- function isValidArkAddress(address) {
1133
- try {
1134
- address_1.ArkAddress.decode(address);
1135
- return true;
1136
- }
1137
- catch (e) {
1138
- return false;
1532
+ // mark vtxo spent & settled, remove boarding utxo
1533
+ async updateDbAfterSettle(inputs, commitmentTxid) {
1534
+ try {
1535
+ const addr = this.arkAddress.encode();
1536
+ const boardingAddress = await this.getBoardingAddress();
1537
+ const spentVtxos = [];
1538
+ const inputArkTxIds = new Set();
1539
+ const boardingUtxoToRemove = new Set();
1540
+ const isVtxo = (input) => "virtualStatus" in input;
1541
+ for (const input of inputs) {
1542
+ if (isVtxo(input)) {
1543
+ // vtxo = mark it settled
1544
+ const vtxo = (0, utils_1.extendVirtualCoin)(this, input);
1545
+ if (vtxo.arkTxId) {
1546
+ inputArkTxIds.add(vtxo.arkTxId);
1547
+ }
1548
+ spentVtxos.push({
1549
+ ...vtxo,
1550
+ virtualStatus: {
1551
+ ...vtxo.virtualStatus,
1552
+ state: "settled",
1553
+ },
1554
+ settledBy: commitmentTxid,
1555
+ isSpent: true,
1556
+ });
1557
+ }
1558
+ else {
1559
+ // boarding utxo = remove it
1560
+ boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
1561
+ }
1562
+ }
1563
+ if (spentVtxos.length > 0) {
1564
+ await this.walletRepository.saveVtxos(addr, spentVtxos);
1565
+ }
1566
+ if (boardingUtxoToRemove.size > 0) {
1567
+ const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
1568
+ const filtered = currentUtxos.filter((u) => !boardingUtxoToRemove.has(`${u.txid}:${u.vout}`));
1569
+ // Clear and re-save the filtered list
1570
+ await this.walletRepository.deleteUtxos(boardingAddress);
1571
+ if (filtered.length > 0) {
1572
+ await this.walletRepository.saveUtxos(boardingAddress, filtered);
1573
+ }
1574
+ }
1575
+ }
1576
+ catch (e) {
1577
+ console.warn("error updating repository after settle", e);
1578
+ }
1139
1579
  }
1140
1580
  }
1581
+ exports.Wallet = Wallet;
1582
+ Wallet.MIN_FEE_RATE = 1; // sats/vbyte
1141
1583
  /**
1142
1584
  * Select virtual coins to reach a target amount, prioritizing those closer to expiry
1143
1585
  * @param coins List of virtual coins to select from