@arkade-os/sdk 0.3.7 → 0.3.9
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 +78 -1
- package/dist/cjs/identity/singleKey.js +33 -1
- package/dist/cjs/index.js +17 -2
- package/dist/cjs/intent/index.js +31 -2
- package/dist/cjs/providers/ark.js +9 -3
- package/dist/cjs/providers/indexer.js +2 -2
- package/dist/cjs/wallet/batch.js +183 -0
- package/dist/cjs/wallet/index.js +15 -0
- package/dist/cjs/wallet/serviceWorker/request.js +0 -2
- package/dist/cjs/wallet/serviceWorker/wallet.js +98 -34
- package/dist/cjs/wallet/serviceWorker/worker.js +169 -69
- package/dist/cjs/wallet/utils.js +2 -2
- package/dist/cjs/wallet/vtxo-manager.js +5 -0
- package/dist/cjs/wallet/wallet.js +399 -356
- package/dist/esm/identity/singleKey.js +31 -0
- package/dist/esm/index.js +12 -7
- package/dist/esm/intent/index.js +31 -2
- package/dist/esm/providers/ark.js +9 -3
- package/dist/esm/providers/indexer.js +2 -2
- package/dist/esm/wallet/batch.js +180 -0
- package/dist/esm/wallet/index.js +14 -0
- package/dist/esm/wallet/serviceWorker/request.js +0 -2
- package/dist/esm/wallet/serviceWorker/wallet.js +96 -33
- package/dist/esm/wallet/serviceWorker/worker.js +171 -71
- package/dist/esm/wallet/utils.js +2 -2
- package/dist/esm/wallet/vtxo-manager.js +6 -1
- package/dist/esm/wallet/wallet.js +400 -359
- package/dist/types/identity/index.d.ts +5 -3
- package/dist/types/identity/singleKey.d.ts +20 -1
- package/dist/types/index.d.ts +11 -8
- package/dist/types/intent/index.d.ts +19 -2
- package/dist/types/providers/ark.d.ts +9 -8
- package/dist/types/providers/indexer.d.ts +2 -2
- package/dist/types/wallet/batch.d.ts +87 -0
- package/dist/types/wallet/index.d.ts +75 -16
- package/dist/types/wallet/serviceWorker/request.d.ts +5 -1
- package/dist/types/wallet/serviceWorker/wallet.d.ts +46 -15
- package/dist/types/wallet/serviceWorker/worker.d.ts +6 -3
- package/dist/types/wallet/utils.d.ts +8 -3
- package/dist/types/wallet/wallet.d.ts +96 -35
- package/package.json +123 -113
|
@@ -1,92 +1,58 @@
|
|
|
1
1
|
import { base64, hex } from "@scure/base";
|
|
2
2
|
import * as bip68 from "bip68";
|
|
3
3
|
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
4
|
-
import { SigHash, Transaction, Address, OutScript
|
|
4
|
+
import { SigHash, Transaction, Address, OutScript } from "@scure/btc-signer";
|
|
5
5
|
import { sha256 } from "@scure/btc-signer/utils.js";
|
|
6
6
|
import { vtxosToTxs } from '../utils/transactionHistory.js';
|
|
7
7
|
import { ArkAddress } from '../script/address.js';
|
|
8
8
|
import { DefaultVtxo } from '../script/default.js';
|
|
9
9
|
import { getNetwork } from '../networks.js';
|
|
10
10
|
import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
11
|
-
import {
|
|
11
|
+
import { RestArkProvider, } from '../providers/ark.js';
|
|
12
12
|
import { buildForfeitTx } from '../forfeit.js';
|
|
13
13
|
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
|
|
14
|
-
import { isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
14
|
+
import { isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
15
15
|
import { VtxoScript } from '../script/base.js';
|
|
16
|
-
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
16
|
+
import { CLTVMultisigTapscript, CSVMultisigTapscript, } from '../script/tapscript.js';
|
|
17
17
|
import { buildOffchainTx, hasBoardingTxExpired } from '../utils/arkTransaction.js';
|
|
18
18
|
import { DEFAULT_RENEWAL_CONFIG } from './vtxo-manager.js';
|
|
19
19
|
import { ArkNote } from '../arknote/index.js';
|
|
20
20
|
import { Intent } from '../intent/index.js';
|
|
21
21
|
import { RestIndexerProvider } from '../providers/indexer.js';
|
|
22
|
-
import { TxTree } from '../tree/txTree.js';
|
|
23
22
|
import { ConditionWitness, VtxoTaprootTree } from '../utils/unknownFields.js';
|
|
24
23
|
import { InMemoryStorageAdapter } from '../storage/inMemory.js';
|
|
25
24
|
import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
|
|
26
25
|
import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
|
|
27
26
|
import { extendCoin, extendVirtualCoin } from './utils.js';
|
|
28
27
|
import { ArkError } from '../providers/errors.js';
|
|
28
|
+
import { Batch } from './batch.js';
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* The wallet does not store any data locally and relies on Ark and onchain
|
|
32
|
-
* providers to fetch UTXOs and VTXOs.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```typescript
|
|
36
|
-
* // Create a wallet with URL configuration
|
|
37
|
-
* const wallet = await Wallet.create({
|
|
38
|
-
* identity: SingleKey.fromHex('your_private_key'),
|
|
39
|
-
* arkServerUrl: 'https://ark.example.com',
|
|
40
|
-
* esploraUrl: 'https://mempool.space/api'
|
|
41
|
-
* });
|
|
42
|
-
*
|
|
43
|
-
* // Or with custom provider instances (e.g., for Expo/React Native)
|
|
44
|
-
* const wallet = await Wallet.create({
|
|
45
|
-
* identity: SingleKey.fromHex('your_private_key'),
|
|
46
|
-
* arkProvider: new ExpoArkProvider('https://ark.example.com'),
|
|
47
|
-
* indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
|
|
48
|
-
* esploraUrl: 'https://mempool.space/api'
|
|
49
|
-
* });
|
|
50
|
-
*
|
|
51
|
-
* // Get addresses
|
|
52
|
-
* const arkAddress = await wallet.getAddress();
|
|
53
|
-
* const boardingAddress = await wallet.getBoardingAddress();
|
|
54
|
-
*
|
|
55
|
-
* // Send bitcoin
|
|
56
|
-
* const txid = await wallet.sendBitcoin({
|
|
57
|
-
* address: 'tb1...',
|
|
58
|
-
* amount: 50000
|
|
59
|
-
* });
|
|
60
|
-
* ```
|
|
30
|
+
* Type guard function to check if an identity has a toReadonly method.
|
|
61
31
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
32
|
+
function hasToReadonly(identity) {
|
|
33
|
+
return (typeof identity === "object" &&
|
|
34
|
+
identity !== null &&
|
|
35
|
+
"toReadonly" in identity &&
|
|
36
|
+
typeof identity.toReadonly === "function");
|
|
37
|
+
}
|
|
38
|
+
export class ReadonlyWallet {
|
|
39
|
+
constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository) {
|
|
64
40
|
this.identity = identity;
|
|
65
41
|
this.network = network;
|
|
66
|
-
this.networkName = networkName;
|
|
67
42
|
this.onchainProvider = onchainProvider;
|
|
68
|
-
this.arkProvider = arkProvider;
|
|
69
43
|
this.indexerProvider = indexerProvider;
|
|
70
44
|
this.arkServerPublicKey = arkServerPublicKey;
|
|
71
45
|
this.offchainTapscript = offchainTapscript;
|
|
72
46
|
this.boardingTapscript = boardingTapscript;
|
|
73
|
-
this.serverUnrollScript = serverUnrollScript;
|
|
74
|
-
this.forfeitOutputScript = forfeitOutputScript;
|
|
75
|
-
this.forfeitPubkey = forfeitPubkey;
|
|
76
47
|
this.dustAmount = dustAmount;
|
|
77
48
|
this.walletRepository = walletRepository;
|
|
78
49
|
this.contractRepository = contractRepository;
|
|
79
|
-
this.renewalConfig = {
|
|
80
|
-
enabled: renewalConfig?.enabled ?? false,
|
|
81
|
-
...DEFAULT_RENEWAL_CONFIG,
|
|
82
|
-
...renewalConfig,
|
|
83
|
-
};
|
|
84
50
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Protected helper to set up shared wallet configuration.
|
|
53
|
+
* Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
|
|
54
|
+
*/
|
|
55
|
+
static async setupWalletConfig(config, pubkey) {
|
|
90
56
|
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
91
57
|
const arkProvider = config.arkProvider ||
|
|
92
58
|
(() => {
|
|
@@ -150,25 +116,32 @@ export class Wallet {
|
|
|
150
116
|
});
|
|
151
117
|
// Save tapscripts
|
|
152
118
|
const offchainTapscript = bareVtxoTapscript;
|
|
153
|
-
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
154
|
-
let serverUnrollScript;
|
|
155
|
-
try {
|
|
156
|
-
const raw = hex.decode(info.checkpointTapscript);
|
|
157
|
-
serverUnrollScript = CSVMultisigTapscript.decode(raw);
|
|
158
|
-
}
|
|
159
|
-
catch (e) {
|
|
160
|
-
throw new Error("Invalid checkpointTapscript from server");
|
|
161
|
-
}
|
|
162
|
-
// parse the server forfeit address
|
|
163
|
-
// server is expecting funds to be sent to this address
|
|
164
|
-
const forfeitPubkey = hex.decode(info.forfeitPubkey).slice(1);
|
|
165
|
-
const forfeitAddress = Address(network).decode(info.forfeitAddress);
|
|
166
|
-
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
167
119
|
// Set up storage and repositories
|
|
168
120
|
const storage = config.storage || new InMemoryStorageAdapter();
|
|
169
121
|
const walletRepository = new WalletRepositoryImpl(storage);
|
|
170
122
|
const contractRepository = new ContractRepositoryImpl(storage);
|
|
171
|
-
return
|
|
123
|
+
return {
|
|
124
|
+
arkProvider,
|
|
125
|
+
indexerProvider,
|
|
126
|
+
onchainProvider,
|
|
127
|
+
network,
|
|
128
|
+
networkName: info.network,
|
|
129
|
+
serverPubKey,
|
|
130
|
+
offchainTapscript,
|
|
131
|
+
boardingTapscript,
|
|
132
|
+
dustAmount: info.dust,
|
|
133
|
+
walletRepository,
|
|
134
|
+
contractRepository,
|
|
135
|
+
info,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
static async create(config) {
|
|
139
|
+
const pubkey = await config.identity.xOnlyPublicKey();
|
|
140
|
+
if (!pubkey) {
|
|
141
|
+
throw new Error("Invalid configured public key");
|
|
142
|
+
}
|
|
143
|
+
const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
144
|
+
return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository);
|
|
172
145
|
}
|
|
173
146
|
get arkAddress() {
|
|
174
147
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
@@ -243,7 +216,7 @@ export class Wallet {
|
|
|
243
216
|
let vtxos = allVtxos.filter(isSpendable);
|
|
244
217
|
// all recoverable vtxos are spendable by definition
|
|
245
218
|
if (!filter.withRecoverable) {
|
|
246
|
-
vtxos = vtxos.filter((vtxo) => !isRecoverable(vtxo));
|
|
219
|
+
vtxos = vtxos.filter((vtxo) => !isRecoverable(vtxo) && !isExpired(vtxo));
|
|
247
220
|
}
|
|
248
221
|
if (filter.withUnrolled) {
|
|
249
222
|
const spentVtxos = allVtxos.filter((vtxo) => !isSpendable(vtxo));
|
|
@@ -252,9 +225,6 @@ export class Wallet {
|
|
|
252
225
|
return vtxos;
|
|
253
226
|
}
|
|
254
227
|
async getTransactionHistory() {
|
|
255
|
-
if (!this.indexerProvider) {
|
|
256
|
-
return [];
|
|
257
|
-
}
|
|
258
228
|
const response = await this.indexerProvider.getVtxos({
|
|
259
229
|
scripts: [hex.encode(this.offchainTapscript.pkScript)],
|
|
260
230
|
});
|
|
@@ -358,6 +328,178 @@ export class Wallet {
|
|
|
358
328
|
await this.walletRepository.saveUtxos(boardingAddress, utxos);
|
|
359
329
|
return utxos;
|
|
360
330
|
}
|
|
331
|
+
async notifyIncomingFunds(eventCallback) {
|
|
332
|
+
const arkAddress = await this.getAddress();
|
|
333
|
+
const boardingAddress = await this.getBoardingAddress();
|
|
334
|
+
let onchainStopFunc;
|
|
335
|
+
let indexerStopFunc;
|
|
336
|
+
if (this.onchainProvider && boardingAddress) {
|
|
337
|
+
const findVoutOnTx = (tx) => {
|
|
338
|
+
return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
339
|
+
};
|
|
340
|
+
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
341
|
+
// find all utxos belonging to our boarding address
|
|
342
|
+
const coins = txs
|
|
343
|
+
// filter txs where address is in output
|
|
344
|
+
.filter((tx) => findVoutOnTx(tx) !== -1)
|
|
345
|
+
// return utxo as Coin
|
|
346
|
+
.map((tx) => {
|
|
347
|
+
const { txid, status } = tx;
|
|
348
|
+
const vout = findVoutOnTx(tx);
|
|
349
|
+
const value = Number(tx.vout[vout].value);
|
|
350
|
+
return { txid, vout, value, status };
|
|
351
|
+
});
|
|
352
|
+
// and notify via callback
|
|
353
|
+
eventCallback({
|
|
354
|
+
type: "utxo",
|
|
355
|
+
coins,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (this.indexerProvider && arkAddress) {
|
|
360
|
+
const offchainScript = this.offchainTapscript;
|
|
361
|
+
const subscriptionId = await this.indexerProvider.subscribeForScripts([
|
|
362
|
+
hex.encode(offchainScript.pkScript),
|
|
363
|
+
]);
|
|
364
|
+
const abortController = new AbortController();
|
|
365
|
+
const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
|
|
366
|
+
indexerStopFunc = async () => {
|
|
367
|
+
abortController.abort();
|
|
368
|
+
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
369
|
+
};
|
|
370
|
+
// Handle subscription updates asynchronously without blocking
|
|
371
|
+
(async () => {
|
|
372
|
+
try {
|
|
373
|
+
for await (const update of subscription) {
|
|
374
|
+
if (update.newVtxos?.length > 0 ||
|
|
375
|
+
update.spentVtxos?.length > 0) {
|
|
376
|
+
eventCallback({
|
|
377
|
+
type: "vtxo",
|
|
378
|
+
newVtxos: update.newVtxos.map((vtxo) => extendVirtualCoin(this, vtxo)),
|
|
379
|
+
spentVtxos: update.spentVtxos.map((vtxo) => extendVirtualCoin(this, vtxo)),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
console.error("Subscription error:", error);
|
|
386
|
+
}
|
|
387
|
+
})();
|
|
388
|
+
}
|
|
389
|
+
const stopFunc = () => {
|
|
390
|
+
onchainStopFunc?.();
|
|
391
|
+
indexerStopFunc?.();
|
|
392
|
+
};
|
|
393
|
+
return stopFunc;
|
|
394
|
+
}
|
|
395
|
+
async fetchPendingTxs() {
|
|
396
|
+
// get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
|
|
397
|
+
const scripts = [hex.encode(this.offchainTapscript.pkScript)];
|
|
398
|
+
let { vtxos } = await this.indexerProvider.getVtxos({
|
|
399
|
+
scripts,
|
|
400
|
+
});
|
|
401
|
+
return vtxos
|
|
402
|
+
.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
403
|
+
vtxo.virtualStatus.state !== "settled" &&
|
|
404
|
+
vtxo.arkTxId !== undefined)
|
|
405
|
+
.map((_) => _.arkTxId);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|
|
410
|
+
* The wallet does not store any data locally and relies on Ark and onchain
|
|
411
|
+
* providers to fetch UTXOs and VTXOs.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```typescript
|
|
415
|
+
* // Create a wallet with URL configuration
|
|
416
|
+
* const wallet = await Wallet.create({
|
|
417
|
+
* identity: SingleKey.fromHex('your_private_key'),
|
|
418
|
+
* arkServerUrl: 'https://ark.example.com',
|
|
419
|
+
* esploraUrl: 'https://mempool.space/api'
|
|
420
|
+
* });
|
|
421
|
+
*
|
|
422
|
+
* // Or with custom provider instances (e.g., for Expo/React Native)
|
|
423
|
+
* const wallet = await Wallet.create({
|
|
424
|
+
* identity: SingleKey.fromHex('your_private_key'),
|
|
425
|
+
* arkProvider: new ExpoArkProvider('https://ark.example.com'),
|
|
426
|
+
* indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
|
|
427
|
+
* esploraUrl: 'https://mempool.space/api'
|
|
428
|
+
* });
|
|
429
|
+
*
|
|
430
|
+
* // Get addresses
|
|
431
|
+
* const arkAddress = await wallet.getAddress();
|
|
432
|
+
* const boardingAddress = await wallet.getBoardingAddress();
|
|
433
|
+
*
|
|
434
|
+
* // Send bitcoin
|
|
435
|
+
* const txid = await wallet.sendBitcoin({
|
|
436
|
+
* address: 'tb1...',
|
|
437
|
+
* amount: 50000
|
|
438
|
+
* });
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
export class Wallet extends ReadonlyWallet {
|
|
442
|
+
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
|
|
443
|
+
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository);
|
|
444
|
+
this.networkName = networkName;
|
|
445
|
+
this.arkProvider = arkProvider;
|
|
446
|
+
this.serverUnrollScript = serverUnrollScript;
|
|
447
|
+
this.forfeitOutputScript = forfeitOutputScript;
|
|
448
|
+
this.forfeitPubkey = forfeitPubkey;
|
|
449
|
+
this.identity = identity;
|
|
450
|
+
this.renewalConfig = {
|
|
451
|
+
enabled: renewalConfig?.enabled ?? false,
|
|
452
|
+
...DEFAULT_RENEWAL_CONFIG,
|
|
453
|
+
...renewalConfig,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
static async create(config) {
|
|
457
|
+
const pubkey = await config.identity.xOnlyPublicKey();
|
|
458
|
+
if (!pubkey) {
|
|
459
|
+
throw new Error("Invalid configured public key");
|
|
460
|
+
}
|
|
461
|
+
const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
462
|
+
// Compute Wallet-specific forfeit and unroll scripts
|
|
463
|
+
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
464
|
+
let serverUnrollScript;
|
|
465
|
+
try {
|
|
466
|
+
const raw = hex.decode(setup.info.checkpointTapscript);
|
|
467
|
+
serverUnrollScript = CSVMultisigTapscript.decode(raw);
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
throw new Error("Invalid checkpointTapscript from server");
|
|
471
|
+
}
|
|
472
|
+
// parse the server forfeit address
|
|
473
|
+
// server is expecting funds to be sent to this address
|
|
474
|
+
const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
475
|
+
const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
|
|
476
|
+
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
477
|
+
return new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Convert this wallet to a readonly wallet.
|
|
481
|
+
*
|
|
482
|
+
* @returns A readonly wallet with the same configuration but readonly identity
|
|
483
|
+
* @example
|
|
484
|
+
* ```typescript
|
|
485
|
+
* const wallet = await Wallet.create({ identity: SingleKey.fromHex('...'), ... });
|
|
486
|
+
* const readonlyWallet = await wallet.toReadonly();
|
|
487
|
+
*
|
|
488
|
+
* // Can query balance and addresses
|
|
489
|
+
* const balance = await readonlyWallet.getBalance();
|
|
490
|
+
* const address = await readonlyWallet.getAddress();
|
|
491
|
+
*
|
|
492
|
+
* // But cannot send transactions (type error)
|
|
493
|
+
* // readonlyWallet.sendBitcoin(...); // TypeScript error
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
async toReadonly() {
|
|
497
|
+
// Check if the identity has a toReadonly method using type guard
|
|
498
|
+
const readonlyIdentity = hasToReadonly(this.identity)
|
|
499
|
+
? await this.identity.toReadonly()
|
|
500
|
+
: this.identity; // Identity extends ReadonlyIdentity, so this is safe
|
|
501
|
+
return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository);
|
|
502
|
+
}
|
|
361
503
|
async sendBitcoin(params) {
|
|
362
504
|
if (params.amount <= 0) {
|
|
363
505
|
throw new Error("Amount must be positive");
|
|
@@ -444,7 +586,7 @@ export class Wallet {
|
|
|
444
586
|
vout: outputs.length - 1,
|
|
445
587
|
createdAt: new Date(createdAt),
|
|
446
588
|
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
447
|
-
intentTapLeafScript: this.offchainTapscript.
|
|
589
|
+
intentTapLeafScript: this.offchainTapscript.forfeit(),
|
|
448
590
|
isUnrolled: false,
|
|
449
591
|
isSpent: false,
|
|
450
592
|
tapTree: this.offchainTapscript.encode(),
|
|
@@ -554,285 +696,31 @@ export class Wallet {
|
|
|
554
696
|
this.makeDeleteIntentSignature(params.inputs),
|
|
555
697
|
]);
|
|
556
698
|
const intentId = await this.safeRegisterIntent(intent);
|
|
699
|
+
const topics = [
|
|
700
|
+
...signingPublicKeys,
|
|
701
|
+
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
702
|
+
];
|
|
703
|
+
const handler = this.createBatchHandler(intentId, params.inputs, session);
|
|
557
704
|
const abortController = new AbortController();
|
|
558
|
-
// listen to settlement events
|
|
559
705
|
try {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
let sweepTapTreeRoot;
|
|
569
|
-
const vtxoChunks = [];
|
|
570
|
-
const connectorsChunks = [];
|
|
571
|
-
let vtxoGraph;
|
|
572
|
-
let connectorsGraph;
|
|
573
|
-
for await (const event of settlementStream) {
|
|
574
|
-
if (eventCallback) {
|
|
575
|
-
eventCallback(event);
|
|
576
|
-
}
|
|
577
|
-
switch (event.type) {
|
|
578
|
-
// the settlement failed
|
|
579
|
-
case SettlementEventType.BatchFailed:
|
|
580
|
-
throw new Error(event.reason);
|
|
581
|
-
case SettlementEventType.BatchStarted:
|
|
582
|
-
if (step !== undefined) {
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
const res = await this.handleBatchStartedEvent(event, intentId, this.forfeitPubkey, this.forfeitOutputScript);
|
|
586
|
-
if (!res.skip) {
|
|
587
|
-
step = event.type;
|
|
588
|
-
sweepTapTreeRoot = res.sweepTapTreeRoot;
|
|
589
|
-
batchId = res.roundId;
|
|
590
|
-
if (!hasOffchainOutputs) {
|
|
591
|
-
// if there are no offchain outputs, we don't have to handle musig2 tree signatures
|
|
592
|
-
// we can directly advance to the finalization step
|
|
593
|
-
step = SettlementEventType.TreeNonces;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
break;
|
|
597
|
-
case SettlementEventType.TreeTx:
|
|
598
|
-
if (step !== SettlementEventType.BatchStarted &&
|
|
599
|
-
step !== SettlementEventType.TreeNonces) {
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
// index 0 = vtxo tree
|
|
603
|
-
if (event.batchIndex === 0) {
|
|
604
|
-
vtxoChunks.push(event.chunk);
|
|
605
|
-
// index 1 = connectors tree
|
|
606
|
-
}
|
|
607
|
-
else if (event.batchIndex === 1) {
|
|
608
|
-
connectorsChunks.push(event.chunk);
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
throw new Error(`Invalid batch index: ${event.batchIndex}`);
|
|
612
|
-
}
|
|
613
|
-
break;
|
|
614
|
-
case SettlementEventType.TreeSignature:
|
|
615
|
-
if (step !== SettlementEventType.TreeNonces) {
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
if (!hasOffchainOutputs) {
|
|
619
|
-
continue;
|
|
620
|
-
}
|
|
621
|
-
if (!vtxoGraph) {
|
|
622
|
-
throw new Error("Vtxo graph not set, something went wrong");
|
|
623
|
-
}
|
|
624
|
-
// index 0 = vtxo graph
|
|
625
|
-
if (event.batchIndex === 0) {
|
|
626
|
-
const tapKeySig = hex.decode(event.signature);
|
|
627
|
-
vtxoGraph.update(event.txid, (tx) => {
|
|
628
|
-
tx.updateInput(0, {
|
|
629
|
-
tapKeySig,
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
break;
|
|
634
|
-
// the server has started the signing process of the vtxo tree transactions
|
|
635
|
-
// the server expects the partial musig2 nonces for each tx
|
|
636
|
-
case SettlementEventType.TreeSigningStarted:
|
|
637
|
-
if (step !== SettlementEventType.BatchStarted) {
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
if (hasOffchainOutputs) {
|
|
641
|
-
if (!session) {
|
|
642
|
-
throw new Error("Signing session not set");
|
|
643
|
-
}
|
|
644
|
-
if (!sweepTapTreeRoot) {
|
|
645
|
-
throw new Error("Sweep tap tree root not set");
|
|
646
|
-
}
|
|
647
|
-
if (vtxoChunks.length === 0) {
|
|
648
|
-
throw new Error("unsigned vtxo graph not received");
|
|
649
|
-
}
|
|
650
|
-
vtxoGraph = TxTree.create(vtxoChunks);
|
|
651
|
-
await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph);
|
|
652
|
-
}
|
|
653
|
-
step = event.type;
|
|
654
|
-
break;
|
|
655
|
-
// the musig2 nonces of the vtxo tree transactions are generated
|
|
656
|
-
// the server expects now the partial musig2 signatures
|
|
657
|
-
case SettlementEventType.TreeNonces:
|
|
658
|
-
if (step !== SettlementEventType.TreeSigningStarted) {
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
if (hasOffchainOutputs) {
|
|
662
|
-
if (!session) {
|
|
663
|
-
throw new Error("Signing session not set");
|
|
664
|
-
}
|
|
665
|
-
const signed = await this.handleSettlementTreeNoncesEvent(event, session);
|
|
666
|
-
if (signed) {
|
|
667
|
-
step = event.type;
|
|
668
|
-
}
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
step = event.type;
|
|
672
|
-
break;
|
|
673
|
-
// the vtxo tree is signed, craft, sign and submit forfeit transactions
|
|
674
|
-
// if any boarding utxos are involved, the settlement tx is also signed
|
|
675
|
-
case SettlementEventType.BatchFinalization:
|
|
676
|
-
if (step !== SettlementEventType.TreeNonces) {
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
if (!this.forfeitOutputScript) {
|
|
680
|
-
throw new Error("Forfeit output script not set");
|
|
681
|
-
}
|
|
682
|
-
if (connectorsChunks.length > 0) {
|
|
683
|
-
connectorsGraph = TxTree.create(connectorsChunks);
|
|
684
|
-
validateConnectorsTxGraph(event.commitmentTx, connectorsGraph);
|
|
685
|
-
}
|
|
686
|
-
await this.handleSettlementFinalizationEvent(event, params.inputs, this.forfeitOutputScript, connectorsGraph);
|
|
687
|
-
step = event.type;
|
|
688
|
-
break;
|
|
689
|
-
// the settlement is done, last event to be received
|
|
690
|
-
case SettlementEventType.BatchFinalized:
|
|
691
|
-
if (step !== SettlementEventType.BatchFinalization) {
|
|
692
|
-
continue;
|
|
693
|
-
}
|
|
694
|
-
if (event.id === batchId) {
|
|
695
|
-
abortController.abort();
|
|
696
|
-
return event.commitmentTxid;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
706
|
+
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
707
|
+
return await Batch.join(stream, handler, {
|
|
708
|
+
abortController,
|
|
709
|
+
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
710
|
+
eventCallback: eventCallback
|
|
711
|
+
? (event) => Promise.resolve(eventCallback(event))
|
|
712
|
+
: undefined,
|
|
713
|
+
});
|
|
700
714
|
}
|
|
701
715
|
catch (error) {
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
try {
|
|
705
|
-
// delete the intent to not be stuck in the queue
|
|
706
|
-
await this.arkProvider.deleteIntent(deleteIntent);
|
|
707
|
-
}
|
|
708
|
-
catch (error) {
|
|
709
|
-
console.error("failed to delete intent: ", error);
|
|
710
|
-
}
|
|
716
|
+
// delete the intent to not be stuck in the queue
|
|
717
|
+
await this.arkProvider.deleteIntent(deleteIntent).catch(() => { });
|
|
711
718
|
throw error;
|
|
712
719
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const arkAddress = await this.getAddress();
|
|
717
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
718
|
-
let onchainStopFunc;
|
|
719
|
-
let indexerStopFunc;
|
|
720
|
-
if (this.onchainProvider && boardingAddress) {
|
|
721
|
-
const findVoutOnTx = (tx) => {
|
|
722
|
-
return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
723
|
-
};
|
|
724
|
-
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
725
|
-
// find all utxos belonging to our boarding address
|
|
726
|
-
const coins = txs
|
|
727
|
-
// filter txs where address is in output
|
|
728
|
-
.filter((tx) => findVoutOnTx(tx) !== -1)
|
|
729
|
-
// return utxo as Coin
|
|
730
|
-
.map((tx) => {
|
|
731
|
-
const { txid, status } = tx;
|
|
732
|
-
const vout = findVoutOnTx(tx);
|
|
733
|
-
const value = Number(tx.vout[vout].value);
|
|
734
|
-
return { txid, vout, value, status };
|
|
735
|
-
});
|
|
736
|
-
// and notify via callback
|
|
737
|
-
eventCallback({
|
|
738
|
-
type: "utxo",
|
|
739
|
-
coins,
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
if (this.indexerProvider && arkAddress) {
|
|
744
|
-
const offchainScript = this.offchainTapscript;
|
|
745
|
-
const subscriptionId = await this.indexerProvider.subscribeForScripts([
|
|
746
|
-
hex.encode(offchainScript.pkScript),
|
|
747
|
-
]);
|
|
748
|
-
const abortController = new AbortController();
|
|
749
|
-
const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
|
|
750
|
-
indexerStopFunc = async () => {
|
|
751
|
-
abortController.abort();
|
|
752
|
-
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
753
|
-
};
|
|
754
|
-
// Handle subscription updates asynchronously without blocking
|
|
755
|
-
(async () => {
|
|
756
|
-
try {
|
|
757
|
-
for await (const update of subscription) {
|
|
758
|
-
if (update.newVtxos?.length > 0 ||
|
|
759
|
-
update.spentVtxos?.length > 0) {
|
|
760
|
-
eventCallback({
|
|
761
|
-
type: "vtxo",
|
|
762
|
-
newVtxos: update.newVtxos.map((vtxo) => extendVirtualCoin(this, vtxo)),
|
|
763
|
-
spentVtxos: update.spentVtxos.map((vtxo) => extendVirtualCoin(this, vtxo)),
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
catch (error) {
|
|
769
|
-
console.error("Subscription error:", error);
|
|
770
|
-
}
|
|
771
|
-
})();
|
|
772
|
-
}
|
|
773
|
-
const stopFunc = () => {
|
|
774
|
-
onchainStopFunc?.();
|
|
775
|
-
indexerStopFunc?.();
|
|
776
|
-
};
|
|
777
|
-
return stopFunc;
|
|
778
|
-
}
|
|
779
|
-
async handleBatchStartedEvent(event, intentId, forfeitPubKey, forfeitOutputScript) {
|
|
780
|
-
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
781
|
-
const intentIdHash = sha256(utf8IntentId);
|
|
782
|
-
const intentIdHashStr = hex.encode(intentIdHash);
|
|
783
|
-
let skip = true;
|
|
784
|
-
// check if our intent ID hash matches any in the event
|
|
785
|
-
for (const idHash of event.intentIdHashes) {
|
|
786
|
-
if (idHash === intentIdHashStr) {
|
|
787
|
-
if (!this.arkProvider) {
|
|
788
|
-
throw new Error("Ark provider not configured");
|
|
789
|
-
}
|
|
790
|
-
await this.arkProvider.confirmRegistration(intentId);
|
|
791
|
-
skip = false;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
if (skip) {
|
|
795
|
-
return { skip };
|
|
796
|
-
}
|
|
797
|
-
const sweepTapscript = CSVMultisigTapscript.encode({
|
|
798
|
-
timelock: {
|
|
799
|
-
value: event.batchExpiry,
|
|
800
|
-
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
801
|
-
},
|
|
802
|
-
pubkeys: [forfeitPubKey],
|
|
803
|
-
}).script;
|
|
804
|
-
const sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
805
|
-
return {
|
|
806
|
-
roundId: event.id,
|
|
807
|
-
sweepTapTreeRoot,
|
|
808
|
-
forfeitOutputScript,
|
|
809
|
-
skip: false,
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
// validates the vtxo tree, creates a signing session and generates the musig2 nonces
|
|
813
|
-
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph) {
|
|
814
|
-
// validate the unsigned vtxo tree
|
|
815
|
-
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
816
|
-
validateVtxoTxGraph(vtxoGraph, commitmentTx, sweepTapTreeRoot);
|
|
817
|
-
// TODO check if our registered outputs are in the vtxo tree
|
|
818
|
-
const sharedOutput = commitmentTx.getOutput(0);
|
|
819
|
-
if (!sharedOutput?.amount) {
|
|
820
|
-
throw new Error("Shared output not found");
|
|
720
|
+
finally {
|
|
721
|
+
// close the stream
|
|
722
|
+
abortController.abort();
|
|
821
723
|
}
|
|
822
|
-
session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
|
|
823
|
-
const pubkey = hex.encode(await session.getPublicKey());
|
|
824
|
-
const nonces = await session.getNonces();
|
|
825
|
-
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
826
|
-
}
|
|
827
|
-
async handleSettlementTreeNoncesEvent(event, session) {
|
|
828
|
-
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
829
|
-
// wait to receive and aggregate all nonces before sending signatures
|
|
830
|
-
if (!hasAllNonces)
|
|
831
|
-
return false;
|
|
832
|
-
const signatures = await session.sign();
|
|
833
|
-
const pubkey = hex.encode(await session.getPublicKey());
|
|
834
|
-
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
835
|
-
return true;
|
|
836
724
|
}
|
|
837
725
|
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
838
726
|
// the signed forfeits transactions to submit
|
|
@@ -922,9 +810,98 @@ export class Wallet {
|
|
|
922
810
|
: undefined);
|
|
923
811
|
}
|
|
924
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* @implements Batch.Handler interface.
|
|
815
|
+
* @param intentId - The intent ID.
|
|
816
|
+
* @param inputs - The inputs of the intent.
|
|
817
|
+
* @param session - The musig2 signing session, if not provided, the signing will be skipped.
|
|
818
|
+
*/
|
|
819
|
+
createBatchHandler(intentId, inputs, session) {
|
|
820
|
+
let sweepTapTreeRoot;
|
|
821
|
+
return {
|
|
822
|
+
onBatchStarted: async (event) => {
|
|
823
|
+
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
824
|
+
const intentIdHash = sha256(utf8IntentId);
|
|
825
|
+
const intentIdHashStr = hex.encode(intentIdHash);
|
|
826
|
+
let skip = true;
|
|
827
|
+
// check if our intent ID hash matches any in the event
|
|
828
|
+
for (const idHash of event.intentIdHashes) {
|
|
829
|
+
if (idHash === intentIdHashStr) {
|
|
830
|
+
if (!this.arkProvider) {
|
|
831
|
+
throw new Error("Ark provider not configured");
|
|
832
|
+
}
|
|
833
|
+
await this.arkProvider.confirmRegistration(intentId);
|
|
834
|
+
skip = false;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (skip) {
|
|
838
|
+
return { skip };
|
|
839
|
+
}
|
|
840
|
+
const sweepTapscript = CSVMultisigTapscript.encode({
|
|
841
|
+
timelock: {
|
|
842
|
+
value: event.batchExpiry,
|
|
843
|
+
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
844
|
+
},
|
|
845
|
+
pubkeys: [this.forfeitPubkey],
|
|
846
|
+
}).script;
|
|
847
|
+
sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
848
|
+
return { skip: false };
|
|
849
|
+
},
|
|
850
|
+
onTreeSigningStarted: async (event, vtxoTree) => {
|
|
851
|
+
if (!session) {
|
|
852
|
+
return { skip: true };
|
|
853
|
+
}
|
|
854
|
+
if (!sweepTapTreeRoot) {
|
|
855
|
+
throw new Error("Sweep tap tree root not set");
|
|
856
|
+
}
|
|
857
|
+
const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
|
|
858
|
+
const signerPublicKey = await session.getPublicKey();
|
|
859
|
+
const xonlySignerPublicKey = signerPublicKey.subarray(1);
|
|
860
|
+
if (!xOnlyPublicKeys.includes(hex.encode(xonlySignerPublicKey))) {
|
|
861
|
+
// not a cosigner, skip the signing
|
|
862
|
+
return { skip: true };
|
|
863
|
+
}
|
|
864
|
+
// validate the unsigned vtxo tree
|
|
865
|
+
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
866
|
+
validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
|
|
867
|
+
// TODO check if our registered outputs are in the vtxo tree
|
|
868
|
+
const sharedOutput = commitmentTx.getOutput(0);
|
|
869
|
+
if (!sharedOutput?.amount) {
|
|
870
|
+
throw new Error("Shared output not found");
|
|
871
|
+
}
|
|
872
|
+
await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
|
|
873
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
874
|
+
const nonces = await session.getNonces();
|
|
875
|
+
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
876
|
+
return { skip: false };
|
|
877
|
+
},
|
|
878
|
+
onTreeNonces: async (event) => {
|
|
879
|
+
if (!session) {
|
|
880
|
+
return { fullySigned: true }; // Signing complete (no signing needed)
|
|
881
|
+
}
|
|
882
|
+
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
883
|
+
// wait to receive and aggregate all nonces before sending signatures
|
|
884
|
+
if (!hasAllNonces)
|
|
885
|
+
return { fullySigned: false };
|
|
886
|
+
const signatures = await session.sign();
|
|
887
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
888
|
+
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
889
|
+
return { fullySigned: true };
|
|
890
|
+
},
|
|
891
|
+
onBatchFinalization: async (event, _, connectorTree) => {
|
|
892
|
+
if (!this.forfeitOutputScript) {
|
|
893
|
+
throw new Error("Forfeit output script not set");
|
|
894
|
+
}
|
|
895
|
+
if (connectorTree) {
|
|
896
|
+
validateConnectorsTxGraph(event.commitmentTx, connectorTree);
|
|
897
|
+
}
|
|
898
|
+
await this.handleSettlementFinalizationEvent(event, inputs, this.forfeitOutputScript, connectorTree);
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
925
902
|
async safeRegisterIntent(intent) {
|
|
926
903
|
try {
|
|
927
|
-
return this.arkProvider.registerIntent(intent);
|
|
904
|
+
return await this.arkProvider.registerIntent(intent);
|
|
928
905
|
}
|
|
929
906
|
catch (error) {
|
|
930
907
|
// catch the "already registered by another intent" error
|
|
@@ -952,12 +929,11 @@ export class Wallet {
|
|
|
952
929
|
expire_at: 0,
|
|
953
930
|
cosigners_public_keys: cosignerPubKeys,
|
|
954
931
|
};
|
|
955
|
-
const
|
|
956
|
-
const proof = Intent.create(encodedMessage, inputs, outputs);
|
|
932
|
+
const proof = Intent.create(message, inputs, outputs);
|
|
957
933
|
const signedProof = await this.identity.sign(proof);
|
|
958
934
|
return {
|
|
959
935
|
proof: base64.encode(signedProof.toPSBT()),
|
|
960
|
-
message
|
|
936
|
+
message,
|
|
961
937
|
};
|
|
962
938
|
}
|
|
963
939
|
async makeDeleteIntentSignature(coins) {
|
|
@@ -966,19 +942,78 @@ export class Wallet {
|
|
|
966
942
|
type: "delete",
|
|
967
943
|
expire_at: 0,
|
|
968
944
|
};
|
|
969
|
-
const
|
|
970
|
-
const proof = Intent.create(encodedMessage, inputs, []);
|
|
945
|
+
const proof = Intent.create(message, inputs, []);
|
|
971
946
|
const signedProof = await this.identity.sign(proof);
|
|
972
947
|
return {
|
|
973
948
|
proof: base64.encode(signedProof.toPSBT()),
|
|
974
|
-
message
|
|
949
|
+
message,
|
|
975
950
|
};
|
|
976
951
|
}
|
|
952
|
+
async makeGetPendingTxIntentSignature(vtxos) {
|
|
953
|
+
const inputs = this.prepareIntentProofInputs(vtxos);
|
|
954
|
+
const message = {
|
|
955
|
+
type: "get-pending-tx",
|
|
956
|
+
expire_at: 0,
|
|
957
|
+
};
|
|
958
|
+
const proof = Intent.create(message, inputs, []);
|
|
959
|
+
const signedProof = await this.identity.sign(proof);
|
|
960
|
+
return {
|
|
961
|
+
proof: base64.encode(signedProof.toPSBT()),
|
|
962
|
+
message,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Finalizes pending transactions by retrieving them from the server and finalizing each one.
|
|
967
|
+
* @param vtxos - Optional list of VTXOs to use instead of retrieving them from the server
|
|
968
|
+
* @returns Array of transaction IDs that were finalized
|
|
969
|
+
*/
|
|
970
|
+
async finalizePendingTxs(vtxos) {
|
|
971
|
+
const MAX_INPUTS_PER_INTENT = 20;
|
|
972
|
+
if (!vtxos || vtxos.length === 0) {
|
|
973
|
+
// get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
|
|
974
|
+
const scripts = [hex.encode(this.offchainTapscript.pkScript)];
|
|
975
|
+
let { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
976
|
+
scripts,
|
|
977
|
+
});
|
|
978
|
+
fetchedVtxos = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
979
|
+
vtxo.virtualStatus.state !== "settled");
|
|
980
|
+
if (fetchedVtxos.length === 0) {
|
|
981
|
+
return { finalized: [], pending: [] };
|
|
982
|
+
}
|
|
983
|
+
vtxos = fetchedVtxos.map((v) => extendVirtualCoin(this, v));
|
|
984
|
+
}
|
|
985
|
+
const finalized = [];
|
|
986
|
+
const pending = [];
|
|
987
|
+
for (let i = 0; i < vtxos.length; i += MAX_INPUTS_PER_INTENT) {
|
|
988
|
+
const batch = vtxos.slice(i, i + MAX_INPUTS_PER_INTENT);
|
|
989
|
+
const intent = await this.makeGetPendingTxIntentSignature(batch);
|
|
990
|
+
const pendingTxs = await this.arkProvider.getPendingTxs(intent);
|
|
991
|
+
// finalize each transaction by signing the checkpoints
|
|
992
|
+
for (const pendingTx of pendingTxs) {
|
|
993
|
+
pending.push(pendingTx.arkTxid);
|
|
994
|
+
try {
|
|
995
|
+
// sign the checkpoints
|
|
996
|
+
const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
|
|
997
|
+
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
998
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
999
|
+
return base64.encode(signedCheckpoint.toPSBT());
|
|
1000
|
+
}));
|
|
1001
|
+
await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
|
|
1002
|
+
finalized.push(pendingTx.arkTxid);
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
console.error(`Failed to finalize transaction ${pendingTx.arkTxid}:`, error);
|
|
1006
|
+
// continue with other transactions even if one fails
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return { finalized, pending };
|
|
1011
|
+
}
|
|
977
1012
|
prepareIntentProofInputs(coins) {
|
|
978
1013
|
const inputs = [];
|
|
979
1014
|
for (const input of coins) {
|
|
980
1015
|
const vtxoScript = VtxoScript.decode(input.tapTree);
|
|
981
|
-
const sequence = getSequence(input);
|
|
1016
|
+
const sequence = getSequence(input.intentTapLeafScript);
|
|
982
1017
|
const unknown = [VtxoTaprootTree.encode(input.tapTree)];
|
|
983
1018
|
if (input.extraWitness) {
|
|
984
1019
|
unknown.push(ConditionWitness.encode(input.extraWitness));
|
|
@@ -999,15 +1034,21 @@ export class Wallet {
|
|
|
999
1034
|
}
|
|
1000
1035
|
}
|
|
1001
1036
|
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
1002
|
-
function getSequence(
|
|
1037
|
+
export function getSequence(tapLeafScript) {
|
|
1003
1038
|
let sequence = undefined;
|
|
1004
1039
|
try {
|
|
1005
|
-
const scriptWithLeafVersion =
|
|
1040
|
+
const scriptWithLeafVersion = tapLeafScript[1];
|
|
1006
1041
|
const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1042
|
+
try {
|
|
1043
|
+
const params = CSVMultisigTapscript.decode(script).params;
|
|
1044
|
+
sequence = bip68.encode(params.timelock.type === "blocks"
|
|
1045
|
+
? { blocks: Number(params.timelock.value) }
|
|
1046
|
+
: { seconds: Number(params.timelock.value) });
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
const params = CLTVMultisigTapscript.decode(script).params;
|
|
1050
|
+
sequence = Number(params.absoluteTimelock);
|
|
1051
|
+
}
|
|
1011
1052
|
}
|
|
1012
1053
|
catch { }
|
|
1013
1054
|
return sequence;
|