@arkade-os/sdk 0.0.16 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/cjs/providers/ark.js +17 -0
- package/dist/cjs/providers/onchain.js +12 -0
- package/dist/cjs/tree/vtxoTree.js +34 -0
- package/dist/cjs/wallet/serviceWorker/request.js +4 -0
- package/dist/cjs/wallet/serviceWorker/response.js +8 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +17 -0
- package/dist/cjs/wallet/serviceWorker/worker.js +28 -0
- package/dist/cjs/wallet/wallet.js +43 -1
- package/dist/esm/providers/ark.js +17 -0
- package/dist/esm/providers/onchain.js +12 -0
- package/dist/esm/tree/vtxoTree.js +35 -1
- package/dist/esm/wallet/serviceWorker/request.js +4 -0
- package/dist/esm/wallet/serviceWorker/response.js +8 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +17 -0
- package/dist/esm/wallet/serviceWorker/worker.js +28 -0
- package/dist/esm/wallet/wallet.js +43 -1
- package/dist/types/providers/ark.d.ts +10 -0
- package/dist/types/providers/onchain.d.ts +10 -0
- package/dist/types/tree/vtxoTree.d.ts +1 -0
- package/dist/types/wallet/index.d.ts +1 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +7 -2
- package/dist/types/wallet/serviceWorker/response.d.ts +6 -1
- package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -1
- package/dist/types/wallet/serviceWorker/worker.d.ts +1 -0
- package/dist/types/wallet/wallet.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
The
|
|
3
|
-
|
|
4
|
-

|
|
1
|
+
# Arkade TypeScript SDK
|
|
2
|
+
The Arkade SDK is a TypeScript library for building Bitcoin wallets with support for both on-chain and off-chain transactions via the Ark protocol.
|
|
5
3
|
|
|
6
4
|
## Installation
|
|
7
5
|
|
|
@@ -92,6 +90,16 @@ console.log('History:', history)
|
|
|
92
90
|
}
|
|
93
91
|
```
|
|
94
92
|
|
|
93
|
+
### Unilateral Exit
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Unilateral exit all vtxos
|
|
97
|
+
await wallet.exit();
|
|
98
|
+
|
|
99
|
+
// Unilateral exit a specific vtxo
|
|
100
|
+
await wallet.exit([{ txid: vtxo.txid, vout: vtxo.vout }]);
|
|
101
|
+
```
|
|
102
|
+
|
|
95
103
|
### Running the wallet in a service worker
|
|
96
104
|
|
|
97
105
|
1. Create a service worker file
|
|
@@ -40,6 +40,23 @@ class RestArkProvider {
|
|
|
40
40
|
spentVtxos: [...(data.spentVtxos || [])].map(convertVtxo),
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
async getRound(txid) {
|
|
44
|
+
const url = `${this.serverUrl}/v1/round/${txid}`;
|
|
45
|
+
const response = await fetch(url);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`Failed to fetch round: ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
const data = (await response.json());
|
|
50
|
+
const round = data.round;
|
|
51
|
+
return {
|
|
52
|
+
id: round.id,
|
|
53
|
+
start: new Date(Number(round.start) * 1000), // Convert from Unix timestamp to Date
|
|
54
|
+
end: new Date(Number(round.end) * 1000), // Convert from Unix timestamp to Date
|
|
55
|
+
vtxoTree: this.toTxTree(round.vtxoTree),
|
|
56
|
+
forfeitTxs: round.forfeitTxs || [],
|
|
57
|
+
connectors: this.toTxTree(round.connectors),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
43
60
|
async submitVirtualTx(psbtBase64) {
|
|
44
61
|
const url = `${this.serverUrl}/v1/redeem-tx`;
|
|
45
62
|
const response = await fetch(url, {
|
|
@@ -57,5 +57,17 @@ class EsploraProvider {
|
|
|
57
57
|
}
|
|
58
58
|
return response.json();
|
|
59
59
|
}
|
|
60
|
+
async getTxStatus(txid) {
|
|
61
|
+
const response = await fetch(`${this.baseUrl}/tx/${txid}/status`);
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to get transaction status: ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
return {
|
|
67
|
+
confirmed: data.confirmed,
|
|
68
|
+
blockTime: data.block_time,
|
|
69
|
+
blockHeight: data.block_height,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
exports.EsploraProvider = EsploraProvider;
|
|
@@ -111,6 +111,11 @@ class TxTree {
|
|
|
111
111
|
}
|
|
112
112
|
return branch;
|
|
113
113
|
}
|
|
114
|
+
// Returns the remaining transactions to broadcast in order to exit the vtxo
|
|
115
|
+
async exitBranch(vtxoTxid, isTxConfirmed) {
|
|
116
|
+
const offchainPart = await getOffchainPart(this.branch(vtxoTxid), isTxConfirmed);
|
|
117
|
+
return offchainPart.map(getExitTransaction);
|
|
118
|
+
}
|
|
114
119
|
// Helper method to find parent of a node
|
|
115
120
|
findParent(node) {
|
|
116
121
|
for (const level of this.tree) {
|
|
@@ -195,3 +200,32 @@ function getCosignerKeys(tx) {
|
|
|
195
200
|
}
|
|
196
201
|
return keys;
|
|
197
202
|
}
|
|
203
|
+
async function getOffchainPart(branch, isTxConfirmed) {
|
|
204
|
+
let offchainPath = [...branch];
|
|
205
|
+
// Iterate from the end of the branch (leaf) to the beginning (root)
|
|
206
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
207
|
+
const node = branch[i];
|
|
208
|
+
// check if the transaction is confirmed on-chain
|
|
209
|
+
if (await isTxConfirmed(node.txid)) {
|
|
210
|
+
// if this is the leaf node, return empty array as everything is confirmed
|
|
211
|
+
if (i === branch.length - 1) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
// otherwise, return the unconfirmed part of the branch
|
|
215
|
+
return branch.slice(i + 1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// no confirmation: everything is offchain
|
|
219
|
+
return offchainPath;
|
|
220
|
+
}
|
|
221
|
+
// getExitTransaction finalizes the psbt's input using the musig2 tapkey signature
|
|
222
|
+
function getExitTransaction(treeNode) {
|
|
223
|
+
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(treeNode.tx));
|
|
224
|
+
const input = tx.getInput(0);
|
|
225
|
+
if (!input.tapKeySig)
|
|
226
|
+
throw new TxTreeError("missing tapkey signature");
|
|
227
|
+
const rawTx = btc_signer_1.RawTx.decode(tx.unsignedTx);
|
|
228
|
+
rawTx.witnesses = [[input.tapKeySig]];
|
|
229
|
+
rawTx.segwitFlag = true;
|
|
230
|
+
return base_1.hex.encode(btc_signer_1.RawTx.encode(rawTx));
|
|
231
|
+
}
|
|
@@ -184,4 +184,12 @@ var Response;
|
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
186
|
Response.clearResponse = clearResponse;
|
|
187
|
+
function exitSuccess(id) {
|
|
188
|
+
return {
|
|
189
|
+
type: "EXIT_SUCCESS",
|
|
190
|
+
success: true,
|
|
191
|
+
id,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
Response.exitSuccess = exitSuccess;
|
|
187
195
|
})(Response || (exports.Response = Response = {}));
|
|
@@ -324,6 +324,23 @@ class ServiceWorkerWallet {
|
|
|
324
324
|
throw new Error(`Failed to get transaction history: ${error}`);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
|
+
async exit(outpoints) {
|
|
328
|
+
const message = {
|
|
329
|
+
type: "EXIT",
|
|
330
|
+
outpoints,
|
|
331
|
+
id: getRandomId(),
|
|
332
|
+
};
|
|
333
|
+
try {
|
|
334
|
+
const response = await this.sendMessage(message);
|
|
335
|
+
if (response.type === "EXIT_SUCCESS") {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
throw new UnexpectedResponseError(response);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw new Error(`Failed to exit: ${error}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
327
344
|
}
|
|
328
345
|
exports.ServiceWorkerWallet = ServiceWorkerWallet;
|
|
329
346
|
function getRandomId() {
|
|
@@ -387,6 +387,30 @@ class Worker {
|
|
|
387
387
|
}
|
|
388
388
|
event.source?.postMessage(response_1.Response.walletStatus(message.id, this.wallet !== undefined));
|
|
389
389
|
}
|
|
390
|
+
async handleExit(event) {
|
|
391
|
+
const message = event.data;
|
|
392
|
+
if (!request_1.Request.isExit(message)) {
|
|
393
|
+
console.error("Invalid EXIT message format", message);
|
|
394
|
+
event.source?.postMessage(response_1.Response.error(message.id, "Invalid EXIT message format"));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!this.wallet) {
|
|
398
|
+
console.error("Wallet not initialized");
|
|
399
|
+
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
await this.wallet.exit(message.outpoints);
|
|
404
|
+
event.source?.postMessage(response_1.Response.exitSuccess(message.id));
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
console.error("Error exiting:", error);
|
|
408
|
+
const errorMessage = error instanceof Error
|
|
409
|
+
? error.message
|
|
410
|
+
: "Unknown error occurred";
|
|
411
|
+
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
390
414
|
async handleMessage(event) {
|
|
391
415
|
this.messageCallback(event);
|
|
392
416
|
const message = event.data;
|
|
@@ -440,6 +464,10 @@ class Worker {
|
|
|
440
464
|
await this.handleGetStatus(event);
|
|
441
465
|
break;
|
|
442
466
|
}
|
|
467
|
+
case "EXIT": {
|
|
468
|
+
await this.handleExit(event);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
443
471
|
case "CLEAR": {
|
|
444
472
|
await this.handleClear(event);
|
|
445
473
|
break;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Wallet = void 0;
|
|
4
4
|
const base_1 = require("@scure/base");
|
|
5
5
|
const payment_1 = require("@scure/btc-signer/payment");
|
|
6
|
+
const btc_signer_1 = require("@scure/btc-signer");
|
|
6
7
|
const psbt_1 = require("@scure/btc-signer/psbt");
|
|
7
8
|
const transactionHistory_1 = require("../utils/transactionHistory");
|
|
8
9
|
const bip21_1 = require("../utils/bip21");
|
|
@@ -19,7 +20,6 @@ const _1 = require(".");
|
|
|
19
20
|
const base_2 = require("../script/base");
|
|
20
21
|
const tapscript_1 = require("../script/tapscript");
|
|
21
22
|
const psbt_2 = require("../utils/psbt");
|
|
22
|
-
const btc_signer_1 = require("@scure/btc-signer");
|
|
23
23
|
const arknote_1 = require("../arknote");
|
|
24
24
|
// Wallet does not store any data and rely on the Ark and onchain providers to fetch utxos and vtxos
|
|
25
25
|
class Wallet {
|
|
@@ -585,6 +585,48 @@ class Wallet {
|
|
|
585
585
|
}
|
|
586
586
|
throw new Error("Settlement failed");
|
|
587
587
|
}
|
|
588
|
+
async exit(outpoints) {
|
|
589
|
+
// TODO store the exit branches in repository
|
|
590
|
+
// exit should not depend on the ark provider
|
|
591
|
+
if (!this.arkProvider) {
|
|
592
|
+
throw new Error("Ark provider not configured");
|
|
593
|
+
}
|
|
594
|
+
let vtxos = await this.getVtxos();
|
|
595
|
+
if (outpoints && outpoints.length > 0) {
|
|
596
|
+
vtxos = vtxos.filter((vtxo) => outpoints.some((outpoint) => vtxo.txid === outpoint.txid &&
|
|
597
|
+
vtxo.vout === outpoint.vout));
|
|
598
|
+
}
|
|
599
|
+
if (vtxos.length === 0) {
|
|
600
|
+
throw new Error("No vtxos to exit");
|
|
601
|
+
}
|
|
602
|
+
const trees = new Map();
|
|
603
|
+
const transactions = [];
|
|
604
|
+
for (const vtxo of vtxos) {
|
|
605
|
+
const batchTxid = vtxo.virtualStatus.batchTxID;
|
|
606
|
+
if (!batchTxid)
|
|
607
|
+
continue;
|
|
608
|
+
if (!trees.has(batchTxid)) {
|
|
609
|
+
const round = await this.arkProvider.getRound(batchTxid);
|
|
610
|
+
trees.set(batchTxid, round.vtxoTree);
|
|
611
|
+
}
|
|
612
|
+
const tree = trees.get(batchTxid);
|
|
613
|
+
if (!tree) {
|
|
614
|
+
throw new Error("Tree not found");
|
|
615
|
+
}
|
|
616
|
+
const exitBranch = await tree.exitBranch(vtxo.txid, async (txid) => {
|
|
617
|
+
const status = await this.onchainProvider.getTxStatus(txid);
|
|
618
|
+
return status.confirmed;
|
|
619
|
+
});
|
|
620
|
+
transactions.push(...exitBranch);
|
|
621
|
+
}
|
|
622
|
+
const broadcastedTxs = new Map();
|
|
623
|
+
for (const tx of transactions) {
|
|
624
|
+
if (broadcastedTxs.has(tx))
|
|
625
|
+
continue;
|
|
626
|
+
const txid = await this.onchainProvider.broadcastTransaction(tx);
|
|
627
|
+
broadcastedTxs.set(txid, true);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
588
630
|
// validates the vtxo tree, creates a signing session and generates the musig2 nonces
|
|
589
631
|
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session) {
|
|
590
632
|
const vtxoTree = event.unsignedVtxoTree;
|
|
@@ -37,6 +37,23 @@ export class RestArkProvider {
|
|
|
37
37
|
spentVtxos: [...(data.spentVtxos || [])].map(convertVtxo),
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
async getRound(txid) {
|
|
41
|
+
const url = `${this.serverUrl}/v1/round/${txid}`;
|
|
42
|
+
const response = await fetch(url);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Failed to fetch round: ${response.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
const data = (await response.json());
|
|
47
|
+
const round = data.round;
|
|
48
|
+
return {
|
|
49
|
+
id: round.id,
|
|
50
|
+
start: new Date(Number(round.start) * 1000), // Convert from Unix timestamp to Date
|
|
51
|
+
end: new Date(Number(round.end) * 1000), // Convert from Unix timestamp to Date
|
|
52
|
+
vtxoTree: this.toTxTree(round.vtxoTree),
|
|
53
|
+
forfeitTxs: round.forfeitTxs || [],
|
|
54
|
+
connectors: this.toTxTree(round.connectors),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
40
57
|
async submitVirtualTx(psbtBase64) {
|
|
41
58
|
const url = `${this.serverUrl}/v1/redeem-tx`;
|
|
42
59
|
const response = await fetch(url, {
|
|
@@ -54,4 +54,16 @@ export class EsploraProvider {
|
|
|
54
54
|
}
|
|
55
55
|
return response.json();
|
|
56
56
|
}
|
|
57
|
+
async getTxStatus(txid) {
|
|
58
|
+
const response = await fetch(`${this.baseUrl}/tx/${txid}/status`);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Failed to get transaction status: ${response.statusText}`);
|
|
61
|
+
}
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
return {
|
|
64
|
+
confirmed: data.confirmed,
|
|
65
|
+
blockTime: data.block_time,
|
|
66
|
+
blockHeight: data.block_height,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
57
69
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as bip68 from "bip68";
|
|
2
|
-
import { ScriptNum, Transaction } from "@scure/btc-signer";
|
|
2
|
+
import { RawTx, ScriptNum, Transaction } from "@scure/btc-signer";
|
|
3
3
|
import { sha256x2 } from "@scure/btc-signer/utils";
|
|
4
4
|
import { base64, hex } from "@scure/base";
|
|
5
5
|
export class TxTreeError extends Error {
|
|
@@ -72,6 +72,11 @@ export class TxTree {
|
|
|
72
72
|
}
|
|
73
73
|
return branch;
|
|
74
74
|
}
|
|
75
|
+
// Returns the remaining transactions to broadcast in order to exit the vtxo
|
|
76
|
+
async exitBranch(vtxoTxid, isTxConfirmed) {
|
|
77
|
+
const offchainPart = await getOffchainPart(this.branch(vtxoTxid), isTxConfirmed);
|
|
78
|
+
return offchainPart.map(getExitTransaction);
|
|
79
|
+
}
|
|
75
80
|
// Helper method to find parent of a node
|
|
76
81
|
findParent(node) {
|
|
77
82
|
for (const level of this.tree) {
|
|
@@ -155,3 +160,32 @@ export function getCosignerKeys(tx) {
|
|
|
155
160
|
}
|
|
156
161
|
return keys;
|
|
157
162
|
}
|
|
163
|
+
async function getOffchainPart(branch, isTxConfirmed) {
|
|
164
|
+
let offchainPath = [...branch];
|
|
165
|
+
// Iterate from the end of the branch (leaf) to the beginning (root)
|
|
166
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
167
|
+
const node = branch[i];
|
|
168
|
+
// check if the transaction is confirmed on-chain
|
|
169
|
+
if (await isTxConfirmed(node.txid)) {
|
|
170
|
+
// if this is the leaf node, return empty array as everything is confirmed
|
|
171
|
+
if (i === branch.length - 1) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
// otherwise, return the unconfirmed part of the branch
|
|
175
|
+
return branch.slice(i + 1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// no confirmation: everything is offchain
|
|
179
|
+
return offchainPath;
|
|
180
|
+
}
|
|
181
|
+
// getExitTransaction finalizes the psbt's input using the musig2 tapkey signature
|
|
182
|
+
function getExitTransaction(treeNode) {
|
|
183
|
+
const tx = Transaction.fromPSBT(base64.decode(treeNode.tx));
|
|
184
|
+
const input = tx.getInput(0);
|
|
185
|
+
if (!input.tapKeySig)
|
|
186
|
+
throw new TxTreeError("missing tapkey signature");
|
|
187
|
+
const rawTx = RawTx.decode(tx.unsignedTx);
|
|
188
|
+
rawTx.witnesses = [[input.tapKeySig]];
|
|
189
|
+
rawTx.segwitFlag = true;
|
|
190
|
+
return hex.encode(RawTx.encode(rawTx));
|
|
191
|
+
}
|
|
@@ -181,4 +181,12 @@ export var Response;
|
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
Response.clearResponse = clearResponse;
|
|
184
|
+
function exitSuccess(id) {
|
|
185
|
+
return {
|
|
186
|
+
type: "EXIT_SUCCESS",
|
|
187
|
+
success: true,
|
|
188
|
+
id,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
Response.exitSuccess = exitSuccess;
|
|
184
192
|
})(Response || (Response = {}));
|
|
@@ -321,6 +321,23 @@ export class ServiceWorkerWallet {
|
|
|
321
321
|
throw new Error(`Failed to get transaction history: ${error}`);
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
+
async exit(outpoints) {
|
|
325
|
+
const message = {
|
|
326
|
+
type: "EXIT",
|
|
327
|
+
outpoints,
|
|
328
|
+
id: getRandomId(),
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
const response = await this.sendMessage(message);
|
|
332
|
+
if (response.type === "EXIT_SUCCESS") {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
throw new UnexpectedResponseError(response);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
throw new Error(`Failed to exit: ${error}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
324
341
|
}
|
|
325
342
|
function getRandomId() {
|
|
326
343
|
const randomValue = crypto.getRandomValues(new Uint8Array(16));
|
|
@@ -384,6 +384,30 @@ export class Worker {
|
|
|
384
384
|
}
|
|
385
385
|
event.source?.postMessage(Response.walletStatus(message.id, this.wallet !== undefined));
|
|
386
386
|
}
|
|
387
|
+
async handleExit(event) {
|
|
388
|
+
const message = event.data;
|
|
389
|
+
if (!Request.isExit(message)) {
|
|
390
|
+
console.error("Invalid EXIT message format", message);
|
|
391
|
+
event.source?.postMessage(Response.error(message.id, "Invalid EXIT message format"));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (!this.wallet) {
|
|
395
|
+
console.error("Wallet not initialized");
|
|
396
|
+
event.source?.postMessage(Response.error(message.id, "Wallet not initialized"));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
await this.wallet.exit(message.outpoints);
|
|
401
|
+
event.source?.postMessage(Response.exitSuccess(message.id));
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
console.error("Error exiting:", error);
|
|
405
|
+
const errorMessage = error instanceof Error
|
|
406
|
+
? error.message
|
|
407
|
+
: "Unknown error occurred";
|
|
408
|
+
event.source?.postMessage(Response.error(message.id, errorMessage));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
387
411
|
async handleMessage(event) {
|
|
388
412
|
this.messageCallback(event);
|
|
389
413
|
const message = event.data;
|
|
@@ -437,6 +461,10 @@ export class Worker {
|
|
|
437
461
|
await this.handleGetStatus(event);
|
|
438
462
|
break;
|
|
439
463
|
}
|
|
464
|
+
case "EXIT": {
|
|
465
|
+
await this.handleExit(event);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
440
468
|
case "CLEAR": {
|
|
441
469
|
await this.handleClear(event);
|
|
442
470
|
break;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { base64, hex } from "@scure/base";
|
|
2
2
|
import { Address, OutScript, p2tr, tapLeafHash, } from "@scure/btc-signer/payment";
|
|
3
|
+
import { Transaction } from "@scure/btc-signer";
|
|
3
4
|
import { TaprootControlBlock } from "@scure/btc-signer/psbt";
|
|
4
5
|
import { vtxosToTxs } from '../utils/transactionHistory.js';
|
|
5
6
|
import { BIP21 } from '../utils/bip21.js';
|
|
@@ -16,7 +17,6 @@ import { TxType, } from './index.js';
|
|
|
16
17
|
import { scriptFromTapLeafScript, VtxoScript } from '../script/base.js';
|
|
17
18
|
import { CSVMultisigTapscript, decodeTapscript, } from '../script/tapscript.js';
|
|
18
19
|
import { createVirtualTx } from '../utils/psbt.js';
|
|
19
|
-
import { Transaction } from "@scure/btc-signer";
|
|
20
20
|
import { ArkNote } from '../arknote/index.js';
|
|
21
21
|
// Wallet does not store any data and rely on the Ark and onchain providers to fetch utxos and vtxos
|
|
22
22
|
export class Wallet {
|
|
@@ -582,6 +582,48 @@ export class Wallet {
|
|
|
582
582
|
}
|
|
583
583
|
throw new Error("Settlement failed");
|
|
584
584
|
}
|
|
585
|
+
async exit(outpoints) {
|
|
586
|
+
// TODO store the exit branches in repository
|
|
587
|
+
// exit should not depend on the ark provider
|
|
588
|
+
if (!this.arkProvider) {
|
|
589
|
+
throw new Error("Ark provider not configured");
|
|
590
|
+
}
|
|
591
|
+
let vtxos = await this.getVtxos();
|
|
592
|
+
if (outpoints && outpoints.length > 0) {
|
|
593
|
+
vtxos = vtxos.filter((vtxo) => outpoints.some((outpoint) => vtxo.txid === outpoint.txid &&
|
|
594
|
+
vtxo.vout === outpoint.vout));
|
|
595
|
+
}
|
|
596
|
+
if (vtxos.length === 0) {
|
|
597
|
+
throw new Error("No vtxos to exit");
|
|
598
|
+
}
|
|
599
|
+
const trees = new Map();
|
|
600
|
+
const transactions = [];
|
|
601
|
+
for (const vtxo of vtxos) {
|
|
602
|
+
const batchTxid = vtxo.virtualStatus.batchTxID;
|
|
603
|
+
if (!batchTxid)
|
|
604
|
+
continue;
|
|
605
|
+
if (!trees.has(batchTxid)) {
|
|
606
|
+
const round = await this.arkProvider.getRound(batchTxid);
|
|
607
|
+
trees.set(batchTxid, round.vtxoTree);
|
|
608
|
+
}
|
|
609
|
+
const tree = trees.get(batchTxid);
|
|
610
|
+
if (!tree) {
|
|
611
|
+
throw new Error("Tree not found");
|
|
612
|
+
}
|
|
613
|
+
const exitBranch = await tree.exitBranch(vtxo.txid, async (txid) => {
|
|
614
|
+
const status = await this.onchainProvider.getTxStatus(txid);
|
|
615
|
+
return status.confirmed;
|
|
616
|
+
});
|
|
617
|
+
transactions.push(...exitBranch);
|
|
618
|
+
}
|
|
619
|
+
const broadcastedTxs = new Map();
|
|
620
|
+
for (const tx of transactions) {
|
|
621
|
+
if (broadcastedTxs.has(tx))
|
|
622
|
+
continue;
|
|
623
|
+
const txid = await this.onchainProvider.broadcastTransaction(tx);
|
|
624
|
+
broadcastedTxs.set(txid, true);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
585
627
|
// validates the vtxo tree, creates a signing session and generates the musig2 nonces
|
|
586
628
|
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session) {
|
|
587
629
|
const vtxoTree = event.unsignedVtxoTree;
|
|
@@ -75,8 +75,17 @@ export interface ArkInfo {
|
|
|
75
75
|
end: number;
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
+
export interface Round {
|
|
79
|
+
id: string;
|
|
80
|
+
start: Date;
|
|
81
|
+
end: Date;
|
|
82
|
+
vtxoTree: TxTree;
|
|
83
|
+
forfeitTxs: string[];
|
|
84
|
+
connectors: TxTree;
|
|
85
|
+
}
|
|
78
86
|
export interface ArkProvider {
|
|
79
87
|
getInfo(): Promise<ArkInfo>;
|
|
88
|
+
getRound(txid: string): Promise<Round>;
|
|
80
89
|
getVirtualCoins(address: string): Promise<{
|
|
81
90
|
spendableVtxos: VirtualCoin[];
|
|
82
91
|
spentVtxos: VirtualCoin[];
|
|
@@ -105,6 +114,7 @@ export declare class RestArkProvider implements ArkProvider {
|
|
|
105
114
|
spendableVtxos: VirtualCoin[];
|
|
106
115
|
spentVtxos: VirtualCoin[];
|
|
107
116
|
}>;
|
|
117
|
+
getRound(txid: string): Promise<Round>;
|
|
108
118
|
submitVirtualTx(psbtBase64: string): Promise<string>;
|
|
109
119
|
subscribeToEvents(callback: (event: ArkEvent) => void): Promise<() => void>;
|
|
110
120
|
registerInputsForNextRound(inputs: Input[]): Promise<{
|
|
@@ -21,6 +21,11 @@ export interface OnchainProvider {
|
|
|
21
21
|
txid: string;
|
|
22
22
|
}[]>;
|
|
23
23
|
getTransactions(address: string): Promise<ExplorerTransaction[]>;
|
|
24
|
+
getTxStatus(txid: string): Promise<{
|
|
25
|
+
confirmed: boolean;
|
|
26
|
+
blockTime?: number;
|
|
27
|
+
blockHeight?: number;
|
|
28
|
+
}>;
|
|
24
29
|
}
|
|
25
30
|
export declare class EsploraProvider implements OnchainProvider {
|
|
26
31
|
private baseUrl;
|
|
@@ -33,4 +38,9 @@ export declare class EsploraProvider implements OnchainProvider {
|
|
|
33
38
|
txid: string;
|
|
34
39
|
}[]>;
|
|
35
40
|
getTransactions(address: string): Promise<ExplorerTransaction[]>;
|
|
41
|
+
getTxStatus(txid: string): Promise<{
|
|
42
|
+
confirmed: boolean;
|
|
43
|
+
blockTime?: number;
|
|
44
|
+
blockHeight?: number;
|
|
45
|
+
}>;
|
|
36
46
|
}
|
|
@@ -20,6 +20,7 @@ export declare class TxTree {
|
|
|
20
20
|
children(nodeTxid: string): TreeNode[];
|
|
21
21
|
numberOfNodes(): number;
|
|
22
22
|
branch(vtxoTxid: string): TreeNode[];
|
|
23
|
+
exitBranch(vtxoTxid: string, isTxConfirmed: (txid: string) => Promise<boolean>): Promise<string[]>;
|
|
23
24
|
private findParent;
|
|
24
25
|
validate(): void;
|
|
25
26
|
}
|
|
@@ -119,4 +119,5 @@ export interface IWallet {
|
|
|
119
119
|
getTransactionHistory(): Promise<ArkTransaction[]>;
|
|
120
120
|
sendBitcoin(params: SendBitcoinParams, zeroFee?: boolean): Promise<string>;
|
|
121
121
|
settle(params?: SettleParams, eventCallback?: (event: SettlementEvent) => void): Promise<string>;
|
|
122
|
+
exit(outpoints?: Outpoint[]): Promise<void>;
|
|
122
123
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NetworkName } from "../../networks";
|
|
2
|
-
import { SettleParams, SendBitcoinParams } from "..";
|
|
2
|
+
import { SettleParams, SendBitcoinParams, Outpoint } from "..";
|
|
3
3
|
export declare namespace Request {
|
|
4
|
-
type Type = "INIT_WALLET" | "SETTLE" | "GET_ADDRESS" | "GET_ADDRESS_INFO" | "GET_BALANCE" | "GET_COINS" | "GET_VTXOS" | "GET_VIRTUAL_COINS" | "GET_BOARDING_UTXOS" | "SEND_BITCOIN" | "GET_TRANSACTION_HISTORY" | "GET_STATUS" | "CLEAR";
|
|
4
|
+
type Type = "INIT_WALLET" | "SETTLE" | "GET_ADDRESS" | "GET_ADDRESS_INFO" | "GET_BALANCE" | "GET_COINS" | "GET_VTXOS" | "GET_VIRTUAL_COINS" | "GET_BOARDING_UTXOS" | "SEND_BITCOIN" | "GET_TRANSACTION_HISTORY" | "GET_STATUS" | "CLEAR" | "EXIT";
|
|
5
5
|
interface Base {
|
|
6
6
|
type: Type;
|
|
7
7
|
id: string;
|
|
@@ -65,4 +65,9 @@ export declare namespace Request {
|
|
|
65
65
|
interface Clear extends Base {
|
|
66
66
|
type: "CLEAR";
|
|
67
67
|
}
|
|
68
|
+
interface Exit extends Base {
|
|
69
|
+
type: "EXIT";
|
|
70
|
+
outpoints?: Outpoint[];
|
|
71
|
+
}
|
|
72
|
+
function isExit(message: Base): message is Exit;
|
|
68
73
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WalletBalance, Coin, VirtualCoin, ArkTransaction, AddressInfo as WalletAddressInfo, IWallet, Addresses } from "..";
|
|
2
2
|
import { SettlementEvent } from "../../providers/ark";
|
|
3
3
|
export declare namespace Response {
|
|
4
|
-
type Type = "WALLET_INITIALIZED" | "SETTLE_EVENT" | "SETTLE_SUCCESS" | "ADDRESS" | "ADDRESS_INFO" | "BALANCE" | "COINS" | "VTXOS" | "VIRTUAL_COINS" | "BOARDING_UTXOS" | "SEND_BITCOIN_SUCCESS" | "TRANSACTION_HISTORY" | "WALLET_STATUS" | "ERROR" | "CLEAR_RESPONSE";
|
|
4
|
+
type Type = "WALLET_INITIALIZED" | "SETTLE_EVENT" | "SETTLE_SUCCESS" | "ADDRESS" | "ADDRESS_INFO" | "BALANCE" | "COINS" | "VTXOS" | "VIRTUAL_COINS" | "BOARDING_UTXOS" | "SEND_BITCOIN_SUCCESS" | "TRANSACTION_HISTORY" | "WALLET_STATUS" | "ERROR" | "CLEAR_RESPONSE" | "EXIT_SUCCESS";
|
|
5
5
|
interface Base {
|
|
6
6
|
type: Type;
|
|
7
7
|
success: boolean;
|
|
@@ -104,4 +104,9 @@ export declare namespace Response {
|
|
|
104
104
|
}
|
|
105
105
|
function isClearResponse(response: Base): response is ClearResponse;
|
|
106
106
|
function clearResponse(id: string, success: boolean): ClearResponse;
|
|
107
|
+
interface ExitSuccess extends Base {
|
|
108
|
+
type: "EXIT_SUCCESS";
|
|
109
|
+
success: true;
|
|
110
|
+
}
|
|
111
|
+
function exitSuccess(id: string): ExitSuccess;
|
|
107
112
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IWallet, WalletBalance, SendBitcoinParams, SettleParams, AddressInfo, Coin, ArkTransaction, WalletConfig, ExtendedCoin, ExtendedVirtualCoin, Addresses } from "..";
|
|
1
|
+
import { IWallet, WalletBalance, SendBitcoinParams, SettleParams, AddressInfo, Coin, ArkTransaction, WalletConfig, ExtendedCoin, ExtendedVirtualCoin, Addresses, Outpoint } from "..";
|
|
2
2
|
import { Response } from "./response";
|
|
3
3
|
import { SettlementEvent } from "../../providers/ark";
|
|
4
4
|
export declare class ServiceWorkerWallet implements IWallet {
|
|
@@ -20,4 +20,5 @@ export declare class ServiceWorkerWallet implements IWallet {
|
|
|
20
20
|
sendBitcoin(params: SendBitcoinParams, zeroFee?: boolean): Promise<string>;
|
|
21
21
|
settle(params?: SettleParams, callback?: (event: SettlementEvent) => void): Promise<string>;
|
|
22
22
|
getTransactionHistory(): Promise<ArkTransaction[]>;
|
|
23
|
+
exit(outpoints?: Outpoint[]): Promise<void>;
|
|
23
24
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ArkAddress } from "../script/address";
|
|
2
2
|
import { DefaultVtxo } from "../script/default";
|
|
3
3
|
import { SettlementEvent } from "../providers/ark";
|
|
4
|
-
import { Addresses, AddressInfo, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, IWallet, SendBitcoinParams, SettleParams, WalletBalance, WalletConfig } from ".";
|
|
4
|
+
import { Addresses, AddressInfo, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, IWallet, Outpoint, SendBitcoinParams, SettleParams, WalletBalance, WalletConfig } from ".";
|
|
5
5
|
export declare class Wallet implements IWallet {
|
|
6
6
|
private identity;
|
|
7
7
|
private network;
|
|
@@ -36,6 +36,7 @@ export declare class Wallet implements IWallet {
|
|
|
36
36
|
private sendOnchain;
|
|
37
37
|
private sendOffchain;
|
|
38
38
|
settle(params?: SettleParams, eventCallback?: (event: SettlementEvent) => void): Promise<string>;
|
|
39
|
+
exit(outpoints?: Outpoint[]): Promise<void>;
|
|
39
40
|
private handleSettlementSigningEvent;
|
|
40
41
|
private handleSettlementSigningNoncesGeneratedEvent;
|
|
41
42
|
private handleSettlementFinalizationEvent;
|