@arkade-os/sdk 0.3.8 → 0.3.10
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 +15 -5
- 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 +163 -72
- package/dist/cjs/wallet/utils.js +2 -2
- package/dist/cjs/wallet/vtxo-manager.js +5 -0
- package/dist/cjs/wallet/wallet.js +358 -360
- 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 +15 -5
- 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 +165 -74
- package/dist/esm/wallet/utils.js +2 -2
- package/dist/esm/wallet/vtxo-manager.js +6 -1
- package/dist/esm/wallet/wallet.js +359 -363
- 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 +76 -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 +87 -36
- package/package.json +1 -1
|
@@ -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");
|
|
@@ -369,7 +511,23 @@ export class Wallet {
|
|
|
369
511
|
const virtualCoins = await this.getVirtualCoins({
|
|
370
512
|
withRecoverable: false,
|
|
371
513
|
});
|
|
372
|
-
|
|
514
|
+
let selected;
|
|
515
|
+
if (params.selectedVtxos) {
|
|
516
|
+
const selectedVtxoSum = params.selectedVtxos
|
|
517
|
+
.map((v) => v.value)
|
|
518
|
+
.reduce((a, b) => a + b, 0);
|
|
519
|
+
if (selectedVtxoSum < params.amount) {
|
|
520
|
+
throw new Error("Selected VTXOs do not cover specified amount");
|
|
521
|
+
}
|
|
522
|
+
const changeAmount = selectedVtxoSum - params.amount;
|
|
523
|
+
selected = {
|
|
524
|
+
inputs: params.selectedVtxos,
|
|
525
|
+
changeAmount: BigInt(changeAmount),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
selected = selectVirtualCoins(virtualCoins, params.amount);
|
|
530
|
+
}
|
|
373
531
|
const selectedLeaf = this.offchainTapscript.forfeit();
|
|
374
532
|
if (!selectedLeaf) {
|
|
375
533
|
throw new Error("Selected leaf not found");
|
|
@@ -444,7 +602,7 @@ export class Wallet {
|
|
|
444
602
|
vout: outputs.length - 1,
|
|
445
603
|
createdAt: new Date(createdAt),
|
|
446
604
|
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
447
|
-
intentTapLeafScript: this.offchainTapscript.
|
|
605
|
+
intentTapLeafScript: this.offchainTapscript.forfeit(),
|
|
448
606
|
isUnrolled: false,
|
|
449
607
|
isSpent: false,
|
|
450
608
|
tapTree: this.offchainTapscript.encode(),
|
|
@@ -554,285 +712,31 @@ export class Wallet {
|
|
|
554
712
|
this.makeDeleteIntentSignature(params.inputs),
|
|
555
713
|
]);
|
|
556
714
|
const intentId = await this.safeRegisterIntent(intent);
|
|
715
|
+
const topics = [
|
|
716
|
+
...signingPublicKeys,
|
|
717
|
+
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
718
|
+
];
|
|
719
|
+
const handler = this.createBatchHandler(intentId, params.inputs, session);
|
|
557
720
|
const abortController = new AbortController();
|
|
558
|
-
// listen to settlement events
|
|
559
721
|
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
|
-
}
|
|
722
|
+
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
723
|
+
return await Batch.join(stream, handler, {
|
|
724
|
+
abortController,
|
|
725
|
+
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
726
|
+
eventCallback: eventCallback
|
|
727
|
+
? (event) => Promise.resolve(eventCallback(event))
|
|
728
|
+
: undefined,
|
|
729
|
+
});
|
|
700
730
|
}
|
|
701
731
|
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
|
-
}
|
|
732
|
+
// delete the intent to not be stuck in the queue
|
|
733
|
+
await this.arkProvider.deleteIntent(deleteIntent).catch(() => { });
|
|
711
734
|
throw error;
|
|
712
735
|
}
|
|
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");
|
|
736
|
+
finally {
|
|
737
|
+
// close the stream
|
|
738
|
+
abortController.abort();
|
|
821
739
|
}
|
|
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
740
|
}
|
|
837
741
|
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
838
742
|
// the signed forfeits transactions to submit
|
|
@@ -922,9 +826,98 @@ export class Wallet {
|
|
|
922
826
|
: undefined);
|
|
923
827
|
}
|
|
924
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* @implements Batch.Handler interface.
|
|
831
|
+
* @param intentId - The intent ID.
|
|
832
|
+
* @param inputs - The inputs of the intent.
|
|
833
|
+
* @param session - The musig2 signing session, if not provided, the signing will be skipped.
|
|
834
|
+
*/
|
|
835
|
+
createBatchHandler(intentId, inputs, session) {
|
|
836
|
+
let sweepTapTreeRoot;
|
|
837
|
+
return {
|
|
838
|
+
onBatchStarted: async (event) => {
|
|
839
|
+
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
840
|
+
const intentIdHash = sha256(utf8IntentId);
|
|
841
|
+
const intentIdHashStr = hex.encode(intentIdHash);
|
|
842
|
+
let skip = true;
|
|
843
|
+
// check if our intent ID hash matches any in the event
|
|
844
|
+
for (const idHash of event.intentIdHashes) {
|
|
845
|
+
if (idHash === intentIdHashStr) {
|
|
846
|
+
if (!this.arkProvider) {
|
|
847
|
+
throw new Error("Ark provider not configured");
|
|
848
|
+
}
|
|
849
|
+
await this.arkProvider.confirmRegistration(intentId);
|
|
850
|
+
skip = false;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (skip) {
|
|
854
|
+
return { skip };
|
|
855
|
+
}
|
|
856
|
+
const sweepTapscript = CSVMultisigTapscript.encode({
|
|
857
|
+
timelock: {
|
|
858
|
+
value: event.batchExpiry,
|
|
859
|
+
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
860
|
+
},
|
|
861
|
+
pubkeys: [this.forfeitPubkey],
|
|
862
|
+
}).script;
|
|
863
|
+
sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
864
|
+
return { skip: false };
|
|
865
|
+
},
|
|
866
|
+
onTreeSigningStarted: async (event, vtxoTree) => {
|
|
867
|
+
if (!session) {
|
|
868
|
+
return { skip: true };
|
|
869
|
+
}
|
|
870
|
+
if (!sweepTapTreeRoot) {
|
|
871
|
+
throw new Error("Sweep tap tree root not set");
|
|
872
|
+
}
|
|
873
|
+
const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
|
|
874
|
+
const signerPublicKey = await session.getPublicKey();
|
|
875
|
+
const xonlySignerPublicKey = signerPublicKey.subarray(1);
|
|
876
|
+
if (!xOnlyPublicKeys.includes(hex.encode(xonlySignerPublicKey))) {
|
|
877
|
+
// not a cosigner, skip the signing
|
|
878
|
+
return { skip: true };
|
|
879
|
+
}
|
|
880
|
+
// validate the unsigned vtxo tree
|
|
881
|
+
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
882
|
+
validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
|
|
883
|
+
// TODO check if our registered outputs are in the vtxo tree
|
|
884
|
+
const sharedOutput = commitmentTx.getOutput(0);
|
|
885
|
+
if (!sharedOutput?.amount) {
|
|
886
|
+
throw new Error("Shared output not found");
|
|
887
|
+
}
|
|
888
|
+
await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
|
|
889
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
890
|
+
const nonces = await session.getNonces();
|
|
891
|
+
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
892
|
+
return { skip: false };
|
|
893
|
+
},
|
|
894
|
+
onTreeNonces: async (event) => {
|
|
895
|
+
if (!session) {
|
|
896
|
+
return { fullySigned: true }; // Signing complete (no signing needed)
|
|
897
|
+
}
|
|
898
|
+
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
899
|
+
// wait to receive and aggregate all nonces before sending signatures
|
|
900
|
+
if (!hasAllNonces)
|
|
901
|
+
return { fullySigned: false };
|
|
902
|
+
const signatures = await session.sign();
|
|
903
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
904
|
+
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
905
|
+
return { fullySigned: true };
|
|
906
|
+
},
|
|
907
|
+
onBatchFinalization: async (event, _, connectorTree) => {
|
|
908
|
+
if (!this.forfeitOutputScript) {
|
|
909
|
+
throw new Error("Forfeit output script not set");
|
|
910
|
+
}
|
|
911
|
+
if (connectorTree) {
|
|
912
|
+
validateConnectorsTxGraph(event.commitmentTx, connectorTree);
|
|
913
|
+
}
|
|
914
|
+
await this.handleSettlementFinalizationEvent(event, inputs, this.forfeitOutputScript, connectorTree);
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
}
|
|
925
918
|
async safeRegisterIntent(intent) {
|
|
926
919
|
try {
|
|
927
|
-
return this.arkProvider.registerIntent(intent);
|
|
920
|
+
return await this.arkProvider.registerIntent(intent);
|
|
928
921
|
}
|
|
929
922
|
catch (error) {
|
|
930
923
|
// catch the "already registered by another intent" error
|
|
@@ -952,12 +945,11 @@ export class Wallet {
|
|
|
952
945
|
expire_at: 0,
|
|
953
946
|
cosigners_public_keys: cosignerPubKeys,
|
|
954
947
|
};
|
|
955
|
-
const
|
|
956
|
-
const proof = Intent.create(encodedMessage, inputs, outputs);
|
|
948
|
+
const proof = Intent.create(message, inputs, outputs);
|
|
957
949
|
const signedProof = await this.identity.sign(proof);
|
|
958
950
|
return {
|
|
959
951
|
proof: base64.encode(signedProof.toPSBT()),
|
|
960
|
-
message
|
|
952
|
+
message,
|
|
961
953
|
};
|
|
962
954
|
}
|
|
963
955
|
async makeDeleteIntentSignature(coins) {
|
|
@@ -966,12 +958,11 @@ export class Wallet {
|
|
|
966
958
|
type: "delete",
|
|
967
959
|
expire_at: 0,
|
|
968
960
|
};
|
|
969
|
-
const
|
|
970
|
-
const proof = Intent.create(encodedMessage, inputs, []);
|
|
961
|
+
const proof = Intent.create(message, inputs, []);
|
|
971
962
|
const signedProof = await this.identity.sign(proof);
|
|
972
963
|
return {
|
|
973
964
|
proof: base64.encode(signedProof.toPSBT()),
|
|
974
|
-
message
|
|
965
|
+
message,
|
|
975
966
|
};
|
|
976
967
|
}
|
|
977
968
|
async makeGetPendingTxIntentSignature(vtxos) {
|
|
@@ -980,12 +971,11 @@ export class Wallet {
|
|
|
980
971
|
type: "get-pending-tx",
|
|
981
972
|
expire_at: 0,
|
|
982
973
|
};
|
|
983
|
-
const
|
|
984
|
-
const proof = Intent.create(encodedMessage, inputs, []);
|
|
974
|
+
const proof = Intent.create(message, inputs, []);
|
|
985
975
|
const signedProof = await this.identity.sign(proof);
|
|
986
976
|
return {
|
|
987
977
|
proof: base64.encode(signedProof.toPSBT()),
|
|
988
|
-
message
|
|
978
|
+
message,
|
|
989
979
|
};
|
|
990
980
|
}
|
|
991
981
|
/**
|
|
@@ -1039,7 +1029,7 @@ export class Wallet {
|
|
|
1039
1029
|
const inputs = [];
|
|
1040
1030
|
for (const input of coins) {
|
|
1041
1031
|
const vtxoScript = VtxoScript.decode(input.tapTree);
|
|
1042
|
-
const sequence = getSequence(input);
|
|
1032
|
+
const sequence = getSequence(input.intentTapLeafScript);
|
|
1043
1033
|
const unknown = [VtxoTaprootTree.encode(input.tapTree)];
|
|
1044
1034
|
if (input.extraWitness) {
|
|
1045
1035
|
unknown.push(ConditionWitness.encode(input.extraWitness));
|
|
@@ -1060,15 +1050,21 @@ export class Wallet {
|
|
|
1060
1050
|
}
|
|
1061
1051
|
}
|
|
1062
1052
|
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
1063
|
-
function getSequence(
|
|
1053
|
+
export function getSequence(tapLeafScript) {
|
|
1064
1054
|
let sequence = undefined;
|
|
1065
1055
|
try {
|
|
1066
|
-
const scriptWithLeafVersion =
|
|
1056
|
+
const scriptWithLeafVersion = tapLeafScript[1];
|
|
1067
1057
|
const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1058
|
+
try {
|
|
1059
|
+
const params = CSVMultisigTapscript.decode(script).params;
|
|
1060
|
+
sequence = bip68.encode(params.timelock.type === "blocks"
|
|
1061
|
+
? { blocks: Number(params.timelock.value) }
|
|
1062
|
+
: { seconds: Number(params.timelock.value) });
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
const params = CLTVMultisigTapscript.decode(script).params;
|
|
1066
|
+
sequence = Number(params.absoluteTimelock);
|
|
1067
|
+
}
|
|
1072
1068
|
}
|
|
1073
1069
|
catch { }
|
|
1074
1070
|
return sequence;
|