@arkade-os/sdk 0.1.4 → 0.2.1
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 +157 -174
- package/dist/cjs/arknote/index.js +61 -58
- package/dist/cjs/bip322/errors.js +13 -0
- package/dist/cjs/bip322/index.js +178 -0
- package/dist/cjs/forfeit.js +14 -25
- package/dist/cjs/identity/singleKey.js +68 -0
- package/dist/cjs/index.js +43 -17
- package/dist/cjs/providers/ark.js +261 -321
- package/dist/cjs/providers/indexer.js +525 -0
- package/dist/cjs/providers/onchain.js +193 -15
- package/dist/cjs/script/address.js +48 -17
- package/dist/cjs/script/base.js +120 -3
- package/dist/cjs/script/default.js +18 -4
- package/dist/cjs/script/tapscript.js +61 -20
- package/dist/cjs/script/vhtlc.js +85 -7
- package/dist/cjs/tree/signingSession.js +63 -106
- package/dist/cjs/tree/txTree.js +193 -0
- package/dist/cjs/tree/validation.js +79 -155
- package/dist/cjs/utils/anchor.js +35 -0
- package/dist/cjs/utils/arkTransaction.js +108 -0
- package/dist/cjs/utils/transactionHistory.js +84 -72
- package/dist/cjs/utils/txSizeEstimator.js +12 -0
- package/dist/cjs/utils/unknownFields.js +211 -0
- package/dist/cjs/wallet/index.js +12 -0
- package/dist/cjs/wallet/onchain.js +201 -0
- package/dist/cjs/wallet/ramps.js +95 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/cjs/wallet/serviceWorker/request.js +15 -12
- package/dist/cjs/wallet/serviceWorker/response.js +22 -27
- package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +61 -34
- package/dist/cjs/wallet/serviceWorker/worker.js +120 -108
- package/dist/cjs/wallet/unroll.js +270 -0
- package/dist/cjs/wallet/wallet.js +701 -454
- package/dist/esm/arknote/index.js +61 -57
- package/dist/esm/bip322/errors.js +9 -0
- package/dist/esm/bip322/index.js +174 -0
- package/dist/esm/forfeit.js +15 -26
- package/dist/esm/identity/singleKey.js +64 -0
- package/dist/esm/index.js +31 -12
- package/dist/esm/providers/ark.js +259 -320
- package/dist/esm/providers/indexer.js +521 -0
- package/dist/esm/providers/onchain.js +193 -15
- package/dist/esm/script/address.js +48 -17
- package/dist/esm/script/base.js +120 -3
- package/dist/esm/script/default.js +18 -4
- package/dist/esm/script/tapscript.js +61 -20
- package/dist/esm/script/vhtlc.js +85 -7
- package/dist/esm/tree/signingSession.js +65 -108
- package/dist/esm/tree/txTree.js +189 -0
- package/dist/esm/tree/validation.js +75 -152
- package/dist/esm/utils/anchor.js +31 -0
- package/dist/esm/utils/arkTransaction.js +105 -0
- package/dist/esm/utils/transactionHistory.js +84 -72
- package/dist/esm/utils/txSizeEstimator.js +12 -0
- package/dist/esm/utils/unknownFields.js +173 -0
- package/dist/esm/wallet/index.js +9 -0
- package/dist/esm/wallet/onchain.js +196 -0
- package/dist/esm/wallet/ramps.js +91 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/esm/wallet/serviceWorker/request.js +15 -12
- package/dist/esm/wallet/serviceWorker/response.js +22 -27
- package/dist/esm/wallet/serviceWorker/utils.js +8 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +62 -35
- package/dist/esm/wallet/serviceWorker/worker.js +120 -108
- package/dist/esm/wallet/unroll.js +267 -0
- package/dist/esm/wallet/wallet.js +674 -461
- package/dist/types/arknote/index.d.ts +40 -13
- package/dist/types/bip322/errors.d.ts +6 -0
- package/dist/types/bip322/index.d.ts +57 -0
- package/dist/types/forfeit.d.ts +2 -14
- package/dist/types/identity/singleKey.d.ts +27 -0
- package/dist/types/index.d.ts +24 -12
- package/dist/types/providers/ark.d.ts +114 -95
- package/dist/types/providers/indexer.d.ts +186 -0
- package/dist/types/providers/onchain.d.ts +41 -11
- package/dist/types/script/address.d.ts +26 -2
- package/dist/types/script/base.d.ts +13 -3
- package/dist/types/script/default.d.ts +22 -0
- package/dist/types/script/tapscript.d.ts +61 -5
- package/dist/types/script/vhtlc.d.ts +27 -0
- package/dist/types/tree/signingSession.d.ts +5 -5
- package/dist/types/tree/txTree.d.ts +28 -0
- package/dist/types/tree/validation.d.ts +15 -22
- package/dist/types/utils/anchor.d.ts +19 -0
- package/dist/types/utils/arkTransaction.d.ts +27 -0
- package/dist/types/utils/transactionHistory.d.ts +7 -1
- package/dist/types/utils/txSizeEstimator.d.ts +3 -0
- package/dist/types/utils/unknownFields.d.ts +83 -0
- package/dist/types/wallet/index.d.ts +51 -50
- package/dist/types/wallet/onchain.d.ts +49 -0
- package/dist/types/wallet/ramps.d.ts +32 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
- package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
- package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
- package/dist/types/wallet/unroll.d.ts +102 -0
- package/dist/types/wallet/wallet.d.ts +71 -25
- package/package.json +37 -35
- package/dist/cjs/identity/inMemoryKey.js +0 -40
- package/dist/cjs/tree/vtxoTree.js +0 -231
- package/dist/cjs/utils/coinselect.js +0 -73
- package/dist/cjs/utils/psbt.js +0 -137
- package/dist/esm/identity/inMemoryKey.js +0 -36
- package/dist/esm/tree/vtxoTree.js +0 -191
- package/dist/esm/utils/coinselect.js +0 -69
- package/dist/esm/utils/psbt.js +0 -131
- package/dist/types/identity/inMemoryKey.d.ts +0 -12
- package/dist/types/tree/vtxoTree.d.ts +0 -33
- package/dist/types/utils/coinselect.d.ts +0 -21
- package/dist/types/utils/psbt.d.ts +0 -11
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/// <reference lib="webworker" />
|
|
2
|
-
import {
|
|
2
|
+
import { SingleKey } from '../../identity/singleKey.js';
|
|
3
|
+
import { isSpendable, isSubdust } from '../index.js';
|
|
3
4
|
import { Wallet } from '../wallet.js';
|
|
4
5
|
import { Request } from './request.js';
|
|
5
6
|
import { Response } from './response.js';
|
|
6
7
|
import { RestArkProvider } from '../../providers/ark.js';
|
|
7
|
-
import { DefaultVtxo } from '../../script/default.js';
|
|
8
8
|
import { IndexedDBVtxoRepository } from './db/vtxo/idb.js';
|
|
9
9
|
import { vtxosToTxs } from '../../utils/transactionHistory.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { RestIndexerProvider } from '../../providers/indexer.js';
|
|
11
|
+
import { base64, hex } from "@scure/base";
|
|
12
|
+
import { Transaction } from "@scure/btc-signer";
|
|
13
|
+
/**
|
|
14
|
+
* Worker is a class letting to interact with ServiceWorkerWallet from the client
|
|
15
|
+
* it aims to be run in a service worker context
|
|
16
|
+
*/
|
|
12
17
|
export class Worker {
|
|
13
18
|
constructor(vtxoRepository = new IndexedDBVtxoRepository(), messageCallback = () => { }) {
|
|
14
19
|
this.vtxoRepository = vtxoRepository;
|
|
@@ -36,41 +41,48 @@ export class Worker {
|
|
|
36
41
|
await this.vtxoRepository.close();
|
|
37
42
|
this.wallet = undefined;
|
|
38
43
|
this.arkProvider = undefined;
|
|
44
|
+
this.indexerProvider = undefined;
|
|
39
45
|
this.vtxoSubscription = undefined;
|
|
40
46
|
}
|
|
41
47
|
async onWalletInitialized() {
|
|
42
48
|
if (!this.wallet ||
|
|
43
49
|
!this.arkProvider ||
|
|
50
|
+
!this.indexerProvider ||
|
|
44
51
|
!this.wallet.offchainTapscript ||
|
|
45
52
|
!this.wallet.boardingTapscript) {
|
|
46
53
|
return;
|
|
47
54
|
}
|
|
48
55
|
// subscribe to address updates
|
|
49
|
-
const addressInfo = await this.wallet.getAddressInfo();
|
|
50
|
-
if (!addressInfo.offchain) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
56
|
await this.vtxoRepository.open();
|
|
54
|
-
// set the initial vtxos state
|
|
55
|
-
const { spendableVtxos, spentVtxos } = await this.arkProvider.getVirtualCoins(addressInfo.offchain.address);
|
|
56
57
|
const encodedOffchainTapscript = this.wallet.offchainTapscript.encode();
|
|
57
58
|
const forfeit = this.wallet.offchainTapscript.forfeit();
|
|
58
|
-
const
|
|
59
|
+
const exit = this.wallet.offchainTapscript.exit();
|
|
60
|
+
const script = hex.encode(this.wallet.offchainTapscript.pkScript);
|
|
61
|
+
// set the initial vtxos state
|
|
62
|
+
const response = await this.indexerProvider.getVtxos({
|
|
63
|
+
scripts: [script],
|
|
64
|
+
});
|
|
65
|
+
const vtxos = response.vtxos.map((vtxo) => ({
|
|
59
66
|
...vtxo,
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
forfeitTapLeafScript: forfeit,
|
|
68
|
+
intentTapLeafScript: exit,
|
|
69
|
+
tapTree: encodedOffchainTapscript,
|
|
62
70
|
}));
|
|
63
71
|
await this.vtxoRepository.addOrUpdate(vtxos);
|
|
64
|
-
this.processVtxoSubscription(
|
|
72
|
+
this.processVtxoSubscription({
|
|
73
|
+
script,
|
|
74
|
+
vtxoScript: this.wallet.offchainTapscript,
|
|
75
|
+
});
|
|
65
76
|
}
|
|
66
|
-
async processVtxoSubscription({
|
|
77
|
+
async processVtxoSubscription({ script, vtxoScript, }) {
|
|
67
78
|
try {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const tapLeafScript = vtxoScript.findLeaf(scripts.forfeit[0]);
|
|
79
|
+
const forfeitTapLeafScript = vtxoScript.forfeit();
|
|
80
|
+
const intentTapLeafScript = vtxoScript.exit();
|
|
71
81
|
const abortController = new AbortController();
|
|
72
|
-
const
|
|
82
|
+
const subscriptionId = await this.indexerProvider.subscribeForScripts([script]);
|
|
83
|
+
const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
|
|
73
84
|
this.vtxoSubscription = abortController;
|
|
85
|
+
const tapTree = vtxoScript.encode();
|
|
74
86
|
for await (const update of subscription) {
|
|
75
87
|
const vtxos = [...update.newVtxos, ...update.spentVtxos];
|
|
76
88
|
if (vtxos.length === 0) {
|
|
@@ -78,8 +90,9 @@ export class Worker {
|
|
|
78
90
|
}
|
|
79
91
|
const extendedVtxos = vtxos.map((vtxo) => ({
|
|
80
92
|
...vtxo,
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
forfeitTapLeafScript,
|
|
94
|
+
intentTapLeafScript,
|
|
95
|
+
tapTree,
|
|
83
96
|
}));
|
|
84
97
|
await this.vtxoRepository.addOrUpdate(extendedVtxos);
|
|
85
98
|
}
|
|
@@ -103,9 +116,9 @@ export class Worker {
|
|
|
103
116
|
}
|
|
104
117
|
try {
|
|
105
118
|
this.arkProvider = new RestArkProvider(message.arkServerUrl);
|
|
119
|
+
this.indexerProvider = new RestIndexerProvider(message.arkServerUrl);
|
|
106
120
|
this.wallet = await Wallet.create({
|
|
107
|
-
|
|
108
|
-
identity: InMemoryKey.fromHex(message.privateKey),
|
|
121
|
+
identity: SingleKey.fromHex(message.privateKey),
|
|
109
122
|
arkServerUrl: message.arkServerUrl,
|
|
110
123
|
arkServerPublicKey: message.arkServerPublicKey,
|
|
111
124
|
});
|
|
@@ -159,7 +172,7 @@ export class Worker {
|
|
|
159
172
|
return;
|
|
160
173
|
}
|
|
161
174
|
try {
|
|
162
|
-
const txid = await this.wallet.sendBitcoin(message.params
|
|
175
|
+
const txid = await this.wallet.sendBitcoin(message.params);
|
|
163
176
|
event.source?.postMessage(Response.sendBitcoinSuccess(message.id, txid));
|
|
164
177
|
}
|
|
165
178
|
catch (error) {
|
|
@@ -183,8 +196,8 @@ export class Worker {
|
|
|
183
196
|
return;
|
|
184
197
|
}
|
|
185
198
|
try {
|
|
186
|
-
const
|
|
187
|
-
event.source?.postMessage(Response.
|
|
199
|
+
const address = await this.wallet.getAddress();
|
|
200
|
+
event.source?.postMessage(Response.address(message.id, address));
|
|
188
201
|
}
|
|
189
202
|
catch (error) {
|
|
190
203
|
console.error("Error getting address:", error);
|
|
@@ -194,11 +207,11 @@ export class Worker {
|
|
|
194
207
|
event.source?.postMessage(Response.error(message.id, errorMessage));
|
|
195
208
|
}
|
|
196
209
|
}
|
|
197
|
-
async
|
|
210
|
+
async handleGetBoardingAddress(event) {
|
|
198
211
|
const message = event.data;
|
|
199
|
-
if (!Request.
|
|
200
|
-
console.error("Invalid
|
|
201
|
-
event.source?.postMessage(Response.error(message.id, "Invalid
|
|
212
|
+
if (!Request.isGetBoardingAddress(message)) {
|
|
213
|
+
console.error("Invalid GET_BOARDING_ADDRESS message format", message);
|
|
214
|
+
event.source?.postMessage(Response.error(message.id, "Invalid GET_BOARDING_ADDRESS message format"));
|
|
202
215
|
return;
|
|
203
216
|
}
|
|
204
217
|
if (!this.wallet) {
|
|
@@ -207,11 +220,11 @@ export class Worker {
|
|
|
207
220
|
return;
|
|
208
221
|
}
|
|
209
222
|
try {
|
|
210
|
-
const
|
|
211
|
-
event.source?.postMessage(Response.
|
|
223
|
+
const address = await this.wallet.getBoardingAddress();
|
|
224
|
+
event.source?.postMessage(Response.boardingAddress(message.id, address));
|
|
212
225
|
}
|
|
213
226
|
catch (error) {
|
|
214
|
-
console.error("Error getting address
|
|
227
|
+
console.error("Error getting boarding address:", error);
|
|
215
228
|
const errorMessage = error instanceof Error
|
|
216
229
|
? error.message
|
|
217
230
|
: "Unknown error occurred";
|
|
@@ -231,40 +244,52 @@ export class Worker {
|
|
|
231
244
|
return;
|
|
232
245
|
}
|
|
233
246
|
try {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
.
|
|
237
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
247
|
+
const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
|
|
248
|
+
this.wallet.getBoardingUtxos(),
|
|
249
|
+
this.vtxoRepository.getSpendableVtxos(),
|
|
250
|
+
this.vtxoRepository.getSweptVtxos(),
|
|
251
|
+
]);
|
|
252
|
+
// boarding
|
|
253
|
+
let confirmed = 0;
|
|
254
|
+
let unconfirmed = 0;
|
|
255
|
+
for (const utxo of boardingUtxos) {
|
|
256
|
+
if (utxo.status.confirmed) {
|
|
257
|
+
confirmed += utxo.value;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
unconfirmed += utxo.value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// offchain
|
|
264
|
+
let settled = 0;
|
|
265
|
+
let preconfirmed = 0;
|
|
266
|
+
let recoverable = 0;
|
|
267
|
+
for (const vtxo of spendableVtxos) {
|
|
268
|
+
if (vtxo.virtualStatus.state === "settled") {
|
|
269
|
+
settled += vtxo.value;
|
|
270
|
+
}
|
|
271
|
+
else if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
272
|
+
preconfirmed += vtxo.value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const vtxo of sweptVtxos) {
|
|
276
|
+
if (isSpendable(vtxo)) {
|
|
277
|
+
recoverable += vtxo.value;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const totalBoarding = confirmed + unconfirmed;
|
|
281
|
+
const totalOffchain = settled + preconfirmed + recoverable;
|
|
255
282
|
event.source?.postMessage(Response.balance(message.id, {
|
|
256
|
-
|
|
257
|
-
confirmed
|
|
258
|
-
unconfirmed
|
|
259
|
-
total:
|
|
283
|
+
boarding: {
|
|
284
|
+
confirmed,
|
|
285
|
+
unconfirmed,
|
|
286
|
+
total: totalBoarding,
|
|
260
287
|
},
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
},
|
|
267
|
-
total: onchainTotal + offchainTotal,
|
|
288
|
+
settled,
|
|
289
|
+
preconfirmed,
|
|
290
|
+
available: settled + preconfirmed,
|
|
291
|
+
recoverable,
|
|
292
|
+
total: totalBoarding + totalOffchain,
|
|
268
293
|
}));
|
|
269
294
|
}
|
|
270
295
|
catch (error) {
|
|
@@ -275,30 +300,6 @@ export class Worker {
|
|
|
275
300
|
event.source?.postMessage(Response.error(message.id, errorMessage));
|
|
276
301
|
}
|
|
277
302
|
}
|
|
278
|
-
async handleGetCoins(event) {
|
|
279
|
-
const message = event.data;
|
|
280
|
-
if (!Request.isGetCoins(message)) {
|
|
281
|
-
console.error("Invalid GET_COINS message format", message);
|
|
282
|
-
event.source?.postMessage(Response.error(message.id, "Invalid GET_COINS message format"));
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (!this.wallet) {
|
|
286
|
-
console.error("Wallet not initialized");
|
|
287
|
-
event.source?.postMessage(Response.error(message.id, "Wallet not initialized"));
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
try {
|
|
291
|
-
const coins = await this.wallet.getCoins();
|
|
292
|
-
event.source?.postMessage(Response.coins(message.id, coins));
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
console.error("Error getting coins:", error);
|
|
296
|
-
const errorMessage = error instanceof Error
|
|
297
|
-
? error.message
|
|
298
|
-
: "Unknown error occurred";
|
|
299
|
-
event.source?.postMessage(Response.error(message.id, errorMessage));
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
303
|
async handleGetVtxos(event) {
|
|
303
304
|
const message = event.data;
|
|
304
305
|
if (!Request.isGetVtxos(message)) {
|
|
@@ -312,7 +313,18 @@ export class Worker {
|
|
|
312
313
|
return;
|
|
313
314
|
}
|
|
314
315
|
try {
|
|
315
|
-
|
|
316
|
+
let vtxos = await this.vtxoRepository.getSpendableVtxos();
|
|
317
|
+
if (!message.filter?.withRecoverable) {
|
|
318
|
+
if (!this.wallet)
|
|
319
|
+
throw new Error("Wallet not initialized");
|
|
320
|
+
// exclude subdust is we don't want recoverable
|
|
321
|
+
vtxos = vtxos.filter((v) => !isSubdust(v, this.wallet.dustAmount));
|
|
322
|
+
}
|
|
323
|
+
if (message.filter?.withRecoverable) {
|
|
324
|
+
// get also swept and spendable vtxos
|
|
325
|
+
const sweptVtxos = await this.vtxoRepository.getSweptVtxos();
|
|
326
|
+
vtxos.push(...sweptVtxos.filter(isSpendable));
|
|
327
|
+
}
|
|
316
328
|
event.source?.postMessage(Response.vtxos(message.id, vtxos));
|
|
317
329
|
}
|
|
318
330
|
catch (error) {
|
|
@@ -360,7 +372,7 @@ export class Worker {
|
|
|
360
372
|
return;
|
|
361
373
|
}
|
|
362
374
|
try {
|
|
363
|
-
const { boardingTxs, roundsToIgnore } = await this.wallet.getBoardingTxs();
|
|
375
|
+
const { boardingTxs, commitmentsToIgnore: roundsToIgnore } = await this.wallet.getBoardingTxs();
|
|
364
376
|
const { spendable, spent } = await this.vtxoRepository.getAllVtxos();
|
|
365
377
|
// convert VTXOs to offchain transactions
|
|
366
378
|
const offchainTxs = vtxosToTxs(spendable, spent, roundsToIgnore);
|
|
@@ -394,11 +406,11 @@ export class Worker {
|
|
|
394
406
|
}
|
|
395
407
|
event.source?.postMessage(Response.walletStatus(message.id, this.wallet !== undefined));
|
|
396
408
|
}
|
|
397
|
-
async
|
|
409
|
+
async handleSign(event) {
|
|
398
410
|
const message = event.data;
|
|
399
|
-
if (!Request.
|
|
400
|
-
console.error("Invalid
|
|
401
|
-
event.source?.postMessage(Response.error(message.id, "Invalid
|
|
411
|
+
if (!Request.isSign(message)) {
|
|
412
|
+
console.error("Invalid SIGN message format", message);
|
|
413
|
+
event.source?.postMessage(Response.error(message.id, "Invalid SIGN message format"));
|
|
402
414
|
return;
|
|
403
415
|
}
|
|
404
416
|
if (!this.wallet) {
|
|
@@ -407,11 +419,15 @@ export class Worker {
|
|
|
407
419
|
return;
|
|
408
420
|
}
|
|
409
421
|
try {
|
|
410
|
-
|
|
411
|
-
|
|
422
|
+
const tx = Transaction.fromPSBT(base64.decode(message.tx), {
|
|
423
|
+
allowUnknown: true,
|
|
424
|
+
allowUnknownInputs: true,
|
|
425
|
+
});
|
|
426
|
+
const signedTx = await this.wallet.identity.sign(tx, message.inputIndexes);
|
|
427
|
+
event.source?.postMessage(Response.signSuccess(message.id, base64.encode(signedTx.toPSBT())));
|
|
412
428
|
}
|
|
413
429
|
catch (error) {
|
|
414
|
-
console.error("Error
|
|
430
|
+
console.error("Error signing:", error);
|
|
415
431
|
const errorMessage = error instanceof Error
|
|
416
432
|
? error.message
|
|
417
433
|
: "Unknown error occurred";
|
|
@@ -443,18 +459,14 @@ export class Worker {
|
|
|
443
459
|
await this.handleGetAddress(event);
|
|
444
460
|
break;
|
|
445
461
|
}
|
|
446
|
-
case "
|
|
447
|
-
await this.
|
|
462
|
+
case "GET_BOARDING_ADDRESS": {
|
|
463
|
+
await this.handleGetBoardingAddress(event);
|
|
448
464
|
break;
|
|
449
465
|
}
|
|
450
466
|
case "GET_BALANCE": {
|
|
451
467
|
await this.handleGetBalance(event);
|
|
452
468
|
break;
|
|
453
469
|
}
|
|
454
|
-
case "GET_COINS": {
|
|
455
|
-
await this.handleGetCoins(event);
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
470
|
case "GET_VTXOS": {
|
|
459
471
|
await this.handleGetVtxos(event);
|
|
460
472
|
break;
|
|
@@ -471,14 +483,14 @@ export class Worker {
|
|
|
471
483
|
await this.handleGetStatus(event);
|
|
472
484
|
break;
|
|
473
485
|
}
|
|
474
|
-
case "EXIT": {
|
|
475
|
-
await this.handleExit(event);
|
|
476
|
-
break;
|
|
477
|
-
}
|
|
478
486
|
case "CLEAR": {
|
|
479
487
|
await this.handleClear(event);
|
|
480
488
|
break;
|
|
481
489
|
}
|
|
490
|
+
case "SIGN": {
|
|
491
|
+
await this.handleSign(event);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
482
494
|
default:
|
|
483
495
|
event.source?.postMessage(Response.error(message.id, "Unknown message type"));
|
|
484
496
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { SigHash, Transaction } from "@scure/btc-signer";
|
|
2
|
+
import { ChainTxType } from '../providers/indexer.js';
|
|
3
|
+
import { base64, hex } from "@scure/base";
|
|
4
|
+
import { VtxoScript } from '../script/base.js';
|
|
5
|
+
import { TaprootControlBlock, } from "@scure/btc-signer/psbt";
|
|
6
|
+
import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
|
|
7
|
+
import { Wallet } from './wallet.js';
|
|
8
|
+
export var Unroll;
|
|
9
|
+
(function (Unroll) {
|
|
10
|
+
let StepType;
|
|
11
|
+
(function (StepType) {
|
|
12
|
+
StepType[StepType["UNROLL"] = 0] = "UNROLL";
|
|
13
|
+
StepType[StepType["WAIT"] = 1] = "WAIT";
|
|
14
|
+
StepType[StepType["DONE"] = 2] = "DONE";
|
|
15
|
+
})(StepType = Unroll.StepType || (Unroll.StepType = {}));
|
|
16
|
+
/**
|
|
17
|
+
* Manages the unrolling process of a VTXO back to the Bitcoin blockchain.
|
|
18
|
+
*
|
|
19
|
+
* The Session class implements an async iterator that processes the unrolling steps:
|
|
20
|
+
* 1. **WAIT**: Waits for a transaction to be confirmed onchain (if it's in mempool)
|
|
21
|
+
* 2. **UNROLL**: Broadcasts the next transaction in the chain to the blockchain
|
|
22
|
+
* 3. **DONE**: Indicates the unrolling process is complete
|
|
23
|
+
*
|
|
24
|
+
* The unrolling process works by traversing the transaction chain from the root (most recent)
|
|
25
|
+
* to the leaf (oldest), broadcasting each transaction that isn't already onchain.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const session = await Unroll.Session.create(vtxoOutpoint, bumper, explorer, indexer);
|
|
30
|
+
*
|
|
31
|
+
* // iterate over the steps
|
|
32
|
+
* for await (const doneStep of session) {
|
|
33
|
+
* switch (doneStep.type) {
|
|
34
|
+
* case Unroll.StepType.WAIT:
|
|
35
|
+
* console.log(`Transaction ${doneStep.txid} confirmed`);
|
|
36
|
+
* break;
|
|
37
|
+
* case Unroll.StepType.UNROLL:
|
|
38
|
+
* console.log(`Broadcasting transaction ${doneStep.tx.id}`);
|
|
39
|
+
* break;
|
|
40
|
+
* case Unroll.StepType.DONE:
|
|
41
|
+
* console.log(`Unrolling complete for VTXO ${doneStep.vtxoTxid}`);
|
|
42
|
+
* break;
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
**/
|
|
47
|
+
class Session {
|
|
48
|
+
constructor(toUnroll, bumper, explorer, indexer) {
|
|
49
|
+
this.toUnroll = toUnroll;
|
|
50
|
+
this.bumper = bumper;
|
|
51
|
+
this.explorer = explorer;
|
|
52
|
+
this.indexer = indexer;
|
|
53
|
+
}
|
|
54
|
+
static async create(toUnroll, bumper, explorer, indexer) {
|
|
55
|
+
const { chain } = await indexer.getVtxoChain(toUnroll);
|
|
56
|
+
return new Session({ ...toUnroll, chain }, bumper, explorer, indexer);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the next step to be executed
|
|
60
|
+
* @returns The next step to be executed + the function to execute it
|
|
61
|
+
*/
|
|
62
|
+
async next() {
|
|
63
|
+
let nextTxToBroadcast;
|
|
64
|
+
const chain = this.toUnroll.chain;
|
|
65
|
+
// Iterate through the chain from the end (root) to the beginning (leaf)
|
|
66
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
67
|
+
const chainTx = chain[i];
|
|
68
|
+
// Skip commitment transactions as they are always onchain
|
|
69
|
+
if (chainTx.type === ChainTxType.COMMITMENT ||
|
|
70
|
+
chainTx.type === ChainTxType.UNSPECIFIED) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
// Check if the transaction is confirmed onchain
|
|
75
|
+
const txInfo = await this.explorer.getTxStatus(chainTx.txid);
|
|
76
|
+
// If found but not confirmed, it means the tx is in the mempool
|
|
77
|
+
// An unilateral exit is running, we must wait for it to be confirmed
|
|
78
|
+
if (!txInfo.confirmed) {
|
|
79
|
+
return {
|
|
80
|
+
type: StepType.WAIT,
|
|
81
|
+
txid: chainTx.txid,
|
|
82
|
+
do: doWait(this.explorer, chainTx.txid),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
// If the tx is not found, it's offchain, let's break
|
|
88
|
+
nextTxToBroadcast = chainTx;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!nextTxToBroadcast) {
|
|
93
|
+
return {
|
|
94
|
+
type: StepType.DONE,
|
|
95
|
+
vtxoTxid: this.toUnroll.txid,
|
|
96
|
+
do: () => Promise.resolve(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Get the virtual transaction data
|
|
100
|
+
const virtualTxs = await this.indexer.getVirtualTxs([
|
|
101
|
+
nextTxToBroadcast.txid,
|
|
102
|
+
]);
|
|
103
|
+
if (virtualTxs.txs.length === 0) {
|
|
104
|
+
throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
|
|
105
|
+
}
|
|
106
|
+
const tx = Transaction.fromPSBT(base64.decode(virtualTxs.txs[0]), {
|
|
107
|
+
allowUnknownInputs: true,
|
|
108
|
+
});
|
|
109
|
+
// finalize the tree transaction
|
|
110
|
+
if (nextTxToBroadcast.type === ChainTxType.TREE) {
|
|
111
|
+
const input = tx.getInput(0);
|
|
112
|
+
if (!input) {
|
|
113
|
+
throw new Error("Input not found");
|
|
114
|
+
}
|
|
115
|
+
const tapKeySig = input.tapKeySig;
|
|
116
|
+
if (!tapKeySig) {
|
|
117
|
+
throw new Error("Tap key sig not found");
|
|
118
|
+
}
|
|
119
|
+
tx.updateInput(0, {
|
|
120
|
+
finalScriptWitness: [tapKeySig],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// finalize ark transaction
|
|
125
|
+
tx.finalize();
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
type: StepType.UNROLL,
|
|
129
|
+
tx,
|
|
130
|
+
do: doUnroll(this.bumper, this.explorer, tx),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Iterate over the steps to be executed and execute them
|
|
135
|
+
* @returns An async iterator over the executed steps
|
|
136
|
+
*/
|
|
137
|
+
async *[Symbol.asyncIterator]() {
|
|
138
|
+
let lastStep;
|
|
139
|
+
do {
|
|
140
|
+
if (lastStep !== undefined) {
|
|
141
|
+
// wait 1 second before trying the next step in order to give time to the
|
|
142
|
+
// explorer to update the tx status
|
|
143
|
+
await sleep(1000);
|
|
144
|
+
}
|
|
145
|
+
const step = await this.next();
|
|
146
|
+
await step.do();
|
|
147
|
+
yield step;
|
|
148
|
+
lastStep = step.type;
|
|
149
|
+
} while (lastStep !== StepType.DONE);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
Unroll.Session = Session;
|
|
153
|
+
/**
|
|
154
|
+
* Complete the unroll of a VTXO by broadcasting the transaction that spends the CSV path.
|
|
155
|
+
* @param wallet the wallet owning the VTXO(s)
|
|
156
|
+
* @param vtxoTxids the txids of the VTXO(s) to complete unroll
|
|
157
|
+
* @param outputAddress the address to send the unrolled funds to
|
|
158
|
+
* @throws if the VTXO(s) are not fully unrolled, if the txids are not found, if the tx is not confirmed, if no exit path is found or not available
|
|
159
|
+
* @returns the txid of the transaction spending the unrolled funds
|
|
160
|
+
*/
|
|
161
|
+
async function completeUnroll(wallet, vtxoTxids, outputAddress) {
|
|
162
|
+
const chainTip = await wallet.onchainProvider.getChainTip();
|
|
163
|
+
let vtxos = await wallet.getVtxos({ withUnrolled: true });
|
|
164
|
+
vtxos = vtxos.filter((vtxo) => vtxoTxids.includes(vtxo.txid));
|
|
165
|
+
if (vtxos.length === 0) {
|
|
166
|
+
throw new Error("No vtxos to complete unroll");
|
|
167
|
+
}
|
|
168
|
+
const inputs = [];
|
|
169
|
+
let totalAmount = 0n;
|
|
170
|
+
const txWeightEstimator = TxWeightEstimator.create();
|
|
171
|
+
for (const vtxo of vtxos) {
|
|
172
|
+
if (!vtxo.isUnrolled) {
|
|
173
|
+
throw new Error(`Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled, use unroll first`);
|
|
174
|
+
}
|
|
175
|
+
const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
|
|
176
|
+
if (!txStatus.confirmed) {
|
|
177
|
+
throw new Error(`tx ${vtxo.txid} is not confirmed`);
|
|
178
|
+
}
|
|
179
|
+
const exit = availableExitPath({ height: txStatus.blockHeight, time: txStatus.blockTime }, chainTip, vtxo);
|
|
180
|
+
if (!exit) {
|
|
181
|
+
throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
|
|
182
|
+
}
|
|
183
|
+
const spendingLeaf = VtxoScript.decode(vtxo.tapTree).findLeaf(hex.encode(exit.script));
|
|
184
|
+
if (!spendingLeaf) {
|
|
185
|
+
throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
|
|
186
|
+
}
|
|
187
|
+
totalAmount += BigInt(vtxo.value);
|
|
188
|
+
inputs.push({
|
|
189
|
+
txid: vtxo.txid,
|
|
190
|
+
index: vtxo.vout,
|
|
191
|
+
tapLeafScript: [spendingLeaf],
|
|
192
|
+
sequence: 0xffffffff - 1,
|
|
193
|
+
witnessUtxo: {
|
|
194
|
+
amount: BigInt(vtxo.value),
|
|
195
|
+
script: VtxoScript.decode(vtxo.tapTree).pkScript,
|
|
196
|
+
},
|
|
197
|
+
sighashType: SigHash.DEFAULT,
|
|
198
|
+
});
|
|
199
|
+
txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, TaprootControlBlock.encode(spendingLeaf[0]).length);
|
|
200
|
+
}
|
|
201
|
+
const tx = new Transaction({ allowUnknownInputs: true, version: 2 });
|
|
202
|
+
for (const input of inputs) {
|
|
203
|
+
tx.addInput(input);
|
|
204
|
+
}
|
|
205
|
+
txWeightEstimator.addP2TROutput();
|
|
206
|
+
let feeRate = await wallet.onchainProvider.getFeeRate();
|
|
207
|
+
if (!feeRate || feeRate < Wallet.MIN_FEE_RATE) {
|
|
208
|
+
feeRate = Wallet.MIN_FEE_RATE;
|
|
209
|
+
}
|
|
210
|
+
const feeAmount = txWeightEstimator.vsize().fee(BigInt(feeRate));
|
|
211
|
+
if (feeAmount > totalAmount) {
|
|
212
|
+
throw new Error("fee amount is greater than the total amount");
|
|
213
|
+
}
|
|
214
|
+
tx.addOutputAddress(outputAddress, totalAmount - feeAmount);
|
|
215
|
+
const signedTx = await wallet.identity.sign(tx);
|
|
216
|
+
signedTx.finalize();
|
|
217
|
+
await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
|
|
218
|
+
return signedTx.id;
|
|
219
|
+
}
|
|
220
|
+
Unroll.completeUnroll = completeUnroll;
|
|
221
|
+
})(Unroll || (Unroll = {}));
|
|
222
|
+
function sleep(ms) {
|
|
223
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
224
|
+
}
|
|
225
|
+
function doUnroll(bumper, onchainProvider, tx) {
|
|
226
|
+
return async () => {
|
|
227
|
+
const [parent, child] = await bumper.bumpP2A(tx);
|
|
228
|
+
await onchainProvider.broadcastTransaction(parent, child);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function doWait(onchainProvider, txid) {
|
|
232
|
+
return () => {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const interval = setInterval(async () => {
|
|
235
|
+
try {
|
|
236
|
+
const txInfo = await onchainProvider.getTxStatus(txid);
|
|
237
|
+
if (txInfo.confirmed) {
|
|
238
|
+
clearInterval(interval);
|
|
239
|
+
resolve();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
clearInterval(interval);
|
|
244
|
+
reject(e);
|
|
245
|
+
}
|
|
246
|
+
}, 5000);
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function availableExitPath(confirmedAt, current, vtxo) {
|
|
251
|
+
const exits = VtxoScript.decode(vtxo.tapTree).exitPaths();
|
|
252
|
+
for (const exit of exits) {
|
|
253
|
+
if (exit.params.timelock.type === "blocks") {
|
|
254
|
+
if (current.height >=
|
|
255
|
+
confirmedAt.height + Number(exit.params.timelock.value)) {
|
|
256
|
+
return exit;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
if (current.time >=
|
|
261
|
+
confirmedAt.time + Number(exit.params.timelock.value)) {
|
|
262
|
+
return exit;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|