@arkade-os/sdk 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +312 -0
- package/dist/cjs/arknote/index.js +86 -0
- package/dist/cjs/forfeit.js +38 -0
- package/dist/cjs/identity/inMemoryKey.js +40 -0
- package/dist/cjs/identity/index.js +2 -0
- package/dist/cjs/index.js +48 -0
- package/dist/cjs/musig2/index.js +10 -0
- package/dist/cjs/musig2/keys.js +57 -0
- package/dist/cjs/musig2/nonces.js +44 -0
- package/dist/cjs/musig2/sign.js +102 -0
- package/dist/cjs/networks.js +26 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/providers/ark.js +530 -0
- package/dist/cjs/providers/onchain.js +61 -0
- package/dist/cjs/script/address.js +45 -0
- package/dist/cjs/script/base.js +51 -0
- package/dist/cjs/script/default.js +40 -0
- package/dist/cjs/script/tapscript.js +528 -0
- package/dist/cjs/script/vhtlc.js +84 -0
- package/dist/cjs/tree/signingSession.js +238 -0
- package/dist/cjs/tree/validation.js +184 -0
- package/dist/cjs/tree/vtxoTree.js +197 -0
- package/dist/cjs/utils/bip21.js +114 -0
- package/dist/cjs/utils/coinselect.js +73 -0
- package/dist/cjs/utils/psbt.js +124 -0
- package/dist/cjs/utils/transactionHistory.js +148 -0
- package/dist/cjs/utils/txSizeEstimator.js +95 -0
- package/dist/cjs/wallet/index.js +8 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +153 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/index.js +2 -0
- package/dist/cjs/wallet/serviceWorker/request.js +75 -0
- package/dist/cjs/wallet/serviceWorker/response.js +187 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +332 -0
- package/dist/cjs/wallet/serviceWorker/worker.js +452 -0
- package/dist/cjs/wallet/wallet.js +720 -0
- package/dist/esm/arknote/index.js +81 -0
- package/dist/esm/forfeit.js +35 -0
- package/dist/esm/identity/inMemoryKey.js +36 -0
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/musig2/index.js +3 -0
- package/dist/esm/musig2/keys.js +21 -0
- package/dist/esm/musig2/nonces.js +8 -0
- package/dist/esm/musig2/sign.js +63 -0
- package/dist/esm/networks.js +22 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/providers/ark.js +526 -0
- package/dist/esm/providers/onchain.js +57 -0
- package/dist/esm/script/address.js +41 -0
- package/dist/esm/script/base.js +46 -0
- package/dist/esm/script/default.js +37 -0
- package/dist/esm/script/tapscript.js +491 -0
- package/dist/esm/script/vhtlc.js +81 -0
- package/dist/esm/tree/signingSession.js +200 -0
- package/dist/esm/tree/validation.js +179 -0
- package/dist/esm/tree/vtxoTree.js +157 -0
- package/dist/esm/utils/bip21.js +110 -0
- package/dist/esm/utils/coinselect.js +69 -0
- package/dist/esm/utils/psbt.js +118 -0
- package/dist/esm/utils/transactionHistory.js +145 -0
- package/dist/esm/utils/txSizeEstimator.js +91 -0
- package/dist/esm/wallet/index.js +5 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +149 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/index.js +1 -0
- package/dist/esm/wallet/serviceWorker/request.js +72 -0
- package/dist/esm/wallet/serviceWorker/response.js +184 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +328 -0
- package/dist/esm/wallet/serviceWorker/worker.js +448 -0
- package/dist/esm/wallet/wallet.js +716 -0
- package/dist/types/arknote/index.d.ts +17 -0
- package/dist/types/forfeit.d.ts +15 -0
- package/dist/types/identity/inMemoryKey.d.ts +12 -0
- package/dist/types/identity/index.d.ts +7 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/musig2/index.d.ts +4 -0
- package/dist/types/musig2/keys.d.ts +9 -0
- package/dist/types/musig2/nonces.d.ts +13 -0
- package/dist/types/musig2/sign.d.ts +27 -0
- package/dist/types/networks.d.ts +16 -0
- package/dist/types/providers/ark.d.ts +126 -0
- package/dist/types/providers/onchain.d.ts +36 -0
- package/dist/types/script/address.d.ts +10 -0
- package/dist/types/script/base.d.ts +26 -0
- package/dist/types/script/default.d.ts +19 -0
- package/dist/types/script/tapscript.d.ts +94 -0
- package/dist/types/script/vhtlc.d.ts +31 -0
- package/dist/types/tree/signingSession.d.ts +32 -0
- package/dist/types/tree/validation.d.ts +22 -0
- package/dist/types/tree/vtxoTree.d.ts +32 -0
- package/dist/types/utils/bip21.d.ts +21 -0
- package/dist/types/utils/coinselect.d.ts +21 -0
- package/dist/types/utils/psbt.d.ts +11 -0
- package/dist/types/utils/transactionHistory.d.ts +2 -0
- package/dist/types/utils/txSizeEstimator.d.ts +27 -0
- package/dist/types/wallet/index.d.ts +122 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +18 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +12 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +68 -0
- package/dist/types/wallet/serviceWorker/response.d.ts +107 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +23 -0
- package/dist/types/wallet/serviceWorker/worker.d.ts +26 -0
- package/dist/types/wallet/wallet.d.ts +42 -0
- package/package.json +88 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Wallet = void 0;
|
|
4
|
+
const base_1 = require("@scure/base");
|
|
5
|
+
const payment_1 = require("@scure/btc-signer/payment");
|
|
6
|
+
const psbt_1 = require("@scure/btc-signer/psbt");
|
|
7
|
+
const transactionHistory_1 = require("../utils/transactionHistory");
|
|
8
|
+
const bip21_1 = require("../utils/bip21");
|
|
9
|
+
const address_1 = require("../script/address");
|
|
10
|
+
const default_1 = require("../script/default");
|
|
11
|
+
const coinselect_1 = require("../utils/coinselect");
|
|
12
|
+
const networks_1 = require("../networks");
|
|
13
|
+
const onchain_1 = require("../providers/onchain");
|
|
14
|
+
const ark_1 = require("../providers/ark");
|
|
15
|
+
const forfeit_1 = require("../forfeit");
|
|
16
|
+
const txSizeEstimator_1 = require("../utils/txSizeEstimator");
|
|
17
|
+
const validation_1 = require("../tree/validation");
|
|
18
|
+
const _1 = require(".");
|
|
19
|
+
const base_2 = require("../script/base");
|
|
20
|
+
const tapscript_1 = require("../script/tapscript");
|
|
21
|
+
const psbt_2 = require("../utils/psbt");
|
|
22
|
+
const btc_signer_1 = require("@scure/btc-signer");
|
|
23
|
+
const arknote_1 = require("../arknote");
|
|
24
|
+
// Wallet does not store any data and rely on the Ark and onchain providers to fetch utxos and vtxos
|
|
25
|
+
class Wallet {
|
|
26
|
+
constructor(identity, network, onchainProvider, onchainP2TR, arkProvider, arkServerPublicKey, offchainTapscript, boardingTapscript) {
|
|
27
|
+
this.identity = identity;
|
|
28
|
+
this.network = network;
|
|
29
|
+
this.onchainProvider = onchainProvider;
|
|
30
|
+
this.onchainP2TR = onchainP2TR;
|
|
31
|
+
this.arkProvider = arkProvider;
|
|
32
|
+
this.arkServerPublicKey = arkServerPublicKey;
|
|
33
|
+
this.offchainTapscript = offchainTapscript;
|
|
34
|
+
this.boardingTapscript = boardingTapscript;
|
|
35
|
+
}
|
|
36
|
+
static async create(config) {
|
|
37
|
+
const network = (0, networks_1.getNetwork)(config.network);
|
|
38
|
+
const onchainProvider = new onchain_1.EsploraProvider(config.esploraUrl || onchain_1.ESPLORA_URL[config.network]);
|
|
39
|
+
// Derive onchain address
|
|
40
|
+
const pubkey = config.identity.xOnlyPublicKey();
|
|
41
|
+
if (!pubkey) {
|
|
42
|
+
throw new Error("Invalid configured public key");
|
|
43
|
+
}
|
|
44
|
+
let arkProvider;
|
|
45
|
+
if (config.arkServerUrl) {
|
|
46
|
+
arkProvider = new ark_1.RestArkProvider(config.arkServerUrl);
|
|
47
|
+
}
|
|
48
|
+
// Save onchain Taproot address key-path only
|
|
49
|
+
const onchainP2TR = (0, payment_1.p2tr)(pubkey, undefined, network);
|
|
50
|
+
if (arkProvider) {
|
|
51
|
+
const info = await arkProvider.getInfo();
|
|
52
|
+
if (info.network !== config.network) {
|
|
53
|
+
throw new Error(`The Ark Server URL expects ${info.network} but ${config.network} was configured`);
|
|
54
|
+
}
|
|
55
|
+
const exitTimelock = {
|
|
56
|
+
value: info.unilateralExitDelay,
|
|
57
|
+
type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
|
|
58
|
+
};
|
|
59
|
+
const boardingTimelock = {
|
|
60
|
+
value: info.unilateralExitDelay * 2n,
|
|
61
|
+
type: info.unilateralExitDelay * 2n < 512n ? "blocks" : "seconds",
|
|
62
|
+
};
|
|
63
|
+
// Generate tapscripts for offchain and boarding address
|
|
64
|
+
const serverPubKey = base_1.hex.decode(info.pubkey).slice(1);
|
|
65
|
+
const bareVtxoTapscript = new default_1.DefaultVtxo.Script({
|
|
66
|
+
pubKey: pubkey,
|
|
67
|
+
serverPubKey,
|
|
68
|
+
csvTimelock: exitTimelock,
|
|
69
|
+
});
|
|
70
|
+
const boardingTapscript = new default_1.DefaultVtxo.Script({
|
|
71
|
+
pubKey: pubkey,
|
|
72
|
+
serverPubKey,
|
|
73
|
+
csvTimelock: boardingTimelock,
|
|
74
|
+
});
|
|
75
|
+
// Save tapscripts
|
|
76
|
+
const offchainTapscript = bareVtxoTapscript;
|
|
77
|
+
return new Wallet(config.identity, network, onchainProvider, onchainP2TR, arkProvider, serverPubKey, offchainTapscript, boardingTapscript);
|
|
78
|
+
}
|
|
79
|
+
return new Wallet(config.identity, network, onchainProvider, onchainP2TR);
|
|
80
|
+
}
|
|
81
|
+
get onchainAddress() {
|
|
82
|
+
return this.onchainP2TR.address || "";
|
|
83
|
+
}
|
|
84
|
+
get boardingAddress() {
|
|
85
|
+
if (!this.boardingTapscript || !this.arkServerPublicKey) {
|
|
86
|
+
throw new Error("Boarding address not configured");
|
|
87
|
+
}
|
|
88
|
+
return this.boardingTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
89
|
+
}
|
|
90
|
+
get boardingOnchainAddress() {
|
|
91
|
+
if (!this.boardingTapscript) {
|
|
92
|
+
throw new Error("Boarding address not configured");
|
|
93
|
+
}
|
|
94
|
+
return this.boardingTapscript.onchainAddress(this.network);
|
|
95
|
+
}
|
|
96
|
+
get offchainAddress() {
|
|
97
|
+
if (!this.offchainTapscript || !this.arkServerPublicKey) {
|
|
98
|
+
throw new Error("Offchain address not configured");
|
|
99
|
+
}
|
|
100
|
+
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
101
|
+
}
|
|
102
|
+
getAddress() {
|
|
103
|
+
const addressInfo = {
|
|
104
|
+
onchain: this.onchainAddress,
|
|
105
|
+
bip21: bip21_1.BIP21.create({
|
|
106
|
+
address: this.onchainAddress,
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
// Only include Ark-related fields if Ark provider is configured and address is available
|
|
110
|
+
if (this.arkProvider &&
|
|
111
|
+
this.offchainTapscript &&
|
|
112
|
+
this.boardingTapscript &&
|
|
113
|
+
this.arkServerPublicKey) {
|
|
114
|
+
const offchainAddress = this.offchainAddress.encode();
|
|
115
|
+
addressInfo.offchain = offchainAddress;
|
|
116
|
+
addressInfo.bip21 = bip21_1.BIP21.create({
|
|
117
|
+
address: this.onchainP2TR.address,
|
|
118
|
+
ark: offchainAddress,
|
|
119
|
+
});
|
|
120
|
+
addressInfo.boarding = this.boardingOnchainAddress;
|
|
121
|
+
}
|
|
122
|
+
return Promise.resolve(addressInfo);
|
|
123
|
+
}
|
|
124
|
+
getAddressInfo() {
|
|
125
|
+
if (!this.arkProvider ||
|
|
126
|
+
!this.offchainTapscript ||
|
|
127
|
+
!this.boardingTapscript ||
|
|
128
|
+
!this.arkServerPublicKey) {
|
|
129
|
+
throw new Error("Ark provider not configured");
|
|
130
|
+
}
|
|
131
|
+
const offchainAddress = this.offchainAddress.encode();
|
|
132
|
+
const boardingAddress = this.boardingOnchainAddress;
|
|
133
|
+
return Promise.resolve({
|
|
134
|
+
offchain: {
|
|
135
|
+
address: offchainAddress,
|
|
136
|
+
scripts: {
|
|
137
|
+
exit: [this.offchainTapscript.exitScript],
|
|
138
|
+
forfeit: [this.offchainTapscript.forfeitScript],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
boarding: {
|
|
142
|
+
address: boardingAddress,
|
|
143
|
+
scripts: {
|
|
144
|
+
exit: [this.boardingTapscript.exitScript],
|
|
145
|
+
forfeit: [this.boardingTapscript.forfeitScript],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async getBalance() {
|
|
151
|
+
// Get onchain coins
|
|
152
|
+
const coins = await this.getCoins();
|
|
153
|
+
const onchainConfirmed = coins
|
|
154
|
+
.filter((coin) => coin.status.confirmed)
|
|
155
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
156
|
+
const onchainUnconfirmed = coins
|
|
157
|
+
.filter((coin) => !coin.status.confirmed)
|
|
158
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
159
|
+
const onchainTotal = onchainConfirmed + onchainUnconfirmed;
|
|
160
|
+
// Get offchain coins if Ark provider is configured
|
|
161
|
+
let offchainSettled = 0;
|
|
162
|
+
let offchainPending = 0;
|
|
163
|
+
let offchainSwept = 0;
|
|
164
|
+
if (this.arkProvider) {
|
|
165
|
+
const vtxos = await this.getVirtualCoins();
|
|
166
|
+
offchainSettled = vtxos
|
|
167
|
+
.filter((coin) => coin.virtualStatus.state === "settled")
|
|
168
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
169
|
+
offchainPending = vtxos
|
|
170
|
+
.filter((coin) => coin.virtualStatus.state === "pending")
|
|
171
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
172
|
+
offchainSwept = vtxos
|
|
173
|
+
.filter((coin) => coin.virtualStatus.state === "swept")
|
|
174
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
175
|
+
}
|
|
176
|
+
const offchainTotal = offchainSettled + offchainPending;
|
|
177
|
+
return {
|
|
178
|
+
onchain: {
|
|
179
|
+
confirmed: onchainConfirmed,
|
|
180
|
+
unconfirmed: onchainUnconfirmed,
|
|
181
|
+
total: onchainTotal,
|
|
182
|
+
},
|
|
183
|
+
offchain: {
|
|
184
|
+
swept: offchainSwept,
|
|
185
|
+
settled: offchainSettled,
|
|
186
|
+
pending: offchainPending,
|
|
187
|
+
total: offchainTotal,
|
|
188
|
+
},
|
|
189
|
+
total: onchainTotal + offchainTotal,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async getCoins() {
|
|
193
|
+
// TODO: add caching logic to lower the number of requests to provider
|
|
194
|
+
const address = await this.getAddress();
|
|
195
|
+
return this.onchainProvider.getCoins(address.onchain);
|
|
196
|
+
}
|
|
197
|
+
async getVtxos() {
|
|
198
|
+
if (!this.arkProvider || !this.offchainTapscript) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const address = await this.getAddress();
|
|
202
|
+
if (!address.offchain) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
const { spendableVtxos } = await this.arkProvider.getVirtualCoins(address.offchain);
|
|
206
|
+
const encodedOffchainTapscript = this.offchainTapscript.encode();
|
|
207
|
+
const forfeit = this.offchainTapscript.forfeit();
|
|
208
|
+
return spendableVtxos.map((vtxo) => ({
|
|
209
|
+
...vtxo,
|
|
210
|
+
tapLeafScript: forfeit,
|
|
211
|
+
scripts: encodedOffchainTapscript,
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
async getVirtualCoins() {
|
|
215
|
+
if (!this.arkProvider) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const address = await this.getAddress();
|
|
219
|
+
if (!address.offchain) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
return this.arkProvider
|
|
223
|
+
.getVirtualCoins(address.offchain)
|
|
224
|
+
.then(({ spendableVtxos }) => spendableVtxos);
|
|
225
|
+
}
|
|
226
|
+
async getTransactionHistory() {
|
|
227
|
+
if (!this.arkProvider) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const { spendableVtxos, spentVtxos } = await this.arkProvider.getVirtualCoins(this.offchainAddress.encode());
|
|
231
|
+
const { boardingTxs, roundsToIgnore } = await this.getBoardingTxs();
|
|
232
|
+
// convert VTXOs to offchain transactions
|
|
233
|
+
const offchainTxs = (0, transactionHistory_1.vtxosToTxs)(spendableVtxos, spentVtxos, roundsToIgnore);
|
|
234
|
+
const txs = [...boardingTxs, ...offchainTxs];
|
|
235
|
+
// sort transactions by creation time in descending order (newest first)
|
|
236
|
+
txs.sort(
|
|
237
|
+
// place createdAt = 0 (unconfirmed txs) first, then descending
|
|
238
|
+
(a, b) => {
|
|
239
|
+
if (a.createdAt === 0)
|
|
240
|
+
return -1;
|
|
241
|
+
if (b.createdAt === 0)
|
|
242
|
+
return 1;
|
|
243
|
+
return b.createdAt - a.createdAt;
|
|
244
|
+
});
|
|
245
|
+
return txs;
|
|
246
|
+
}
|
|
247
|
+
async getBoardingTxs() {
|
|
248
|
+
if (!this.boardingAddress) {
|
|
249
|
+
return { boardingTxs: [], roundsToIgnore: new Set() };
|
|
250
|
+
}
|
|
251
|
+
const boardingAddress = this.boardingOnchainAddress;
|
|
252
|
+
const txs = await this.onchainProvider.getTransactions(boardingAddress);
|
|
253
|
+
const utxos = [];
|
|
254
|
+
const roundsToIgnore = new Set();
|
|
255
|
+
for (const tx of txs) {
|
|
256
|
+
for (let i = 0; i < tx.vout.length; i++) {
|
|
257
|
+
const vout = tx.vout[i];
|
|
258
|
+
if (vout.scriptpubkey_address === boardingAddress) {
|
|
259
|
+
const spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
|
|
260
|
+
const spentStatus = spentStatuses[i];
|
|
261
|
+
if (spentStatus?.spent) {
|
|
262
|
+
roundsToIgnore.add(spentStatus.txid);
|
|
263
|
+
}
|
|
264
|
+
utxos.push({
|
|
265
|
+
txid: tx.txid,
|
|
266
|
+
vout: i,
|
|
267
|
+
value: Number(vout.value),
|
|
268
|
+
status: {
|
|
269
|
+
confirmed: tx.status.confirmed,
|
|
270
|
+
block_time: tx.status.block_time,
|
|
271
|
+
},
|
|
272
|
+
virtualStatus: {
|
|
273
|
+
state: spentStatus?.spent ? "swept" : "pending",
|
|
274
|
+
batchTxID: spentStatus?.spent
|
|
275
|
+
? spentStatus.txid
|
|
276
|
+
: undefined,
|
|
277
|
+
},
|
|
278
|
+
createdAt: tx.status.confirmed
|
|
279
|
+
? new Date(tx.status.block_time * 1000)
|
|
280
|
+
: new Date(0),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const unconfirmedTxs = [];
|
|
286
|
+
const confirmedTxs = [];
|
|
287
|
+
for (const utxo of utxos) {
|
|
288
|
+
const tx = {
|
|
289
|
+
key: {
|
|
290
|
+
boardingTxid: utxo.txid,
|
|
291
|
+
roundTxid: "",
|
|
292
|
+
redeemTxid: "",
|
|
293
|
+
},
|
|
294
|
+
amount: utxo.value,
|
|
295
|
+
type: _1.TxType.TxReceived,
|
|
296
|
+
settled: utxo.virtualStatus.state === "swept",
|
|
297
|
+
createdAt: utxo.status.block_time
|
|
298
|
+
? new Date(utxo.status.block_time * 1000).getTime()
|
|
299
|
+
: 0,
|
|
300
|
+
};
|
|
301
|
+
if (!utxo.status.block_time) {
|
|
302
|
+
unconfirmedTxs.push(tx);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
confirmedTxs.push(tx);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
boardingTxs: [...unconfirmedTxs, ...confirmedTxs],
|
|
310
|
+
roundsToIgnore,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
async getBoardingUtxos() {
|
|
314
|
+
if (!this.boardingAddress || !this.boardingTapscript) {
|
|
315
|
+
throw new Error("Boarding address not configured");
|
|
316
|
+
}
|
|
317
|
+
const boardingUtxos = await this.onchainProvider.getCoins(this.boardingOnchainAddress);
|
|
318
|
+
const encodedBoardingTapscript = this.boardingTapscript.encode();
|
|
319
|
+
const forfeit = this.boardingTapscript.forfeit();
|
|
320
|
+
return boardingUtxos.map((utxo) => ({
|
|
321
|
+
...utxo,
|
|
322
|
+
tapLeafScript: forfeit,
|
|
323
|
+
scripts: encodedBoardingTapscript,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
async sendBitcoin(params, zeroFee = true) {
|
|
327
|
+
if (params.amount <= 0) {
|
|
328
|
+
throw new Error("Amount must be positive");
|
|
329
|
+
}
|
|
330
|
+
if (params.amount < Wallet.DUST_AMOUNT) {
|
|
331
|
+
throw new Error("Amount is below dust limit");
|
|
332
|
+
}
|
|
333
|
+
// If Ark is configured and amount is suitable, send via offchain
|
|
334
|
+
if (this.arkProvider && this.isOffchainSuitable(params.address)) {
|
|
335
|
+
return this.sendOffchain(params, zeroFee);
|
|
336
|
+
}
|
|
337
|
+
// Otherwise, send via onchain
|
|
338
|
+
return this.sendOnchain(params);
|
|
339
|
+
}
|
|
340
|
+
isOffchainSuitable(address) {
|
|
341
|
+
try {
|
|
342
|
+
address_1.ArkAddress.decode(address);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async sendOnchain(params) {
|
|
350
|
+
const coins = await this.getCoins();
|
|
351
|
+
const feeRate = params.feeRate || Wallet.FEE_RATE;
|
|
352
|
+
// Ensure fee is an integer by rounding up
|
|
353
|
+
const estimatedFee = Math.ceil(174 * feeRate);
|
|
354
|
+
const totalNeeded = params.amount + estimatedFee;
|
|
355
|
+
// Select coins
|
|
356
|
+
const selected = (0, coinselect_1.selectCoins)(coins, totalNeeded);
|
|
357
|
+
if (!selected.inputs) {
|
|
358
|
+
throw new Error("Insufficient funds");
|
|
359
|
+
}
|
|
360
|
+
// Create transaction
|
|
361
|
+
let tx = new btc_signer_1.Transaction();
|
|
362
|
+
// Add inputs
|
|
363
|
+
for (const input of selected.inputs) {
|
|
364
|
+
tx.addInput({
|
|
365
|
+
txid: input.txid,
|
|
366
|
+
index: input.vout,
|
|
367
|
+
witnessUtxo: {
|
|
368
|
+
script: this.onchainP2TR.script,
|
|
369
|
+
amount: BigInt(input.value),
|
|
370
|
+
},
|
|
371
|
+
tapInternalKey: this.onchainP2TR.tapInternalKey,
|
|
372
|
+
tapMerkleRoot: this.onchainP2TR.tapMerkleRoot,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// Add payment output
|
|
376
|
+
tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
|
|
377
|
+
// Add change output if needed
|
|
378
|
+
if (selected.changeAmount > 0) {
|
|
379
|
+
tx.addOutputAddress(this.onchainAddress, BigInt(selected.changeAmount), this.network);
|
|
380
|
+
}
|
|
381
|
+
// Sign inputs and Finalize
|
|
382
|
+
tx = await this.identity.sign(tx);
|
|
383
|
+
tx.finalize();
|
|
384
|
+
// Broadcast
|
|
385
|
+
const txid = await this.onchainProvider.broadcastTransaction(tx.hex);
|
|
386
|
+
return txid;
|
|
387
|
+
}
|
|
388
|
+
async sendOffchain(params, zeroFee = true) {
|
|
389
|
+
if (!this.arkProvider ||
|
|
390
|
+
!this.offchainAddress ||
|
|
391
|
+
!this.offchainTapscript) {
|
|
392
|
+
throw new Error("wallet not initialized");
|
|
393
|
+
}
|
|
394
|
+
const virtualCoins = await this.getVirtualCoins();
|
|
395
|
+
const estimatedFee = zeroFee
|
|
396
|
+
? 0
|
|
397
|
+
: Math.ceil(174 * (params.feeRate || Wallet.FEE_RATE));
|
|
398
|
+
const totalNeeded = params.amount + estimatedFee;
|
|
399
|
+
const selected = (0, coinselect_1.selectVirtualCoins)(virtualCoins, totalNeeded);
|
|
400
|
+
if (!selected || !selected.inputs) {
|
|
401
|
+
throw new Error("Insufficient funds");
|
|
402
|
+
}
|
|
403
|
+
const selectedLeaf = this.offchainTapscript.forfeit();
|
|
404
|
+
if (!selectedLeaf) {
|
|
405
|
+
throw new Error("Selected leaf not found");
|
|
406
|
+
}
|
|
407
|
+
const outputs = [
|
|
408
|
+
{
|
|
409
|
+
address: params.address,
|
|
410
|
+
amount: BigInt(params.amount),
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
// add change output if needed
|
|
414
|
+
if (selected.changeAmount > 0) {
|
|
415
|
+
outputs.push({
|
|
416
|
+
address: this.offchainAddress.encode(),
|
|
417
|
+
amount: BigInt(selected.changeAmount),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const scripts = this.offchainTapscript.encode();
|
|
421
|
+
let tx = (0, psbt_2.createVirtualTx)(selected.inputs.map((input) => ({
|
|
422
|
+
...input,
|
|
423
|
+
tapLeafScript: selectedLeaf,
|
|
424
|
+
scripts,
|
|
425
|
+
})), outputs);
|
|
426
|
+
tx = await this.identity.sign(tx);
|
|
427
|
+
const psbt = base_1.base64.encode(tx.toPSBT());
|
|
428
|
+
return this.arkProvider.submitVirtualTx(psbt);
|
|
429
|
+
}
|
|
430
|
+
async settle(params, eventCallback) {
|
|
431
|
+
if (!this.arkProvider) {
|
|
432
|
+
throw new Error("Ark provider not configured");
|
|
433
|
+
}
|
|
434
|
+
// validate arknotes inputs
|
|
435
|
+
if (params?.inputs) {
|
|
436
|
+
for (const input of params.inputs) {
|
|
437
|
+
if (typeof input === "string") {
|
|
438
|
+
try {
|
|
439
|
+
arknote_1.ArkNote.fromString(input);
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
throw new Error(`Invalid arknote "${input}"`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// if no params are provided, use all boarding and offchain utxos as inputs
|
|
448
|
+
// and send all to the offchain address
|
|
449
|
+
if (!params) {
|
|
450
|
+
if (!this.offchainAddress) {
|
|
451
|
+
throw new Error("Offchain address not configured");
|
|
452
|
+
}
|
|
453
|
+
let amount = 0;
|
|
454
|
+
const boardingUtxos = await this.getBoardingUtxos();
|
|
455
|
+
amount += boardingUtxos.reduce((sum, input) => sum + input.value, 0);
|
|
456
|
+
const vtxos = await this.getVtxos();
|
|
457
|
+
amount += vtxos.reduce((sum, input) => sum + input.value, 0);
|
|
458
|
+
const inputs = [...boardingUtxos, ...vtxos];
|
|
459
|
+
if (inputs.length === 0) {
|
|
460
|
+
throw new Error("No inputs found");
|
|
461
|
+
}
|
|
462
|
+
params = {
|
|
463
|
+
inputs,
|
|
464
|
+
outputs: [
|
|
465
|
+
{
|
|
466
|
+
address: this.offchainAddress.encode(),
|
|
467
|
+
amount: BigInt(amount),
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
// register inputs
|
|
473
|
+
const { requestId } = await this.arkProvider.registerInputsForNextRound(params.inputs.map((input) => {
|
|
474
|
+
if (typeof input === "string") {
|
|
475
|
+
return input;
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
outpoint: input,
|
|
479
|
+
tapscripts: input.scripts,
|
|
480
|
+
};
|
|
481
|
+
}));
|
|
482
|
+
const hasOffchainOutputs = params.outputs.some((output) => this.isOffchainSuitable(output.address));
|
|
483
|
+
// session holds the state of the musig2 signing process of the vtxo tree
|
|
484
|
+
let session;
|
|
485
|
+
const signingPublicKeys = [];
|
|
486
|
+
if (hasOffchainOutputs) {
|
|
487
|
+
session = this.identity.signerSession();
|
|
488
|
+
signingPublicKeys.push(base_1.hex.encode(session.getPublicKey()));
|
|
489
|
+
}
|
|
490
|
+
// register outputs
|
|
491
|
+
await this.arkProvider.registerOutputsForNextRound(requestId, params.outputs, signingPublicKeys);
|
|
492
|
+
// start pinging every seconds
|
|
493
|
+
const interval = setInterval(() => {
|
|
494
|
+
this.arkProvider?.ping(requestId).catch(stopPing);
|
|
495
|
+
}, 1000);
|
|
496
|
+
let pingRunning = true;
|
|
497
|
+
const stopPing = () => {
|
|
498
|
+
if (pingRunning) {
|
|
499
|
+
pingRunning = false;
|
|
500
|
+
clearInterval(interval);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const abortController = new AbortController();
|
|
504
|
+
// listen to settlement events
|
|
505
|
+
try {
|
|
506
|
+
const settlementStream = this.arkProvider.getEventStream(abortController.signal);
|
|
507
|
+
let step;
|
|
508
|
+
if (!hasOffchainOutputs) {
|
|
509
|
+
// if there are no offchain outputs, we don't have to handle musig2 tree signatures
|
|
510
|
+
// we can directly advance to the finalization step
|
|
511
|
+
step = ark_1.SettlementEventType.SigningNoncesGenerated;
|
|
512
|
+
}
|
|
513
|
+
const info = await this.arkProvider.getInfo();
|
|
514
|
+
const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
|
|
515
|
+
timelock: {
|
|
516
|
+
value: info.batchExpiry,
|
|
517
|
+
type: info.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
518
|
+
},
|
|
519
|
+
pubkeys: [base_1.hex.decode(info.pubkey).slice(1)],
|
|
520
|
+
}).script;
|
|
521
|
+
const sweepTapTreeRoot = (0, payment_1.tapLeafHash)(sweepTapscript);
|
|
522
|
+
for await (const event of settlementStream) {
|
|
523
|
+
if (eventCallback) {
|
|
524
|
+
eventCallback(event);
|
|
525
|
+
}
|
|
526
|
+
switch (event.type) {
|
|
527
|
+
// the settlement failed
|
|
528
|
+
case ark_1.SettlementEventType.Failed:
|
|
529
|
+
if (step === undefined) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
stopPing();
|
|
533
|
+
throw new Error(event.reason);
|
|
534
|
+
// the server has started the signing process of the vtxo tree transactions
|
|
535
|
+
// the server expects the partial musig2 nonces for each tx
|
|
536
|
+
case ark_1.SettlementEventType.SigningStart:
|
|
537
|
+
if (step !== undefined) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
stopPing();
|
|
541
|
+
if (hasOffchainOutputs) {
|
|
542
|
+
if (!session) {
|
|
543
|
+
throw new Error("Signing session not found");
|
|
544
|
+
}
|
|
545
|
+
await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session);
|
|
546
|
+
}
|
|
547
|
+
break;
|
|
548
|
+
// the musig2 nonces of the vtxo tree transactions are generated
|
|
549
|
+
// the server expects now the partial musig2 signatures
|
|
550
|
+
case ark_1.SettlementEventType.SigningNoncesGenerated:
|
|
551
|
+
if (step !== ark_1.SettlementEventType.SigningStart) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
stopPing();
|
|
555
|
+
if (hasOffchainOutputs) {
|
|
556
|
+
if (!session) {
|
|
557
|
+
throw new Error("Signing session not found");
|
|
558
|
+
}
|
|
559
|
+
await this.handleSettlementSigningNoncesGeneratedEvent(event, session);
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
// the vtxo tree is signed, craft, sign and submit forfeit transactions
|
|
563
|
+
// if any boarding utxos are involved, the settlement tx is also signed
|
|
564
|
+
case ark_1.SettlementEventType.Finalization:
|
|
565
|
+
if (step !== ark_1.SettlementEventType.SigningNoncesGenerated) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
stopPing();
|
|
569
|
+
await this.handleSettlementFinalizationEvent(event, params.inputs, info);
|
|
570
|
+
break;
|
|
571
|
+
// the settlement is done, last event to be received
|
|
572
|
+
case ark_1.SettlementEventType.Finalized:
|
|
573
|
+
if (step !== ark_1.SettlementEventType.Finalization) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
abortController.abort();
|
|
577
|
+
return event.roundTxid;
|
|
578
|
+
}
|
|
579
|
+
step = event.type;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
abortController.abort();
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
throw new Error("Settlement failed");
|
|
587
|
+
}
|
|
588
|
+
// validates the vtxo tree, creates a signing session and generates the musig2 nonces
|
|
589
|
+
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session) {
|
|
590
|
+
const vtxoTree = event.unsignedVtxoTree;
|
|
591
|
+
if (!this.arkProvider) {
|
|
592
|
+
throw new Error("Ark provider not configured");
|
|
593
|
+
}
|
|
594
|
+
// validate the unsigned vtxo tree
|
|
595
|
+
(0, validation_1.validateVtxoTree)(event.unsignedSettlementTx, vtxoTree, sweepTapTreeRoot);
|
|
596
|
+
// TODO check if our registered outputs are in the vtxo tree
|
|
597
|
+
const settlementPsbt = base_1.base64.decode(event.unsignedSettlementTx);
|
|
598
|
+
const settlementTx = btc_signer_1.Transaction.fromPSBT(settlementPsbt);
|
|
599
|
+
const sharedOutput = settlementTx.getOutput(0);
|
|
600
|
+
if (!sharedOutput?.amount) {
|
|
601
|
+
throw new Error("Shared output not found");
|
|
602
|
+
}
|
|
603
|
+
session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
|
|
604
|
+
await this.arkProvider.submitTreeNonces(event.id, base_1.hex.encode(session.getPublicKey()), session.getNonces());
|
|
605
|
+
}
|
|
606
|
+
async handleSettlementSigningNoncesGeneratedEvent(event, session) {
|
|
607
|
+
if (!this.arkProvider) {
|
|
608
|
+
throw new Error("Ark provider not configured");
|
|
609
|
+
}
|
|
610
|
+
session.setAggregatedNonces(event.treeNonces);
|
|
611
|
+
const signatures = session.sign();
|
|
612
|
+
await this.arkProvider.submitTreeSignatures(event.id, base_1.hex.encode(session.getPublicKey()), signatures);
|
|
613
|
+
}
|
|
614
|
+
async handleSettlementFinalizationEvent(event, inputs, infos) {
|
|
615
|
+
if (!this.arkProvider) {
|
|
616
|
+
throw new Error("Ark provider not configured");
|
|
617
|
+
}
|
|
618
|
+
// parse the server forfeit address
|
|
619
|
+
// server is expecting funds to be sent to this address
|
|
620
|
+
const forfeitAddress = (0, payment_1.Address)(this.network).decode(infos.forfeitAddress);
|
|
621
|
+
const serverPkScript = payment_1.OutScript.encode(forfeitAddress);
|
|
622
|
+
// the signed forfeits transactions to submit
|
|
623
|
+
const signedForfeits = [];
|
|
624
|
+
const vtxos = await this.getVirtualCoins();
|
|
625
|
+
let settlementPsbt = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.roundTx));
|
|
626
|
+
let hasBoardingUtxos = false;
|
|
627
|
+
let connectorsTreeValid = false;
|
|
628
|
+
for (const input of inputs) {
|
|
629
|
+
if (typeof input === "string")
|
|
630
|
+
continue; // skip notes
|
|
631
|
+
// check if the input is an offchain "virtual" coin
|
|
632
|
+
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
633
|
+
// boarding utxo, we need to sign the settlement tx
|
|
634
|
+
if (!vtxo) {
|
|
635
|
+
hasBoardingUtxos = true;
|
|
636
|
+
const inputIndexes = [];
|
|
637
|
+
for (let i = 0; i < settlementPsbt.inputsLength; i++) {
|
|
638
|
+
const settlementInput = settlementPsbt.getInput(i);
|
|
639
|
+
if (!settlementInput.txid ||
|
|
640
|
+
settlementInput.index === undefined) {
|
|
641
|
+
throw new Error("The server returned incomplete data. No settlement input found in the PSBT");
|
|
642
|
+
}
|
|
643
|
+
const inputTxId = base_1.hex.encode(settlementInput.txid);
|
|
644
|
+
if (inputTxId !== input.txid)
|
|
645
|
+
continue;
|
|
646
|
+
if (settlementInput.index !== input.vout)
|
|
647
|
+
continue;
|
|
648
|
+
// input found in the settlement tx, sign it
|
|
649
|
+
settlementPsbt.updateInput(i, {
|
|
650
|
+
tapLeafScript: [input.tapLeafScript],
|
|
651
|
+
});
|
|
652
|
+
inputIndexes.push(i);
|
|
653
|
+
}
|
|
654
|
+
settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (!connectorsTreeValid) {
|
|
658
|
+
// validate that the connectors tree is valid and contains our expected connectors
|
|
659
|
+
(0, validation_1.validateConnectorsTree)(event.roundTx, event.connectors);
|
|
660
|
+
connectorsTreeValid = true;
|
|
661
|
+
}
|
|
662
|
+
const forfeitControlBlock = psbt_1.TaprootControlBlock.encode(input.tapLeafScript[0]);
|
|
663
|
+
const tapscript = (0, tapscript_1.decodeTapscript)((0, base_2.scriptFromTapLeafScript)(input.tapLeafScript));
|
|
664
|
+
const fees = txSizeEstimator_1.TxWeightEstimator.create()
|
|
665
|
+
.addKeySpendInput() // connector
|
|
666
|
+
.addTapscriptInput(tapscript.witnessSize(100), // TODO: handle conditional script
|
|
667
|
+
input.tapLeafScript[1].length - 1, forfeitControlBlock.length)
|
|
668
|
+
.addP2WKHOutput()
|
|
669
|
+
.vsize()
|
|
670
|
+
.fee(event.minRelayFeeRate);
|
|
671
|
+
const connectorsLeaves = event.connectors.leaves();
|
|
672
|
+
const connectorOutpoint = event.connectorsIndex.get(`${vtxo.txid}:${vtxo.vout}`);
|
|
673
|
+
if (!connectorOutpoint) {
|
|
674
|
+
throw new Error("Connector outpoint not found");
|
|
675
|
+
}
|
|
676
|
+
let connectorOutput;
|
|
677
|
+
for (const leaf of connectorsLeaves) {
|
|
678
|
+
if (leaf.txid === connectorOutpoint.txid) {
|
|
679
|
+
try {
|
|
680
|
+
const connectorTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(leaf.tx));
|
|
681
|
+
connectorOutput = connectorTx.getOutput(connectorOutpoint.vout);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
throw new Error("Invalid connector tx");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (!connectorOutput ||
|
|
690
|
+
!connectorOutput.amount ||
|
|
691
|
+
!connectorOutput.script) {
|
|
692
|
+
throw new Error("Connector output not found");
|
|
693
|
+
}
|
|
694
|
+
let forfeitTx = (0, forfeit_1.buildForfeitTx)({
|
|
695
|
+
connectorInput: connectorOutpoint,
|
|
696
|
+
connectorAmount: connectorOutput.amount,
|
|
697
|
+
feeAmount: fees,
|
|
698
|
+
serverPkScript,
|
|
699
|
+
connectorPkScript: connectorOutput.script,
|
|
700
|
+
vtxoAmount: BigInt(vtxo.value),
|
|
701
|
+
vtxoInput: input,
|
|
702
|
+
vtxoPkScript: base_2.VtxoScript.decode(input.scripts).pkScript,
|
|
703
|
+
});
|
|
704
|
+
// add the tapscript
|
|
705
|
+
forfeitTx.updateInput(1, {
|
|
706
|
+
tapLeafScript: [input.tapLeafScript],
|
|
707
|
+
});
|
|
708
|
+
// do not sign the connector input
|
|
709
|
+
forfeitTx = await this.identity.sign(forfeitTx, [1]);
|
|
710
|
+
signedForfeits.push(base_1.base64.encode(forfeitTx.toPSBT()));
|
|
711
|
+
}
|
|
712
|
+
await this.arkProvider.submitSignedForfeitTxs(signedForfeits, hasBoardingUtxos
|
|
713
|
+
? base_1.base64.encode(settlementPsbt.toPSBT())
|
|
714
|
+
: undefined);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
exports.Wallet = Wallet;
|
|
718
|
+
// TODO get dust from ark server?
|
|
719
|
+
Wallet.DUST_AMOUNT = BigInt(546); // Bitcoin dust limit in satoshis = 546
|
|
720
|
+
Wallet.FEE_RATE = 1; // sats/vbyte
|