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