@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,7 +1,6 @@
1
1
  import { base64, hex } from "@scure/base";
2
- import * as bip68 from "bip68";
3
2
  import { tapLeafHash } from "@scure/btc-signer/payment.js";
4
- import { SigHash, Transaction, Address, OutScript } from "@scure/btc-signer";
3
+ import { Address, OutScript, SigHash, Transaction } from "@scure/btc-signer";
5
4
  import { sha256 } from "@scure/btc-signer/utils.js";
6
5
  import { ArkAddress } from '../script/address.js';
7
6
  import { DefaultVtxo } from '../script/default.js';
@@ -10,23 +9,28 @@ import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
10
9
  import { RestArkProvider, } from '../providers/ark.js';
11
10
  import { buildForfeitTx } from '../forfeit.js';
12
11
  import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
12
+ import { validateBatchRecipients } from './validation.js';
13
13
  import { isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
14
+ import { createAssetPacket, selectedCoinsToAssetInputs, selectCoinsWithAsset, } from './asset.js';
14
15
  import { VtxoScript } from '../script/base.js';
15
- import { CLTVMultisigTapscript, CSVMultisigTapscript, } from '../script/tapscript.js';
16
- import { buildOffchainTx, hasBoardingTxExpired } from '../utils/arkTransaction.js';
16
+ import { CSVMultisigTapscript } from '../script/tapscript.js';
17
+ import { buildOffchainTx, hasBoardingTxExpired, isValidArkAddress, } from '../utils/arkTransaction.js';
17
18
  import { DEFAULT_RENEWAL_CONFIG } from './vtxo-manager.js';
18
19
  import { ArkNote } from '../arknote/index.js';
19
20
  import { Intent } from '../intent/index.js';
20
21
  import { RestIndexerProvider } from '../providers/indexer.js';
21
- import { ConditionWitness, VtxoTaprootTree } from '../utils/unknownFields.js';
22
- import { InMemoryStorageAdapter } from '../storage/inMemory.js';
23
- import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
24
- import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
25
- import { extendCoin, extendVirtualCoin } from './utils.js';
22
+ import { extendCoin, extendVirtualCoin, validateRecipients } from './utils.js';
26
23
  import { ArkError } from '../providers/errors.js';
27
24
  import { Batch } from './batch.js';
28
25
  import { Estimator } from '../arkfee/index.js';
29
26
  import { buildTransactionHistory } from '../utils/transactionHistory.js';
27
+ import { AssetManager, ReadonlyAssetManager } from './asset-manager.js';
28
+ import { DelegateVtxo } from '../script/delegate.js';
29
+ import { DelegatorManagerImpl } from './delegator.js';
30
+ import { IndexedDBContractRepository, IndexedDBWalletRepository, } from '../repositories/index.js';
31
+ import { ContractManager } from '../contracts/contractManager.js';
32
+ import { contractHandlers } from '../contracts/handlers/index.js';
33
+ import { timelockToSequence } from '../contracts/handlers/helpers.js';
30
34
  /**
31
35
  * Type guard function to check if an identity has a toReadonly method.
32
36
  */
@@ -37,7 +41,10 @@ function hasToReadonly(identity) {
37
41
  typeof identity.toReadonly === "function");
38
42
  }
39
43
  export class ReadonlyWallet {
40
- constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository) {
44
+ get assetManager() {
45
+ return this._assetManager;
46
+ }
47
+ constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig) {
41
48
  this.identity = identity;
42
49
  this.network = network;
43
50
  this.onchainProvider = onchainProvider;
@@ -48,12 +55,15 @@ export class ReadonlyWallet {
48
55
  this.dustAmount = dustAmount;
49
56
  this.walletRepository = walletRepository;
50
57
  this.contractRepository = contractRepository;
58
+ this.delegatorProvider = delegatorProvider;
59
+ this.watcherConfig = watcherConfig;
60
+ this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
51
61
  }
52
62
  /**
53
63
  * Protected helper to set up shared wallet configuration.
54
64
  * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
55
65
  */
56
- static async setupWalletConfig(config, pubkey) {
66
+ static async setupWalletConfig(config, pubKey) {
57
67
  // Use provided arkProvider instance or create a new one from arkServerUrl
58
68
  const arkProvider = config.arkProvider ||
59
69
  (() => {
@@ -105,22 +115,26 @@ export class ReadonlyWallet {
105
115
  };
106
116
  // Generate tapscripts for offchain and boarding address
107
117
  const serverPubKey = hex.decode(info.signerPubkey).slice(1);
108
- const bareVtxoTapscript = new DefaultVtxo.Script({
109
- pubKey: pubkey,
118
+ const delegatePubKey = config.delegatorProvider
119
+ ? await config.delegatorProvider
120
+ .getDelegateInfo()
121
+ .then((info) => hex.decode(info.pubkey).slice(1))
122
+ : undefined;
123
+ const offchainOptions = {
124
+ pubKey,
110
125
  serverPubKey,
111
126
  csvTimelock: exitTimelock,
112
- });
127
+ };
128
+ const offchainTapscript = !delegatePubKey
129
+ ? new DefaultVtxo.Script(offchainOptions)
130
+ : new DelegateVtxo.Script({ ...offchainOptions, delegatePubKey });
113
131
  const boardingTapscript = new DefaultVtxo.Script({
114
- pubKey: pubkey,
115
- serverPubKey,
132
+ ...offchainOptions,
116
133
  csvTimelock: boardingTimelock,
117
134
  });
118
- // Save tapscripts
119
- const offchainTapscript = bareVtxoTapscript;
120
- // Set up storage and repositories
121
- const storage = config.storage || new InMemoryStorageAdapter();
122
- const walletRepository = new WalletRepositoryImpl(storage);
123
- const contractRepository = new ContractRepositoryImpl(storage);
135
+ const walletRepository = config.storage?.walletRepository ?? new IndexedDBWalletRepository();
136
+ const contractRepository = config.storage?.contractRepository ??
137
+ new IndexedDBContractRepository();
124
138
  return {
125
139
  arkProvider,
126
140
  indexerProvider,
@@ -134,6 +148,7 @@ export class ReadonlyWallet {
134
148
  walletRepository,
135
149
  contractRepository,
136
150
  info,
151
+ delegatorProvider: config.delegatorProvider,
137
152
  };
138
153
  }
139
154
  static async create(config) {
@@ -142,11 +157,18 @@ export class ReadonlyWallet {
142
157
  throw new Error("Invalid configured public key");
143
158
  }
144
159
  const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
145
- return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository);
160
+ 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);
146
161
  }
147
162
  get arkAddress() {
148
163
  return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
149
164
  }
165
+ /**
166
+ * Get the contract script for the wallet's default address.
167
+ * This is the pkScript hex, used to identify the wallet in ContractManager.
168
+ */
169
+ get defaultContractScript() {
170
+ return hex.encode(this.offchainTapscript.pkScript);
171
+ }
150
172
  async getAddress() {
151
173
  return this.arkAddress.encode();
152
174
  }
@@ -184,6 +206,22 @@ export class ReadonlyWallet {
184
206
  .reduce((sum, coin) => sum + coin.value, 0);
185
207
  const totalBoarding = confirmed + unconfirmed;
186
208
  const totalOffchain = settled + preconfirmed + recoverable;
209
+ // aggregate asset balances from spendable vtxos
210
+ const assetBalances = new Map();
211
+ for (const vtxo of vtxos) {
212
+ if (!isSpendable(vtxo))
213
+ continue;
214
+ if (vtxo.assets) {
215
+ for (const a of vtxo.assets) {
216
+ const current = assetBalances.get(a.assetId) ?? 0;
217
+ assetBalances.set(a.assetId, current + a.amount);
218
+ }
219
+ }
220
+ }
221
+ const assets = Array.from(assetBalances.entries()).map(([assetId, amount]) => ({
222
+ assetId,
223
+ amount,
224
+ }));
187
225
  return {
188
226
  boarding: {
189
227
  confirmed,
@@ -195,20 +233,39 @@ export class ReadonlyWallet {
195
233
  available: settled + preconfirmed,
196
234
  recoverable,
197
235
  total: totalBoarding + totalOffchain,
236
+ assets,
198
237
  };
199
238
  }
200
239
  async getVtxos(filter) {
201
240
  const address = await this.getAddress();
202
- // Try to get from cache first first (optional fast path)
203
- // const cachedVtxos = await this.walletRepository.getVtxos(address);
204
- // if (cachedVtxos.length) return cachedVtxos;
205
- // For now, always fetch fresh data from provider and update cache
206
- // In future, we can add cache invalidation logic based on timestamps
207
- const vtxos = await this.getVirtualCoins(filter);
208
- const extendedVtxos = vtxos.map((vtxo) => extendVirtualCoin(this, vtxo));
241
+ const scriptMap = await this.getScriptMap();
242
+ const f = filter ?? { withRecoverable: true, withUnrolled: false };
243
+ const allExtended = [];
244
+ // Query each script separately so we can extend VTXOs with the correct tapscript
245
+ for (const [scriptHex, vtxoScript] of scriptMap) {
246
+ const response = await this.indexerProvider.getVtxos({
247
+ scripts: [scriptHex],
248
+ });
249
+ let vtxos = response.vtxos.filter(isSpendable);
250
+ if (!f.withRecoverable) {
251
+ vtxos = vtxos.filter((vtxo) => !isRecoverable(vtxo) && !isExpired(vtxo));
252
+ }
253
+ if (f.withUnrolled) {
254
+ const spentVtxos = response.vtxos.filter((vtxo) => !isSpendable(vtxo));
255
+ vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
256
+ }
257
+ for (const vtxo of vtxos) {
258
+ allExtended.push({
259
+ ...vtxo,
260
+ forfeitTapLeafScript: vtxoScript.forfeit(),
261
+ intentTapLeafScript: vtxoScript.forfeit(),
262
+ tapTree: vtxoScript.encode(),
263
+ });
264
+ }
265
+ }
209
266
  // Update cache with fresh data
210
- await this.walletRepository.saveVtxos(address, extendedVtxos);
211
- return extendedVtxos;
267
+ await this.walletRepository.saveVtxos(address, allExtended);
268
+ return allExtended;
212
269
  }
213
270
  async getVirtualCoins(filter = { withRecoverable: true, withUnrolled: false }) {
214
271
  const scripts = [hex.encode(this.offchainTapscript.pkScript)];
@@ -226,9 +283,8 @@ export class ReadonlyWallet {
226
283
  return vtxos;
227
284
  }
228
285
  async getTransactionHistory() {
229
- const response = await this.indexerProvider.getVtxos({
230
- scripts: [hex.encode(this.offchainTapscript.pkScript)],
231
- });
286
+ const scripts = await this.getWalletScripts();
287
+ const response = await this.indexerProvider.getVtxos({ scripts });
232
288
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
233
289
  const getTxCreatedAt = (txid) => this.indexerProvider
234
290
  .getVtxos({ outpoints: [{ txid, vout: 0 }] })
@@ -338,17 +394,20 @@ export class ReadonlyWallet {
338
394
  });
339
395
  }
340
396
  if (this.indexerProvider && arkAddress) {
341
- const offchainScript = this.offchainTapscript;
342
- const subscriptionId = await this.indexerProvider.subscribeForScripts([
343
- hex.encode(offchainScript.pkScript),
344
- ]);
397
+ const walletScripts = await this.getWalletScripts();
398
+ const subscriptionId = await this.indexerProvider.subscribeForScripts(walletScripts);
345
399
  const abortController = new AbortController();
346
400
  const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
347
401
  indexerStopFunc = async () => {
348
402
  abortController.abort();
349
403
  await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
350
404
  };
351
- // Handle subscription updates asynchronously without blocking
405
+ // Handle subscription updates asynchronously without blocking.
406
+ // Note: subscription covers all wallet scripts (default + delegate),
407
+ // but we can't determine which script each VTXO belongs to from the
408
+ // subscription event. VTXOs are extended with the current offchainTapscript;
409
+ // this is for notification/display only — not for spending.
410
+ // For correct extension metadata, use getVtxos() which queries per-script.
352
411
  (async () => {
353
412
  try {
354
413
  for await (const update of subscription) {
@@ -375,7 +434,7 @@ export class ReadonlyWallet {
375
434
  }
376
435
  async fetchPendingTxs() {
377
436
  // get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
378
- const scripts = [hex.encode(this.offchainTapscript.pkScript)];
437
+ const scripts = await this.getWalletScripts();
379
438
  let { vtxos } = await this.indexerProvider.getVtxos({
380
439
  scripts,
381
440
  });
@@ -385,6 +444,183 @@ export class ReadonlyWallet {
385
444
  vtxo.arkTxId !== undefined)
386
445
  .map((_) => _.arkTxId);
387
446
  }
447
+ // ========================================================================
448
+ // Multi-script support (default + delegate addresses)
449
+ // ========================================================================
450
+ /**
451
+ * Get all pkScript hex strings for the wallet's own addresses
452
+ * (both delegate and non-delegate, current and historical).
453
+ * Falls back to only the current script if ContractManager is not yet initialized.
454
+ */
455
+ async getWalletScripts() {
456
+ if (this._contractManager) {
457
+ try {
458
+ const contracts = await this._contractManager.getContracts({
459
+ type: ["default", "delegate"],
460
+ });
461
+ if (contracts.length > 0) {
462
+ return contracts.map((c) => c.script);
463
+ }
464
+ }
465
+ catch {
466
+ // fall through to current script only
467
+ }
468
+ }
469
+ return [hex.encode(this.offchainTapscript.pkScript)];
470
+ }
471
+ /**
472
+ * Build a map of scriptHex → VtxoScript for all wallet contracts,
473
+ * so VTXOs can be extended with the correct tapscript per contract.
474
+ */
475
+ async getScriptMap() {
476
+ const map = new Map();
477
+ // Always include the current script
478
+ const currentScriptHex = hex.encode(this.offchainTapscript.pkScript);
479
+ map.set(currentScriptHex, this.offchainTapscript);
480
+ if (this._contractManager) {
481
+ try {
482
+ const contracts = await this._contractManager.getContracts({
483
+ type: ["default", "delegate"],
484
+ });
485
+ for (const contract of contracts) {
486
+ if (map.has(contract.script))
487
+ continue;
488
+ const handler = contractHandlers.get(contract.type);
489
+ if (handler) {
490
+ const script = handler.createScript(contract.params);
491
+ map.set(contract.script, script);
492
+ }
493
+ }
494
+ }
495
+ catch {
496
+ // ContractManager error — only current script in map
497
+ }
498
+ }
499
+ return map;
500
+ }
501
+ // ========================================================================
502
+ // Contract Management
503
+ // ========================================================================
504
+ /**
505
+ * Get the ContractManager for managing contracts including the wallet's default address.
506
+ *
507
+ * The ContractManager handles:
508
+ * - The wallet's default receiving address (as a "default" contract)
509
+ * - External contracts (Boltz swaps, HTLCs, etc.)
510
+ * - Multi-contract watching with resilient connections
511
+ *
512
+ * @example
513
+ * ```typescript
514
+ * const manager = await wallet.getContractManager();
515
+ *
516
+ * // Create a contract for a Boltz swap
517
+ * const contract = await manager.createContract({
518
+ * label: "Boltz Swap",
519
+ * type: "vhtlc",
520
+ * params: { ... },
521
+ * script: swapScript,
522
+ * address: swapAddress,
523
+ * });
524
+ *
525
+ * // Start watching for events (includes wallet's default address)
526
+ * const stop = await manager.onContractEvent((event) => {
527
+ * console.log(`${event.type} on ${event.contractScript}`);
528
+ * });
529
+ * ```
530
+ */
531
+ async getContractManager() {
532
+ // Return existing manager if already initialized
533
+ if (this._contractManager) {
534
+ return this._contractManager;
535
+ }
536
+ // If initialization is in progress, wait for it
537
+ if (this._contractManagerInitializing) {
538
+ return this._contractManagerInitializing;
539
+ }
540
+ // Start initialization and store the promise
541
+ this._contractManagerInitializing = this.initializeContractManager();
542
+ try {
543
+ const manager = await this._contractManagerInitializing;
544
+ this._contractManager = manager;
545
+ return manager;
546
+ }
547
+ catch (error) {
548
+ // Clear the initializing promise so subsequent calls can retry
549
+ this._contractManagerInitializing = undefined;
550
+ throw error;
551
+ }
552
+ finally {
553
+ // Clear the initializing promise after completion
554
+ this._contractManagerInitializing = undefined;
555
+ }
556
+ }
557
+ async initializeContractManager() {
558
+ const manager = await ContractManager.create({
559
+ indexerProvider: this.indexerProvider,
560
+ contractRepository: this.contractRepository,
561
+ walletRepository: this.walletRepository,
562
+ getDefaultAddress: () => this.getAddress(),
563
+ watcherConfig: this.watcherConfig,
564
+ });
565
+ // Register the wallet's current address as a contract
566
+ const csvTimelock = this.offchainTapscript.options.csvTimelock ??
567
+ DefaultVtxo.Script.DEFAULT_TIMELOCK;
568
+ const csvTimelockStr = timelockToSequence(csvTimelock).toString();
569
+ const isDelegateScript = this.offchainTapscript instanceof DelegateVtxo.Script;
570
+ if (isDelegateScript) {
571
+ const delegateScript = this
572
+ .offchainTapscript;
573
+ // Register the delegate contract (current address)
574
+ await manager.createContract({
575
+ type: "delegate",
576
+ params: {
577
+ pubKey: hex.encode(delegateScript.options.pubKey),
578
+ serverPubKey: hex.encode(delegateScript.options.serverPubKey),
579
+ delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
580
+ csvTimelock: csvTimelockStr,
581
+ },
582
+ script: this.defaultContractScript,
583
+ address: await this.getAddress(),
584
+ state: "active",
585
+ });
586
+ // Also register the non-delegate version so old VTXOs remain visible
587
+ const nonDelegateScript = new DefaultVtxo.Script({
588
+ pubKey: delegateScript.options.pubKey,
589
+ serverPubKey: delegateScript.options.serverPubKey,
590
+ csvTimelock,
591
+ });
592
+ await manager.createContract({
593
+ type: "default",
594
+ params: {
595
+ pubKey: hex.encode(delegateScript.options.pubKey),
596
+ serverPubKey: hex.encode(delegateScript.options.serverPubKey),
597
+ csvTimelock: csvTimelockStr,
598
+ },
599
+ script: hex.encode(nonDelegateScript.pkScript),
600
+ address: nonDelegateScript
601
+ .address(this.network.hrp, this.arkServerPublicKey)
602
+ .encode(),
603
+ state: "active",
604
+ });
605
+ }
606
+ else {
607
+ // Register the default contract (current address)
608
+ await manager.createContract({
609
+ type: "default",
610
+ params: {
611
+ pubKey: hex.encode(this.offchainTapscript.options.pubKey),
612
+ serverPubKey: hex.encode(this.offchainTapscript.options.serverPubKey),
613
+ csvTimelock: csvTimelockStr,
614
+ },
615
+ script: this.defaultContractScript,
616
+ address: await this.getAddress(),
617
+ state: "active",
618
+ });
619
+ // Any old "delegate" contract from a prior wallet incarnation
620
+ // is already loaded by ContractManager.initialize() from ContractRepository
621
+ }
622
+ return manager;
623
+ }
388
624
  }
389
625
  /**
390
626
  * Main wallet implementation for Bitcoin transactions with Ark protocol support.
@@ -420,8 +656,8 @@ export class ReadonlyWallet {
420
656
  * ```
421
657
  */
422
658
  export class Wallet extends ReadonlyWallet {
423
- constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
424
- super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository);
659
+ constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig, delegatorProvider, watcherConfig) {
660
+ super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
425
661
  this.networkName = networkName;
426
662
  this.arkProvider = arkProvider;
427
663
  this.serverUnrollScript = serverUnrollScript;
@@ -433,6 +669,13 @@ export class Wallet extends ReadonlyWallet {
433
669
  ...DEFAULT_RENEWAL_CONFIG,
434
670
  ...renewalConfig,
435
671
  };
672
+ this.delegatorManager = delegatorProvider
673
+ ? new DelegatorManagerImpl(delegatorProvider, arkProvider, identity)
674
+ : undefined;
675
+ }
676
+ get assetManager() {
677
+ this._walletAssetManager ?? (this._walletAssetManager = new AssetManager(this));
678
+ return this._walletAssetManager;
436
679
  }
437
680
  static async create(config) {
438
681
  const pubkey = await config.identity.xOnlyPublicKey();
@@ -455,7 +698,7 @@ export class Wallet extends ReadonlyWallet {
455
698
  const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
456
699
  const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
457
700
  const forfeitOutputScript = OutScript.encode(forfeitAddress);
458
- 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);
701
+ 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);
459
702
  }
460
703
  /**
461
704
  * Convert this wallet to a readonly wallet.
@@ -479,7 +722,7 @@ export class Wallet extends ReadonlyWallet {
479
722
  const readonlyIdentity = hasToReadonly(this.identity)
480
723
  ? await this.identity.toReadonly()
481
724
  : this.identity; // Identity extends ReadonlyIdentity, so this is safe
482
- return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository);
725
+ 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);
483
726
  }
484
727
  async sendBitcoin(params) {
485
728
  if (params.amount <= 0) {
@@ -488,12 +731,7 @@ export class Wallet extends ReadonlyWallet {
488
731
  if (!isValidArkAddress(params.address)) {
489
732
  throw new Error("Invalid Ark address " + params.address);
490
733
  }
491
- // recoverable and subdust coins can't be spent in offchain tx
492
- const virtualCoins = await this.getVirtualCoins({
493
- withRecoverable: false,
494
- });
495
- let selected;
496
- if (params.selectedVtxos) {
734
+ if (params.selectedVtxos && params.selectedVtxos.length > 0) {
497
735
  const selectedVtxoSum = params.selectedVtxos
498
736
  .map((v) => v.value)
499
737
  .reduce((a, b) => a + b, 0);
@@ -501,125 +739,38 @@ export class Wallet extends ReadonlyWallet {
501
739
  throw new Error("Selected VTXOs do not cover specified amount");
502
740
  }
503
741
  const changeAmount = selectedVtxoSum - params.amount;
504
- selected = {
742
+ const selected = {
505
743
  inputs: params.selectedVtxos,
506
744
  changeAmount: BigInt(changeAmount),
507
745
  };
508
- }
509
- else {
510
- selected = selectVirtualCoins(virtualCoins, params.amount);
511
- }
512
- const selectedLeaf = this.offchainTapscript.forfeit();
513
- if (!selectedLeaf) {
514
- throw new Error("Selected leaf not found");
515
- }
516
- const outputAddress = ArkAddress.decode(params.address);
517
- const outputScript = BigInt(params.amount) < this.dustAmount
518
- ? outputAddress.subdustPkScript
519
- : outputAddress.pkScript;
520
- const outputs = [
521
- {
522
- script: outputScript,
523
- amount: BigInt(params.amount),
524
- },
525
- ];
526
- // add change output if needed
527
- if (selected.changeAmount > 0n) {
528
- const changeOutputScript = selected.changeAmount < this.dustAmount
529
- ? this.arkAddress.subdustPkScript
530
- : this.arkAddress.pkScript;
531
- outputs.push({
532
- script: changeOutputScript,
533
- amount: BigInt(selected.changeAmount),
534
- });
535
- }
536
- const tapTree = this.offchainTapscript.encode();
537
- const offchainTx = buildOffchainTx(selected.inputs.map((input) => ({
538
- ...input,
539
- tapLeafScript: selectedLeaf,
540
- tapTree,
541
- })), outputs, this.serverUnrollScript);
542
- const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
543
- const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
544
- // sign the checkpoints
545
- const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
546
- const tx = Transaction.fromPSBT(base64.decode(c));
547
- const signedCheckpoint = await this.identity.sign(tx);
548
- return base64.encode(signedCheckpoint.toPSBT());
549
- }));
550
- await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
551
- try {
552
- // mark VTXOs as spent and optionally add the change VTXO
553
- const spentVtxos = [];
554
- const commitmentTxIds = new Set();
555
- let batchExpiry = Number.MAX_SAFE_INTEGER;
556
- for (const [inputIndex, input] of selected.inputs.entries()) {
557
- const vtxo = extendVirtualCoin(this, input);
558
- const checkpointB64 = signedCheckpointTxs[inputIndex];
559
- const checkpoint = Transaction.fromPSBT(base64.decode(checkpointB64));
560
- spentVtxos.push({
561
- ...vtxo,
562
- virtualStatus: { ...vtxo.virtualStatus, state: "spent" },
563
- spentBy: checkpoint.id,
564
- arkTxId: arkTxid,
565
- isSpent: true,
566
- });
567
- if (vtxo.virtualStatus.commitmentTxIds) {
568
- for (const commitmentTxId of vtxo.virtualStatus
569
- .commitmentTxIds) {
570
- commitmentTxIds.add(commitmentTxId);
571
- }
572
- }
573
- if (vtxo.virtualStatus.batchExpiry) {
574
- batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
575
- }
576
- }
577
- const createdAt = Date.now();
578
- const addr = this.arkAddress.encode();
579
- if (selected.changeAmount > 0n &&
580
- batchExpiry !== Number.MAX_SAFE_INTEGER) {
581
- const changeVtxo = {
582
- txid: arkTxid,
583
- vout: outputs.length - 1,
584
- createdAt: new Date(createdAt),
585
- forfeitTapLeafScript: this.offchainTapscript.forfeit(),
586
- intentTapLeafScript: this.offchainTapscript.forfeit(),
587
- isUnrolled: false,
588
- isSpent: false,
589
- tapTree: this.offchainTapscript.encode(),
590
- value: Number(selected.changeAmount),
591
- virtualStatus: {
592
- state: "preconfirmed",
593
- commitmentTxIds: Array.from(commitmentTxIds),
594
- batchExpiry,
595
- },
596
- status: {
597
- confirmed: false,
598
- },
599
- };
600
- await this.walletRepository.saveVtxos(addr, [changeVtxo]);
601
- }
602
- await this.walletRepository.saveVtxos(addr, spentVtxos);
603
- await this.walletRepository.saveTransactions(addr, [
746
+ const outputAddress = ArkAddress.decode(params.address);
747
+ const outputScript = BigInt(params.amount) < this.dustAmount
748
+ ? outputAddress.subdustPkScript
749
+ : outputAddress.pkScript;
750
+ const outputs = [
604
751
  {
605
- key: {
606
- boardingTxid: "",
607
- commitmentTxid: "",
608
- arkTxid: arkTxid,
609
- },
610
- amount: params.amount,
611
- type: TxType.TxSent,
612
- settled: false,
613
- createdAt: Date.now(),
752
+ script: outputScript,
753
+ amount: BigInt(params.amount),
614
754
  },
615
- ]);
616
- }
617
- catch (e) {
618
- console.warn("error saving offchain tx to repository", e);
619
- }
620
- finally {
755
+ ];
756
+ // add change output if needed
757
+ if (selected.changeAmount > 0n) {
758
+ const changeOutputScript = selected.changeAmount < this.dustAmount
759
+ ? this.arkAddress.subdustPkScript
760
+ : this.arkAddress.pkScript;
761
+ outputs.push({
762
+ script: changeOutputScript,
763
+ amount: BigInt(selected.changeAmount),
764
+ });
765
+ }
766
+ const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
767
+ await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
621
768
  return arkTxid;
622
769
  }
770
+ return this.send({
771
+ address: params.address,
772
+ amount: params.amount,
773
+ });
623
774
  }
624
775
  async settle(params, eventCallback) {
625
776
  if (params?.inputs) {
@@ -720,6 +871,49 @@ export class Wallet extends ReadonlyWallet {
720
871
  script,
721
872
  });
722
873
  }
874
+ // if some of the inputs hold assets, build the asset packet and append as output
875
+ // in the intent proof tx, there is a "fake" input at index 0
876
+ // so the real coin indices are offset by +1
877
+ const assetInputs = new Map();
878
+ for (let i = 0; i < params.inputs.length; i++) {
879
+ if ("assets" in params.inputs[i]) {
880
+ const assets = params.inputs[i]
881
+ .assets;
882
+ if (assets && assets.length > 0) {
883
+ assetInputs.set(i + 1, assets);
884
+ }
885
+ }
886
+ }
887
+ let outputAssets;
888
+ let assetOutputIndex; // where to send the assets
889
+ if (assetInputs.size > 0) {
890
+ // collect all input assets and assign them to the first offchain output
891
+ const allAssets = new Map();
892
+ for (const [, assets] of assetInputs) {
893
+ for (const asset of assets) {
894
+ const existing = allAssets.get(asset.assetId) ?? 0n;
895
+ allAssets.set(asset.assetId, existing + BigInt(asset.amount));
896
+ }
897
+ }
898
+ outputAssets = [];
899
+ for (const [assetId, amount] of allAssets) {
900
+ outputAssets.push({ assetId, amount: Number(amount) });
901
+ }
902
+ const firstOffchainIndex = params.outputs.findIndex((_, i) => !onchainOutputIndexes.includes(i));
903
+ if (firstOffchainIndex === -1) {
904
+ throw new Error("Cannot settle assets without an offchain output");
905
+ }
906
+ assetOutputIndex = firstOffchainIndex;
907
+ }
908
+ const recipients = params.outputs.map((output, i) => ({
909
+ address: output.address,
910
+ amount: Number(output.amount),
911
+ assets: i === assetOutputIndex ? outputAssets : undefined,
912
+ }));
913
+ if (outputAssets && outputAssets.length > 0) {
914
+ const assetPacket = createAssetPacket(assetInputs, recipients);
915
+ outputs.push(assetPacket.txOut());
916
+ }
723
917
  // session holds the state of the musig2 signing process of the vtxo tree
724
918
  let session;
725
919
  const signingPublicKeys = [];
@@ -736,17 +930,19 @@ export class Wallet extends ReadonlyWallet {
736
930
  ...signingPublicKeys,
737
931
  ...params.inputs.map((input) => `${input.txid}:${input.vout}`),
738
932
  ];
739
- const handler = this.createBatchHandler(intentId, params.inputs, session);
933
+ const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
740
934
  const abortController = new AbortController();
741
935
  try {
742
936
  const stream = this.arkProvider.getEventStream(abortController.signal, topics);
743
- return await Batch.join(stream, handler, {
937
+ const commitmentTxid = await Batch.join(stream, handler, {
744
938
  abortController,
745
939
  skipVtxoTreeSigning: !hasOffchainOutputs,
746
940
  eventCallback: eventCallback
747
941
  ? (event) => Promise.resolve(eventCallback(event))
748
942
  : undefined,
749
943
  });
944
+ await this.updateDbAfterSettle(params.inputs, commitmentTxid);
945
+ return commitmentTxid;
750
946
  }
751
947
  catch (error) {
752
948
  // delete the intent to not be stuck in the queue
@@ -851,8 +1047,9 @@ export class Wallet extends ReadonlyWallet {
851
1047
  * @param intentId - The intent ID.
852
1048
  * @param inputs - The inputs of the intent.
853
1049
  * @param session - The musig2 signing session, if not provided, the signing will be skipped.
1050
+ * @param expectedRecipients - Expected recipients to validate in the vtxo tree.
854
1051
  */
855
- createBatchHandler(intentId, inputs, session) {
1052
+ createBatchHandler(intentId, inputs, expectedRecipients, session) {
856
1053
  let sweepTapTreeRoot;
857
1054
  return {
858
1055
  onBatchStarted: async (event) => {
@@ -900,7 +1097,10 @@ export class Wallet extends ReadonlyWallet {
900
1097
  // validate the unsigned vtxo tree
901
1098
  const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
902
1099
  validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
903
- // TODO check if our registered outputs are in the vtxo tree
1100
+ // validate that all expected receivers are in the vtxo tree with correct amounts and assets
1101
+ if (expectedRecipients && expectedRecipients.length > 0) {
1102
+ validateBatchRecipients(commitmentTx, vtxoTree.leaves(), expectedRecipients, this.network);
1103
+ }
904
1104
  const sharedOutput = commitmentTx.getOutput(0);
905
1105
  if (!sharedOutput?.amount) {
906
1106
  throw new Error("Shared output not found");
@@ -956,16 +1156,15 @@ export class Wallet extends ReadonlyWallet {
956
1156
  throw error;
957
1157
  }
958
1158
  }
959
- async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
960
- const inputs = this.prepareIntentProofInputs(coins);
1159
+ async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
961
1160
  const message = {
962
1161
  type: "register",
963
1162
  onchain_output_indexes: onchainOutputsIndexes,
964
- valid_at: 0,
1163
+ valid_at: validAt ? Math.floor(validAt) : 0,
965
1164
  expire_at: 0,
966
1165
  cosigners_public_keys: cosignerPubKeys,
967
1166
  };
968
- const proof = Intent.create(message, inputs, outputs);
1167
+ const proof = Intent.create(message, coins, outputs);
969
1168
  const signedProof = await this.identity.sign(proof);
970
1169
  return {
971
1170
  proof: base64.encode(signedProof.toPSBT()),
@@ -973,25 +1172,23 @@ export class Wallet extends ReadonlyWallet {
973
1172
  };
974
1173
  }
975
1174
  async makeDeleteIntentSignature(coins) {
976
- const inputs = this.prepareIntentProofInputs(coins);
977
1175
  const message = {
978
1176
  type: "delete",
979
1177
  expire_at: 0,
980
1178
  };
981
- const proof = Intent.create(message, inputs, []);
1179
+ const proof = Intent.create(message, coins, []);
982
1180
  const signedProof = await this.identity.sign(proof);
983
1181
  return {
984
1182
  proof: base64.encode(signedProof.toPSBT()),
985
1183
  message,
986
1184
  };
987
1185
  }
988
- async makeGetPendingTxIntentSignature(vtxos) {
989
- const inputs = this.prepareIntentProofInputs(vtxos);
1186
+ async makeGetPendingTxIntentSignature(coins) {
990
1187
  const message = {
991
1188
  type: "get-pending-tx",
992
1189
  expire_at: 0,
993
1190
  };
994
- const proof = Intent.create(message, inputs, []);
1191
+ const proof = Intent.create(message, coins, []);
995
1192
  const signedProof = await this.identity.sign(proof);
996
1193
  return {
997
1194
  proof: base64.encode(signedProof.toPSBT()),
@@ -1006,17 +1203,28 @@ export class Wallet extends ReadonlyWallet {
1006
1203
  async finalizePendingTxs(vtxos) {
1007
1204
  const MAX_INPUTS_PER_INTENT = 20;
1008
1205
  if (!vtxos || vtxos.length === 0) {
1009
- // get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
1010
- const scripts = [hex.encode(this.offchainTapscript.pkScript)];
1011
- let { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1012
- scripts,
1013
- });
1014
- fetchedVtxos = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1015
- vtxo.virtualStatus.state !== "settled");
1016
- if (fetchedVtxos.length === 0) {
1206
+ // Query per-script so each VTXO is extended with the correct tapscript
1207
+ const scriptMap = await this.getScriptMap();
1208
+ const allExtended = [];
1209
+ for (const [scriptHex, vtxoScript] of scriptMap) {
1210
+ const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1211
+ scripts: [scriptHex],
1212
+ });
1213
+ const pending = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1214
+ vtxo.virtualStatus.state !== "settled");
1215
+ for (const vtxo of pending) {
1216
+ allExtended.push({
1217
+ ...vtxo,
1218
+ forfeitTapLeafScript: vtxoScript.forfeit(),
1219
+ intentTapLeafScript: vtxoScript.forfeit(),
1220
+ tapTree: vtxoScript.encode(),
1221
+ });
1222
+ }
1223
+ }
1224
+ if (allExtended.length === 0) {
1017
1225
  return { finalized: [], pending: [] };
1018
1226
  }
1019
- vtxos = fetchedVtxos.map((v) => extendVirtualCoin(this, v));
1227
+ vtxos = allExtended;
1020
1228
  }
1021
1229
  const finalized = [];
1022
1230
  const pending = [];
@@ -1045,66 +1253,333 @@ export class Wallet extends ReadonlyWallet {
1045
1253
  }
1046
1254
  return { finalized, pending };
1047
1255
  }
1048
- prepareIntentProofInputs(coins) {
1049
- const inputs = [];
1050
- for (const input of coins) {
1051
- const vtxoScript = VtxoScript.decode(input.tapTree);
1052
- const sequence = getSequence(input.intentTapLeafScript);
1053
- const unknown = [VtxoTaprootTree.encode(input.tapTree)];
1054
- if (input.extraWitness) {
1055
- unknown.push(ConditionWitness.encode(input.extraWitness));
1256
+ /**
1257
+ * Send BTC and/or assets to one or more recipients.
1258
+ *
1259
+ * @param recipients - Array of recipients with their addresses, BTC amounts, and assets
1260
+ * @returns Promise resolving to the ark transaction ID
1261
+ *
1262
+ * @example
1263
+ * ```typescript
1264
+ * const txid = await wallet.send({
1265
+ * address: 'ark1...',
1266
+ * amount: 1000, // (optional, default to dust) btc amount to send to the output
1267
+ * assets: [{ assetId: 'abc123...', amount: 50 }] // (optional) list of assets to send
1268
+ * });
1269
+ * ```
1270
+ */
1271
+ async send(...args) {
1272
+ if (args.length === 0) {
1273
+ throw new Error("At least one receiver is required");
1274
+ }
1275
+ // validate recipients and populate undefined amount with dust amount
1276
+ const recipients = validateRecipients(args, Number(this.dustAmount));
1277
+ const address = await this.getAddress();
1278
+ const outputAddress = ArkAddress.decode(address);
1279
+ const virtualCoins = await this.getVirtualCoins({
1280
+ withRecoverable: false,
1281
+ });
1282
+ // keep track of asset changes
1283
+ const assetChanges = new Map();
1284
+ let selectedCoins = [];
1285
+ let btcAmountToSelect = 0;
1286
+ for (const recipient of recipients) {
1287
+ btcAmountToSelect += Math.max(recipient.amount, Number(this.dustAmount));
1288
+ }
1289
+ // select assets
1290
+ for (const recipient of recipients) {
1291
+ if (!recipient.assets) {
1292
+ continue;
1056
1293
  }
1057
- inputs.push({
1058
- txid: hex.decode(input.txid),
1059
- index: input.vout,
1060
- witnessUtxo: {
1061
- amount: BigInt(input.value),
1062
- script: vtxoScript.pkScript,
1063
- },
1064
- sequence,
1065
- tapLeafScript: [input.intentTapLeafScript],
1066
- unknown,
1294
+ for (const receiverAsset of recipient.assets) {
1295
+ let amountToSelect = BigInt(receiverAsset.amount);
1296
+ // check if existing change covers the needed amount
1297
+ const existingChange = assetChanges.get(receiverAsset.assetId) ?? 0n;
1298
+ if (existingChange >= amountToSelect) {
1299
+ assetChanges.set(receiverAsset.assetId, existingChange - amountToSelect);
1300
+ if (assetChanges.get(receiverAsset.assetId) === 0n) {
1301
+ assetChanges.delete(receiverAsset.assetId);
1302
+ }
1303
+ continue;
1304
+ }
1305
+ if (existingChange > 0n) {
1306
+ amountToSelect -= existingChange;
1307
+ assetChanges.delete(receiverAsset.assetId);
1308
+ }
1309
+ const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1310
+ const { selected, totalAssetAmount } = selectCoinsWithAsset(availableCoins, receiverAsset.assetId, amountToSelect);
1311
+ for (const coin of selected) {
1312
+ selectedCoins.push(coin);
1313
+ // asset coins contain btc, subtract from total amount to select
1314
+ btcAmountToSelect -= coin.value;
1315
+ // coin may contain other assets, add them to asset changes
1316
+ if (coin.assets) {
1317
+ for (const a of coin.assets) {
1318
+ if (a.assetId === receiverAsset.assetId) {
1319
+ continue;
1320
+ }
1321
+ const existing = assetChanges.get(a.assetId) ?? 0n;
1322
+ assetChanges.set(a.assetId, existing + BigInt(a.amount));
1323
+ }
1324
+ }
1325
+ }
1326
+ const assetChangeAmount = totalAssetAmount - amountToSelect;
1327
+ if (assetChangeAmount > 0n) {
1328
+ const existing = assetChanges.get(receiverAsset.assetId) ?? 0n;
1329
+ assetChanges.set(receiverAsset.assetId, existing + assetChangeAmount);
1330
+ }
1331
+ }
1332
+ }
1333
+ // select remaining btc
1334
+ if (btcAmountToSelect > 0) {
1335
+ const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1336
+ const { inputs: btcCoins } = selectVirtualCoins(availableCoins, btcAmountToSelect);
1337
+ // some coins may contain assets, add them to asset changes
1338
+ for (const coin of btcCoins) {
1339
+ if (coin.assets) {
1340
+ for (const asset of coin.assets) {
1341
+ const existing = assetChanges.get(asset.assetId) ?? 0n;
1342
+ assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
1343
+ }
1344
+ }
1345
+ }
1346
+ selectedCoins = [...selectedCoins, ...btcCoins];
1347
+ }
1348
+ let totalBtcSelected = selectedCoins.reduce((sum, c) => sum + c.value, 0);
1349
+ // build tx outputs
1350
+ const outputs = recipients.map((recipient) => ({
1351
+ script: recipient.script,
1352
+ amount: BigInt(recipient.amount),
1353
+ }));
1354
+ const totalBtcOutput = outputs.reduce((sum, o) => sum + Number(o.amount), 0);
1355
+ let changeAmount = totalBtcSelected - totalBtcOutput;
1356
+ // enforce minimum change amount when there are asset changes
1357
+ if (assetChanges.size > 0 && changeAmount < Number(this.dustAmount)) {
1358
+ const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
1359
+ const { inputs: extraCoins } = selectVirtualCoins(availableCoins, Number(this.dustAmount) - changeAmount);
1360
+ for (const coin of extraCoins) {
1361
+ if (coin.assets) {
1362
+ for (const asset of coin.assets) {
1363
+ const existing = assetChanges.get(asset.assetId) ?? 0n;
1364
+ assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
1365
+ }
1366
+ }
1367
+ }
1368
+ selectedCoins = [...selectedCoins, ...extraCoins];
1369
+ totalBtcSelected += extraCoins.reduce((sum, c) => sum + c.value, 0);
1370
+ changeAmount = totalBtcSelected - totalBtcOutput;
1371
+ }
1372
+ // build change receiver with BTC change and all asset changes
1373
+ let changeReceiver;
1374
+ let changeIndex = 0;
1375
+ if (changeAmount > 0) {
1376
+ const changeAssets = [];
1377
+ for (const [assetId, amount] of assetChanges) {
1378
+ if (amount > 0n) {
1379
+ changeAssets.push({ assetId, amount: Number(amount) });
1380
+ }
1381
+ }
1382
+ changeIndex = outputs.length;
1383
+ outputs.push({
1384
+ script: BigInt(changeAmount) < this.dustAmount
1385
+ ? outputAddress.subdustPkScript
1386
+ : outputAddress.pkScript,
1387
+ amount: BigInt(changeAmount),
1067
1388
  });
1389
+ changeReceiver = {
1390
+ address: address,
1391
+ amount: changeAmount,
1392
+ assets: changeAssets.length > 0 ? changeAssets : undefined,
1393
+ };
1394
+ }
1395
+ // create asset packet only if there are assets involved
1396
+ const assetInputs = selectedCoinsToAssetInputs(selectedCoins);
1397
+ const hasAssets = assetInputs.size > 0 ||
1398
+ recipients.some((r) => r.assets && r.assets.length > 0);
1399
+ if (hasAssets) {
1400
+ const assetPacket = createAssetPacket(assetInputs, recipients, changeReceiver);
1401
+ outputs.push(assetPacket.txOut());
1068
1402
  }
1069
- return inputs;
1403
+ const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
1404
+ const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
1405
+ await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, changeReceiver?.assets);
1406
+ return arkTxid;
1070
1407
  }
1071
- }
1072
- Wallet.MIN_FEE_RATE = 1; // sats/vbyte
1073
- export function getSequence(tapLeafScript) {
1074
- let sequence = undefined;
1075
- try {
1076
- const scriptWithLeafVersion = tapLeafScript[1];
1077
- const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
1408
+ /**
1409
+ * Build an offchain transaction from the given inputs and outputs,
1410
+ * sign it, submit to the ark provider, and finalize.
1411
+ * @returns The ark transaction id and server-signed checkpoint PSBTs (for bookkeeping)
1412
+ */
1413
+ async buildAndSubmitOffchainTx(inputs, outputs) {
1414
+ const tapLeafScript = this.offchainTapscript.forfeit();
1415
+ if (!tapLeafScript) {
1416
+ throw new Error("Selected leaf not found");
1417
+ }
1418
+ const tapTree = this.offchainTapscript.encode();
1419
+ const offchainTx = buildOffchainTx(inputs.map((input) => ({
1420
+ ...input,
1421
+ tapLeafScript,
1422
+ tapTree,
1423
+ })), outputs, this.serverUnrollScript);
1424
+ const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
1425
+ const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
1426
+ const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
1427
+ const tx = Transaction.fromPSBT(base64.decode(c));
1428
+ const signedCheckpoint = await this.identity.sign(tx);
1429
+ return base64.encode(signedCheckpoint.toPSBT());
1430
+ }));
1431
+ await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
1432
+ return { arkTxid, signedCheckpointTxs };
1433
+ }
1434
+ // mark vtxo spent and save change vtxo if any
1435
+ async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, changeAssets) {
1078
1436
  try {
1079
- const params = CSVMultisigTapscript.decode(script).params;
1080
- sequence = bip68.encode(params.timelock.type === "blocks"
1081
- ? { blocks: Number(params.timelock.value) }
1082
- : { seconds: Number(params.timelock.value) });
1437
+ const spentVtxos = [];
1438
+ const commitmentTxIds = new Set();
1439
+ let batchExpiry = Number.MAX_SAFE_INTEGER;
1440
+ if (inputs.length !== signedCheckpointTxs.length) {
1441
+ console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
1442
+ }
1443
+ const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
1444
+ for (const [inputIndex, input] of inputs.entries()) {
1445
+ const vtxo = extendVirtualCoin(this, input);
1446
+ if (inputIndex < safeLength &&
1447
+ signedCheckpointTxs[inputIndex]) {
1448
+ const checkpoint = Transaction.fromPSBT(base64.decode(signedCheckpointTxs[inputIndex]));
1449
+ spentVtxos.push({
1450
+ ...vtxo,
1451
+ virtualStatus: {
1452
+ ...vtxo.virtualStatus,
1453
+ state: "spent",
1454
+ },
1455
+ spentBy: checkpoint.id,
1456
+ arkTxId: arkTxid,
1457
+ isSpent: true,
1458
+ });
1459
+ }
1460
+ else {
1461
+ spentVtxos.push({
1462
+ ...vtxo,
1463
+ virtualStatus: {
1464
+ ...vtxo.virtualStatus,
1465
+ state: "spent",
1466
+ },
1467
+ arkTxId: arkTxid,
1468
+ isSpent: true,
1469
+ });
1470
+ }
1471
+ if (vtxo.virtualStatus.commitmentTxIds) {
1472
+ for (const id of vtxo.virtualStatus.commitmentTxIds) {
1473
+ commitmentTxIds.add(id);
1474
+ }
1475
+ }
1476
+ if (vtxo.virtualStatus.batchExpiry) {
1477
+ batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
1478
+ }
1479
+ }
1480
+ const createdAt = Date.now();
1481
+ const addr = this.arkAddress.encode();
1482
+ // Only save a change VTXO for preconfirmed coins (those with a batchExpiry).
1483
+ // Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
1484
+ let changeVtxo;
1485
+ if (changeAmount > 0n && batchExpiry !== Number.MAX_SAFE_INTEGER) {
1486
+ changeVtxo = {
1487
+ txid: arkTxid,
1488
+ vout: changeVout,
1489
+ createdAt: new Date(createdAt),
1490
+ forfeitTapLeafScript: this.offchainTapscript.forfeit(),
1491
+ intentTapLeafScript: this.offchainTapscript.forfeit(),
1492
+ isUnrolled: false,
1493
+ isSpent: false,
1494
+ tapTree: this.offchainTapscript.encode(),
1495
+ value: Number(changeAmount),
1496
+ virtualStatus: {
1497
+ state: "preconfirmed",
1498
+ commitmentTxIds: Array.from(commitmentTxIds),
1499
+ batchExpiry,
1500
+ },
1501
+ status: {
1502
+ confirmed: false,
1503
+ },
1504
+ assets: changeAssets,
1505
+ };
1506
+ }
1507
+ await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
1508
+ await this.walletRepository.saveTransactions(addr, [
1509
+ {
1510
+ key: {
1511
+ boardingTxid: "",
1512
+ commitmentTxid: "",
1513
+ arkTxid: arkTxid,
1514
+ },
1515
+ amount: sentAmount,
1516
+ type: TxType.TxSent,
1517
+ settled: false,
1518
+ createdAt,
1519
+ },
1520
+ ]);
1083
1521
  }
1084
- catch {
1085
- const params = CLTVMultisigTapscript.decode(script).params;
1086
- sequence = Number(params.absoluteTimelock);
1522
+ catch (e) {
1523
+ console.warn("error saving offchain tx to repository", e);
1087
1524
  }
1088
1525
  }
1089
- catch { }
1090
- return sequence;
1091
- }
1092
- function isValidArkAddress(address) {
1093
- try {
1094
- ArkAddress.decode(address);
1095
- return true;
1096
- }
1097
- catch (e) {
1098
- return false;
1526
+ // mark vtxo spent & settled, remove boarding utxo
1527
+ async updateDbAfterSettle(inputs, commitmentTxid) {
1528
+ try {
1529
+ const addr = this.arkAddress.encode();
1530
+ const boardingAddress = await this.getBoardingAddress();
1531
+ const spentVtxos = [];
1532
+ const inputArkTxIds = new Set();
1533
+ const boardingUtxoToRemove = new Set();
1534
+ const isVtxo = (input) => "virtualStatus" in input;
1535
+ for (const input of inputs) {
1536
+ if (isVtxo(input)) {
1537
+ // vtxo = mark it settled
1538
+ const vtxo = extendVirtualCoin(this, input);
1539
+ if (vtxo.arkTxId) {
1540
+ inputArkTxIds.add(vtxo.arkTxId);
1541
+ }
1542
+ spentVtxos.push({
1543
+ ...vtxo,
1544
+ virtualStatus: {
1545
+ ...vtxo.virtualStatus,
1546
+ state: "settled",
1547
+ },
1548
+ settledBy: commitmentTxid,
1549
+ isSpent: true,
1550
+ });
1551
+ }
1552
+ else {
1553
+ // boarding utxo = remove it
1554
+ boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
1555
+ }
1556
+ }
1557
+ if (spentVtxos.length > 0) {
1558
+ await this.walletRepository.saveVtxos(addr, spentVtxos);
1559
+ }
1560
+ if (boardingUtxoToRemove.size > 0) {
1561
+ const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
1562
+ const filtered = currentUtxos.filter((u) => !boardingUtxoToRemove.has(`${u.txid}:${u.vout}`));
1563
+ // Clear and re-save the filtered list
1564
+ await this.walletRepository.deleteUtxos(boardingAddress);
1565
+ if (filtered.length > 0) {
1566
+ await this.walletRepository.saveUtxos(boardingAddress, filtered);
1567
+ }
1568
+ }
1569
+ }
1570
+ catch (e) {
1571
+ console.warn("error updating repository after settle", e);
1572
+ }
1099
1573
  }
1100
1574
  }
1575
+ Wallet.MIN_FEE_RATE = 1; // sats/vbyte
1101
1576
  /**
1102
1577
  * Select virtual coins to reach a target amount, prioritizing those closer to expiry
1103
1578
  * @param coins List of virtual coins to select from
1104
1579
  * @param targetAmount Target amount to reach in satoshis
1105
1580
  * @returns Selected coins and change amount
1106
1581
  */
1107
- function selectVirtualCoins(coins, targetAmount) {
1582
+ export function selectVirtualCoins(coins, targetAmount) {
1108
1583
  // Sort VTXOs by expiry (ascending) and amount (descending)
1109
1584
  const sortedCoins = [...coins].sort((a, b) => {
1110
1585
  // First sort by expiry if available