@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.
- package/README.md +312 -0
- package/dist/cjs/arknote/index.js +86 -0
- package/dist/cjs/forfeit.js +38 -0
- package/dist/cjs/identity/inMemoryKey.js +40 -0
- package/dist/cjs/identity/index.js +2 -0
- package/dist/cjs/index.js +48 -0
- package/dist/cjs/musig2/index.js +10 -0
- package/dist/cjs/musig2/keys.js +57 -0
- package/dist/cjs/musig2/nonces.js +44 -0
- package/dist/cjs/musig2/sign.js +102 -0
- package/dist/cjs/networks.js +26 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/providers/ark.js +530 -0
- package/dist/cjs/providers/onchain.js +61 -0
- package/dist/cjs/script/address.js +45 -0
- package/dist/cjs/script/base.js +51 -0
- package/dist/cjs/script/default.js +40 -0
- package/dist/cjs/script/tapscript.js +528 -0
- package/dist/cjs/script/vhtlc.js +84 -0
- package/dist/cjs/tree/signingSession.js +238 -0
- package/dist/cjs/tree/validation.js +184 -0
- package/dist/cjs/tree/vtxoTree.js +197 -0
- package/dist/cjs/utils/bip21.js +114 -0
- package/dist/cjs/utils/coinselect.js +73 -0
- package/dist/cjs/utils/psbt.js +124 -0
- package/dist/cjs/utils/transactionHistory.js +148 -0
- package/dist/cjs/utils/txSizeEstimator.js +95 -0
- package/dist/cjs/wallet/index.js +8 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +153 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/index.js +2 -0
- package/dist/cjs/wallet/serviceWorker/request.js +75 -0
- package/dist/cjs/wallet/serviceWorker/response.js +187 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +332 -0
- package/dist/cjs/wallet/serviceWorker/worker.js +452 -0
- package/dist/cjs/wallet/wallet.js +720 -0
- package/dist/esm/arknote/index.js +81 -0
- package/dist/esm/forfeit.js +35 -0
- package/dist/esm/identity/inMemoryKey.js +36 -0
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/musig2/index.js +3 -0
- package/dist/esm/musig2/keys.js +21 -0
- package/dist/esm/musig2/nonces.js +8 -0
- package/dist/esm/musig2/sign.js +63 -0
- package/dist/esm/networks.js +22 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/providers/ark.js +526 -0
- package/dist/esm/providers/onchain.js +57 -0
- package/dist/esm/script/address.js +41 -0
- package/dist/esm/script/base.js +46 -0
- package/dist/esm/script/default.js +37 -0
- package/dist/esm/script/tapscript.js +491 -0
- package/dist/esm/script/vhtlc.js +81 -0
- package/dist/esm/tree/signingSession.js +200 -0
- package/dist/esm/tree/validation.js +179 -0
- package/dist/esm/tree/vtxoTree.js +157 -0
- package/dist/esm/utils/bip21.js +110 -0
- package/dist/esm/utils/coinselect.js +69 -0
- package/dist/esm/utils/psbt.js +118 -0
- package/dist/esm/utils/transactionHistory.js +145 -0
- package/dist/esm/utils/txSizeEstimator.js +91 -0
- package/dist/esm/wallet/index.js +5 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +149 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/index.js +1 -0
- package/dist/esm/wallet/serviceWorker/request.js +72 -0
- package/dist/esm/wallet/serviceWorker/response.js +184 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +328 -0
- package/dist/esm/wallet/serviceWorker/worker.js +448 -0
- package/dist/esm/wallet/wallet.js +716 -0
- package/dist/types/arknote/index.d.ts +17 -0
- package/dist/types/forfeit.d.ts +15 -0
- package/dist/types/identity/inMemoryKey.d.ts +12 -0
- package/dist/types/identity/index.d.ts +7 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/musig2/index.d.ts +4 -0
- package/dist/types/musig2/keys.d.ts +9 -0
- package/dist/types/musig2/nonces.d.ts +13 -0
- package/dist/types/musig2/sign.d.ts +27 -0
- package/dist/types/networks.d.ts +16 -0
- package/dist/types/providers/ark.d.ts +126 -0
- package/dist/types/providers/onchain.d.ts +36 -0
- package/dist/types/script/address.d.ts +10 -0
- package/dist/types/script/base.d.ts +26 -0
- package/dist/types/script/default.d.ts +19 -0
- package/dist/types/script/tapscript.d.ts +94 -0
- package/dist/types/script/vhtlc.d.ts +31 -0
- package/dist/types/tree/signingSession.d.ts +32 -0
- package/dist/types/tree/validation.d.ts +22 -0
- package/dist/types/tree/vtxoTree.d.ts +32 -0
- package/dist/types/utils/bip21.d.ts +21 -0
- package/dist/types/utils/coinselect.d.ts +21 -0
- package/dist/types/utils/psbt.d.ts +11 -0
- package/dist/types/utils/transactionHistory.d.ts +2 -0
- package/dist/types/utils/txSizeEstimator.d.ts +27 -0
- package/dist/types/wallet/index.d.ts +122 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +18 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +12 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +68 -0
- package/dist/types/wallet/serviceWorker/response.d.ts +107 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +23 -0
- package/dist/types/wallet/serviceWorker/worker.d.ts +26 -0
- package/dist/types/wallet/wallet.d.ts +42 -0
- 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
|
+
}
|