@arkade-os/sdk 0.3.13 → 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 (240) 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/utils.js +1 -0
  35. package/dist/cjs/repositories/contractRepository.js +0 -103
  36. package/dist/cjs/repositories/inMemory/contractRepository.js +55 -0
  37. package/dist/cjs/repositories/inMemory/walletRepository.js +80 -0
  38. package/dist/cjs/repositories/index.js +16 -0
  39. package/dist/cjs/repositories/indexedDB/contractRepository.js +187 -0
  40. package/dist/cjs/repositories/indexedDB/db.js +57 -0
  41. package/dist/cjs/repositories/indexedDB/schema.js +159 -0
  42. package/dist/cjs/repositories/indexedDB/walletRepository.js +338 -0
  43. package/dist/cjs/repositories/indexedDB/websqlAdapter.js +144 -0
  44. package/dist/cjs/repositories/migrations/contractRepositoryImpl.js +127 -0
  45. package/dist/cjs/repositories/migrations/fromStorageAdapter.js +66 -0
  46. package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +180 -0
  47. package/dist/cjs/repositories/walletRepository.js +0 -169
  48. package/dist/cjs/script/base.js +54 -0
  49. package/dist/cjs/script/delegate.js +49 -0
  50. package/dist/cjs/storage/asyncStorage.js +4 -1
  51. package/dist/cjs/storage/fileSystem.js +3 -0
  52. package/dist/cjs/storage/inMemory.js +3 -0
  53. package/dist/cjs/storage/indexedDB.js +5 -1
  54. package/dist/cjs/storage/localStorage.js +3 -0
  55. package/dist/cjs/utils/arkTransaction.js +16 -0
  56. package/dist/cjs/utils/transactionHistory.js +50 -0
  57. package/dist/cjs/wallet/asset-manager.js +338 -0
  58. package/dist/cjs/wallet/asset.js +117 -0
  59. package/dist/cjs/wallet/batch.js +1 -1
  60. package/dist/cjs/wallet/delegator.js +235 -0
  61. package/dist/cjs/wallet/expo/background.js +133 -0
  62. package/dist/cjs/wallet/expo/index.js +9 -0
  63. package/dist/cjs/wallet/expo/wallet.js +231 -0
  64. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +568 -0
  65. package/dist/cjs/wallet/serviceWorker/wallet.js +383 -102
  66. package/dist/cjs/wallet/utils.js +58 -0
  67. package/dist/cjs/wallet/validation.js +151 -0
  68. package/dist/cjs/wallet/vtxo-manager.js +1 -1
  69. package/dist/cjs/wallet/wallet.js +702 -260
  70. package/dist/cjs/worker/browser/service-worker-manager.js +82 -0
  71. package/dist/cjs/{wallet/serviceWorker → worker/browser}/utils.js +2 -1
  72. package/dist/cjs/worker/expo/asyncStorageTaskQueue.js +78 -0
  73. package/dist/cjs/worker/expo/index.js +12 -0
  74. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +61 -0
  75. package/dist/cjs/worker/expo/processors/index.js +6 -0
  76. package/dist/cjs/worker/expo/taskQueue.js +41 -0
  77. package/dist/cjs/worker/expo/taskRunner.js +57 -0
  78. package/dist/cjs/worker/messageBus.js +252 -0
  79. package/dist/esm/adapters/expo-db.js +27 -0
  80. package/dist/esm/asset/assetGroup.js +137 -0
  81. package/dist/esm/asset/assetId.js +84 -0
  82. package/dist/esm/asset/assetInput.js +199 -0
  83. package/dist/esm/asset/assetOutput.js +154 -0
  84. package/dist/esm/asset/assetRef.js +78 -0
  85. package/dist/esm/asset/index.js +8 -0
  86. package/dist/esm/asset/metadata.js +167 -0
  87. package/dist/esm/asset/packet.js +159 -0
  88. package/dist/esm/asset/types.js +22 -0
  89. package/dist/esm/asset/utils.js +99 -0
  90. package/dist/esm/contracts/arkcontract.js +141 -0
  91. package/dist/esm/contracts/contractManager.js +432 -0
  92. package/dist/esm/contracts/contractWatcher.js +563 -0
  93. package/dist/esm/contracts/handlers/default.js +82 -0
  94. package/dist/esm/contracts/handlers/delegate.js +86 -0
  95. package/dist/esm/contracts/handlers/helpers.js +66 -0
  96. package/dist/esm/contracts/handlers/index.js +12 -0
  97. package/dist/esm/contracts/handlers/registry.js +86 -0
  98. package/dist/esm/contracts/handlers/vhtlc.js +190 -0
  99. package/dist/esm/contracts/index.js +13 -0
  100. package/dist/esm/contracts/types.js +1 -0
  101. package/dist/esm/db/manager.js +92 -0
  102. package/dist/esm/forfeit.js +11 -8
  103. package/dist/esm/identity/index.js +1 -0
  104. package/dist/esm/identity/seedIdentity.js +249 -0
  105. package/dist/esm/index.js +25 -15
  106. package/dist/esm/intent/index.js +28 -2
  107. package/dist/esm/providers/ark.js +7 -0
  108. package/dist/esm/providers/delegator.js +62 -0
  109. package/dist/esm/providers/expoIndexer.js +5 -0
  110. package/dist/esm/providers/indexer.js +68 -1
  111. package/dist/esm/providers/utils.js +1 -0
  112. package/dist/esm/repositories/contractRepository.js +1 -101
  113. package/dist/esm/repositories/inMemory/contractRepository.js +51 -0
  114. package/dist/esm/repositories/inMemory/walletRepository.js +76 -0
  115. package/dist/esm/repositories/index.js +8 -0
  116. package/dist/esm/repositories/indexedDB/contractRepository.js +183 -0
  117. package/dist/esm/repositories/indexedDB/db.js +42 -0
  118. package/dist/esm/repositories/indexedDB/schema.js +155 -0
  119. package/dist/esm/repositories/indexedDB/walletRepository.js +334 -0
  120. package/dist/esm/repositories/indexedDB/websqlAdapter.js +138 -0
  121. package/dist/esm/repositories/migrations/contractRepositoryImpl.js +121 -0
  122. package/dist/esm/repositories/migrations/fromStorageAdapter.js +58 -0
  123. package/dist/esm/repositories/migrations/walletRepositoryImpl.js +176 -0
  124. package/dist/esm/repositories/walletRepository.js +1 -167
  125. package/dist/esm/script/base.js +21 -1
  126. package/dist/esm/script/delegate.js +46 -0
  127. package/dist/esm/storage/asyncStorage.js +4 -1
  128. package/dist/esm/storage/fileSystem.js +3 -0
  129. package/dist/esm/storage/inMemory.js +3 -0
  130. package/dist/esm/storage/indexedDB.js +5 -1
  131. package/dist/esm/storage/localStorage.js +3 -0
  132. package/dist/esm/utils/arkTransaction.js +15 -0
  133. package/dist/esm/utils/transactionHistory.js +50 -0
  134. package/dist/esm/wallet/asset-manager.js +333 -0
  135. package/dist/esm/wallet/asset.js +111 -0
  136. package/dist/esm/wallet/batch.js +1 -1
  137. package/dist/esm/wallet/delegator.js +231 -0
  138. package/dist/esm/wallet/expo/background.js +128 -0
  139. package/dist/esm/wallet/expo/index.js +2 -0
  140. package/dist/esm/wallet/expo/wallet.js +194 -0
  141. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +564 -0
  142. package/dist/esm/wallet/serviceWorker/wallet.js +382 -101
  143. package/dist/esm/wallet/utils.js +54 -0
  144. package/dist/esm/wallet/validation.js +139 -0
  145. package/dist/esm/wallet/vtxo-manager.js +1 -1
  146. package/dist/esm/wallet/wallet.js +704 -229
  147. package/dist/esm/worker/browser/service-worker-manager.js +76 -0
  148. package/dist/esm/{wallet/serviceWorker → worker/browser}/utils.js +2 -1
  149. package/dist/esm/worker/expo/asyncStorageTaskQueue.js +74 -0
  150. package/dist/esm/worker/expo/index.js +4 -0
  151. package/dist/esm/worker/expo/processors/contractPollProcessor.js +58 -0
  152. package/dist/esm/worker/expo/processors/index.js +1 -0
  153. package/dist/esm/worker/expo/taskQueue.js +37 -0
  154. package/dist/esm/worker/expo/taskRunner.js +54 -0
  155. package/dist/esm/worker/messageBus.js +248 -0
  156. package/dist/types/adapters/expo-db.d.ts +7 -0
  157. package/dist/types/asset/assetGroup.d.ts +28 -0
  158. package/dist/types/asset/assetId.d.ts +19 -0
  159. package/dist/types/asset/assetInput.d.ts +46 -0
  160. package/dist/types/asset/assetOutput.d.ts +39 -0
  161. package/dist/types/asset/assetRef.d.ts +25 -0
  162. package/dist/types/asset/index.d.ts +8 -0
  163. package/dist/types/asset/metadata.d.ts +37 -0
  164. package/dist/types/asset/packet.d.ts +27 -0
  165. package/dist/types/asset/types.d.ts +18 -0
  166. package/dist/types/asset/utils.d.ts +21 -0
  167. package/dist/types/contracts/arkcontract.d.ts +101 -0
  168. package/dist/types/contracts/contractManager.d.ts +331 -0
  169. package/dist/types/contracts/contractWatcher.d.ts +192 -0
  170. package/dist/types/contracts/handlers/default.d.ts +19 -0
  171. package/dist/types/contracts/handlers/delegate.d.ts +21 -0
  172. package/dist/types/contracts/handlers/helpers.d.ts +18 -0
  173. package/dist/types/contracts/handlers/index.d.ts +7 -0
  174. package/dist/types/contracts/handlers/registry.d.ts +65 -0
  175. package/dist/types/contracts/handlers/vhtlc.d.ts +32 -0
  176. package/dist/types/contracts/index.d.ts +14 -0
  177. package/dist/types/contracts/types.d.ts +222 -0
  178. package/dist/types/db/manager.d.ts +22 -0
  179. package/dist/types/forfeit.d.ts +2 -1
  180. package/dist/types/identity/index.d.ts +1 -0
  181. package/dist/types/identity/seedIdentity.d.ts +128 -0
  182. package/dist/types/index.d.ts +21 -12
  183. package/dist/types/intent/index.d.ts +2 -1
  184. package/dist/types/providers/ark.d.ts +11 -2
  185. package/dist/types/providers/delegator.d.ts +29 -0
  186. package/dist/types/providers/indexer.d.ts +11 -1
  187. package/dist/types/repositories/contractRepository.d.ts +30 -19
  188. package/dist/types/repositories/inMemory/contractRepository.d.ts +17 -0
  189. package/dist/types/repositories/inMemory/walletRepository.d.ts +26 -0
  190. package/dist/types/repositories/index.d.ts +7 -0
  191. package/dist/types/repositories/indexedDB/contractRepository.d.ts +21 -0
  192. package/dist/types/repositories/indexedDB/db.d.ts +56 -0
  193. package/dist/types/repositories/indexedDB/schema.d.ts +8 -0
  194. package/dist/types/repositories/indexedDB/walletRepository.d.ts +25 -0
  195. package/dist/types/repositories/indexedDB/websqlAdapter.d.ts +49 -0
  196. package/dist/types/repositories/migrations/contractRepositoryImpl.d.ts +24 -0
  197. package/dist/types/repositories/migrations/fromStorageAdapter.d.ts +19 -0
  198. package/dist/types/repositories/migrations/walletRepositoryImpl.d.ts +27 -0
  199. package/dist/types/repositories/walletRepository.d.ts +13 -24
  200. package/dist/types/script/base.d.ts +1 -0
  201. package/dist/types/script/delegate.d.ts +36 -0
  202. package/dist/types/storage/asyncStorage.d.ts +4 -0
  203. package/dist/types/storage/fileSystem.d.ts +3 -0
  204. package/dist/types/storage/inMemory.d.ts +3 -0
  205. package/dist/types/storage/index.d.ts +3 -0
  206. package/dist/types/storage/indexedDB.d.ts +3 -0
  207. package/dist/types/storage/localStorage.d.ts +3 -0
  208. package/dist/types/utils/arkTransaction.d.ts +6 -0
  209. package/dist/types/wallet/asset-manager.d.ts +78 -0
  210. package/dist/types/wallet/asset.d.ts +21 -0
  211. package/dist/types/wallet/batch.d.ts +1 -1
  212. package/dist/types/wallet/delegator.d.ts +24 -0
  213. package/dist/types/wallet/expo/background.d.ts +66 -0
  214. package/dist/types/wallet/expo/index.d.ts +4 -0
  215. package/dist/types/wallet/expo/wallet.d.ts +97 -0
  216. package/dist/types/wallet/index.d.ts +75 -2
  217. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +366 -0
  218. package/dist/types/wallet/serviceWorker/wallet.d.ts +20 -11
  219. package/dist/types/wallet/utils.d.ts +12 -1
  220. package/dist/types/wallet/validation.d.ts +24 -0
  221. package/dist/types/wallet/wallet.d.ts +111 -17
  222. package/dist/types/worker/browser/service-worker-manager.d.ts +21 -0
  223. package/dist/types/{wallet/serviceWorker → worker/browser}/utils.d.ts +2 -1
  224. package/dist/types/worker/expo/asyncStorageTaskQueue.d.ts +46 -0
  225. package/dist/types/worker/expo/index.d.ts +7 -0
  226. package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +14 -0
  227. package/dist/types/worker/expo/processors/index.d.ts +1 -0
  228. package/dist/types/worker/expo/taskQueue.d.ts +50 -0
  229. package/dist/types/worker/expo/taskRunner.d.ts +42 -0
  230. package/dist/types/worker/messageBus.d.ts +109 -0
  231. package/package.json +65 -11
  232. package/dist/cjs/wallet/serviceWorker/request.js +0 -78
  233. package/dist/cjs/wallet/serviceWorker/response.js +0 -222
  234. package/dist/cjs/wallet/serviceWorker/worker.js +0 -655
  235. package/dist/esm/wallet/serviceWorker/request.js +0 -75
  236. package/dist/esm/wallet/serviceWorker/response.js +0 -219
  237. package/dist/esm/wallet/serviceWorker/worker.js +0 -651
  238. package/dist/types/wallet/serviceWorker/request.d.ts +0 -74
  239. package/dist/types/wallet/serviceWorker/response.d.ts +0 -123
  240. package/dist/types/wallet/serviceWorker/worker.d.ts +0 -53
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Watches multiple contracts for VTXO changes with resilient connection handling.
3
+ *
4
+ * Features:
5
+ * - Automatic reconnection with exponential backoff
6
+ * - Failsafe polling to catch missed events
7
+ * - Polls immediately after (re)connection to sync state
8
+ * - Graceful handling of subscription failures
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const watcher = new ContractWatcher({
13
+ * indexerProvider: wallet.indexerProvider,
14
+ * });
15
+ *
16
+ * // Add the wallet's default contract
17
+ * await watcher.addContract(defaultContract);
18
+ *
19
+ * // Add additional contracts (swaps, etc.)
20
+ * await watcher.addContract(swapContract);
21
+ *
22
+ * // Start watching for events
23
+ * const stop = await watcher.startWatching((event) => {
24
+ * console.log(`${event.type} on contract ${event.contractScript}`);
25
+ * });
26
+ *
27
+ * // Later: stop watching
28
+ * stop();
29
+ * ```
30
+ */
31
+ export class ContractWatcher {
32
+ constructor(config) {
33
+ this.contracts = new Map();
34
+ this.isWatching = false;
35
+ this.connectionState = "disconnected";
36
+ this.reconnectAttempts = 0;
37
+ this.config = {
38
+ failsafePollIntervalMs: 60000, // 1 minute
39
+ reconnectDelayMs: 1000, // 1 second
40
+ maxReconnectDelayMs: 30000, // 30 seconds
41
+ maxReconnectAttempts: 0, // unlimited
42
+ ...config,
43
+ };
44
+ }
45
+ /**
46
+ * Add a contract to be watched.
47
+ *
48
+ * Active contracts are immediately subscribed. All contracts are polled
49
+ * to discover any existing VTXOs (which may cause them to be watched
50
+ * even if inactive).
51
+ */
52
+ async addContract(contract) {
53
+ const state = {
54
+ contract,
55
+ lastKnownVtxos: new Map(),
56
+ };
57
+ this.contracts.set(contract.script, state);
58
+ // If we're already watching, poll to discover VTXOs and update subscription
59
+ if (this.isWatching) {
60
+ // Poll first to discover VTXOs (may affect whether we watch this contract)
61
+ await this.pollContracts([contract.script]);
62
+ // Update subscription based on active state and VTXOs
63
+ await this.tryUpdateSubscription();
64
+ }
65
+ }
66
+ /**
67
+ * Update an existing contract.
68
+ */
69
+ async updateContract(contract) {
70
+ const existing = this.contracts.get(contract.script);
71
+ if (!existing) {
72
+ throw new Error(`Contract ${contract.script} not found`);
73
+ }
74
+ existing.contract = contract;
75
+ if (this.isWatching) {
76
+ await this.tryUpdateSubscription();
77
+ }
78
+ }
79
+ /**
80
+ * Remove a contract from watching.
81
+ */
82
+ async removeContract(contractScript) {
83
+ const state = this.contracts.get(contractScript);
84
+ if (state) {
85
+ this.contracts.delete(contractScript);
86
+ if (this.isWatching) {
87
+ await this.tryUpdateSubscription();
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Get all in-memory contracts.
93
+ */
94
+ getAllContracts() {
95
+ return Array.from(this.contracts.values()).map((s) => s.contract);
96
+ }
97
+ /**
98
+ * Get all active in-memory contracts.
99
+ */
100
+ getActiveContracts() {
101
+ return this.getAllContracts().filter((c) => c.state === "active");
102
+ }
103
+ /**
104
+ * Get scripts that should be watched.
105
+ *
106
+ * Returns scripts for:
107
+ * - All active contracts
108
+ * - All contracts with known VTXOs (regardless of state)
109
+ *
110
+ * This ensures we continue monitoring contracts even after they're
111
+ * deactivated, as long as they have unspent VTXOs.
112
+ */
113
+ getScriptsToWatch() {
114
+ const scripts = new Set();
115
+ for (const [, state] of this.contracts) {
116
+ // Always watch active contracts
117
+ if (state.contract.state === "active") {
118
+ scripts.add(state.contract.script);
119
+ continue;
120
+ }
121
+ // Also watch inactive/expired contracts that have VTXOs
122
+ if (state.lastKnownVtxos.size > 0) {
123
+ scripts.add(state.contract.script);
124
+ }
125
+ }
126
+ return Array.from(scripts);
127
+ }
128
+ /**
129
+ * Get VTXOs for contracts, grouped by contract script.
130
+ * Uses Repository.
131
+ */
132
+ async getContractVtxos(options) {
133
+ const { contractScripts, includeSpent } = options;
134
+ const repo = this.config.walletRepository;
135
+ const contractsToQuery = Array.from(this.contracts.values());
136
+ const asyncResults = contractsToQuery
137
+ .filter((_) => {
138
+ if (contractScripts &&
139
+ !contractScripts.includes(_.contract.script))
140
+ return false;
141
+ return true;
142
+ })
143
+ .map(async (state) => {
144
+ // Use contract address as cache key
145
+ const cached = await repo.getVtxos(state.contract.address);
146
+ if (cached.length > 0) {
147
+ // Convert to ContractVtxo with contractScript
148
+ const contractVtxos = cached.map((v) => ({
149
+ ...v,
150
+ contractScript: state.contract.script,
151
+ }));
152
+ const filtered = includeSpent
153
+ ? contractVtxos
154
+ : contractVtxos.filter((v) => !v.isSpent);
155
+ return [[state.contract.script, filtered]];
156
+ }
157
+ return [];
158
+ });
159
+ const results = await Promise.all(asyncResults);
160
+ return new Map(results.flat(1));
161
+ }
162
+ /**
163
+ * Start watching for VTXO events across all active contracts.
164
+ */
165
+ async startWatching(callback) {
166
+ if (this.isWatching) {
167
+ throw new Error("Already watching");
168
+ }
169
+ this.eventCallback = callback;
170
+ this.isWatching = true;
171
+ this.abortController = new AbortController();
172
+ this.reconnectAttempts = 0;
173
+ // Start connection
174
+ await this.connect();
175
+ // Start failsafe polling
176
+ this.startFailsafePolling();
177
+ return () => this.stopWatching();
178
+ }
179
+ /**
180
+ * Stop watching for events.
181
+ */
182
+ async stopWatching() {
183
+ this.isWatching = false;
184
+ this.connectionState = "disconnected";
185
+ this.abortController?.abort();
186
+ // Clear timers
187
+ if (this.reconnectTimeoutId) {
188
+ clearTimeout(this.reconnectTimeoutId);
189
+ this.reconnectTimeoutId = undefined;
190
+ }
191
+ if (this.failsafePollIntervalId) {
192
+ clearInterval(this.failsafePollIntervalId);
193
+ this.failsafePollIntervalId = undefined;
194
+ }
195
+ // Unsubscribe
196
+ if (this.subscriptionId) {
197
+ try {
198
+ await this.config.indexerProvider.unsubscribeForScripts(this.subscriptionId);
199
+ }
200
+ catch {
201
+ // Ignore unsubscribe errors
202
+ }
203
+ this.subscriptionId = undefined;
204
+ }
205
+ this.eventCallback = undefined;
206
+ }
207
+ /**
208
+ * Check if currently watching.
209
+ */
210
+ isCurrentlyWatching() {
211
+ return this.isWatching;
212
+ }
213
+ /**
214
+ * Get current connection state.
215
+ */
216
+ getConnectionState() {
217
+ return this.connectionState;
218
+ }
219
+ /**
220
+ * Force a poll of all active contracts.
221
+ * Useful for manual refresh or after app resume.
222
+ */
223
+ async forcePoll() {
224
+ if (!this.isWatching)
225
+ return;
226
+ await this.pollAllContracts();
227
+ }
228
+ /**
229
+ * Check for expired contracts, update their state, and emit events.
230
+ */
231
+ checkExpiredContracts() {
232
+ const now = Date.now();
233
+ const expired = [];
234
+ for (const state of this.contracts.values()) {
235
+ const contract = state.contract;
236
+ if (contract.state === "active" &&
237
+ contract.expiresAt &&
238
+ contract.expiresAt <= now) {
239
+ contract.state = "inactive";
240
+ expired.push(contract);
241
+ this.eventCallback?.({
242
+ type: "contract_expired",
243
+ contractScript: contract.script,
244
+ contract,
245
+ timestamp: now,
246
+ });
247
+ }
248
+ }
249
+ }
250
+ /**
251
+ * Connect to the subscription.
252
+ */
253
+ async connect() {
254
+ if (!this.isWatching)
255
+ return;
256
+ this.connectionState = "connecting";
257
+ try {
258
+ await this.updateSubscription();
259
+ // Poll immediately after connection to sync state
260
+ await this.pollAllContracts();
261
+ this.connectionState = "connected";
262
+ this.reconnectAttempts = 0;
263
+ // Start listening
264
+ this.listenLoop().catch((e) => {
265
+ // This is handled asynchronously otherwise `connect()` would hang
266
+ // indefinitely and block the caller.
267
+ // Error management must be implemented to ensure the connection
268
+ // is restored and events are fired.
269
+ console.error(e);
270
+ this.connectionState = "disconnected";
271
+ this.eventCallback?.({
272
+ type: "connection_reset",
273
+ timestamp: Date.now(),
274
+ });
275
+ this.scheduleReconnect();
276
+ });
277
+ }
278
+ catch (error) {
279
+ console.error("ContractWatcher connection failed:", error);
280
+ this.connectionState = "disconnected";
281
+ this.eventCallback?.({
282
+ type: "connection_reset",
283
+ timestamp: Date.now(),
284
+ });
285
+ this.scheduleReconnect();
286
+ }
287
+ }
288
+ /**
289
+ * Schedule a reconnection attempt.
290
+ */
291
+ scheduleReconnect() {
292
+ if (!this.isWatching)
293
+ return;
294
+ // Check max attempts
295
+ if (this.config.maxReconnectAttempts > 0 &&
296
+ this.reconnectAttempts >= this.config.maxReconnectAttempts) {
297
+ console.error(`ContractWatcher: Max reconnection attempts (${this.config.maxReconnectAttempts}) reached`);
298
+ return;
299
+ }
300
+ this.connectionState = "reconnecting";
301
+ this.reconnectAttempts++;
302
+ // Calculate delay with exponential backoff
303
+ const delay = Math.min(this.config.reconnectDelayMs *
304
+ Math.pow(2, this.reconnectAttempts - 1), this.config.maxReconnectDelayMs);
305
+ this.reconnectTimeoutId = setTimeout(() => {
306
+ this.reconnectTimeoutId = undefined;
307
+ this.connect();
308
+ }, delay);
309
+ }
310
+ /**
311
+ * Start the failsafe polling interval.
312
+ */
313
+ startFailsafePolling() {
314
+ if (this.failsafePollIntervalId) {
315
+ clearInterval(this.failsafePollIntervalId);
316
+ }
317
+ this.failsafePollIntervalId = setInterval(() => {
318
+ if (this.isWatching) {
319
+ this.pollAllContracts().catch((error) => {
320
+ console.error("ContractWatcher failsafe poll failed:", error);
321
+ });
322
+ }
323
+ }, this.config.failsafePollIntervalMs);
324
+ }
325
+ /**
326
+ * Poll all active contracts for current state.
327
+ */
328
+ async pollAllContracts() {
329
+ const activeScripts = this.getActiveContracts().map((c) => c.script);
330
+ if (activeScripts.length === 0)
331
+ return;
332
+ await this.pollContracts(activeScripts);
333
+ }
334
+ /**
335
+ * Poll specific contracts and emit events for changes.
336
+ */
337
+ async pollContracts(contractScripts) {
338
+ if (!this.eventCallback)
339
+ return;
340
+ const now = Date.now();
341
+ try {
342
+ // Load all the VTXOs for these contracts, from DB
343
+ const vtxosMap = await this.getContractVtxos({
344
+ contractScripts,
345
+ includeSpent: false, // only spendable ones!
346
+ });
347
+ for (const contractScript of contractScripts) {
348
+ const state = this.contracts.get(contractScript);
349
+ if (!state)
350
+ continue;
351
+ const currentVtxos = vtxosMap.get(contractScript) || [];
352
+ const currentKeys = new Set(currentVtxos.map((v) => `${v.txid}:${v.vout}`));
353
+ // Find new VTXOs and add them to the contract's state
354
+ const newVtxos = [];
355
+ for (const vtxo of currentVtxos) {
356
+ const key = `${vtxo.txid}:${vtxo.vout}`;
357
+ if (!state.lastKnownVtxos.has(key)) {
358
+ newVtxos.push(vtxo);
359
+ state.lastKnownVtxos.set(key, vtxo);
360
+ }
361
+ }
362
+ // Find spent VTXOs and remove them from the contract's state
363
+ const spentVtxos = [];
364
+ for (const [key, vtxo] of state.lastKnownVtxos) {
365
+ if (!currentKeys.has(key)) {
366
+ spentVtxos.push(vtxo);
367
+ state.lastKnownVtxos.delete(key);
368
+ }
369
+ }
370
+ // Emit events
371
+ if (newVtxos.length > 0) {
372
+ this.emitVtxoEvent(contractScript, newVtxos, "vtxo_received", now);
373
+ }
374
+ if (spentVtxos.length > 0) {
375
+ // Note: We can't distinguish spent vs swept from polling alone
376
+ // The subscription provides more accurate event types
377
+ this.emitVtxoEvent(contractScript, spentVtxos, "vtxo_spent", now);
378
+ }
379
+ }
380
+ }
381
+ catch (error) {
382
+ console.error("ContractWatcher poll failed:", error);
383
+ // Don't throw - polling failures shouldn't crash the watcher
384
+ }
385
+ }
386
+ async tryUpdateSubscription() {
387
+ try {
388
+ await this.updateSubscription();
389
+ }
390
+ catch (error) {
391
+ // nothing, the connection will be retried later
392
+ }
393
+ }
394
+ /**
395
+ * Update the subscription with scripts that should be watched.
396
+ *
397
+ * Watches both active contracts and contracts with VTXOs.
398
+ */
399
+ async updateSubscription() {
400
+ const scriptsToWatch = this.getScriptsToWatch();
401
+ if (scriptsToWatch.length === 0) {
402
+ if (this.subscriptionId) {
403
+ try {
404
+ await this.config.indexerProvider.unsubscribeForScripts(this.subscriptionId);
405
+ }
406
+ catch {
407
+ // Ignore
408
+ }
409
+ this.subscriptionId = undefined;
410
+ }
411
+ return;
412
+ }
413
+ this.subscriptionId =
414
+ await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
415
+ }
416
+ /**
417
+ * Main listening loop for subscription events.
418
+ */
419
+ async listenLoop() {
420
+ if (!this.subscriptionId || !this.abortController || !this.isWatching) {
421
+ if (this.isWatching) {
422
+ this.connectionState = "disconnected";
423
+ this.scheduleReconnect();
424
+ }
425
+ return;
426
+ }
427
+ const subscription = this.config.indexerProvider.getSubscription(this.subscriptionId, this.abortController.signal);
428
+ for await (const update of subscription) {
429
+ if (!this.isWatching)
430
+ break;
431
+ this.handleSubscriptionUpdate(update);
432
+ }
433
+ // Stream ended normally - reconnect if still watching
434
+ if (this.isWatching) {
435
+ this.connectionState = "disconnected";
436
+ this.scheduleReconnect();
437
+ }
438
+ }
439
+ /**
440
+ * Handle a subscription update.
441
+ */
442
+ handleSubscriptionUpdate(update) {
443
+ if (!this.eventCallback)
444
+ return;
445
+ const timestamp = Date.now();
446
+ const scripts = update.scripts || [];
447
+ if (update.newVtxos?.length) {
448
+ this.processSubscriptionVtxos(update.newVtxos, scripts, "vtxo_received", timestamp);
449
+ }
450
+ if (update.spentVtxos?.length) {
451
+ this.processSubscriptionVtxos(update.spentVtxos, scripts, "vtxo_spent", timestamp);
452
+ }
453
+ }
454
+ /**
455
+ * Process VTXOs from subscription and route to correct contracts.
456
+ * Uses the scripts from the subscription response to determine contract ownership.
457
+ */
458
+ processSubscriptionVtxos(vtxos, scripts, eventType, timestamp) {
459
+ // If we have exactly one script, all VTXOs belong to that contract
460
+ // Otherwise, we can't reliably determine ownership without script in VirtualCoin
461
+ if (scripts.length === 1) {
462
+ const contractScript = scripts[0];
463
+ if (contractScript) {
464
+ // Update tracking
465
+ const state = this.contracts.get(contractScript);
466
+ if (state) {
467
+ for (const vtxo of vtxos) {
468
+ const key = `${vtxo.txid}:${vtxo.vout}`;
469
+ if (eventType === "vtxo_received") {
470
+ state.lastKnownVtxos.set(key, vtxo);
471
+ }
472
+ else if (eventType === "vtxo_spent") {
473
+ state.lastKnownVtxos.delete(key);
474
+ }
475
+ }
476
+ }
477
+ this.emitVtxoEvent(contractScript, vtxos, eventType, timestamp);
478
+ }
479
+ return;
480
+ }
481
+ // Multiple scripts - assign VTXOs to all matching contracts
482
+ // This is a limitation: we can't know which VTXO belongs to which script
483
+ // In practice, subscription events usually come with a single script context
484
+ for (const script of scripts) {
485
+ const contractScript = script;
486
+ if (contractScript) {
487
+ const state = this.contracts.get(contractScript);
488
+ if (state) {
489
+ for (const vtxo of vtxos) {
490
+ const key = `${vtxo.txid}:${vtxo.vout}`;
491
+ if (eventType === "vtxo_received") {
492
+ state.lastKnownVtxos.set(key, vtxo);
493
+ }
494
+ else {
495
+ state.lastKnownVtxos.delete(key);
496
+ }
497
+ }
498
+ }
499
+ this.emitVtxoEvent(contractScript, vtxos, eventType, timestamp);
500
+ }
501
+ }
502
+ }
503
+ /**
504
+ * Emit a VTXO event for a contract.
505
+ */
506
+ emitVtxoEvent(contractScript, vtxos, eventType, timestamp) {
507
+ if (!this.eventCallback)
508
+ return;
509
+ const state = this.contracts.get(contractScript);
510
+ // ensure we check somehow regularly
511
+ this.checkExpiredContracts();
512
+ switch (eventType) {
513
+ case "vtxo_received":
514
+ if (!state)
515
+ return;
516
+ this.eventCallback({
517
+ type: "vtxo_received",
518
+ vtxos: vtxos.map((v) => ({
519
+ ...v,
520
+ contractScript,
521
+ // These fields may not be available from basic VirtualCoin
522
+ forfeitTapLeafScript: undefined,
523
+ intentTapLeafScript: undefined,
524
+ tapTree: undefined,
525
+ })),
526
+ contractScript,
527
+ contract: state.contract,
528
+ timestamp,
529
+ });
530
+ return;
531
+ case "vtxo_spent":
532
+ if (!state)
533
+ return;
534
+ this.eventCallback({
535
+ type: "vtxo_spent",
536
+ vtxos: vtxos.map((v) => ({
537
+ ...v,
538
+ contractScript,
539
+ // These fields may not be available from basic VirtualCoin
540
+ forfeitTapLeafScript: undefined,
541
+ intentTapLeafScript: undefined,
542
+ tapTree: undefined,
543
+ })),
544
+ contractScript,
545
+ contract: state.contract,
546
+ timestamp,
547
+ });
548
+ return;
549
+ case "contract_expired":
550
+ if (!state)
551
+ return;
552
+ this.eventCallback({
553
+ type: "contract_expired",
554
+ contractScript,
555
+ contract: state.contract,
556
+ timestamp,
557
+ });
558
+ return;
559
+ default:
560
+ return;
561
+ }
562
+ }
563
+ }
@@ -0,0 +1,82 @@
1
+ import { hex } from "@scure/base";
2
+ import { DefaultVtxo } from '../../script/default.js';
3
+ import { isCsvSpendable, sequenceToTimelock, timelockToSequence, } from './helpers.js';
4
+ /**
5
+ * Handler for default wallet VTXOs.
6
+ *
7
+ * Default contracts use the standard forfeit + exit tapscript:
8
+ * - forfeit: (Alice + Server) multisig for collaborative spending
9
+ * - exit: (Alice) + CSV timelock for unilateral exit
10
+ */
11
+ export const DefaultContractHandler = {
12
+ type: "default",
13
+ createScript(params) {
14
+ const typed = this.deserializeParams(params);
15
+ return new DefaultVtxo.Script(typed);
16
+ },
17
+ serializeParams(params) {
18
+ return {
19
+ pubKey: hex.encode(params.pubKey),
20
+ serverPubKey: hex.encode(params.serverPubKey),
21
+ csvTimelock: timelockToSequence(params.csvTimelock).toString(),
22
+ };
23
+ },
24
+ deserializeParams(params) {
25
+ const csvTimelock = params.csvTimelock
26
+ ? sequenceToTimelock(Number(params.csvTimelock))
27
+ : DefaultVtxo.Script.DEFAULT_TIMELOCK;
28
+ return {
29
+ pubKey: hex.decode(params.pubKey),
30
+ serverPubKey: hex.decode(params.serverPubKey),
31
+ csvTimelock,
32
+ };
33
+ },
34
+ selectPath(script, contract, context) {
35
+ if (context.collaborative) {
36
+ // Use forfeit path for collaborative spending
37
+ return { leaf: script.forfeit() };
38
+ }
39
+ // Use exit path for unilateral exit (only if CSV is satisfied)
40
+ const sequence = contract.params.csvTimelock
41
+ ? Number(contract.params.csvTimelock)
42
+ : undefined;
43
+ if (!isCsvSpendable(context, sequence)) {
44
+ return null;
45
+ }
46
+ return {
47
+ leaf: script.exit(),
48
+ sequence,
49
+ };
50
+ },
51
+ getAllSpendingPaths(script, contract, context) {
52
+ const paths = [];
53
+ // Forfeit path available with server cooperation
54
+ if (context.collaborative) {
55
+ paths.push({ leaf: script.forfeit() });
56
+ }
57
+ // Exit path always possible (CSV checked at tx time)
58
+ const exitPath = { leaf: script.exit() };
59
+ if (contract.params.csvTimelock) {
60
+ exitPath.sequence = Number(contract.params.csvTimelock);
61
+ }
62
+ paths.push(exitPath);
63
+ return paths;
64
+ },
65
+ getSpendablePaths(script, contract, context) {
66
+ const paths = [];
67
+ if (context.collaborative) {
68
+ paths.push({ leaf: script.forfeit() });
69
+ }
70
+ const exitSequence = contract.params.csvTimelock
71
+ ? Number(contract.params.csvTimelock)
72
+ : undefined;
73
+ if (isCsvSpendable(context, exitSequence)) {
74
+ const exitPath = { leaf: script.exit() };
75
+ if (exitSequence !== undefined) {
76
+ exitPath.sequence = exitSequence;
77
+ }
78
+ paths.push(exitPath);
79
+ }
80
+ return paths;
81
+ },
82
+ };