@arkade-os/sdk 0.3.0-alpha.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -14
- package/dist/cjs/adapters/expo.js +8 -0
- package/dist/cjs/arknote/index.js +3 -3
- package/dist/cjs/forfeit.js +2 -2
- package/dist/cjs/identity/singleKey.js +8 -8
- package/dist/cjs/index.js +14 -5
- package/dist/cjs/{bip322 → intent}/index.js +38 -61
- package/dist/cjs/musig2/index.js +2 -1
- package/dist/cjs/musig2/nonces.js +4 -0
- package/dist/cjs/providers/ark.js +76 -45
- package/dist/cjs/providers/errors.js +59 -0
- package/dist/cjs/providers/expoArk.js +82 -0
- package/dist/cjs/providers/expoIndexer.js +105 -0
- package/dist/cjs/providers/expoUtils.js +124 -0
- package/dist/cjs/providers/indexer.js +3 -1
- package/dist/cjs/providers/onchain.js +19 -20
- package/dist/cjs/repositories/walletRepository.js +64 -28
- package/dist/cjs/script/base.js +15 -7
- package/dist/cjs/script/tapscript.js +20 -21
- package/dist/cjs/script/vhtlc.js +2 -2
- package/dist/cjs/tree/signingSession.js +44 -11
- package/dist/cjs/tree/txTree.js +3 -4
- package/dist/cjs/tree/validation.js +2 -3
- package/dist/cjs/utils/arkTransaction.js +118 -15
- package/dist/cjs/utils/transaction.js +28 -0
- package/dist/cjs/utils/unknownFields.js +7 -7
- package/dist/cjs/wallet/index.js +1 -1
- package/dist/cjs/wallet/onchain.js +6 -7
- package/dist/cjs/wallet/serviceWorker/response.js +32 -0
- package/dist/cjs/wallet/serviceWorker/utils.js +2 -9
- package/dist/cjs/wallet/serviceWorker/wallet.js +7 -8
- package/dist/cjs/wallet/serviceWorker/worker.js +48 -32
- package/dist/cjs/wallet/unroll.js +7 -9
- package/dist/cjs/wallet/utils.js +20 -0
- package/dist/cjs/wallet/vtxo-manager.js +323 -0
- package/dist/cjs/wallet/wallet.js +165 -174
- package/dist/esm/adapters/expo.js +3 -0
- package/dist/esm/arknote/index.js +2 -2
- package/dist/esm/forfeit.js +1 -1
- package/dist/esm/identity/singleKey.js +9 -9
- package/dist/esm/index.js +14 -10
- package/dist/esm/{bip322 → intent}/index.js +32 -54
- package/dist/esm/musig2/index.js +1 -1
- package/dist/esm/musig2/nonces.js +3 -0
- package/dist/esm/providers/ark.js +76 -45
- package/dist/esm/providers/errors.js +54 -0
- package/dist/esm/providers/expoArk.js +78 -0
- package/dist/esm/providers/expoIndexer.js +101 -0
- package/dist/esm/providers/expoUtils.js +87 -0
- package/dist/esm/providers/indexer.js +3 -1
- package/dist/esm/providers/onchain.js +19 -20
- package/dist/esm/repositories/walletRepository.js +64 -28
- package/dist/esm/script/base.js +12 -4
- package/dist/esm/script/tapscript.js +1 -2
- package/dist/esm/script/vhtlc.js +1 -1
- package/dist/esm/tree/signingSession.js +45 -12
- package/dist/esm/tree/txTree.js +3 -4
- package/dist/esm/tree/validation.js +2 -3
- package/dist/esm/utils/arkTransaction.js +110 -9
- package/dist/esm/utils/transaction.js +24 -0
- package/dist/esm/utils/unknownFields.js +3 -3
- package/dist/esm/wallet/index.js +1 -1
- package/dist/esm/wallet/onchain.js +3 -4
- package/dist/esm/wallet/serviceWorker/response.js +32 -0
- package/dist/esm/wallet/serviceWorker/utils.js +1 -8
- package/dist/esm/wallet/serviceWorker/wallet.js +8 -9
- package/dist/esm/wallet/serviceWorker/worker.js +49 -33
- package/dist/esm/wallet/unroll.js +5 -7
- package/dist/esm/wallet/utils.js +16 -0
- package/dist/esm/wallet/vtxo-manager.js +317 -0
- package/dist/esm/wallet/wallet.js +159 -168
- package/dist/types/adapters/expo.d.ts +4 -0
- package/dist/types/arknote/index.d.ts +1 -1
- package/dist/types/forfeit.d.ts +2 -2
- package/dist/types/identity/index.d.ts +2 -2
- package/dist/types/identity/singleKey.d.ts +2 -2
- package/dist/types/index.d.ts +11 -9
- package/dist/types/intent/index.d.ts +41 -0
- package/dist/types/musig2/index.d.ts +1 -1
- package/dist/types/musig2/nonces.d.ts +1 -0
- package/dist/types/providers/ark.d.ts +197 -27
- package/dist/types/providers/errors.d.ts +13 -0
- package/dist/types/providers/expoArk.d.ts +22 -0
- package/dist/types/providers/expoIndexer.d.ts +18 -0
- package/dist/types/providers/expoUtils.d.ts +18 -0
- package/dist/types/providers/indexer.d.ts +8 -8
- package/dist/types/providers/onchain.d.ts +6 -2
- package/dist/types/repositories/walletRepository.d.ts +9 -5
- package/dist/types/script/base.d.ts +5 -2
- package/dist/types/tree/signingSession.d.ts +16 -11
- package/dist/types/utils/anchor.d.ts +2 -2
- package/dist/types/utils/arkTransaction.d.ts +15 -5
- package/dist/types/utils/transaction.d.ts +13 -0
- package/dist/types/utils/unknownFields.d.ts +4 -4
- package/dist/types/wallet/index.d.ts +47 -7
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/response.d.ts +16 -2
- package/dist/types/wallet/serviceWorker/utils.d.ts +1 -2
- package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -2
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -1
- package/dist/types/wallet/unroll.d.ts +1 -1
- package/dist/types/wallet/utils.d.ts +3 -0
- package/dist/types/wallet/vtxo-manager.d.ts +179 -0
- package/dist/types/wallet/wallet.d.ts +17 -5
- package/package.json +11 -3
- package/dist/cjs/bip322/errors.js +0 -13
- package/dist/esm/bip322/errors.js +0 -9
- package/dist/types/bip322/errors.d.ts +0 -6
- package/dist/types/bip322/index.d.ts +0 -57
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { base64, hex } from "@scure/base";
|
|
2
2
|
import * as bip68 from "bip68";
|
|
3
|
-
import {
|
|
4
|
-
import { SigHash, Transaction } from "@scure/btc-signer
|
|
5
|
-
import {
|
|
3
|
+
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
4
|
+
import { SigHash, Transaction, Address, OutScript, } from "@scure/btc-signer";
|
|
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';
|
|
@@ -12,18 +12,19 @@ import { SettlementEventType, RestArkProvider, } from '../providers/ark.js';
|
|
|
12
12
|
import { buildForfeitTx } from '../forfeit.js';
|
|
13
13
|
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
|
|
14
14
|
import { isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
15
|
-
import { sha256, sha256x2 } from "@scure/btc-signer/utils.js";
|
|
16
15
|
import { VtxoScript } from '../script/base.js';
|
|
17
16
|
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
18
|
-
import { buildOffchainTx } from '../utils/arkTransaction.js';
|
|
17
|
+
import { buildOffchainTx, hasBoardingTxExpired } from '../utils/arkTransaction.js';
|
|
18
|
+
import { DEFAULT_RENEWAL_CONFIG } from './vtxo-manager.js';
|
|
19
19
|
import { ArkNote } from '../arknote/index.js';
|
|
20
|
-
import {
|
|
20
|
+
import { Intent } from '../intent/index.js';
|
|
21
21
|
import { RestIndexerProvider } from '../providers/indexer.js';
|
|
22
22
|
import { TxTree } from '../tree/txTree.js';
|
|
23
|
+
import { ConditionWitness, VtxoTaprootTree } from '../utils/unknownFields.js';
|
|
23
24
|
import { InMemoryStorageAdapter } from '../storage/inMemory.js';
|
|
24
25
|
import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
|
|
25
26
|
import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
|
|
26
|
-
import { extendVirtualCoin } from './
|
|
27
|
+
import { extendCoin, extendVirtualCoin } from './utils.js';
|
|
27
28
|
/**
|
|
28
29
|
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|
|
29
30
|
* The wallet does not store any data locally and relies on Ark and onchain
|
|
@@ -31,13 +32,21 @@ import { extendVirtualCoin } from './serviceWorker/utils.js';
|
|
|
31
32
|
*
|
|
32
33
|
* @example
|
|
33
34
|
* ```typescript
|
|
34
|
-
* // Create a wallet
|
|
35
|
+
* // Create a wallet with URL configuration
|
|
35
36
|
* const wallet = await Wallet.create({
|
|
36
37
|
* identity: SingleKey.fromHex('your_private_key'),
|
|
37
38
|
* arkServerUrl: 'https://ark.example.com',
|
|
38
39
|
* esploraUrl: 'https://mempool.space/api'
|
|
39
40
|
* });
|
|
40
41
|
*
|
|
42
|
+
* // Or with custom provider instances (e.g., for Expo/React Native)
|
|
43
|
+
* const wallet = await Wallet.create({
|
|
44
|
+
* identity: SingleKey.fromHex('your_private_key'),
|
|
45
|
+
* arkProvider: new ExpoArkProvider('https://ark.example.com'),
|
|
46
|
+
* indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
|
|
47
|
+
* esploraUrl: 'https://mempool.space/api'
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
41
50
|
* // Get addresses
|
|
42
51
|
* const arkAddress = await wallet.getAddress();
|
|
43
52
|
* const boardingAddress = await wallet.getBoardingAddress();
|
|
@@ -50,7 +59,7 @@ import { extendVirtualCoin } from './serviceWorker/utils.js';
|
|
|
50
59
|
* ```
|
|
51
60
|
*/
|
|
52
61
|
export class Wallet {
|
|
53
|
-
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, dustAmount, walletRepository, contractRepository) {
|
|
62
|
+
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
|
|
54
63
|
this.identity = identity;
|
|
55
64
|
this.network = network;
|
|
56
65
|
this.networkName = networkName;
|
|
@@ -62,20 +71,45 @@ export class Wallet {
|
|
|
62
71
|
this.boardingTapscript = boardingTapscript;
|
|
63
72
|
this.serverUnrollScript = serverUnrollScript;
|
|
64
73
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
74
|
+
this.forfeitPubkey = forfeitPubkey;
|
|
65
75
|
this.dustAmount = dustAmount;
|
|
66
76
|
this.walletRepository = walletRepository;
|
|
67
77
|
this.contractRepository = contractRepository;
|
|
78
|
+
this.renewalConfig = {
|
|
79
|
+
enabled: renewalConfig?.enabled ?? false,
|
|
80
|
+
...DEFAULT_RENEWAL_CONFIG,
|
|
81
|
+
...renewalConfig,
|
|
82
|
+
};
|
|
68
83
|
}
|
|
69
84
|
static async create(config) {
|
|
70
85
|
const pubkey = await config.identity.xOnlyPublicKey();
|
|
71
86
|
if (!pubkey) {
|
|
72
87
|
throw new Error("Invalid configured public key");
|
|
73
88
|
}
|
|
74
|
-
|
|
75
|
-
const
|
|
89
|
+
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
90
|
+
const arkProvider = config.arkProvider ||
|
|
91
|
+
(() => {
|
|
92
|
+
if (!config.arkServerUrl) {
|
|
93
|
+
throw new Error("Either arkProvider or arkServerUrl must be provided");
|
|
94
|
+
}
|
|
95
|
+
return new RestArkProvider(config.arkServerUrl);
|
|
96
|
+
})();
|
|
97
|
+
// Extract arkServerUrl from provider if not explicitly provided
|
|
98
|
+
const arkServerUrl = config.arkServerUrl || arkProvider.serverUrl;
|
|
99
|
+
if (!arkServerUrl) {
|
|
100
|
+
throw new Error("Could not determine arkServerUrl from provider");
|
|
101
|
+
}
|
|
102
|
+
// Use provided indexerProvider instance or create a new one
|
|
103
|
+
// indexerUrl defaults to arkServerUrl if not provided
|
|
104
|
+
const indexerUrl = config.indexerUrl || arkServerUrl;
|
|
105
|
+
const indexerProvider = config.indexerProvider || new RestIndexerProvider(indexerUrl);
|
|
76
106
|
const info = await arkProvider.getInfo();
|
|
77
107
|
const network = getNetwork(info.network);
|
|
78
|
-
|
|
108
|
+
// Extract esploraUrl from provider if not explicitly provided
|
|
109
|
+
const esploraUrl = config.esploraUrl || ESPLORA_URL[info.network];
|
|
110
|
+
// Use provided onchainProvider instance or create a new one
|
|
111
|
+
const onchainProvider = config.onchainProvider || new EsploraProvider(esploraUrl);
|
|
112
|
+
// Generate timelocks
|
|
79
113
|
const exitTimelock = {
|
|
80
114
|
value: info.unilateralExitDelay,
|
|
81
115
|
type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
|
|
@@ -99,17 +133,24 @@ export class Wallet {
|
|
|
99
133
|
// Save tapscripts
|
|
100
134
|
const offchainTapscript = bareVtxoTapscript;
|
|
101
135
|
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
102
|
-
|
|
103
|
-
|
|
136
|
+
let serverUnrollScript;
|
|
137
|
+
try {
|
|
138
|
+
const raw = hex.decode(info.checkpointTapscript);
|
|
139
|
+
serverUnrollScript = CSVMultisigTapscript.decode(raw);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
throw new Error("Invalid checkpointTapscript from server");
|
|
143
|
+
}
|
|
104
144
|
// parse the server forfeit address
|
|
105
145
|
// server is expecting funds to be sent to this address
|
|
146
|
+
const forfeitPubkey = hex.decode(info.forfeitPubkey).slice(1);
|
|
106
147
|
const forfeitAddress = Address(network).decode(info.forfeitAddress);
|
|
107
148
|
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
108
149
|
// Set up storage and repositories
|
|
109
150
|
const storage = config.storage || new InMemoryStorageAdapter();
|
|
110
151
|
const walletRepository = new WalletRepositoryImpl(storage);
|
|
111
152
|
const contractRepository = new ContractRepositoryImpl(storage);
|
|
112
|
-
return new Wallet(config.identity, network, info.network, onchainProvider, arkProvider, indexerProvider, serverPubKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, info.dust, walletRepository, contractRepository);
|
|
153
|
+
return new Wallet(config.identity, network, info.network, onchainProvider, arkProvider, indexerProvider, serverPubKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, info.dust, walletRepository, contractRepository, config.renewalConfig);
|
|
113
154
|
}
|
|
114
155
|
get arkAddress() {
|
|
115
156
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
@@ -171,40 +212,24 @@ export class Wallet {
|
|
|
171
212
|
// if (cachedVtxos.length) return cachedVtxos;
|
|
172
213
|
// For now, always fetch fresh data from provider and update cache
|
|
173
214
|
// In future, we can add cache invalidation logic based on timestamps
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const forfeit = this.offchainTapscript.forfeit();
|
|
177
|
-
const exit = this.offchainTapscript.exit();
|
|
178
|
-
const extendedVtxos = spendableVtxos.map((vtxo) => ({
|
|
179
|
-
...vtxo,
|
|
180
|
-
forfeitTapLeafScript: forfeit,
|
|
181
|
-
intentTapLeafScript: exit,
|
|
182
|
-
tapTree: encodedOffchainTapscript,
|
|
183
|
-
}));
|
|
215
|
+
const vtxos = await this.getVirtualCoins(filter);
|
|
216
|
+
const extendedVtxos = vtxos.map((vtxo) => extendVirtualCoin(this, vtxo));
|
|
184
217
|
// Update cache with fresh data
|
|
185
218
|
await this.walletRepository.saveVtxos(address, extendedVtxos);
|
|
186
219
|
return extendedVtxos;
|
|
187
220
|
}
|
|
188
221
|
async getVirtualCoins(filter = { withRecoverable: true, withUnrolled: false }) {
|
|
189
222
|
const scripts = [hex.encode(this.offchainTapscript.pkScript)];
|
|
190
|
-
const response = await this.indexerProvider.getVtxos({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const response = await this.indexerProvider.getVtxos({
|
|
197
|
-
scripts,
|
|
198
|
-
recoverableOnly: true,
|
|
199
|
-
});
|
|
200
|
-
vtxos.push(...response.vtxos);
|
|
223
|
+
const response = await this.indexerProvider.getVtxos({ scripts });
|
|
224
|
+
const allVtxos = response.vtxos;
|
|
225
|
+
let vtxos = allVtxos.filter(isSpendable);
|
|
226
|
+
// all recoverable vtxos are spendable by definition
|
|
227
|
+
if (!filter.withRecoverable) {
|
|
228
|
+
vtxos = vtxos.filter((vtxo) => !isRecoverable(vtxo));
|
|
201
229
|
}
|
|
202
230
|
if (filter.withUnrolled) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
spentOnly: true,
|
|
206
|
-
});
|
|
207
|
-
vtxos.push(...response.vtxos.filter((vtxo) => vtxo.isUnrolled));
|
|
231
|
+
const spentVtxos = allVtxos.filter((vtxo) => !isSpendable(vtxo));
|
|
232
|
+
vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
|
|
208
233
|
}
|
|
209
234
|
return vtxos;
|
|
210
235
|
}
|
|
@@ -242,10 +267,10 @@ export class Wallet {
|
|
|
242
267
|
return txs;
|
|
243
268
|
}
|
|
244
269
|
async getBoardingTxs() {
|
|
245
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
246
|
-
const txs = await this.onchainProvider.getTransactions(boardingAddress);
|
|
247
270
|
const utxos = [];
|
|
248
271
|
const commitmentsToIgnore = new Set();
|
|
272
|
+
const boardingAddress = await this.getBoardingAddress();
|
|
273
|
+
const txs = await this.onchainProvider.getTransactions(boardingAddress);
|
|
249
274
|
for (const tx of txs) {
|
|
250
275
|
for (let i = 0; i < tx.vout.length; i++) {
|
|
251
276
|
const vout = tx.vout[i];
|
|
@@ -308,15 +333,12 @@ export class Wallet {
|
|
|
308
333
|
async getBoardingUtxos() {
|
|
309
334
|
const boardingAddress = await this.getBoardingAddress();
|
|
310
335
|
const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
intentTapLeafScript: exit,
|
|
318
|
-
tapTree: encodedBoardingTapscript,
|
|
319
|
-
}));
|
|
336
|
+
const utxos = boardingUtxos.map((utxo) => {
|
|
337
|
+
return extendCoin(this, utxo);
|
|
338
|
+
});
|
|
339
|
+
// Save boardingUtxos using unified repository
|
|
340
|
+
await this.walletRepository.saveUtxos(boardingAddress, utxos);
|
|
341
|
+
return utxos;
|
|
320
342
|
}
|
|
321
343
|
async sendBitcoin(params) {
|
|
322
344
|
if (params.amount <= 0) {
|
|
@@ -386,13 +408,15 @@ export class Wallet {
|
|
|
386
408
|
}
|
|
387
409
|
}
|
|
388
410
|
}
|
|
389
|
-
// if no params are provided, use all boarding and offchain
|
|
411
|
+
// if no params are provided, use all non expired boarding utxos and offchain vtxos as inputs
|
|
390
412
|
// and send all to the offchain address
|
|
391
413
|
if (!params) {
|
|
392
414
|
let amount = 0;
|
|
393
|
-
const
|
|
415
|
+
const exitScript = CSVMultisigTapscript.decode(hex.decode(this.boardingTapscript.exitScript));
|
|
416
|
+
const boardingTimelock = exitScript.params.timelock;
|
|
417
|
+
const boardingUtxos = (await this.getBoardingUtxos()).filter((utxo) => !hasBoardingTxExpired(utxo, boardingTimelock));
|
|
394
418
|
amount += boardingUtxos.reduce((sum, input) => sum + input.value, 0);
|
|
395
|
-
const vtxos = await this.getVtxos();
|
|
419
|
+
const vtxos = await this.getVtxos({ withRecoverable: true });
|
|
396
420
|
amount += vtxos.reduce((sum, input) => sum + input.value, 0);
|
|
397
421
|
const inputs = [...boardingUtxos, ...vtxos];
|
|
398
422
|
if (inputs.length === 0) {
|
|
@@ -435,7 +459,7 @@ export class Wallet {
|
|
|
435
459
|
const signingPublicKeys = [];
|
|
436
460
|
if (hasOffchainOutputs) {
|
|
437
461
|
session = this.identity.signerSession();
|
|
438
|
-
signingPublicKeys.push(hex.encode(session.getPublicKey()));
|
|
462
|
+
signingPublicKeys.push(hex.encode(await session.getPublicKey()));
|
|
439
463
|
}
|
|
440
464
|
const [intent, deleteIntent] = await Promise.all([
|
|
441
465
|
this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
|
|
@@ -451,8 +475,8 @@ export class Wallet {
|
|
|
451
475
|
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
452
476
|
];
|
|
453
477
|
const settlementStream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
454
|
-
//
|
|
455
|
-
let
|
|
478
|
+
// batchId, sweepTapTreeRoot and forfeitOutputScript are set once the BatchStarted event is received
|
|
479
|
+
let batchId;
|
|
456
480
|
let sweepTapTreeRoot;
|
|
457
481
|
const vtxoChunks = [];
|
|
458
482
|
const connectorsChunks = [];
|
|
@@ -465,30 +489,26 @@ export class Wallet {
|
|
|
465
489
|
switch (event.type) {
|
|
466
490
|
// the settlement failed
|
|
467
491
|
case SettlementEventType.BatchFailed:
|
|
468
|
-
|
|
469
|
-
if (event.id === roundId) {
|
|
470
|
-
throw new Error(event.reason);
|
|
471
|
-
}
|
|
472
|
-
break;
|
|
492
|
+
throw new Error(event.reason);
|
|
473
493
|
case SettlementEventType.BatchStarted:
|
|
474
494
|
if (step !== undefined) {
|
|
475
495
|
continue;
|
|
476
496
|
}
|
|
477
|
-
const res = await this.handleBatchStartedEvent(event, intentId, this.
|
|
497
|
+
const res = await this.handleBatchStartedEvent(event, intentId, this.forfeitPubkey, this.forfeitOutputScript);
|
|
478
498
|
if (!res.skip) {
|
|
479
499
|
step = event.type;
|
|
480
500
|
sweepTapTreeRoot = res.sweepTapTreeRoot;
|
|
481
|
-
|
|
501
|
+
batchId = res.roundId;
|
|
482
502
|
if (!hasOffchainOutputs) {
|
|
483
503
|
// if there are no offchain outputs, we don't have to handle musig2 tree signatures
|
|
484
504
|
// we can directly advance to the finalization step
|
|
485
|
-
step = SettlementEventType.
|
|
505
|
+
step = SettlementEventType.TreeNonces;
|
|
486
506
|
}
|
|
487
507
|
}
|
|
488
508
|
break;
|
|
489
509
|
case SettlementEventType.TreeTx:
|
|
490
510
|
if (step !== SettlementEventType.BatchStarted &&
|
|
491
|
-
step !== SettlementEventType.
|
|
511
|
+
step !== SettlementEventType.TreeNonces) {
|
|
492
512
|
continue;
|
|
493
513
|
}
|
|
494
514
|
// index 0 = vtxo tree
|
|
@@ -504,7 +524,7 @@ export class Wallet {
|
|
|
504
524
|
}
|
|
505
525
|
break;
|
|
506
526
|
case SettlementEventType.TreeSignature:
|
|
507
|
-
if (step !== SettlementEventType.
|
|
527
|
+
if (step !== SettlementEventType.TreeNonces) {
|
|
508
528
|
continue;
|
|
509
529
|
}
|
|
510
530
|
if (!hasOffchainOutputs) {
|
|
@@ -546,7 +566,7 @@ export class Wallet {
|
|
|
546
566
|
break;
|
|
547
567
|
// the musig2 nonces of the vtxo tree transactions are generated
|
|
548
568
|
// the server expects now the partial musig2 signatures
|
|
549
|
-
case SettlementEventType.
|
|
569
|
+
case SettlementEventType.TreeNonces:
|
|
550
570
|
if (step !== SettlementEventType.TreeSigningStarted) {
|
|
551
571
|
continue;
|
|
552
572
|
}
|
|
@@ -554,14 +574,18 @@ export class Wallet {
|
|
|
554
574
|
if (!session) {
|
|
555
575
|
throw new Error("Signing session not set");
|
|
556
576
|
}
|
|
557
|
-
await this.
|
|
577
|
+
const signed = await this.handleSettlementTreeNoncesEvent(event, session);
|
|
578
|
+
if (signed) {
|
|
579
|
+
step = event.type;
|
|
580
|
+
}
|
|
581
|
+
break;
|
|
558
582
|
}
|
|
559
583
|
step = event.type;
|
|
560
584
|
break;
|
|
561
585
|
// the vtxo tree is signed, craft, sign and submit forfeit transactions
|
|
562
586
|
// if any boarding utxos are involved, the settlement tx is also signed
|
|
563
587
|
case SettlementEventType.BatchFinalization:
|
|
564
|
-
if (step !== SettlementEventType.
|
|
588
|
+
if (step !== SettlementEventType.TreeNonces) {
|
|
565
589
|
continue;
|
|
566
590
|
}
|
|
567
591
|
if (!this.forfeitOutputScript) {
|
|
@@ -579,8 +603,10 @@ export class Wallet {
|
|
|
579
603
|
if (step !== SettlementEventType.BatchFinalization) {
|
|
580
604
|
continue;
|
|
581
605
|
}
|
|
582
|
-
|
|
583
|
-
|
|
606
|
+
if (event.id === batchId) {
|
|
607
|
+
abortController.abort();
|
|
608
|
+
return event.commitmentTxid;
|
|
609
|
+
}
|
|
584
610
|
}
|
|
585
611
|
}
|
|
586
612
|
}
|
|
@@ -602,22 +628,22 @@ export class Wallet {
|
|
|
602
628
|
let onchainStopFunc;
|
|
603
629
|
let indexerStopFunc;
|
|
604
630
|
if (this.onchainProvider && boardingAddress) {
|
|
631
|
+
const findVoutOnTx = (tx) => {
|
|
632
|
+
return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
633
|
+
};
|
|
605
634
|
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
635
|
+
// find all utxos belonging to our boarding address
|
|
606
636
|
const coins = txs
|
|
637
|
+
// filter txs where address is in output
|
|
638
|
+
.filter((tx) => findVoutOnTx(tx) !== -1)
|
|
639
|
+
// return utxo as Coin
|
|
607
640
|
.map((tx) => {
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
txid: tx.txid,
|
|
615
|
-
vout,
|
|
616
|
-
value: Number(tx.vout[vout].value),
|
|
617
|
-
status: tx.status,
|
|
618
|
-
};
|
|
619
|
-
})
|
|
620
|
-
.filter((coin) => coin !== null);
|
|
641
|
+
const { txid, status } = tx;
|
|
642
|
+
const vout = findVoutOnTx(tx);
|
|
643
|
+
const value = Number(tx.vout[vout].value);
|
|
644
|
+
return { txid, vout, value, status };
|
|
645
|
+
});
|
|
646
|
+
// and notify via callback
|
|
621
647
|
eventCallback({
|
|
622
648
|
type: "utxo",
|
|
623
649
|
coins,
|
|
@@ -659,10 +685,10 @@ export class Wallet {
|
|
|
659
685
|
};
|
|
660
686
|
return stopFunc;
|
|
661
687
|
}
|
|
662
|
-
async handleBatchStartedEvent(event, intentId,
|
|
688
|
+
async handleBatchStartedEvent(event, intentId, forfeitPubKey, forfeitOutputScript) {
|
|
663
689
|
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
664
690
|
const intentIdHash = sha256(utf8IntentId);
|
|
665
|
-
const intentIdHashStr = hex.encode(
|
|
691
|
+
const intentIdHashStr = hex.encode(intentIdHash);
|
|
666
692
|
let skip = true;
|
|
667
693
|
// check if our intent ID hash matches any in the event
|
|
668
694
|
for (const idHash of event.intentIdHashes) {
|
|
@@ -682,7 +708,7 @@ export class Wallet {
|
|
|
682
708
|
value: event.batchExpiry,
|
|
683
709
|
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
684
710
|
},
|
|
685
|
-
pubkeys: [
|
|
711
|
+
pubkeys: [forfeitPubKey],
|
|
686
712
|
}).script;
|
|
687
713
|
const sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
688
714
|
return {
|
|
@@ -703,12 +729,19 @@ export class Wallet {
|
|
|
703
729
|
throw new Error("Shared output not found");
|
|
704
730
|
}
|
|
705
731
|
session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
|
|
706
|
-
|
|
732
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
733
|
+
const nonces = await session.getNonces();
|
|
734
|
+
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
707
735
|
}
|
|
708
|
-
async
|
|
709
|
-
session.
|
|
710
|
-
|
|
711
|
-
|
|
736
|
+
async handleSettlementTreeNoncesEvent(event, session) {
|
|
737
|
+
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
738
|
+
// wait to receive and aggregate all nonces before sending signatures
|
|
739
|
+
if (!hasAllNonces)
|
|
740
|
+
return false;
|
|
741
|
+
const signatures = await session.sign();
|
|
742
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
743
|
+
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
744
|
+
return true;
|
|
712
745
|
}
|
|
713
746
|
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
714
747
|
// the signed forfeits transactions to submit
|
|
@@ -723,8 +756,6 @@ export class Wallet {
|
|
|
723
756
|
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
724
757
|
// boarding utxo, we need to sign the settlement tx
|
|
725
758
|
if (!vtxo) {
|
|
726
|
-
hasBoardingUtxos = true;
|
|
727
|
-
const inputIndexes = [];
|
|
728
759
|
for (let i = 0; i < settlementPsbt.inputsLength; i++) {
|
|
729
760
|
const settlementInput = settlementPsbt.getInput(i);
|
|
730
761
|
if (!settlementInput.txid ||
|
|
@@ -740,9 +771,12 @@ export class Wallet {
|
|
|
740
771
|
settlementPsbt.updateInput(i, {
|
|
741
772
|
tapLeafScript: [input.forfeitTapLeafScript],
|
|
742
773
|
});
|
|
743
|
-
|
|
774
|
+
settlementPsbt = await this.identity.sign(settlementPsbt, [
|
|
775
|
+
i,
|
|
776
|
+
]);
|
|
777
|
+
hasBoardingUtxos = true;
|
|
778
|
+
break;
|
|
744
779
|
}
|
|
745
|
-
settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
|
|
746
780
|
continue;
|
|
747
781
|
}
|
|
748
782
|
if (isRecoverable(vtxo) || isSubdust(vtxo, this.dustAmount)) {
|
|
@@ -756,7 +790,7 @@ export class Wallet {
|
|
|
756
790
|
throw new Error("not enough connectors received");
|
|
757
791
|
}
|
|
758
792
|
const connectorLeaf = connectorsLeaves[connectorIndex];
|
|
759
|
-
const connectorTxId =
|
|
793
|
+
const connectorTxId = connectorLeaf.id;
|
|
760
794
|
const connectorOutput = connectorLeaf.getOutput(0);
|
|
761
795
|
if (!connectorOutput) {
|
|
762
796
|
throw new Error("connector output not found");
|
|
@@ -797,111 +831,68 @@ export class Wallet {
|
|
|
797
831
|
: undefined);
|
|
798
832
|
}
|
|
799
833
|
}
|
|
800
|
-
async makeRegisterIntentSignature(
|
|
834
|
+
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
|
|
801
835
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
802
|
-
const
|
|
836
|
+
const inputs = this.prepareIntentProofInputs(coins);
|
|
803
837
|
const message = {
|
|
804
838
|
type: "register",
|
|
805
|
-
input_tap_trees: inputTapTrees,
|
|
806
839
|
onchain_output_indexes: onchainOutputsIndexes,
|
|
807
840
|
valid_at: nowSeconds,
|
|
808
841
|
expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
|
|
809
842
|
cosigners_public_keys: cosignerPubKeys,
|
|
810
843
|
};
|
|
811
844
|
const encodedMessage = JSON.stringify(message, null, 0);
|
|
812
|
-
const
|
|
845
|
+
const proof = Intent.create(encodedMessage, inputs, outputs);
|
|
846
|
+
const signedProof = await this.identity.sign(proof);
|
|
813
847
|
return {
|
|
814
|
-
|
|
848
|
+
proof: base64.encode(signedProof.toPSBT()),
|
|
815
849
|
message: encodedMessage,
|
|
816
850
|
};
|
|
817
851
|
}
|
|
818
|
-
async makeDeleteIntentSignature(
|
|
852
|
+
async makeDeleteIntentSignature(coins) {
|
|
819
853
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
820
|
-
const
|
|
854
|
+
const inputs = this.prepareIntentProofInputs(coins);
|
|
821
855
|
const message = {
|
|
822
856
|
type: "delete",
|
|
823
857
|
expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
|
|
824
858
|
};
|
|
825
859
|
const encodedMessage = JSON.stringify(message, null, 0);
|
|
826
|
-
const
|
|
860
|
+
const proof = Intent.create(encodedMessage, inputs, []);
|
|
861
|
+
const signedProof = await this.identity.sign(proof);
|
|
827
862
|
return {
|
|
828
|
-
|
|
863
|
+
proof: base64.encode(signedProof.toPSBT()),
|
|
829
864
|
message: encodedMessage,
|
|
830
865
|
};
|
|
831
866
|
}
|
|
832
|
-
|
|
867
|
+
prepareIntentProofInputs(coins) {
|
|
833
868
|
const inputs = [];
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
|
|
869
|
+
for (const input of coins) {
|
|
870
|
+
const vtxoScript = VtxoScript.decode(input.tapTree);
|
|
871
|
+
const sequence = getSequence(input);
|
|
872
|
+
const unknown = [VtxoTaprootTree.encode(input.tapTree)];
|
|
873
|
+
if (input.extraWitness) {
|
|
874
|
+
unknown.push(ConditionWitness.encode(input.extraWitness));
|
|
875
|
+
}
|
|
839
876
|
inputs.push({
|
|
840
|
-
txid: hex.decode(
|
|
841
|
-
index:
|
|
877
|
+
txid: hex.decode(input.txid),
|
|
878
|
+
index: input.vout,
|
|
842
879
|
witnessUtxo: {
|
|
843
|
-
amount: BigInt(
|
|
880
|
+
amount: BigInt(input.value),
|
|
844
881
|
script: vtxoScript.pkScript,
|
|
845
882
|
},
|
|
846
883
|
sequence,
|
|
847
|
-
tapLeafScript: [
|
|
884
|
+
tapLeafScript: [input.intentTapLeafScript],
|
|
885
|
+
unknown,
|
|
848
886
|
});
|
|
849
|
-
inputTapTrees.push(hex.encode(bip322Input.tapTree));
|
|
850
|
-
inputExtraWitnesses.push(bip322Input.extraWitness || []);
|
|
851
887
|
}
|
|
852
|
-
return
|
|
853
|
-
inputs,
|
|
854
|
-
inputTapTrees,
|
|
855
|
-
finalizer: finalizeWithExtraWitnesses(inputExtraWitnesses),
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
async makeBIP322Signature(message, inputs, finalizer, outputs) {
|
|
859
|
-
const proof = BIP322.create(message, inputs, outputs);
|
|
860
|
-
const signedProof = await this.identity.sign(proof);
|
|
861
|
-
return BIP322.signature(signedProof, finalizer);
|
|
888
|
+
return inputs;
|
|
862
889
|
}
|
|
863
890
|
}
|
|
864
891
|
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
865
|
-
function
|
|
866
|
-
return function (tx) {
|
|
867
|
-
for (let i = 0; i < tx.inputsLength; i++) {
|
|
868
|
-
try {
|
|
869
|
-
tx.finalizeIdx(i);
|
|
870
|
-
}
|
|
871
|
-
catch (e) {
|
|
872
|
-
// handle empty witness error
|
|
873
|
-
if (e instanceof Error &&
|
|
874
|
-
e.message.includes("finalize/taproot: empty witness")) {
|
|
875
|
-
const tapLeaves = tx.getInput(i).tapLeafScript;
|
|
876
|
-
if (!tapLeaves || tapLeaves.length <= 0)
|
|
877
|
-
throw e;
|
|
878
|
-
const [cb, s] = tapLeaves[0];
|
|
879
|
-
const script = s.slice(0, -1);
|
|
880
|
-
tx.updateInput(i, {
|
|
881
|
-
finalScriptWitness: [
|
|
882
|
-
script,
|
|
883
|
-
TaprootControlBlock.encode(cb),
|
|
884
|
-
],
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
const finalScriptWitness = tx.getInput(i).finalScriptWitness;
|
|
889
|
-
if (!finalScriptWitness)
|
|
890
|
-
throw new Error("input not finalized");
|
|
891
|
-
// input 0 and 1 spend the same pkscript
|
|
892
|
-
const extra = inputExtraWitnesses[i === 0 ? 0 : i - 1];
|
|
893
|
-
if (extra && extra.length > 0) {
|
|
894
|
-
tx.updateInput(i, {
|
|
895
|
-
finalScriptWitness: [...extra, ...finalScriptWitness],
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
function getSequence(bip322Input) {
|
|
892
|
+
function getSequence(coin) {
|
|
902
893
|
let sequence = undefined;
|
|
903
894
|
try {
|
|
904
|
-
const scriptWithLeafVersion =
|
|
895
|
+
const scriptWithLeafVersion = coin.intentTapLeafScript[1];
|
|
905
896
|
const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
|
|
906
897
|
const params = CSVMultisigTapscript.decode(script).params;
|
|
907
898
|
sequence = bip68.encode(params.timelock.type === "blocks"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TapLeafScript, VtxoScript } from "../script/base";
|
|
2
1
|
import { Bytes } from "@scure/btc-signer/utils.js";
|
|
2
|
+
import { TapLeafScript, VtxoScript } from "../script/base";
|
|
3
3
|
import { ExtendedCoin, Status } from "../wallet";
|
|
4
4
|
/**
|
|
5
5
|
* ArkNotes are special virtual coins in the Ark protocol that can be created
|
package/dist/types/forfeit.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Transaction } from "
|
|
2
|
-
import { TransactionInputUpdate } from "@scure/btc-signer/psbt";
|
|
1
|
+
import { Transaction } from "./utils/transaction";
|
|
2
|
+
import { TransactionInputUpdate } from "@scure/btc-signer/psbt.js";
|
|
3
3
|
export declare function buildForfeitTx(inputs: TransactionInputUpdate[], forfeitPkScript: Uint8Array, txLocktime?: number): Transaction;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Transaction } from "
|
|
1
|
+
import { Transaction } from "../utils/transaction";
|
|
2
2
|
import { SignerSession } from "../tree/signingSession";
|
|
3
3
|
export interface Identity {
|
|
4
4
|
signerSession(): SignerSession;
|
|
5
5
|
xOnlyPublicKey(): Promise<Uint8Array>;
|
|
6
6
|
compressedPublicKey(): Promise<Uint8Array>;
|
|
7
|
-
signMessage(message:
|
|
7
|
+
signMessage(message: Uint8Array, signatureType: "schnorr" | "ecdsa"): Promise<Uint8Array>;
|
|
8
8
|
sign(tx: Transaction, inputIndexes?: number[]): Promise<Transaction>;
|
|
9
9
|
}
|
|
10
10
|
export * from "./singleKey";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Transaction } from "@scure/btc-signer/transaction.js";
|
|
2
1
|
import { Identity } from ".";
|
|
2
|
+
import { Transaction } from "../utils/transaction";
|
|
3
3
|
import { SignerSession } from "../tree/signingSession";
|
|
4
4
|
/**
|
|
5
5
|
* In-memory single key implementation for Bitcoin transaction signing.
|
|
@@ -35,5 +35,5 @@ export declare class SingleKey implements Identity {
|
|
|
35
35
|
compressedPublicKey(): Promise<Uint8Array>;
|
|
36
36
|
xOnlyPublicKey(): Promise<Uint8Array>;
|
|
37
37
|
signerSession(): SignerSession;
|
|
38
|
-
signMessage(message:
|
|
38
|
+
signMessage(message: Uint8Array, signatureType?: "schnorr" | "ecdsa"): Promise<Uint8Array>;
|
|
39
39
|
}
|