@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
|
@@ -33,7 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.Wallet = void 0;
|
|
36
|
+
exports.Wallet = exports.ReadonlyWallet = void 0;
|
|
37
|
+
exports.getSequence = getSequence;
|
|
37
38
|
exports.waitForIncomingFunds = waitForIncomingFunds;
|
|
38
39
|
const base_1 = require("@scure/base");
|
|
39
40
|
const bip68 = __importStar(require("bip68"));
|
|
@@ -56,74 +57,40 @@ const vtxo_manager_1 = require("./vtxo-manager");
|
|
|
56
57
|
const arknote_1 = require("../arknote");
|
|
57
58
|
const intent_1 = require("../intent");
|
|
58
59
|
const indexer_1 = require("../providers/indexer");
|
|
59
|
-
const txTree_1 = require("../tree/txTree");
|
|
60
60
|
const unknownFields_1 = require("../utils/unknownFields");
|
|
61
61
|
const inMemory_1 = require("../storage/inMemory");
|
|
62
62
|
const walletRepository_1 = require("../repositories/walletRepository");
|
|
63
63
|
const contractRepository_1 = require("../repositories/contractRepository");
|
|
64
64
|
const utils_1 = require("./utils");
|
|
65
65
|
const errors_1 = require("../providers/errors");
|
|
66
|
+
const batch_1 = require("./batch");
|
|
66
67
|
/**
|
|
67
|
-
*
|
|
68
|
-
* The wallet does not store any data locally and relies on Ark and onchain
|
|
69
|
-
* providers to fetch UTXOs and VTXOs.
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* ```typescript
|
|
73
|
-
* // Create a wallet with URL configuration
|
|
74
|
-
* const wallet = await Wallet.create({
|
|
75
|
-
* identity: SingleKey.fromHex('your_private_key'),
|
|
76
|
-
* arkServerUrl: 'https://ark.example.com',
|
|
77
|
-
* esploraUrl: 'https://mempool.space/api'
|
|
78
|
-
* });
|
|
79
|
-
*
|
|
80
|
-
* // Or with custom provider instances (e.g., for Expo/React Native)
|
|
81
|
-
* const wallet = await Wallet.create({
|
|
82
|
-
* identity: SingleKey.fromHex('your_private_key'),
|
|
83
|
-
* arkProvider: new ExpoArkProvider('https://ark.example.com'),
|
|
84
|
-
* indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
|
|
85
|
-
* esploraUrl: 'https://mempool.space/api'
|
|
86
|
-
* });
|
|
87
|
-
*
|
|
88
|
-
* // Get addresses
|
|
89
|
-
* const arkAddress = await wallet.getAddress();
|
|
90
|
-
* const boardingAddress = await wallet.getBoardingAddress();
|
|
91
|
-
*
|
|
92
|
-
* // Send bitcoin
|
|
93
|
-
* const txid = await wallet.sendBitcoin({
|
|
94
|
-
* address: 'tb1...',
|
|
95
|
-
* amount: 50000
|
|
96
|
-
* });
|
|
97
|
-
* ```
|
|
68
|
+
* Type guard function to check if an identity has a toReadonly method.
|
|
98
69
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
70
|
+
function hasToReadonly(identity) {
|
|
71
|
+
return (typeof identity === "object" &&
|
|
72
|
+
identity !== null &&
|
|
73
|
+
"toReadonly" in identity &&
|
|
74
|
+
typeof identity.toReadonly === "function");
|
|
75
|
+
}
|
|
76
|
+
class ReadonlyWallet {
|
|
77
|
+
constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository) {
|
|
101
78
|
this.identity = identity;
|
|
102
79
|
this.network = network;
|
|
103
|
-
this.networkName = networkName;
|
|
104
80
|
this.onchainProvider = onchainProvider;
|
|
105
|
-
this.arkProvider = arkProvider;
|
|
106
81
|
this.indexerProvider = indexerProvider;
|
|
107
82
|
this.arkServerPublicKey = arkServerPublicKey;
|
|
108
83
|
this.offchainTapscript = offchainTapscript;
|
|
109
84
|
this.boardingTapscript = boardingTapscript;
|
|
110
|
-
this.serverUnrollScript = serverUnrollScript;
|
|
111
|
-
this.forfeitOutputScript = forfeitOutputScript;
|
|
112
|
-
this.forfeitPubkey = forfeitPubkey;
|
|
113
85
|
this.dustAmount = dustAmount;
|
|
114
86
|
this.walletRepository = walletRepository;
|
|
115
87
|
this.contractRepository = contractRepository;
|
|
116
|
-
this.renewalConfig = {
|
|
117
|
-
enabled: renewalConfig?.enabled ?? false,
|
|
118
|
-
...vtxo_manager_1.DEFAULT_RENEWAL_CONFIG,
|
|
119
|
-
...renewalConfig,
|
|
120
|
-
};
|
|
121
88
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Protected helper to set up shared wallet configuration.
|
|
91
|
+
* Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
|
|
92
|
+
*/
|
|
93
|
+
static async setupWalletConfig(config, pubkey) {
|
|
127
94
|
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
128
95
|
const arkProvider = config.arkProvider ||
|
|
129
96
|
(() => {
|
|
@@ -187,25 +154,32 @@ class Wallet {
|
|
|
187
154
|
});
|
|
188
155
|
// Save tapscripts
|
|
189
156
|
const offchainTapscript = bareVtxoTapscript;
|
|
190
|
-
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
191
|
-
let serverUnrollScript;
|
|
192
|
-
try {
|
|
193
|
-
const raw = base_1.hex.decode(info.checkpointTapscript);
|
|
194
|
-
serverUnrollScript = tapscript_1.CSVMultisigTapscript.decode(raw);
|
|
195
|
-
}
|
|
196
|
-
catch (e) {
|
|
197
|
-
throw new Error("Invalid checkpointTapscript from server");
|
|
198
|
-
}
|
|
199
|
-
// parse the server forfeit address
|
|
200
|
-
// server is expecting funds to be sent to this address
|
|
201
|
-
const forfeitPubkey = base_1.hex.decode(info.forfeitPubkey).slice(1);
|
|
202
|
-
const forfeitAddress = (0, btc_signer_1.Address)(network).decode(info.forfeitAddress);
|
|
203
|
-
const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
|
|
204
157
|
// Set up storage and repositories
|
|
205
158
|
const storage = config.storage || new inMemory_1.InMemoryStorageAdapter();
|
|
206
159
|
const walletRepository = new walletRepository_1.WalletRepositoryImpl(storage);
|
|
207
160
|
const contractRepository = new contractRepository_1.ContractRepositoryImpl(storage);
|
|
208
|
-
return
|
|
161
|
+
return {
|
|
162
|
+
arkProvider,
|
|
163
|
+
indexerProvider,
|
|
164
|
+
onchainProvider,
|
|
165
|
+
network,
|
|
166
|
+
networkName: info.network,
|
|
167
|
+
serverPubKey,
|
|
168
|
+
offchainTapscript,
|
|
169
|
+
boardingTapscript,
|
|
170
|
+
dustAmount: info.dust,
|
|
171
|
+
walletRepository,
|
|
172
|
+
contractRepository,
|
|
173
|
+
info,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
static async create(config) {
|
|
177
|
+
const pubkey = await config.identity.xOnlyPublicKey();
|
|
178
|
+
if (!pubkey) {
|
|
179
|
+
throw new Error("Invalid configured public key");
|
|
180
|
+
}
|
|
181
|
+
const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
182
|
+
return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository);
|
|
209
183
|
}
|
|
210
184
|
get arkAddress() {
|
|
211
185
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
@@ -280,7 +254,7 @@ class Wallet {
|
|
|
280
254
|
let vtxos = allVtxos.filter(_1.isSpendable);
|
|
281
255
|
// all recoverable vtxos are spendable by definition
|
|
282
256
|
if (!filter.withRecoverable) {
|
|
283
|
-
vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo));
|
|
257
|
+
vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo) && !(0, _1.isExpired)(vtxo));
|
|
284
258
|
}
|
|
285
259
|
if (filter.withUnrolled) {
|
|
286
260
|
const spentVtxos = allVtxos.filter((vtxo) => !(0, _1.isSpendable)(vtxo));
|
|
@@ -289,9 +263,6 @@ class Wallet {
|
|
|
289
263
|
return vtxos;
|
|
290
264
|
}
|
|
291
265
|
async getTransactionHistory() {
|
|
292
|
-
if (!this.indexerProvider) {
|
|
293
|
-
return [];
|
|
294
|
-
}
|
|
295
266
|
const response = await this.indexerProvider.getVtxos({
|
|
296
267
|
scripts: [base_1.hex.encode(this.offchainTapscript.pkScript)],
|
|
297
268
|
});
|
|
@@ -395,6 +366,179 @@ class Wallet {
|
|
|
395
366
|
await this.walletRepository.saveUtxos(boardingAddress, utxos);
|
|
396
367
|
return utxos;
|
|
397
368
|
}
|
|
369
|
+
async notifyIncomingFunds(eventCallback) {
|
|
370
|
+
const arkAddress = await this.getAddress();
|
|
371
|
+
const boardingAddress = await this.getBoardingAddress();
|
|
372
|
+
let onchainStopFunc;
|
|
373
|
+
let indexerStopFunc;
|
|
374
|
+
if (this.onchainProvider && boardingAddress) {
|
|
375
|
+
const findVoutOnTx = (tx) => {
|
|
376
|
+
return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
377
|
+
};
|
|
378
|
+
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
379
|
+
// find all utxos belonging to our boarding address
|
|
380
|
+
const coins = txs
|
|
381
|
+
// filter txs where address is in output
|
|
382
|
+
.filter((tx) => findVoutOnTx(tx) !== -1)
|
|
383
|
+
// return utxo as Coin
|
|
384
|
+
.map((tx) => {
|
|
385
|
+
const { txid, status } = tx;
|
|
386
|
+
const vout = findVoutOnTx(tx);
|
|
387
|
+
const value = Number(tx.vout[vout].value);
|
|
388
|
+
return { txid, vout, value, status };
|
|
389
|
+
});
|
|
390
|
+
// and notify via callback
|
|
391
|
+
eventCallback({
|
|
392
|
+
type: "utxo",
|
|
393
|
+
coins,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (this.indexerProvider && arkAddress) {
|
|
398
|
+
const offchainScript = this.offchainTapscript;
|
|
399
|
+
const subscriptionId = await this.indexerProvider.subscribeForScripts([
|
|
400
|
+
base_1.hex.encode(offchainScript.pkScript),
|
|
401
|
+
]);
|
|
402
|
+
const abortController = new AbortController();
|
|
403
|
+
const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
|
|
404
|
+
indexerStopFunc = async () => {
|
|
405
|
+
abortController.abort();
|
|
406
|
+
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
407
|
+
};
|
|
408
|
+
// Handle subscription updates asynchronously without blocking
|
|
409
|
+
(async () => {
|
|
410
|
+
try {
|
|
411
|
+
for await (const update of subscription) {
|
|
412
|
+
if (update.newVtxos?.length > 0 ||
|
|
413
|
+
update.spentVtxos?.length > 0) {
|
|
414
|
+
eventCallback({
|
|
415
|
+
type: "vtxo",
|
|
416
|
+
newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
|
|
417
|
+
spentVtxos: update.spentVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
console.error("Subscription error:", error);
|
|
424
|
+
}
|
|
425
|
+
})();
|
|
426
|
+
}
|
|
427
|
+
const stopFunc = () => {
|
|
428
|
+
onchainStopFunc?.();
|
|
429
|
+
indexerStopFunc?.();
|
|
430
|
+
};
|
|
431
|
+
return stopFunc;
|
|
432
|
+
}
|
|
433
|
+
async fetchPendingTxs() {
|
|
434
|
+
// get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
|
|
435
|
+
const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
|
|
436
|
+
let { vtxos } = await this.indexerProvider.getVtxos({
|
|
437
|
+
scripts,
|
|
438
|
+
});
|
|
439
|
+
return vtxos
|
|
440
|
+
.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
441
|
+
vtxo.virtualStatus.state !== "settled" &&
|
|
442
|
+
vtxo.arkTxId !== undefined)
|
|
443
|
+
.map((_) => _.arkTxId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
exports.ReadonlyWallet = ReadonlyWallet;
|
|
447
|
+
/**
|
|
448
|
+
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|
|
449
|
+
* The wallet does not store any data locally and relies on Ark and onchain
|
|
450
|
+
* providers to fetch UTXOs and VTXOs.
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```typescript
|
|
454
|
+
* // Create a wallet with URL configuration
|
|
455
|
+
* const wallet = await Wallet.create({
|
|
456
|
+
* identity: SingleKey.fromHex('your_private_key'),
|
|
457
|
+
* arkServerUrl: 'https://ark.example.com',
|
|
458
|
+
* esploraUrl: 'https://mempool.space/api'
|
|
459
|
+
* });
|
|
460
|
+
*
|
|
461
|
+
* // Or with custom provider instances (e.g., for Expo/React Native)
|
|
462
|
+
* const wallet = await Wallet.create({
|
|
463
|
+
* identity: SingleKey.fromHex('your_private_key'),
|
|
464
|
+
* arkProvider: new ExpoArkProvider('https://ark.example.com'),
|
|
465
|
+
* indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
|
|
466
|
+
* esploraUrl: 'https://mempool.space/api'
|
|
467
|
+
* });
|
|
468
|
+
*
|
|
469
|
+
* // Get addresses
|
|
470
|
+
* const arkAddress = await wallet.getAddress();
|
|
471
|
+
* const boardingAddress = await wallet.getBoardingAddress();
|
|
472
|
+
*
|
|
473
|
+
* // Send bitcoin
|
|
474
|
+
* const txid = await wallet.sendBitcoin({
|
|
475
|
+
* address: 'tb1...',
|
|
476
|
+
* amount: 50000
|
|
477
|
+
* });
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
class Wallet extends ReadonlyWallet {
|
|
481
|
+
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
|
|
482
|
+
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository);
|
|
483
|
+
this.networkName = networkName;
|
|
484
|
+
this.arkProvider = arkProvider;
|
|
485
|
+
this.serverUnrollScript = serverUnrollScript;
|
|
486
|
+
this.forfeitOutputScript = forfeitOutputScript;
|
|
487
|
+
this.forfeitPubkey = forfeitPubkey;
|
|
488
|
+
this.identity = identity;
|
|
489
|
+
this.renewalConfig = {
|
|
490
|
+
enabled: renewalConfig?.enabled ?? false,
|
|
491
|
+
...vtxo_manager_1.DEFAULT_RENEWAL_CONFIG,
|
|
492
|
+
...renewalConfig,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
static async create(config) {
|
|
496
|
+
const pubkey = await config.identity.xOnlyPublicKey();
|
|
497
|
+
if (!pubkey) {
|
|
498
|
+
throw new Error("Invalid configured public key");
|
|
499
|
+
}
|
|
500
|
+
const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
501
|
+
// Compute Wallet-specific forfeit and unroll scripts
|
|
502
|
+
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
503
|
+
let serverUnrollScript;
|
|
504
|
+
try {
|
|
505
|
+
const raw = base_1.hex.decode(setup.info.checkpointTapscript);
|
|
506
|
+
serverUnrollScript = tapscript_1.CSVMultisigTapscript.decode(raw);
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
throw new Error("Invalid checkpointTapscript from server");
|
|
510
|
+
}
|
|
511
|
+
// parse the server forfeit address
|
|
512
|
+
// server is expecting funds to be sent to this address
|
|
513
|
+
const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
514
|
+
const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
|
|
515
|
+
const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
|
|
516
|
+
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);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Convert this wallet to a readonly wallet.
|
|
520
|
+
*
|
|
521
|
+
* @returns A readonly wallet with the same configuration but readonly identity
|
|
522
|
+
* @example
|
|
523
|
+
* ```typescript
|
|
524
|
+
* const wallet = await Wallet.create({ identity: SingleKey.fromHex('...'), ... });
|
|
525
|
+
* const readonlyWallet = await wallet.toReadonly();
|
|
526
|
+
*
|
|
527
|
+
* // Can query balance and addresses
|
|
528
|
+
* const balance = await readonlyWallet.getBalance();
|
|
529
|
+
* const address = await readonlyWallet.getAddress();
|
|
530
|
+
*
|
|
531
|
+
* // But cannot send transactions (type error)
|
|
532
|
+
* // readonlyWallet.sendBitcoin(...); // TypeScript error
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
async toReadonly() {
|
|
536
|
+
// Check if the identity has a toReadonly method using type guard
|
|
537
|
+
const readonlyIdentity = hasToReadonly(this.identity)
|
|
538
|
+
? await this.identity.toReadonly()
|
|
539
|
+
: this.identity; // Identity extends ReadonlyIdentity, so this is safe
|
|
540
|
+
return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository);
|
|
541
|
+
}
|
|
398
542
|
async sendBitcoin(params) {
|
|
399
543
|
if (params.amount <= 0) {
|
|
400
544
|
throw new Error("Amount must be positive");
|
|
@@ -481,7 +625,7 @@ class Wallet {
|
|
|
481
625
|
vout: outputs.length - 1,
|
|
482
626
|
createdAt: new Date(createdAt),
|
|
483
627
|
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
484
|
-
intentTapLeafScript: this.offchainTapscript.
|
|
628
|
+
intentTapLeafScript: this.offchainTapscript.forfeit(),
|
|
485
629
|
isUnrolled: false,
|
|
486
630
|
isSpent: false,
|
|
487
631
|
tapTree: this.offchainTapscript.encode(),
|
|
@@ -591,285 +735,31 @@ class Wallet {
|
|
|
591
735
|
this.makeDeleteIntentSignature(params.inputs),
|
|
592
736
|
]);
|
|
593
737
|
const intentId = await this.safeRegisterIntent(intent);
|
|
738
|
+
const topics = [
|
|
739
|
+
...signingPublicKeys,
|
|
740
|
+
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
741
|
+
];
|
|
742
|
+
const handler = this.createBatchHandler(intentId, params.inputs, session);
|
|
594
743
|
const abortController = new AbortController();
|
|
595
|
-
// listen to settlement events
|
|
596
744
|
try {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
let sweepTapTreeRoot;
|
|
606
|
-
const vtxoChunks = [];
|
|
607
|
-
const connectorsChunks = [];
|
|
608
|
-
let vtxoGraph;
|
|
609
|
-
let connectorsGraph;
|
|
610
|
-
for await (const event of settlementStream) {
|
|
611
|
-
if (eventCallback) {
|
|
612
|
-
eventCallback(event);
|
|
613
|
-
}
|
|
614
|
-
switch (event.type) {
|
|
615
|
-
// the settlement failed
|
|
616
|
-
case ark_1.SettlementEventType.BatchFailed:
|
|
617
|
-
throw new Error(event.reason);
|
|
618
|
-
case ark_1.SettlementEventType.BatchStarted:
|
|
619
|
-
if (step !== undefined) {
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
const res = await this.handleBatchStartedEvent(event, intentId, this.forfeitPubkey, this.forfeitOutputScript);
|
|
623
|
-
if (!res.skip) {
|
|
624
|
-
step = event.type;
|
|
625
|
-
sweepTapTreeRoot = res.sweepTapTreeRoot;
|
|
626
|
-
batchId = res.roundId;
|
|
627
|
-
if (!hasOffchainOutputs) {
|
|
628
|
-
// if there are no offchain outputs, we don't have to handle musig2 tree signatures
|
|
629
|
-
// we can directly advance to the finalization step
|
|
630
|
-
step = ark_1.SettlementEventType.TreeNonces;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
break;
|
|
634
|
-
case ark_1.SettlementEventType.TreeTx:
|
|
635
|
-
if (step !== ark_1.SettlementEventType.BatchStarted &&
|
|
636
|
-
step !== ark_1.SettlementEventType.TreeNonces) {
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
// index 0 = vtxo tree
|
|
640
|
-
if (event.batchIndex === 0) {
|
|
641
|
-
vtxoChunks.push(event.chunk);
|
|
642
|
-
// index 1 = connectors tree
|
|
643
|
-
}
|
|
644
|
-
else if (event.batchIndex === 1) {
|
|
645
|
-
connectorsChunks.push(event.chunk);
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
throw new Error(`Invalid batch index: ${event.batchIndex}`);
|
|
649
|
-
}
|
|
650
|
-
break;
|
|
651
|
-
case ark_1.SettlementEventType.TreeSignature:
|
|
652
|
-
if (step !== ark_1.SettlementEventType.TreeNonces) {
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
if (!hasOffchainOutputs) {
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
if (!vtxoGraph) {
|
|
659
|
-
throw new Error("Vtxo graph not set, something went wrong");
|
|
660
|
-
}
|
|
661
|
-
// index 0 = vtxo graph
|
|
662
|
-
if (event.batchIndex === 0) {
|
|
663
|
-
const tapKeySig = base_1.hex.decode(event.signature);
|
|
664
|
-
vtxoGraph.update(event.txid, (tx) => {
|
|
665
|
-
tx.updateInput(0, {
|
|
666
|
-
tapKeySig,
|
|
667
|
-
});
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
break;
|
|
671
|
-
// the server has started the signing process of the vtxo tree transactions
|
|
672
|
-
// the server expects the partial musig2 nonces for each tx
|
|
673
|
-
case ark_1.SettlementEventType.TreeSigningStarted:
|
|
674
|
-
if (step !== ark_1.SettlementEventType.BatchStarted) {
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (hasOffchainOutputs) {
|
|
678
|
-
if (!session) {
|
|
679
|
-
throw new Error("Signing session not set");
|
|
680
|
-
}
|
|
681
|
-
if (!sweepTapTreeRoot) {
|
|
682
|
-
throw new Error("Sweep tap tree root not set");
|
|
683
|
-
}
|
|
684
|
-
if (vtxoChunks.length === 0) {
|
|
685
|
-
throw new Error("unsigned vtxo graph not received");
|
|
686
|
-
}
|
|
687
|
-
vtxoGraph = txTree_1.TxTree.create(vtxoChunks);
|
|
688
|
-
await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph);
|
|
689
|
-
}
|
|
690
|
-
step = event.type;
|
|
691
|
-
break;
|
|
692
|
-
// the musig2 nonces of the vtxo tree transactions are generated
|
|
693
|
-
// the server expects now the partial musig2 signatures
|
|
694
|
-
case ark_1.SettlementEventType.TreeNonces:
|
|
695
|
-
if (step !== ark_1.SettlementEventType.TreeSigningStarted) {
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (hasOffchainOutputs) {
|
|
699
|
-
if (!session) {
|
|
700
|
-
throw new Error("Signing session not set");
|
|
701
|
-
}
|
|
702
|
-
const signed = await this.handleSettlementTreeNoncesEvent(event, session);
|
|
703
|
-
if (signed) {
|
|
704
|
-
step = event.type;
|
|
705
|
-
}
|
|
706
|
-
break;
|
|
707
|
-
}
|
|
708
|
-
step = event.type;
|
|
709
|
-
break;
|
|
710
|
-
// the vtxo tree is signed, craft, sign and submit forfeit transactions
|
|
711
|
-
// if any boarding utxos are involved, the settlement tx is also signed
|
|
712
|
-
case ark_1.SettlementEventType.BatchFinalization:
|
|
713
|
-
if (step !== ark_1.SettlementEventType.TreeNonces) {
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
if (!this.forfeitOutputScript) {
|
|
717
|
-
throw new Error("Forfeit output script not set");
|
|
718
|
-
}
|
|
719
|
-
if (connectorsChunks.length > 0) {
|
|
720
|
-
connectorsGraph = txTree_1.TxTree.create(connectorsChunks);
|
|
721
|
-
(0, validation_1.validateConnectorsTxGraph)(event.commitmentTx, connectorsGraph);
|
|
722
|
-
}
|
|
723
|
-
await this.handleSettlementFinalizationEvent(event, params.inputs, this.forfeitOutputScript, connectorsGraph);
|
|
724
|
-
step = event.type;
|
|
725
|
-
break;
|
|
726
|
-
// the settlement is done, last event to be received
|
|
727
|
-
case ark_1.SettlementEventType.BatchFinalized:
|
|
728
|
-
if (step !== ark_1.SettlementEventType.BatchFinalization) {
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
if (event.id === batchId) {
|
|
732
|
-
abortController.abort();
|
|
733
|
-
return event.commitmentTxid;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
745
|
+
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
746
|
+
return await batch_1.Batch.join(stream, handler, {
|
|
747
|
+
abortController,
|
|
748
|
+
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
749
|
+
eventCallback: eventCallback
|
|
750
|
+
? (event) => Promise.resolve(eventCallback(event))
|
|
751
|
+
: undefined,
|
|
752
|
+
});
|
|
737
753
|
}
|
|
738
754
|
catch (error) {
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
try {
|
|
742
|
-
// delete the intent to not be stuck in the queue
|
|
743
|
-
await this.arkProvider.deleteIntent(deleteIntent);
|
|
744
|
-
}
|
|
745
|
-
catch (error) {
|
|
746
|
-
console.error("failed to delete intent: ", error);
|
|
747
|
-
}
|
|
755
|
+
// delete the intent to not be stuck in the queue
|
|
756
|
+
await this.arkProvider.deleteIntent(deleteIntent).catch(() => { });
|
|
748
757
|
throw error;
|
|
749
758
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const arkAddress = await this.getAddress();
|
|
754
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
755
|
-
let onchainStopFunc;
|
|
756
|
-
let indexerStopFunc;
|
|
757
|
-
if (this.onchainProvider && boardingAddress) {
|
|
758
|
-
const findVoutOnTx = (tx) => {
|
|
759
|
-
return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
760
|
-
};
|
|
761
|
-
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
762
|
-
// find all utxos belonging to our boarding address
|
|
763
|
-
const coins = txs
|
|
764
|
-
// filter txs where address is in output
|
|
765
|
-
.filter((tx) => findVoutOnTx(tx) !== -1)
|
|
766
|
-
// return utxo as Coin
|
|
767
|
-
.map((tx) => {
|
|
768
|
-
const { txid, status } = tx;
|
|
769
|
-
const vout = findVoutOnTx(tx);
|
|
770
|
-
const value = Number(tx.vout[vout].value);
|
|
771
|
-
return { txid, vout, value, status };
|
|
772
|
-
});
|
|
773
|
-
// and notify via callback
|
|
774
|
-
eventCallback({
|
|
775
|
-
type: "utxo",
|
|
776
|
-
coins,
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
if (this.indexerProvider && arkAddress) {
|
|
781
|
-
const offchainScript = this.offchainTapscript;
|
|
782
|
-
const subscriptionId = await this.indexerProvider.subscribeForScripts([
|
|
783
|
-
base_1.hex.encode(offchainScript.pkScript),
|
|
784
|
-
]);
|
|
785
|
-
const abortController = new AbortController();
|
|
786
|
-
const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
|
|
787
|
-
indexerStopFunc = async () => {
|
|
788
|
-
abortController.abort();
|
|
789
|
-
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
790
|
-
};
|
|
791
|
-
// Handle subscription updates asynchronously without blocking
|
|
792
|
-
(async () => {
|
|
793
|
-
try {
|
|
794
|
-
for await (const update of subscription) {
|
|
795
|
-
if (update.newVtxos?.length > 0 ||
|
|
796
|
-
update.spentVtxos?.length > 0) {
|
|
797
|
-
eventCallback({
|
|
798
|
-
type: "vtxo",
|
|
799
|
-
newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
|
|
800
|
-
spentVtxos: update.spentVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
catch (error) {
|
|
806
|
-
console.error("Subscription error:", error);
|
|
807
|
-
}
|
|
808
|
-
})();
|
|
809
|
-
}
|
|
810
|
-
const stopFunc = () => {
|
|
811
|
-
onchainStopFunc?.();
|
|
812
|
-
indexerStopFunc?.();
|
|
813
|
-
};
|
|
814
|
-
return stopFunc;
|
|
815
|
-
}
|
|
816
|
-
async handleBatchStartedEvent(event, intentId, forfeitPubKey, forfeitOutputScript) {
|
|
817
|
-
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
818
|
-
const intentIdHash = (0, utils_js_1.sha256)(utf8IntentId);
|
|
819
|
-
const intentIdHashStr = base_1.hex.encode(intentIdHash);
|
|
820
|
-
let skip = true;
|
|
821
|
-
// check if our intent ID hash matches any in the event
|
|
822
|
-
for (const idHash of event.intentIdHashes) {
|
|
823
|
-
if (idHash === intentIdHashStr) {
|
|
824
|
-
if (!this.arkProvider) {
|
|
825
|
-
throw new Error("Ark provider not configured");
|
|
826
|
-
}
|
|
827
|
-
await this.arkProvider.confirmRegistration(intentId);
|
|
828
|
-
skip = false;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (skip) {
|
|
832
|
-
return { skip };
|
|
833
|
-
}
|
|
834
|
-
const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
|
|
835
|
-
timelock: {
|
|
836
|
-
value: event.batchExpiry,
|
|
837
|
-
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
838
|
-
},
|
|
839
|
-
pubkeys: [forfeitPubKey],
|
|
840
|
-
}).script;
|
|
841
|
-
const sweepTapTreeRoot = (0, payment_js_1.tapLeafHash)(sweepTapscript);
|
|
842
|
-
return {
|
|
843
|
-
roundId: event.id,
|
|
844
|
-
sweepTapTreeRoot,
|
|
845
|
-
forfeitOutputScript,
|
|
846
|
-
skip: false,
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
// validates the vtxo tree, creates a signing session and generates the musig2 nonces
|
|
850
|
-
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph) {
|
|
851
|
-
// validate the unsigned vtxo tree
|
|
852
|
-
const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
|
|
853
|
-
(0, validation_1.validateVtxoTxGraph)(vtxoGraph, commitmentTx, sweepTapTreeRoot);
|
|
854
|
-
// TODO check if our registered outputs are in the vtxo tree
|
|
855
|
-
const sharedOutput = commitmentTx.getOutput(0);
|
|
856
|
-
if (!sharedOutput?.amount) {
|
|
857
|
-
throw new Error("Shared output not found");
|
|
759
|
+
finally {
|
|
760
|
+
// close the stream
|
|
761
|
+
abortController.abort();
|
|
858
762
|
}
|
|
859
|
-
session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
|
|
860
|
-
const pubkey = base_1.hex.encode(await session.getPublicKey());
|
|
861
|
-
const nonces = await session.getNonces();
|
|
862
|
-
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
863
|
-
}
|
|
864
|
-
async handleSettlementTreeNoncesEvent(event, session) {
|
|
865
|
-
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
866
|
-
// wait to receive and aggregate all nonces before sending signatures
|
|
867
|
-
if (!hasAllNonces)
|
|
868
|
-
return false;
|
|
869
|
-
const signatures = await session.sign();
|
|
870
|
-
const pubkey = base_1.hex.encode(await session.getPublicKey());
|
|
871
|
-
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
872
|
-
return true;
|
|
873
763
|
}
|
|
874
764
|
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
875
765
|
// the signed forfeits transactions to submit
|
|
@@ -959,9 +849,98 @@ class Wallet {
|
|
|
959
849
|
: undefined);
|
|
960
850
|
}
|
|
961
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* @implements Batch.Handler interface.
|
|
854
|
+
* @param intentId - The intent ID.
|
|
855
|
+
* @param inputs - The inputs of the intent.
|
|
856
|
+
* @param session - The musig2 signing session, if not provided, the signing will be skipped.
|
|
857
|
+
*/
|
|
858
|
+
createBatchHandler(intentId, inputs, session) {
|
|
859
|
+
let sweepTapTreeRoot;
|
|
860
|
+
return {
|
|
861
|
+
onBatchStarted: async (event) => {
|
|
862
|
+
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
863
|
+
const intentIdHash = (0, utils_js_1.sha256)(utf8IntentId);
|
|
864
|
+
const intentIdHashStr = base_1.hex.encode(intentIdHash);
|
|
865
|
+
let skip = true;
|
|
866
|
+
// check if our intent ID hash matches any in the event
|
|
867
|
+
for (const idHash of event.intentIdHashes) {
|
|
868
|
+
if (idHash === intentIdHashStr) {
|
|
869
|
+
if (!this.arkProvider) {
|
|
870
|
+
throw new Error("Ark provider not configured");
|
|
871
|
+
}
|
|
872
|
+
await this.arkProvider.confirmRegistration(intentId);
|
|
873
|
+
skip = false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (skip) {
|
|
877
|
+
return { skip };
|
|
878
|
+
}
|
|
879
|
+
const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
|
|
880
|
+
timelock: {
|
|
881
|
+
value: event.batchExpiry,
|
|
882
|
+
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
883
|
+
},
|
|
884
|
+
pubkeys: [this.forfeitPubkey],
|
|
885
|
+
}).script;
|
|
886
|
+
sweepTapTreeRoot = (0, payment_js_1.tapLeafHash)(sweepTapscript);
|
|
887
|
+
return { skip: false };
|
|
888
|
+
},
|
|
889
|
+
onTreeSigningStarted: async (event, vtxoTree) => {
|
|
890
|
+
if (!session) {
|
|
891
|
+
return { skip: true };
|
|
892
|
+
}
|
|
893
|
+
if (!sweepTapTreeRoot) {
|
|
894
|
+
throw new Error("Sweep tap tree root not set");
|
|
895
|
+
}
|
|
896
|
+
const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
|
|
897
|
+
const signerPublicKey = await session.getPublicKey();
|
|
898
|
+
const xonlySignerPublicKey = signerPublicKey.subarray(1);
|
|
899
|
+
if (!xOnlyPublicKeys.includes(base_1.hex.encode(xonlySignerPublicKey))) {
|
|
900
|
+
// not a cosigner, skip the signing
|
|
901
|
+
return { skip: true };
|
|
902
|
+
}
|
|
903
|
+
// validate the unsigned vtxo tree
|
|
904
|
+
const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
|
|
905
|
+
(0, validation_1.validateVtxoTxGraph)(vtxoTree, commitmentTx, sweepTapTreeRoot);
|
|
906
|
+
// TODO check if our registered outputs are in the vtxo tree
|
|
907
|
+
const sharedOutput = commitmentTx.getOutput(0);
|
|
908
|
+
if (!sharedOutput?.amount) {
|
|
909
|
+
throw new Error("Shared output not found");
|
|
910
|
+
}
|
|
911
|
+
await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
|
|
912
|
+
const pubkey = base_1.hex.encode(await session.getPublicKey());
|
|
913
|
+
const nonces = await session.getNonces();
|
|
914
|
+
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
915
|
+
return { skip: false };
|
|
916
|
+
},
|
|
917
|
+
onTreeNonces: async (event) => {
|
|
918
|
+
if (!session) {
|
|
919
|
+
return { fullySigned: true }; // Signing complete (no signing needed)
|
|
920
|
+
}
|
|
921
|
+
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
922
|
+
// wait to receive and aggregate all nonces before sending signatures
|
|
923
|
+
if (!hasAllNonces)
|
|
924
|
+
return { fullySigned: false };
|
|
925
|
+
const signatures = await session.sign();
|
|
926
|
+
const pubkey = base_1.hex.encode(await session.getPublicKey());
|
|
927
|
+
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
928
|
+
return { fullySigned: true };
|
|
929
|
+
},
|
|
930
|
+
onBatchFinalization: async (event, _, connectorTree) => {
|
|
931
|
+
if (!this.forfeitOutputScript) {
|
|
932
|
+
throw new Error("Forfeit output script not set");
|
|
933
|
+
}
|
|
934
|
+
if (connectorTree) {
|
|
935
|
+
(0, validation_1.validateConnectorsTxGraph)(event.commitmentTx, connectorTree);
|
|
936
|
+
}
|
|
937
|
+
await this.handleSettlementFinalizationEvent(event, inputs, this.forfeitOutputScript, connectorTree);
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
962
941
|
async safeRegisterIntent(intent) {
|
|
963
942
|
try {
|
|
964
|
-
return this.arkProvider.registerIntent(intent);
|
|
943
|
+
return await this.arkProvider.registerIntent(intent);
|
|
965
944
|
}
|
|
966
945
|
catch (error) {
|
|
967
946
|
// catch the "already registered by another intent" error
|
|
@@ -989,12 +968,11 @@ class Wallet {
|
|
|
989
968
|
expire_at: 0,
|
|
990
969
|
cosigners_public_keys: cosignerPubKeys,
|
|
991
970
|
};
|
|
992
|
-
const
|
|
993
|
-
const proof = intent_1.Intent.create(encodedMessage, inputs, outputs);
|
|
971
|
+
const proof = intent_1.Intent.create(message, inputs, outputs);
|
|
994
972
|
const signedProof = await this.identity.sign(proof);
|
|
995
973
|
return {
|
|
996
974
|
proof: base_1.base64.encode(signedProof.toPSBT()),
|
|
997
|
-
message
|
|
975
|
+
message,
|
|
998
976
|
};
|
|
999
977
|
}
|
|
1000
978
|
async makeDeleteIntentSignature(coins) {
|
|
@@ -1003,19 +981,78 @@ class Wallet {
|
|
|
1003
981
|
type: "delete",
|
|
1004
982
|
expire_at: 0,
|
|
1005
983
|
};
|
|
1006
|
-
const
|
|
1007
|
-
const proof = intent_1.Intent.create(encodedMessage, inputs, []);
|
|
984
|
+
const proof = intent_1.Intent.create(message, inputs, []);
|
|
1008
985
|
const signedProof = await this.identity.sign(proof);
|
|
1009
986
|
return {
|
|
1010
987
|
proof: base_1.base64.encode(signedProof.toPSBT()),
|
|
1011
|
-
message
|
|
988
|
+
message,
|
|
1012
989
|
};
|
|
1013
990
|
}
|
|
991
|
+
async makeGetPendingTxIntentSignature(vtxos) {
|
|
992
|
+
const inputs = this.prepareIntentProofInputs(vtxos);
|
|
993
|
+
const message = {
|
|
994
|
+
type: "get-pending-tx",
|
|
995
|
+
expire_at: 0,
|
|
996
|
+
};
|
|
997
|
+
const proof = intent_1.Intent.create(message, inputs, []);
|
|
998
|
+
const signedProof = await this.identity.sign(proof);
|
|
999
|
+
return {
|
|
1000
|
+
proof: base_1.base64.encode(signedProof.toPSBT()),
|
|
1001
|
+
message,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Finalizes pending transactions by retrieving them from the server and finalizing each one.
|
|
1006
|
+
* @param vtxos - Optional list of VTXOs to use instead of retrieving them from the server
|
|
1007
|
+
* @returns Array of transaction IDs that were finalized
|
|
1008
|
+
*/
|
|
1009
|
+
async finalizePendingTxs(vtxos) {
|
|
1010
|
+
const MAX_INPUTS_PER_INTENT = 20;
|
|
1011
|
+
if (!vtxos || vtxos.length === 0) {
|
|
1012
|
+
// get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
|
|
1013
|
+
const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
|
|
1014
|
+
let { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1015
|
+
scripts,
|
|
1016
|
+
});
|
|
1017
|
+
fetchedVtxos = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
1018
|
+
vtxo.virtualStatus.state !== "settled");
|
|
1019
|
+
if (fetchedVtxos.length === 0) {
|
|
1020
|
+
return { finalized: [], pending: [] };
|
|
1021
|
+
}
|
|
1022
|
+
vtxos = fetchedVtxos.map((v) => (0, utils_1.extendVirtualCoin)(this, v));
|
|
1023
|
+
}
|
|
1024
|
+
const finalized = [];
|
|
1025
|
+
const pending = [];
|
|
1026
|
+
for (let i = 0; i < vtxos.length; i += MAX_INPUTS_PER_INTENT) {
|
|
1027
|
+
const batch = vtxos.slice(i, i + MAX_INPUTS_PER_INTENT);
|
|
1028
|
+
const intent = await this.makeGetPendingTxIntentSignature(batch);
|
|
1029
|
+
const pendingTxs = await this.arkProvider.getPendingTxs(intent);
|
|
1030
|
+
// finalize each transaction by signing the checkpoints
|
|
1031
|
+
for (const pendingTx of pendingTxs) {
|
|
1032
|
+
pending.push(pendingTx.arkTxid);
|
|
1033
|
+
try {
|
|
1034
|
+
// sign the checkpoints
|
|
1035
|
+
const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
|
|
1036
|
+
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
1037
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
1038
|
+
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
1039
|
+
}));
|
|
1040
|
+
await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
|
|
1041
|
+
finalized.push(pendingTx.arkTxid);
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
console.error(`Failed to finalize transaction ${pendingTx.arkTxid}:`, error);
|
|
1045
|
+
// continue with other transactions even if one fails
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return { finalized, pending };
|
|
1050
|
+
}
|
|
1014
1051
|
prepareIntentProofInputs(coins) {
|
|
1015
1052
|
const inputs = [];
|
|
1016
1053
|
for (const input of coins) {
|
|
1017
1054
|
const vtxoScript = base_2.VtxoScript.decode(input.tapTree);
|
|
1018
|
-
const sequence = getSequence(input);
|
|
1055
|
+
const sequence = getSequence(input.intentTapLeafScript);
|
|
1019
1056
|
const unknown = [unknownFields_1.VtxoTaprootTree.encode(input.tapTree)];
|
|
1020
1057
|
if (input.extraWitness) {
|
|
1021
1058
|
unknown.push(unknownFields_1.ConditionWitness.encode(input.extraWitness));
|
|
@@ -1037,15 +1074,21 @@ class Wallet {
|
|
|
1037
1074
|
}
|
|
1038
1075
|
exports.Wallet = Wallet;
|
|
1039
1076
|
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
1040
|
-
function getSequence(
|
|
1077
|
+
function getSequence(tapLeafScript) {
|
|
1041
1078
|
let sequence = undefined;
|
|
1042
1079
|
try {
|
|
1043
|
-
const scriptWithLeafVersion =
|
|
1080
|
+
const scriptWithLeafVersion = tapLeafScript[1];
|
|
1044
1081
|
const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1082
|
+
try {
|
|
1083
|
+
const params = tapscript_1.CSVMultisigTapscript.decode(script).params;
|
|
1084
|
+
sequence = bip68.encode(params.timelock.type === "blocks"
|
|
1085
|
+
? { blocks: Number(params.timelock.value) }
|
|
1086
|
+
: { seconds: Number(params.timelock.value) });
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
const params = tapscript_1.CLTVMultisigTapscript.decode(script).params;
|
|
1090
|
+
sequence = Number(params.absoluteTimelock);
|
|
1091
|
+
}
|
|
1049
1092
|
}
|
|
1050
1093
|
catch { }
|
|
1051
1094
|
return sequence;
|