@arkade-os/sdk 0.0.16

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 (103) hide show
  1. package/README.md +312 -0
  2. package/dist/cjs/arknote/index.js +86 -0
  3. package/dist/cjs/forfeit.js +38 -0
  4. package/dist/cjs/identity/inMemoryKey.js +40 -0
  5. package/dist/cjs/identity/index.js +2 -0
  6. package/dist/cjs/index.js +48 -0
  7. package/dist/cjs/musig2/index.js +10 -0
  8. package/dist/cjs/musig2/keys.js +57 -0
  9. package/dist/cjs/musig2/nonces.js +44 -0
  10. package/dist/cjs/musig2/sign.js +102 -0
  11. package/dist/cjs/networks.js +26 -0
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/providers/ark.js +530 -0
  14. package/dist/cjs/providers/onchain.js +61 -0
  15. package/dist/cjs/script/address.js +45 -0
  16. package/dist/cjs/script/base.js +51 -0
  17. package/dist/cjs/script/default.js +40 -0
  18. package/dist/cjs/script/tapscript.js +528 -0
  19. package/dist/cjs/script/vhtlc.js +84 -0
  20. package/dist/cjs/tree/signingSession.js +238 -0
  21. package/dist/cjs/tree/validation.js +184 -0
  22. package/dist/cjs/tree/vtxoTree.js +197 -0
  23. package/dist/cjs/utils/bip21.js +114 -0
  24. package/dist/cjs/utils/coinselect.js +73 -0
  25. package/dist/cjs/utils/psbt.js +124 -0
  26. package/dist/cjs/utils/transactionHistory.js +148 -0
  27. package/dist/cjs/utils/txSizeEstimator.js +95 -0
  28. package/dist/cjs/wallet/index.js +8 -0
  29. package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +153 -0
  30. package/dist/cjs/wallet/serviceWorker/db/vtxo/index.js +2 -0
  31. package/dist/cjs/wallet/serviceWorker/request.js +75 -0
  32. package/dist/cjs/wallet/serviceWorker/response.js +187 -0
  33. package/dist/cjs/wallet/serviceWorker/wallet.js +332 -0
  34. package/dist/cjs/wallet/serviceWorker/worker.js +452 -0
  35. package/dist/cjs/wallet/wallet.js +720 -0
  36. package/dist/esm/arknote/index.js +81 -0
  37. package/dist/esm/forfeit.js +35 -0
  38. package/dist/esm/identity/inMemoryKey.js +36 -0
  39. package/dist/esm/identity/index.js +1 -0
  40. package/dist/esm/index.js +39 -0
  41. package/dist/esm/musig2/index.js +3 -0
  42. package/dist/esm/musig2/keys.js +21 -0
  43. package/dist/esm/musig2/nonces.js +8 -0
  44. package/dist/esm/musig2/sign.js +63 -0
  45. package/dist/esm/networks.js +22 -0
  46. package/dist/esm/package.json +3 -0
  47. package/dist/esm/providers/ark.js +526 -0
  48. package/dist/esm/providers/onchain.js +57 -0
  49. package/dist/esm/script/address.js +41 -0
  50. package/dist/esm/script/base.js +46 -0
  51. package/dist/esm/script/default.js +37 -0
  52. package/dist/esm/script/tapscript.js +491 -0
  53. package/dist/esm/script/vhtlc.js +81 -0
  54. package/dist/esm/tree/signingSession.js +200 -0
  55. package/dist/esm/tree/validation.js +179 -0
  56. package/dist/esm/tree/vtxoTree.js +157 -0
  57. package/dist/esm/utils/bip21.js +110 -0
  58. package/dist/esm/utils/coinselect.js +69 -0
  59. package/dist/esm/utils/psbt.js +118 -0
  60. package/dist/esm/utils/transactionHistory.js +145 -0
  61. package/dist/esm/utils/txSizeEstimator.js +91 -0
  62. package/dist/esm/wallet/index.js +5 -0
  63. package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +149 -0
  64. package/dist/esm/wallet/serviceWorker/db/vtxo/index.js +1 -0
  65. package/dist/esm/wallet/serviceWorker/request.js +72 -0
  66. package/dist/esm/wallet/serviceWorker/response.js +184 -0
  67. package/dist/esm/wallet/serviceWorker/wallet.js +328 -0
  68. package/dist/esm/wallet/serviceWorker/worker.js +448 -0
  69. package/dist/esm/wallet/wallet.js +716 -0
  70. package/dist/types/arknote/index.d.ts +17 -0
  71. package/dist/types/forfeit.d.ts +15 -0
  72. package/dist/types/identity/inMemoryKey.d.ts +12 -0
  73. package/dist/types/identity/index.d.ts +7 -0
  74. package/dist/types/index.d.ts +22 -0
  75. package/dist/types/musig2/index.d.ts +4 -0
  76. package/dist/types/musig2/keys.d.ts +9 -0
  77. package/dist/types/musig2/nonces.d.ts +13 -0
  78. package/dist/types/musig2/sign.d.ts +27 -0
  79. package/dist/types/networks.d.ts +16 -0
  80. package/dist/types/providers/ark.d.ts +126 -0
  81. package/dist/types/providers/onchain.d.ts +36 -0
  82. package/dist/types/script/address.d.ts +10 -0
  83. package/dist/types/script/base.d.ts +26 -0
  84. package/dist/types/script/default.d.ts +19 -0
  85. package/dist/types/script/tapscript.d.ts +94 -0
  86. package/dist/types/script/vhtlc.d.ts +31 -0
  87. package/dist/types/tree/signingSession.d.ts +32 -0
  88. package/dist/types/tree/validation.d.ts +22 -0
  89. package/dist/types/tree/vtxoTree.d.ts +32 -0
  90. package/dist/types/utils/bip21.d.ts +21 -0
  91. package/dist/types/utils/coinselect.d.ts +21 -0
  92. package/dist/types/utils/psbt.d.ts +11 -0
  93. package/dist/types/utils/transactionHistory.d.ts +2 -0
  94. package/dist/types/utils/txSizeEstimator.d.ts +27 -0
  95. package/dist/types/wallet/index.d.ts +122 -0
  96. package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +18 -0
  97. package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +12 -0
  98. package/dist/types/wallet/serviceWorker/request.d.ts +68 -0
  99. package/dist/types/wallet/serviceWorker/response.d.ts +107 -0
  100. package/dist/types/wallet/serviceWorker/wallet.d.ts +23 -0
  101. package/dist/types/wallet/serviceWorker/worker.d.ts +26 -0
  102. package/dist/types/wallet/wallet.d.ts +42 -0
  103. package/package.json +88 -0
@@ -0,0 +1,526 @@
1
+ import { TxTree } from '../tree/vtxoTree.js';
2
+ import { hex } from "@scure/base";
3
+ export var SettlementEventType;
4
+ (function (SettlementEventType) {
5
+ SettlementEventType["Finalization"] = "finalization";
6
+ SettlementEventType["Finalized"] = "finalized";
7
+ SettlementEventType["Failed"] = "failed";
8
+ SettlementEventType["SigningStart"] = "signing_start";
9
+ SettlementEventType["SigningNoncesGenerated"] = "signing_nonces_generated";
10
+ })(SettlementEventType || (SettlementEventType = {}));
11
+ export class RestArkProvider {
12
+ constructor(serverUrl) {
13
+ this.serverUrl = serverUrl;
14
+ }
15
+ async getInfo() {
16
+ const url = `${this.serverUrl}/v1/info`;
17
+ const response = await fetch(url);
18
+ if (!response.ok) {
19
+ throw new Error(`Failed to get server info: ${response.statusText}`);
20
+ }
21
+ const fromServer = await response.json();
22
+ return {
23
+ ...fromServer,
24
+ unilateralExitDelay: BigInt(fromServer.unilateralExitDelay ?? 0),
25
+ batchExpiry: BigInt(fromServer.vtxoTreeExpiry ?? 0),
26
+ };
27
+ }
28
+ async getVirtualCoins(address) {
29
+ const url = `${this.serverUrl}/v1/vtxos/${address}`;
30
+ const response = await fetch(url);
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to fetch VTXOs: ${response.statusText}`);
33
+ }
34
+ const data = await response.json();
35
+ return {
36
+ spendableVtxos: [...(data.spendableVtxos || [])].map(convertVtxo),
37
+ spentVtxos: [...(data.spentVtxos || [])].map(convertVtxo),
38
+ };
39
+ }
40
+ async submitVirtualTx(psbtBase64) {
41
+ const url = `${this.serverUrl}/v1/redeem-tx`;
42
+ const response = await fetch(url, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/json",
46
+ },
47
+ body: JSON.stringify({
48
+ redeem_tx: psbtBase64,
49
+ }),
50
+ });
51
+ if (!response.ok) {
52
+ const errorText = await response.text();
53
+ try {
54
+ const grpcError = JSON.parse(errorText);
55
+ // gRPC errors usually have a message and code field
56
+ throw new Error(`Failed to submit virtual transaction: ${grpcError.message || grpcError.error || errorText}`);
57
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
58
+ }
59
+ catch (_) {
60
+ // If JSON parse fails, use the raw error text
61
+ throw new Error(`Failed to submit virtual transaction: ${errorText}`);
62
+ }
63
+ }
64
+ const data = await response.json();
65
+ // Handle both current and future response formats
66
+ return data.txid || data.signedRedeemTx;
67
+ }
68
+ async subscribeToEvents(callback) {
69
+ const url = `${this.serverUrl}/v1/events`;
70
+ let abortController = new AbortController();
71
+ (async () => {
72
+ while (!abortController.signal.aborted) {
73
+ try {
74
+ const response = await fetch(url, {
75
+ headers: {
76
+ Accept: "application/json",
77
+ },
78
+ signal: abortController.signal,
79
+ });
80
+ if (!response.ok) {
81
+ throw new Error(`Unexpected status ${response.status} when fetching event stream`);
82
+ }
83
+ if (!response.body) {
84
+ throw new Error("Response body is null");
85
+ }
86
+ const reader = response.body.getReader();
87
+ const decoder = new TextDecoder();
88
+ let buffer = "";
89
+ while (!abortController.signal.aborted) {
90
+ const { done, value } = await reader.read();
91
+ if (done)
92
+ break;
93
+ // Append new data to buffer and split by newlines
94
+ buffer += decoder.decode(value, { stream: true });
95
+ const lines = buffer.split("\n");
96
+ // Process all complete lines
97
+ for (let i = 0; i < lines.length - 1; i++) {
98
+ const line = lines[i].trim();
99
+ if (!line)
100
+ continue;
101
+ try {
102
+ const data = JSON.parse(line);
103
+ callback(data);
104
+ }
105
+ catch (err) {
106
+ console.error("Failed to parse event:", err);
107
+ }
108
+ }
109
+ // Keep the last partial line in the buffer
110
+ buffer = lines[lines.length - 1];
111
+ }
112
+ }
113
+ catch (error) {
114
+ if (!abortController.signal.aborted) {
115
+ console.error("Event stream error:", error);
116
+ }
117
+ }
118
+ }
119
+ })();
120
+ // Return unsubscribe function
121
+ return () => {
122
+ abortController.abort();
123
+ // Create a new controller for potential future subscriptions
124
+ abortController = new AbortController();
125
+ };
126
+ }
127
+ async registerInputsForNextRound(inputs) {
128
+ const url = `${this.serverUrl}/v1/round/registerInputs`;
129
+ const vtxoInputs = [];
130
+ const noteInputs = [];
131
+ for (const input of inputs) {
132
+ if (typeof input === "string") {
133
+ noteInputs.push(input);
134
+ }
135
+ else {
136
+ vtxoInputs.push({
137
+ outpoint: {
138
+ txid: input.outpoint.txid,
139
+ vout: input.outpoint.vout,
140
+ },
141
+ tapscripts: {
142
+ scripts: input.tapscripts,
143
+ },
144
+ });
145
+ }
146
+ }
147
+ const response = await fetch(url, {
148
+ method: "POST",
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ },
152
+ body: JSON.stringify({
153
+ inputs: vtxoInputs,
154
+ notes: noteInputs,
155
+ }),
156
+ });
157
+ if (!response.ok) {
158
+ const errorText = await response.text();
159
+ throw new Error(`Failed to register inputs: ${errorText}`);
160
+ }
161
+ const data = await response.json();
162
+ return { requestId: data.requestId };
163
+ }
164
+ async registerOutputsForNextRound(requestId, outputs, cosignersPublicKeys, signingAll = false) {
165
+ const url = `${this.serverUrl}/v1/round/registerOutputs`;
166
+ const response = await fetch(url, {
167
+ method: "POST",
168
+ headers: {
169
+ "Content-Type": "application/json",
170
+ },
171
+ body: JSON.stringify({
172
+ requestId,
173
+ outputs: outputs.map((output) => ({
174
+ address: output.address,
175
+ amount: output.amount.toString(10),
176
+ })),
177
+ musig2: {
178
+ cosignersPublicKeys,
179
+ signingAll,
180
+ },
181
+ }),
182
+ });
183
+ if (!response.ok) {
184
+ const errorText = await response.text();
185
+ throw new Error(`Failed to register outputs: ${errorText}`);
186
+ }
187
+ }
188
+ async submitTreeNonces(settlementID, pubkey, nonces) {
189
+ const url = `${this.serverUrl}/v1/round/tree/submitNonces`;
190
+ const response = await fetch(url, {
191
+ method: "POST",
192
+ headers: {
193
+ "Content-Type": "application/json",
194
+ },
195
+ body: JSON.stringify({
196
+ roundId: settlementID,
197
+ pubkey,
198
+ treeNonces: encodeNoncesMatrix(nonces),
199
+ }),
200
+ });
201
+ if (!response.ok) {
202
+ const errorText = await response.text();
203
+ throw new Error(`Failed to submit tree nonces: ${errorText}`);
204
+ }
205
+ }
206
+ async submitTreeSignatures(settlementID, pubkey, signatures) {
207
+ const url = `${this.serverUrl}/v1/round/tree/submitSignatures`;
208
+ const response = await fetch(url, {
209
+ method: "POST",
210
+ headers: {
211
+ "Content-Type": "application/json",
212
+ },
213
+ body: JSON.stringify({
214
+ roundId: settlementID,
215
+ pubkey,
216
+ treeSignatures: encodeSignaturesMatrix(signatures),
217
+ }),
218
+ });
219
+ if (!response.ok) {
220
+ const errorText = await response.text();
221
+ throw new Error(`Failed to submit tree signatures: ${errorText}`);
222
+ }
223
+ }
224
+ async submitSignedForfeitTxs(signedForfeitTxs, signedRoundTx) {
225
+ const url = `${this.serverUrl}/v1/round/submitForfeitTxs`;
226
+ const response = await fetch(url, {
227
+ method: "POST",
228
+ headers: {
229
+ "Content-Type": "application/json",
230
+ },
231
+ body: JSON.stringify({
232
+ signedForfeitTxs: signedForfeitTxs,
233
+ signedRoundTx: signedRoundTx,
234
+ }),
235
+ });
236
+ if (!response.ok) {
237
+ throw new Error(`Failed to submit forfeit transactions: ${response.statusText}`);
238
+ }
239
+ }
240
+ async ping(requestId) {
241
+ const url = `${this.serverUrl}/v1/round/ping/${requestId}`;
242
+ const response = await fetch(url);
243
+ if (!response.ok) {
244
+ throw new Error(`Ping failed: ${response.statusText}`);
245
+ }
246
+ }
247
+ async *getEventStream(signal) {
248
+ const url = `${this.serverUrl}/v1/events`;
249
+ while (!signal?.aborted) {
250
+ try {
251
+ const response = await fetch(url, {
252
+ headers: {
253
+ Accept: "application/json",
254
+ },
255
+ signal,
256
+ });
257
+ if (!response.ok) {
258
+ throw new Error(`Unexpected status ${response.status} when fetching event stream`);
259
+ }
260
+ if (!response.body) {
261
+ throw new Error("Response body is null");
262
+ }
263
+ const reader = response.body.getReader();
264
+ const decoder = new TextDecoder();
265
+ let buffer = "";
266
+ while (!signal?.aborted) {
267
+ const { done, value } = await reader.read();
268
+ if (done) {
269
+ break;
270
+ }
271
+ // Append new data to buffer and split by newlines
272
+ buffer += decoder.decode(value, { stream: true });
273
+ const lines = buffer.split("\n");
274
+ // Process all complete lines
275
+ for (let i = 0; i < lines.length - 1; i++) {
276
+ const line = lines[i].trim();
277
+ if (!line)
278
+ continue;
279
+ try {
280
+ const data = JSON.parse(line);
281
+ const event = this.parseSettlementEvent(data.result);
282
+ if (event) {
283
+ yield event;
284
+ }
285
+ }
286
+ catch (err) {
287
+ console.error("Failed to parse event:", err);
288
+ throw err;
289
+ }
290
+ }
291
+ // Keep the last partial line in the buffer
292
+ buffer = lines[lines.length - 1];
293
+ }
294
+ }
295
+ catch (error) {
296
+ if (error instanceof Error && error.name === "AbortError") {
297
+ break;
298
+ }
299
+ console.error("Event stream error:", error);
300
+ throw error;
301
+ }
302
+ }
303
+ }
304
+ async *subscribeForAddress(address, abortSignal) {
305
+ const url = `${this.serverUrl}/v1/vtxos/${address}/subscribe`;
306
+ while (!abortSignal.aborted) {
307
+ try {
308
+ const response = await fetch(url, {
309
+ headers: {
310
+ Accept: "application/json",
311
+ },
312
+ });
313
+ if (!response.ok) {
314
+ throw new Error(`Unexpected status ${response.status} when subscribing to address updates`);
315
+ }
316
+ if (!response.body) {
317
+ throw new Error("Response body is null");
318
+ }
319
+ const reader = response.body.getReader();
320
+ const decoder = new TextDecoder();
321
+ let buffer = "";
322
+ while (!abortSignal.aborted) {
323
+ const { done, value } = await reader.read();
324
+ if (done) {
325
+ break;
326
+ }
327
+ buffer += decoder.decode(value, { stream: true });
328
+ const lines = buffer.split("\n");
329
+ for (let i = 0; i < lines.length - 1; i++) {
330
+ const line = lines[i].trim();
331
+ if (!line)
332
+ continue;
333
+ try {
334
+ const data = JSON.parse(line);
335
+ if ("result" in data) {
336
+ yield {
337
+ newVtxos: (data.result.newVtxos || []).map(convertVtxo),
338
+ spentVtxos: (data.result.spentVtxos || []).map(convertVtxo),
339
+ };
340
+ }
341
+ }
342
+ catch (err) {
343
+ console.error("Failed to parse address update:", err);
344
+ throw err;
345
+ }
346
+ }
347
+ buffer = lines[lines.length - 1];
348
+ }
349
+ }
350
+ catch (error) {
351
+ console.error("Address subscription error:", error);
352
+ throw error;
353
+ }
354
+ }
355
+ }
356
+ toConnectorsIndex(connectorsIndex) {
357
+ return new Map(Object.entries(connectorsIndex).map(([key, value]) => [
358
+ key,
359
+ { txid: value.txid, vout: value.vout },
360
+ ]));
361
+ }
362
+ toTxTree(t) {
363
+ // collect the parent txids to determine later if a node is a leaf
364
+ const parentTxids = new Set();
365
+ t.levels.forEach((level) => level.nodes.forEach((node) => {
366
+ if (node.parentTxid) {
367
+ parentTxids.add(node.parentTxid);
368
+ }
369
+ }));
370
+ return new TxTree(t.levels.map((level) => level.nodes.map((node) => ({
371
+ txid: node.txid,
372
+ tx: node.tx,
373
+ parentTxid: node.parentTxid,
374
+ leaf: !parentTxids.has(node.txid),
375
+ }))));
376
+ }
377
+ parseSettlementEvent(data) {
378
+ // Check for Finalization event
379
+ if (data.roundFinalization) {
380
+ return {
381
+ type: SettlementEventType.Finalization,
382
+ id: data.roundFinalization.id,
383
+ roundTx: data.roundFinalization.roundTx,
384
+ vtxoTree: this.toTxTree(data.roundFinalization.vtxoTree),
385
+ connectors: this.toTxTree(data.roundFinalization.connectors),
386
+ connectorsIndex: this.toConnectorsIndex(data.roundFinalization.connectorsIndex),
387
+ // divide by 1000 to convert to sat/vbyte
388
+ minRelayFeeRate: BigInt(data.roundFinalization.minRelayFeeRate) /
389
+ BigInt(1000),
390
+ };
391
+ }
392
+ // Check for Finalized event
393
+ if (data.roundFinalized) {
394
+ return {
395
+ type: SettlementEventType.Finalized,
396
+ id: data.roundFinalized.id,
397
+ roundTxid: data.roundFinalized.roundTxid,
398
+ };
399
+ }
400
+ // Check for Failed event
401
+ if (data.roundFailed) {
402
+ return {
403
+ type: SettlementEventType.Failed,
404
+ id: data.roundFailed.id,
405
+ reason: data.roundFailed.reason,
406
+ };
407
+ }
408
+ // Check for Signing event
409
+ if (data.roundSigning) {
410
+ return {
411
+ type: SettlementEventType.SigningStart,
412
+ id: data.roundSigning.id,
413
+ cosignersPublicKeys: data.roundSigning.cosignersPubkeys,
414
+ unsignedVtxoTree: this.toTxTree(data.roundSigning.unsignedVtxoTree),
415
+ unsignedSettlementTx: data.roundSigning.unsignedRoundTx,
416
+ };
417
+ }
418
+ // Check for SigningNoncesGenerated event
419
+ if (data.roundSigningNoncesGenerated) {
420
+ return {
421
+ type: SettlementEventType.SigningNoncesGenerated,
422
+ id: data.roundSigningNoncesGenerated.id,
423
+ treeNonces: decodeNoncesMatrix(hex.decode(data.roundSigningNoncesGenerated.treeNonces)),
424
+ };
425
+ }
426
+ console.warn("Unknown event structure:", data);
427
+ return null;
428
+ }
429
+ }
430
+ function encodeMatrix(matrix) {
431
+ // Calculate total size needed:
432
+ // 4 bytes for number of rows
433
+ // For each row: 4 bytes for length + sum of encoded cell lengths + isNil byte * cell count
434
+ let totalSize = 4;
435
+ for (const row of matrix) {
436
+ totalSize += 4; // row length
437
+ for (const cell of row) {
438
+ totalSize += 1;
439
+ totalSize += cell.length;
440
+ }
441
+ }
442
+ // Create buffer and DataView
443
+ const buffer = new ArrayBuffer(totalSize);
444
+ const view = new DataView(buffer);
445
+ let offset = 0;
446
+ // Write number of rows
447
+ view.setUint32(offset, matrix.length, true); // true for little-endian
448
+ offset += 4;
449
+ // Write each row
450
+ for (const row of matrix) {
451
+ // Write row length
452
+ view.setUint32(offset, row.length, true);
453
+ offset += 4;
454
+ // Write each cell
455
+ for (const cell of row) {
456
+ const notNil = cell.length > 0;
457
+ view.setInt8(offset, notNil ? 1 : 0);
458
+ offset += 1;
459
+ if (!notNil) {
460
+ continue;
461
+ }
462
+ new Uint8Array(buffer).set(cell, offset);
463
+ offset += cell.length;
464
+ }
465
+ }
466
+ return new Uint8Array(buffer);
467
+ }
468
+ function decodeMatrix(matrix, cellLength) {
469
+ // Create DataView to read the buffer
470
+ const view = new DataView(matrix.buffer, matrix.byteOffset, matrix.byteLength);
471
+ let offset = 0;
472
+ // Read number of rows
473
+ const numRows = view.getUint32(offset, true); // true for little-endian
474
+ offset += 4;
475
+ // Initialize result matrix
476
+ const result = [];
477
+ // Read each row
478
+ for (let i = 0; i < numRows; i++) {
479
+ // Read row length
480
+ const rowLength = view.getUint32(offset, true);
481
+ offset += 4;
482
+ const row = [];
483
+ // Read each cell in the row
484
+ for (let j = 0; j < rowLength; j++) {
485
+ const notNil = view.getUint8(offset) === 1;
486
+ offset += 1;
487
+ if (notNil) {
488
+ const cell = new Uint8Array(matrix.buffer, matrix.byteOffset + offset, cellLength);
489
+ row.push(new Uint8Array(cell));
490
+ offset += cellLength;
491
+ }
492
+ else {
493
+ row.push(new Uint8Array());
494
+ }
495
+ }
496
+ result.push(row);
497
+ }
498
+ return result;
499
+ }
500
+ function decodeNoncesMatrix(matrix) {
501
+ const decoded = decodeMatrix(matrix, 66);
502
+ return decoded.map((row) => row.map((nonce) => ({ pubNonce: nonce })));
503
+ }
504
+ function encodeNoncesMatrix(nonces) {
505
+ return hex.encode(encodeMatrix(nonces.map((row) => row.map((nonce) => (nonce ? nonce.pubNonce : new Uint8Array())))));
506
+ }
507
+ function encodeSignaturesMatrix(signatures) {
508
+ return hex.encode(encodeMatrix(signatures.map((row) => row.map((s) => (s ? s.encode() : new Uint8Array())))));
509
+ }
510
+ function convertVtxo(vtxo) {
511
+ return {
512
+ txid: vtxo.outpoint.txid,
513
+ vout: vtxo.outpoint.vout,
514
+ value: Number(vtxo.amount),
515
+ status: {
516
+ confirmed: !!vtxo.roundTxid,
517
+ },
518
+ virtualStatus: {
519
+ state: vtxo.isPending ? "pending" : "settled",
520
+ batchTxID: vtxo.roundTxid,
521
+ batchExpiry: vtxo.expireAt ? Number(vtxo.expireAt) : undefined,
522
+ },
523
+ spentBy: vtxo.spentBy,
524
+ createdAt: new Date(vtxo.createdAt * 1000),
525
+ };
526
+ }
@@ -0,0 +1,57 @@
1
+ export const ESPLORA_URL = {
2
+ bitcoin: "https://mempool.space/api",
3
+ testnet: "https://mempool.space/testnet/api",
4
+ signet: "https://mempool.space/signet/api",
5
+ mutinynet: "https://mutinynet.com/api",
6
+ regtest: "http://localhost:3000",
7
+ };
8
+ export class EsploraProvider {
9
+ constructor(baseUrl) {
10
+ this.baseUrl = baseUrl;
11
+ }
12
+ async getCoins(address) {
13
+ const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
14
+ if (!response.ok) {
15
+ throw new Error(`Failed to fetch UTXOs: ${response.statusText}`);
16
+ }
17
+ return response.json();
18
+ }
19
+ async getFeeRate() {
20
+ const response = await fetch(`${this.baseUrl}/v1/fees/recommended`);
21
+ if (!response.ok) {
22
+ throw new Error(`Failed to fetch fee rate: ${response.statusText}`);
23
+ }
24
+ const fees = await response.json();
25
+ return fees.halfHourFee; // Return the "medium" priority fee rate
26
+ }
27
+ async broadcastTransaction(txHex) {
28
+ const response = await fetch(`${this.baseUrl}/tx`, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "text/plain",
32
+ },
33
+ body: txHex,
34
+ });
35
+ if (!response.ok) {
36
+ const error = await response.text();
37
+ throw new Error(`Failed to broadcast transaction: ${error}`);
38
+ }
39
+ return response.text(); // Returns the txid
40
+ }
41
+ async getTxOutspends(txid) {
42
+ const response = await fetch(`${this.baseUrl}/tx/${txid}/outspends`);
43
+ if (!response.ok) {
44
+ const error = await response.text();
45
+ throw new Error(`Failed to get transaction outspends: ${error}`);
46
+ }
47
+ return response.json();
48
+ }
49
+ async getTransactions(address) {
50
+ const response = await fetch(`${this.baseUrl}/address/${address}/txs`);
51
+ if (!response.ok) {
52
+ const error = await response.text();
53
+ throw new Error(`Failed to get transactions: ${error}`);
54
+ }
55
+ return response.json();
56
+ }
57
+ }
@@ -0,0 +1,41 @@
1
+ import { bech32m } from "@scure/base";
2
+ import { Script } from "@scure/btc-signer";
3
+ // ArkAddress is a bech32m encoded address with a custom HRP (ark/tark)
4
+ export class ArkAddress {
5
+ constructor(serverPubKey, tweakedPubKey, hrp) {
6
+ this.serverPubKey = serverPubKey;
7
+ this.tweakedPubKey = tweakedPubKey;
8
+ this.hrp = hrp;
9
+ if (serverPubKey.length !== 32) {
10
+ throw new Error("Invalid server public key length");
11
+ }
12
+ if (tweakedPubKey.length !== 32) {
13
+ throw new Error("Invalid tweaked public key length");
14
+ }
15
+ }
16
+ static decode(address) {
17
+ const decoded = bech32m.decodeUnsafe(address, 1023);
18
+ if (!decoded) {
19
+ throw new Error("Invalid address");
20
+ }
21
+ const data = new Uint8Array(bech32m.fromWords(decoded.words));
22
+ // First 32 bytes are server pubkey, next 32 bytes are tweaked pubkey
23
+ if (data.length !== 64) {
24
+ throw new Error("Invalid data length");
25
+ }
26
+ const serverPubKey = data.slice(0, 32);
27
+ const tweakedPubKey = data.slice(32, 64);
28
+ return new ArkAddress(serverPubKey, tweakedPubKey, decoded.prefix);
29
+ }
30
+ encode() {
31
+ // Combine server pubkey and tweaked pubkey
32
+ const data = new Uint8Array(64);
33
+ data.set(this.serverPubKey, 0);
34
+ data.set(this.tweakedPubKey, 32);
35
+ const words = bech32m.toWords(data);
36
+ return bech32m.encode(this.hrp, words, 1023);
37
+ }
38
+ get pkScript() {
39
+ return Script.encode(["OP_1", this.tweakedPubKey]);
40
+ }
41
+ }
@@ -0,0 +1,46 @@
1
+ import { Address, p2tr, TAP_LEAF_VERSION, taprootListToTree, } from "@scure/btc-signer/payment";
2
+ import { TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer/utils";
3
+ import { ArkAddress } from './address.js';
4
+ import { Script } from "@scure/btc-signer";
5
+ import { hex } from "@scure/base";
6
+ export function scriptFromTapLeafScript(leaf) {
7
+ return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
8
+ }
9
+ export class VtxoScript {
10
+ static decode(scripts) {
11
+ return new VtxoScript(scripts.map(hex.decode));
12
+ }
13
+ constructor(scripts) {
14
+ this.scripts = scripts;
15
+ const tapTree = taprootListToTree(scripts.map((script) => ({ script, leafVersion: TAP_LEAF_VERSION })));
16
+ const payment = p2tr(TAPROOT_UNSPENDABLE_KEY, tapTree, undefined, true);
17
+ if (!payment.tapLeafScript ||
18
+ payment.tapLeafScript.length !== scripts.length) {
19
+ throw new Error("invalid scripts");
20
+ }
21
+ this.leaves = payment.tapLeafScript;
22
+ this.tweakedPublicKey = payment.tweakedPubkey;
23
+ }
24
+ encode() {
25
+ return this.scripts.map(hex.encode);
26
+ }
27
+ address(prefix, serverPubKey) {
28
+ return new ArkAddress(serverPubKey, this.tweakedPublicKey, prefix);
29
+ }
30
+ get pkScript() {
31
+ return Script.encode(["OP_1", this.tweakedPublicKey]);
32
+ }
33
+ onchainAddress(network) {
34
+ return Address(network).encode({
35
+ type: "tr",
36
+ pubkey: this.tweakedPublicKey,
37
+ });
38
+ }
39
+ findLeaf(scriptHex) {
40
+ const leaf = this.leaves.find((leaf) => hex.encode(scriptFromTapLeafScript(leaf)) === scriptHex);
41
+ if (!leaf) {
42
+ throw new Error(`leaf '${scriptHex}' not found`);
43
+ }
44
+ return leaf;
45
+ }
46
+ }