@arkade-os/sdk 0.3.1-alpha.4 → 0.3.1-alpha.6
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 +11 -27
- package/dist/cjs/forfeit.js +2 -5
- package/dist/cjs/identity/singleKey.js +4 -5
- package/dist/cjs/index.js +5 -4
- package/dist/cjs/intent/index.js +3 -8
- package/dist/cjs/providers/onchain.js +19 -20
- package/dist/cjs/repositories/walletRepository.js +64 -2
- package/dist/cjs/script/base.js +14 -5
- package/dist/cjs/utils/arkTransaction.js +3 -5
- package/dist/cjs/utils/transaction.js +28 -0
- package/dist/cjs/wallet/onchain.js +4 -4
- package/dist/cjs/wallet/serviceWorker/worker.js +19 -2
- package/dist/cjs/wallet/unroll.js +3 -4
- package/dist/cjs/wallet/utils.js +9 -0
- package/dist/cjs/wallet/vtxo-manager.js +31 -89
- package/dist/cjs/wallet/wallet.js +11 -13
- package/dist/esm/forfeit.js +1 -4
- package/dist/esm/identity/singleKey.js +3 -4
- package/dist/esm/index.js +3 -3
- package/dist/esm/intent/index.js +2 -7
- package/dist/esm/providers/onchain.js +19 -20
- package/dist/esm/repositories/walletRepository.js +64 -2
- package/dist/esm/script/base.js +11 -2
- package/dist/esm/utils/arkTransaction.js +3 -5
- package/dist/esm/utils/transaction.js +24 -0
- package/dist/esm/wallet/onchain.js +3 -3
- package/dist/esm/wallet/serviceWorker/worker.js +21 -4
- package/dist/esm/wallet/unroll.js +4 -5
- package/dist/esm/wallet/utils.js +8 -0
- package/dist/esm/wallet/vtxo-manager.js +30 -85
- package/dist/esm/wallet/wallet.js +12 -14
- package/dist/types/forfeit.d.ts +1 -1
- package/dist/types/identity/index.d.ts +1 -1
- package/dist/types/identity/singleKey.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/intent/index.d.ts +1 -1
- package/dist/types/providers/onchain.d.ts +6 -2
- package/dist/types/repositories/walletRepository.d.ts +9 -1
- package/dist/types/script/base.d.ts +2 -0
- package/dist/types/utils/arkTransaction.d.ts +1 -3
- package/dist/types/utils/transaction.d.ts +13 -0
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/worker.d.ts +4 -0
- package/dist/types/wallet/unroll.d.ts +1 -1
- package/dist/types/wallet/utils.d.ts +2 -1
- package/dist/types/wallet/vtxo-manager.d.ts +7 -35
- package/package.json +1 -1
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = void 0;
|
|
4
4
|
exports.isVtxoExpiringSoon = isVtxoExpiringSoon;
|
|
5
|
-
exports.
|
|
6
|
-
exports.calculateExpiryThreshold = calculateExpiryThreshold;
|
|
7
|
-
exports.getMinimumExpiry = getMinimumExpiry;
|
|
8
|
-
exports.calculateDynamicThreshold = calculateDynamicThreshold;
|
|
5
|
+
exports.getExpiringAndRecoverableVtxos = getExpiringAndRecoverableVtxos;
|
|
9
6
|
const _1 = require(".");
|
|
10
7
|
/**
|
|
11
8
|
* Default renewal configuration values
|
|
@@ -13,6 +10,9 @@ const _1 = require(".");
|
|
|
13
10
|
exports.DEFAULT_RENEWAL_CONFIG = {
|
|
14
11
|
thresholdPercentage: 10,
|
|
15
12
|
};
|
|
13
|
+
function getDustAmount(wallet) {
|
|
14
|
+
return "dustAmount" in wallet ? wallet.dustAmount : 330n;
|
|
15
|
+
}
|
|
16
16
|
/**
|
|
17
17
|
* Filter VTXOs that are recoverable (swept and still spendable, or preconfirmed subdust)
|
|
18
18
|
*
|
|
@@ -82,80 +82,35 @@ function getRecoverableWithSubdust(vtxos, dustAmount) {
|
|
|
82
82
|
* @param thresholdMs - Threshold in milliseconds from now
|
|
83
83
|
* @returns true if VTXO expires within threshold, false otherwise
|
|
84
84
|
*/
|
|
85
|
-
function isVtxoExpiringSoon(vtxo,
|
|
85
|
+
function isVtxoExpiringSoon(vtxo, percentage) {
|
|
86
86
|
const { batchExpiry } = vtxo.virtualStatus;
|
|
87
|
-
// No expiry set means it doesn't expire
|
|
88
87
|
if (!batchExpiry) {
|
|
89
|
-
return false;
|
|
88
|
+
return false; // it doesn't expire
|
|
90
89
|
}
|
|
91
90
|
const now = Date.now();
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
if (batchExpiry <= now) {
|
|
92
|
+
return false; // already expired
|
|
93
|
+
}
|
|
94
|
+
// It shouldn't happen, but let's be safe
|
|
95
|
+
if (!vtxo.createdAt) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const duration = batchExpiry - vtxo.createdAt.getTime();
|
|
99
|
+
const softExpiry = batchExpiry - (duration * percentage) / 100;
|
|
100
|
+
return softExpiry > 0 && softExpiry <= now;
|
|
94
101
|
}
|
|
95
102
|
/**
|
|
96
|
-
* Filter VTXOs that are expiring soon
|
|
103
|
+
* Filter VTXOs that are expiring soon or are recoverable/subdust
|
|
97
104
|
*
|
|
98
105
|
* @param vtxos - Array of virtual coins to check
|
|
99
106
|
* @param thresholdMs - Threshold in milliseconds from now
|
|
107
|
+
* @param dustAmount - Dust threshold amount in satoshis
|
|
100
108
|
* @returns Array of VTXOs expiring within threshold
|
|
101
109
|
*/
|
|
102
|
-
function
|
|
103
|
-
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
* Calculate expiry threshold in milliseconds based on batch expiry and percentage
|
|
107
|
-
*
|
|
108
|
-
* @param batchExpiry - Batch expiry timestamp in milliseconds
|
|
109
|
-
* @param percentage - Percentage of total time (0-100)
|
|
110
|
-
* @returns Threshold timestamp in milliseconds from now
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* // VTXO expires in 10 days, threshold is 10%
|
|
114
|
-
* const expiry = Date.now() + 10 * 24 * 60 * 60 * 1000;
|
|
115
|
-
* const threshold = calculateExpiryThreshold(expiry, 10);
|
|
116
|
-
* // Returns 1 day in milliseconds (10% of 10 days)
|
|
117
|
-
*/
|
|
118
|
-
function calculateExpiryThreshold(batchExpiry, percentage) {
|
|
119
|
-
if (percentage < 0 || percentage > 100) {
|
|
120
|
-
throw new Error("Percentage must be between 0 and 100");
|
|
121
|
-
}
|
|
122
|
-
const now = Date.now();
|
|
123
|
-
const totalTime = batchExpiry - now;
|
|
124
|
-
if (totalTime <= 0) {
|
|
125
|
-
// Already expired
|
|
126
|
-
return 0;
|
|
127
|
-
}
|
|
128
|
-
// Calculate threshold as percentage of total time
|
|
129
|
-
return Math.floor((totalTime * percentage) / 100);
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Get the minimum expiry time from a list of VTXOs
|
|
133
|
-
*
|
|
134
|
-
* @param vtxos - Array of virtual coins
|
|
135
|
-
* @returns Minimum batch expiry timestamp, or undefined if no VTXOs have expiry
|
|
136
|
-
*/
|
|
137
|
-
function getMinimumExpiry(vtxos) {
|
|
138
|
-
const expiries = vtxos
|
|
139
|
-
.map((v) => v.virtualStatus.batchExpiry)
|
|
140
|
-
.filter((e) => e !== undefined);
|
|
141
|
-
if (expiries.length === 0) {
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
return Math.min(...expiries);
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Calculate dynamic threshold based on the earliest expiring VTXO
|
|
148
|
-
*
|
|
149
|
-
* @param vtxos - Array of virtual coins
|
|
150
|
-
* @param percentage - Percentage of time until expiry (0-100)
|
|
151
|
-
* @returns Threshold in milliseconds, or undefined if no VTXOs have expiry
|
|
152
|
-
*/
|
|
153
|
-
function calculateDynamicThreshold(vtxos, percentage) {
|
|
154
|
-
const minExpiry = getMinimumExpiry(vtxos);
|
|
155
|
-
if (!minExpiry) {
|
|
156
|
-
return undefined;
|
|
157
|
-
}
|
|
158
|
-
return calculateExpiryThreshold(minExpiry, percentage);
|
|
110
|
+
function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
|
|
111
|
+
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, percentage) ||
|
|
112
|
+
(0, _1.isRecoverable)(vtxo) ||
|
|
113
|
+
(0, _1.isSubdust)(vtxo, dustAmount));
|
|
159
114
|
}
|
|
160
115
|
/**
|
|
161
116
|
* VtxoManager is a unified class for managing VTXO lifecycle operations including
|
|
@@ -236,9 +191,7 @@ class VtxoManager {
|
|
|
236
191
|
withUnrolled: false,
|
|
237
192
|
});
|
|
238
193
|
// Get dust amount from wallet
|
|
239
|
-
const dustAmount =
|
|
240
|
-
? this.wallet.dustAmount
|
|
241
|
-
: 1000n;
|
|
194
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
242
195
|
// Filter recoverable VTXOs and handle subdust logic
|
|
243
196
|
const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
|
|
244
197
|
if (vtxosToRecover.length === 0) {
|
|
@@ -281,9 +234,7 @@ class VtxoManager {
|
|
|
281
234
|
withRecoverable: true,
|
|
282
235
|
withUnrolled: false,
|
|
283
236
|
});
|
|
284
|
-
const dustAmount =
|
|
285
|
-
? this.wallet.dustAmount
|
|
286
|
-
: 1000n;
|
|
237
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
287
238
|
const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
|
|
288
239
|
// Calculate subdust amount separately for reporting
|
|
289
240
|
const subdustAmount = vtxosToRecover
|
|
@@ -313,23 +264,16 @@ class VtxoManager {
|
|
|
313
264
|
* ```
|
|
314
265
|
*/
|
|
315
266
|
async getExpiringVtxos(thresholdPercentage) {
|
|
316
|
-
|
|
317
|
-
return [];
|
|
318
|
-
}
|
|
319
|
-
const vtxos = await this.wallet.getVtxos();
|
|
267
|
+
const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
|
|
320
268
|
const percentage = thresholdPercentage ??
|
|
321
|
-
this.renewalConfig
|
|
269
|
+
this.renewalConfig?.thresholdPercentage ??
|
|
322
270
|
exports.DEFAULT_RENEWAL_CONFIG.thresholdPercentage;
|
|
323
|
-
|
|
324
|
-
if (!threshold) {
|
|
325
|
-
return [];
|
|
326
|
-
}
|
|
327
|
-
return getExpiringVtxos(vtxos, threshold);
|
|
271
|
+
return getExpiringAndRecoverableVtxos(vtxos, percentage, getDustAmount(this.wallet));
|
|
328
272
|
}
|
|
329
273
|
/**
|
|
330
|
-
* Renew VTXOs by settling them back to the wallet's address
|
|
274
|
+
* Renew expiring VTXOs by settling them back to the wallet's address
|
|
331
275
|
*
|
|
332
|
-
* This method collects all spendable VTXOs (including recoverable ones) and settles
|
|
276
|
+
* This method collects all expiring spendable VTXOs (including recoverable ones) and settles
|
|
333
277
|
* them back to the wallet, effectively refreshing their expiration time. This is the
|
|
334
278
|
* primary way to prevent VTXOs from expiring.
|
|
335
279
|
*
|
|
@@ -353,15 +297,13 @@ class VtxoManager {
|
|
|
353
297
|
*/
|
|
354
298
|
async renewVtxos(eventCallback) {
|
|
355
299
|
// Get all VTXOs (including recoverable ones)
|
|
356
|
-
const vtxos = await this.
|
|
300
|
+
const vtxos = await this.getExpiringVtxos();
|
|
357
301
|
if (vtxos.length === 0) {
|
|
358
302
|
throw new Error("No VTXOs available to renew");
|
|
359
303
|
}
|
|
360
304
|
const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
361
305
|
// Get dust amount from wallet
|
|
362
|
-
const dustAmount =
|
|
363
|
-
? this.wallet.dustAmount
|
|
364
|
-
: 1000n;
|
|
306
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
365
307
|
// Check if total amount is above dust threshold
|
|
366
308
|
if (BigInt(totalAmount) < dustAmount) {
|
|
367
309
|
throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
|
|
@@ -370,15 +370,12 @@ class Wallet {
|
|
|
370
370
|
async getBoardingUtxos() {
|
|
371
371
|
const boardingAddress = await this.getBoardingAddress();
|
|
372
372
|
const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
intentTapLeafScript: exit,
|
|
380
|
-
tapTree: encodedBoardingTapscript,
|
|
381
|
-
}));
|
|
373
|
+
const utxos = boardingUtxos.map((utxo) => {
|
|
374
|
+
return (0, utils_1.extendCoin)(this, utxo);
|
|
375
|
+
});
|
|
376
|
+
// Save boardingUtxos using unified repository
|
|
377
|
+
await this.walletRepository.saveUtxos(boardingAddress, utxos);
|
|
378
|
+
return utxos;
|
|
382
379
|
}
|
|
383
380
|
async sendBitcoin(params) {
|
|
384
381
|
if (params.amount <= 0) {
|
|
@@ -798,8 +795,6 @@ class Wallet {
|
|
|
798
795
|
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
799
796
|
// boarding utxo, we need to sign the settlement tx
|
|
800
797
|
if (!vtxo) {
|
|
801
|
-
hasBoardingUtxos = true;
|
|
802
|
-
const inputIndexes = [];
|
|
803
798
|
for (let i = 0; i < settlementPsbt.inputsLength; i++) {
|
|
804
799
|
const settlementInput = settlementPsbt.getInput(i);
|
|
805
800
|
if (!settlementInput.txid ||
|
|
@@ -815,9 +810,12 @@ class Wallet {
|
|
|
815
810
|
settlementPsbt.updateInput(i, {
|
|
816
811
|
tapLeafScript: [input.forfeitTapLeafScript],
|
|
817
812
|
});
|
|
818
|
-
|
|
813
|
+
settlementPsbt = await this.identity.sign(settlementPsbt, [
|
|
814
|
+
i,
|
|
815
|
+
]);
|
|
816
|
+
hasBoardingUtxos = true;
|
|
817
|
+
break;
|
|
819
818
|
}
|
|
820
|
-
settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
|
|
821
819
|
continue;
|
|
822
820
|
}
|
|
823
821
|
if ((0, _1.isRecoverable)(vtxo) || (0, _1.isSubdust)(vtxo, this.dustAmount)) {
|
package/dist/esm/forfeit.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { Transaction } from
|
|
1
|
+
import { Transaction } from './utils/transaction.js';
|
|
2
2
|
import { P2A } from './utils/anchor.js';
|
|
3
3
|
export function buildForfeitTx(inputs, forfeitPkScript, txLocktime) {
|
|
4
4
|
const tx = new Transaction({
|
|
5
5
|
version: 3,
|
|
6
6
|
lockTime: txLocktime,
|
|
7
|
-
allowUnknownOutputs: true,
|
|
8
|
-
allowUnknown: true,
|
|
9
|
-
allowUnknownInputs: true,
|
|
10
7
|
});
|
|
11
8
|
let amount = 0n;
|
|
12
9
|
for (const input of inputs) {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { pubECDSA, pubSchnorr, randomPrivateKeyBytes, } from "@scure/btc-signer/utils.js";
|
|
2
|
-
import { SigHash } from "@scure/btc-signer
|
|
2
|
+
import { SigHash } from "@scure/btc-signer";
|
|
3
3
|
import { hex } from "@scure/base";
|
|
4
4
|
import { TreeSignerSession } from '../tree/signingSession.js';
|
|
5
5
|
import { schnorr, sign } from "@noble/secp256k1";
|
|
6
|
-
const ZERO_32 = new Uint8Array(32).fill(0);
|
|
7
6
|
const ALL_SIGHASH = Object.values(SigHash).filter((x) => typeof x === "number");
|
|
8
7
|
/**
|
|
9
8
|
* In-memory single key implementation for Bitcoin transaction signing.
|
|
@@ -48,7 +47,7 @@ export class SingleKey {
|
|
|
48
47
|
const txCpy = tx.clone();
|
|
49
48
|
if (!inputIndexes) {
|
|
50
49
|
try {
|
|
51
|
-
if (!txCpy.sign(this.key, ALL_SIGHASH
|
|
50
|
+
if (!txCpy.sign(this.key, ALL_SIGHASH)) {
|
|
52
51
|
throw new Error("Failed to sign transaction");
|
|
53
52
|
}
|
|
54
53
|
}
|
|
@@ -64,7 +63,7 @@ export class SingleKey {
|
|
|
64
63
|
return txCpy;
|
|
65
64
|
}
|
|
66
65
|
for (const inputIndex of inputIndexes) {
|
|
67
|
-
if (!txCpy.signIdx(this.key, inputIndex, ALL_SIGHASH
|
|
66
|
+
if (!txCpy.signIdx(this.key, inputIndex, ALL_SIGHASH)) {
|
|
68
67
|
throw new Error(`Failed to sign input #${inputIndex}`);
|
|
69
68
|
}
|
|
70
69
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Transaction } from
|
|
1
|
+
import { Transaction } from './utils/transaction.js';
|
|
2
2
|
import { SingleKey } from './identity/singleKey.js';
|
|
3
3
|
import { ArkAddress } from './script/address.js';
|
|
4
4
|
import { VHTLC } from './script/vhtlc.js';
|
|
5
5
|
import { DefaultVtxo } from './script/default.js';
|
|
6
|
-
import { VtxoScript } from './script/base.js';
|
|
6
|
+
import { VtxoScript, TapTreeCoder, } from './script/base.js';
|
|
7
7
|
import { TxType, } from './wallet/index.js';
|
|
8
8
|
import { Wallet, waitForIncomingFunds } from './wallet/wallet.js';
|
|
9
9
|
import { TxTree } from './tree/txTree.js';
|
|
@@ -41,7 +41,7 @@ TxType, IndexerTxType, ChainTxType, SettlementEventType,
|
|
|
41
41
|
// Service Worker
|
|
42
42
|
setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response,
|
|
43
43
|
// Tapscript
|
|
44
|
-
decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript,
|
|
44
|
+
decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder,
|
|
45
45
|
// Ark PSBT fields
|
|
46
46
|
ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness,
|
|
47
47
|
// Utils
|
package/dist/esm/intent/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { OP,
|
|
1
|
+
import { OP, Script, SigHash } from "@scure/btc-signer";
|
|
2
2
|
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
3
|
+
import { Transaction } from '../utils/transaction.js';
|
|
3
4
|
/**
|
|
4
5
|
* Intent proof implementation for Bitcoin message signing.
|
|
5
6
|
*
|
|
@@ -84,9 +85,6 @@ function craftToSpendTx(message, pkScript) {
|
|
|
84
85
|
const messageHash = hashMessage(message);
|
|
85
86
|
const tx = new Transaction({
|
|
86
87
|
version: 0,
|
|
87
|
-
allowUnknownOutputs: true,
|
|
88
|
-
allowUnknown: true,
|
|
89
|
-
allowUnknownInputs: true,
|
|
90
88
|
});
|
|
91
89
|
// add input with zero hash and max index
|
|
92
90
|
tx.addInput({
|
|
@@ -109,9 +107,6 @@ function craftToSignTx(toSpend, inputs, outputs) {
|
|
|
109
107
|
const firstInput = inputs[0];
|
|
110
108
|
const tx = new Transaction({
|
|
111
109
|
version: 2,
|
|
112
|
-
allowUnknownOutputs: outputs.length === 0,
|
|
113
|
-
allowUnknown: true,
|
|
114
|
-
allowUnknownInputs: true,
|
|
115
110
|
lockTime: 0,
|
|
116
111
|
});
|
|
117
112
|
// add the first "toSpend" input
|
|
@@ -18,9 +18,10 @@ export const ESPLORA_URL = {
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export class EsploraProvider {
|
|
21
|
-
constructor(baseUrl) {
|
|
21
|
+
constructor(baseUrl, opts) {
|
|
22
22
|
this.baseUrl = baseUrl;
|
|
23
|
-
this.
|
|
23
|
+
this.pollingInterval = opts?.pollingInterval ?? 15000;
|
|
24
|
+
this.forcePolling = opts?.forcePolling ?? false;
|
|
24
25
|
}
|
|
25
26
|
async getCoins(address) {
|
|
26
27
|
const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
|
|
@@ -91,13 +92,9 @@ export class EsploraProvider {
|
|
|
91
92
|
let intervalId = null;
|
|
92
93
|
const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
|
|
93
94
|
const poll = async () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// websocket is not reliable, so we will fallback to polling
|
|
98
|
-
const pollingInterval = 5000; // 5 seconds
|
|
99
|
-
const getAllTxs = () => {
|
|
100
|
-
return Promise.all(addresses.map((address) => this.getTransactions(address))).then((txArrays) => txArrays.flat());
|
|
95
|
+
const getAllTxs = async () => {
|
|
96
|
+
const txArrays = await Promise.all(addresses.map((address) => this.getTransactions(address)));
|
|
97
|
+
return txArrays.flat();
|
|
101
98
|
};
|
|
102
99
|
// initial fetch to get existing transactions
|
|
103
100
|
const initialTxs = await getAllTxs();
|
|
@@ -122,9 +119,19 @@ export class EsploraProvider {
|
|
|
122
119
|
catch (error) {
|
|
123
120
|
console.error("Error in polling mechanism:", error);
|
|
124
121
|
}
|
|
125
|
-
}, pollingInterval);
|
|
122
|
+
}, this.pollingInterval);
|
|
126
123
|
};
|
|
127
124
|
let ws = null;
|
|
125
|
+
const stopFunc = () => {
|
|
126
|
+
if (ws)
|
|
127
|
+
ws.close();
|
|
128
|
+
if (intervalId)
|
|
129
|
+
clearInterval(intervalId);
|
|
130
|
+
};
|
|
131
|
+
if (this.forcePolling) {
|
|
132
|
+
await poll();
|
|
133
|
+
return stopFunc;
|
|
134
|
+
}
|
|
128
135
|
try {
|
|
129
136
|
ws = new WebSocket(wsUrl);
|
|
130
137
|
ws.addEventListener("open", () => {
|
|
@@ -171,13 +178,6 @@ export class EsploraProvider {
|
|
|
171
178
|
// if websocket is not available, fallback to polling
|
|
172
179
|
await poll();
|
|
173
180
|
}
|
|
174
|
-
const stopFunc = () => {
|
|
175
|
-
if (ws && ws.readyState === WebSocket.OPEN)
|
|
176
|
-
ws.close();
|
|
177
|
-
if (intervalId)
|
|
178
|
-
clearInterval(intervalId);
|
|
179
|
-
this.polling = false;
|
|
180
|
-
};
|
|
181
181
|
return stopFunc;
|
|
182
182
|
}
|
|
183
183
|
async getChainTip() {
|
|
@@ -245,8 +245,7 @@ const isExplorerTransaction = (tx) => {
|
|
|
245
245
|
return (typeof tx.txid === "string" &&
|
|
246
246
|
Array.isArray(tx.vout) &&
|
|
247
247
|
tx.vout.every((vout) => typeof vout.scriptpubkey_address === "string" &&
|
|
248
|
-
typeof vout.value === "
|
|
248
|
+
typeof vout.value === "number") &&
|
|
249
249
|
typeof tx.status === "object" &&
|
|
250
|
-
typeof tx.status.confirmed === "boolean"
|
|
251
|
-
typeof tx.status.block_time === "number");
|
|
250
|
+
typeof tx.status.confirmed === "boolean");
|
|
252
251
|
};
|
|
@@ -12,7 +12,14 @@ const serializeVtxo = (v) => ({
|
|
|
12
12
|
tapTree: toHex(v.tapTree),
|
|
13
13
|
forfeitTapLeafScript: serializeTapLeaf(v.forfeitTapLeafScript),
|
|
14
14
|
intentTapLeafScript: serializeTapLeaf(v.intentTapLeafScript),
|
|
15
|
-
extraWitness: v.extraWitness?.map(
|
|
15
|
+
extraWitness: v.extraWitness?.map(toHex),
|
|
16
|
+
});
|
|
17
|
+
const serializeUtxo = (u) => ({
|
|
18
|
+
...u,
|
|
19
|
+
tapTree: toHex(u.tapTree),
|
|
20
|
+
forfeitTapLeafScript: serializeTapLeaf(u.forfeitTapLeafScript),
|
|
21
|
+
intentTapLeafScript: serializeTapLeaf(u.intentTapLeafScript),
|
|
22
|
+
extraWitness: u.extraWitness?.map(toHex),
|
|
16
23
|
});
|
|
17
24
|
const deserializeTapLeaf = (t) => {
|
|
18
25
|
const cb = TaprootControlBlock.decode(fromHex(t.cb));
|
|
@@ -24,13 +31,21 @@ const deserializeVtxo = (o) => ({
|
|
|
24
31
|
tapTree: fromHex(o.tapTree),
|
|
25
32
|
forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
|
|
26
33
|
intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
|
|
27
|
-
extraWitness: o.extraWitness?.map(
|
|
34
|
+
extraWitness: o.extraWitness?.map(fromHex),
|
|
35
|
+
});
|
|
36
|
+
const deserializeUtxo = (o) => ({
|
|
37
|
+
...o,
|
|
38
|
+
tapTree: fromHex(o.tapTree),
|
|
39
|
+
forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
|
|
40
|
+
intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
|
|
41
|
+
extraWitness: o.extraWitness?.map(fromHex),
|
|
28
42
|
});
|
|
29
43
|
export class WalletRepositoryImpl {
|
|
30
44
|
constructor(storage) {
|
|
31
45
|
this.storage = storage;
|
|
32
46
|
this.cache = {
|
|
33
47
|
vtxos: new Map(),
|
|
48
|
+
utxos: new Map(),
|
|
34
49
|
transactions: new Map(),
|
|
35
50
|
walletState: null,
|
|
36
51
|
initialized: new Set(),
|
|
@@ -83,6 +98,53 @@ export class WalletRepositoryImpl {
|
|
|
83
98
|
this.cache.vtxos.set(address, []);
|
|
84
99
|
await this.storage.removeItem(`vtxos:${address}`);
|
|
85
100
|
}
|
|
101
|
+
async getUtxos(address) {
|
|
102
|
+
const cacheKey = `utxos:${address}`;
|
|
103
|
+
if (this.cache.utxos.has(address)) {
|
|
104
|
+
return this.cache.utxos.get(address);
|
|
105
|
+
}
|
|
106
|
+
const stored = await this.storage.getItem(cacheKey);
|
|
107
|
+
if (!stored) {
|
|
108
|
+
this.cache.utxos.set(address, []);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(stored);
|
|
113
|
+
const utxos = parsed.map(deserializeUtxo);
|
|
114
|
+
this.cache.utxos.set(address, utxos.slice());
|
|
115
|
+
return utxos.slice();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(`Failed to parse UTXOs for address ${address}:`, error);
|
|
119
|
+
this.cache.utxos.set(address, []);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async saveUtxos(address, utxos) {
|
|
124
|
+
const storedUtxos = await this.getUtxos(address);
|
|
125
|
+
utxos.forEach((utxo) => {
|
|
126
|
+
const existing = storedUtxos.findIndex((u) => u.txid === utxo.txid && u.vout === utxo.vout);
|
|
127
|
+
if (existing !== -1) {
|
|
128
|
+
storedUtxos[existing] = utxo;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
storedUtxos.push(utxo);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
this.cache.utxos.set(address, storedUtxos.slice());
|
|
135
|
+
await this.storage.setItem(`utxos:${address}`, JSON.stringify(storedUtxos.map(serializeUtxo)));
|
|
136
|
+
}
|
|
137
|
+
async removeUtxo(address, utxoId) {
|
|
138
|
+
const utxos = await this.getUtxos(address);
|
|
139
|
+
const [txid, vout] = utxoId.split(":");
|
|
140
|
+
const filtered = utxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
|
|
141
|
+
this.cache.utxos.set(address, filtered.slice());
|
|
142
|
+
await this.storage.setItem(`utxos:${address}`, JSON.stringify(filtered.map(serializeUtxo)));
|
|
143
|
+
}
|
|
144
|
+
async clearUtxos(address) {
|
|
145
|
+
this.cache.utxos.set(address, []);
|
|
146
|
+
await this.storage.removeItem(`utxos:${address}`);
|
|
147
|
+
}
|
|
86
148
|
async getTransactionHistory(address) {
|
|
87
149
|
const cacheKey = `tx:${address}`;
|
|
88
150
|
if (this.cache.transactions.has(address)) {
|
package/dist/esm/script/base.js
CHANGED
|
@@ -4,7 +4,7 @@ import { PSBTOutput } from "@scure/btc-signer/psbt.js";
|
|
|
4
4
|
import { hex } from "@scure/base";
|
|
5
5
|
import { ArkAddress } from './address.js';
|
|
6
6
|
import { ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
|
|
7
|
-
const TapTreeCoder = PSBTOutput.tapTree[2];
|
|
7
|
+
export const TapTreeCoder = PSBTOutput.tapTree[2];
|
|
8
8
|
export function scriptFromTapLeafScript(leaf) {
|
|
9
9
|
return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
|
|
10
10
|
}
|
|
@@ -24,7 +24,16 @@ export class VtxoScript {
|
|
|
24
24
|
}
|
|
25
25
|
constructor(scripts) {
|
|
26
26
|
this.scripts = scripts;
|
|
27
|
-
|
|
27
|
+
// reverse the scripts if the number of scripts is odd
|
|
28
|
+
// this is to be compatible with arkd algorithm computing taproot tree from list of tapscripts
|
|
29
|
+
// the scripts must be reversed only HERE while we compute the tweaked public key
|
|
30
|
+
// but the original order should be preserved while encoding as taptree
|
|
31
|
+
// note: .slice().reverse() is used instead of .reverse() to avoid mutating the original array
|
|
32
|
+
const list = scripts.length % 2 !== 0 ? scripts.slice().reverse() : scripts;
|
|
33
|
+
const tapTree = taprootListToTree(list.map((script) => ({
|
|
34
|
+
script,
|
|
35
|
+
leafVersion: TAP_LEAF_VERSION,
|
|
36
|
+
})));
|
|
28
37
|
const payment = p2tr(TAPROOT_UNSPENDABLE_KEY, tapTree, undefined, true);
|
|
29
38
|
if (!payment.tapLeafScript ||
|
|
30
39
|
payment.tapLeafScript.length !== scripts.length) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
2
2
|
import { hex } from "@scure/base";
|
|
3
|
-
import { DEFAULT_SEQUENCE,
|
|
3
|
+
import { DEFAULT_SEQUENCE, SigHash } from "@scure/btc-signer";
|
|
4
4
|
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
5
5
|
import { CLTVMultisigTapscript, decodeTapscript, } from '../script/tapscript.js';
|
|
6
6
|
import { scriptFromTapLeafScript, VtxoScript, } from '../script/base.js';
|
|
7
7
|
import { P2A } from './anchor.js';
|
|
8
8
|
import { setArkPsbtField, VtxoTaprootTree } from './unknownFields.js';
|
|
9
|
+
import { Transaction } from './transaction.js';
|
|
9
10
|
/**
|
|
10
11
|
* Builds an offchain transaction with checkpoint transactions.
|
|
11
12
|
*
|
|
@@ -45,8 +46,6 @@ function buildVirtualTx(inputs, outputs) {
|
|
|
45
46
|
}
|
|
46
47
|
const tx = new Transaction({
|
|
47
48
|
version: 3,
|
|
48
|
-
allowUnknown: true,
|
|
49
|
-
allowUnknownOutputs: true,
|
|
50
49
|
lockTime: Number(lockTime),
|
|
51
50
|
});
|
|
52
51
|
for (const [i, input] of inputs.entries()) {
|
|
@@ -71,8 +70,7 @@ function buildVirtualTx(inputs, outputs) {
|
|
|
71
70
|
}
|
|
72
71
|
function buildCheckpointTx(vtxo, serverUnrollScript) {
|
|
73
72
|
// create the checkpoint vtxo script from collaborative closure
|
|
74
|
-
const collaborativeClosure = decodeTapscript(vtxo.
|
|
75
|
-
scriptFromTapLeafScript(vtxo.tapLeafScript));
|
|
73
|
+
const collaborativeClosure = decodeTapscript(scriptFromTapLeafScript(vtxo.tapLeafScript));
|
|
76
74
|
// create the checkpoint vtxo script combining collaborative closure and server unroll script
|
|
77
75
|
const checkpointVtxoScript = new VtxoScript([
|
|
78
76
|
serverUnrollScript.script,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Transaction as BtcSignerTransaction } from "@scure/btc-signer";
|
|
2
|
+
/**
|
|
3
|
+
* Transaction is a wrapper around the @scure/btc-signer Transaction class.
|
|
4
|
+
* It adds the Ark protocol specific options to the transaction.
|
|
5
|
+
*/
|
|
6
|
+
export class Transaction extends BtcSignerTransaction {
|
|
7
|
+
constructor(opts) {
|
|
8
|
+
super(withArkOpts(opts));
|
|
9
|
+
}
|
|
10
|
+
static fromPSBT(psbt_, opts) {
|
|
11
|
+
return BtcSignerTransaction.fromPSBT(psbt_, withArkOpts(opts));
|
|
12
|
+
}
|
|
13
|
+
static fromRaw(raw, opts) {
|
|
14
|
+
return BtcSignerTransaction.fromRaw(raw, withArkOpts(opts));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
Transaction.ARK_TX_OPTS = {
|
|
18
|
+
allowUnknown: true,
|
|
19
|
+
allowUnknownOutputs: true,
|
|
20
|
+
allowUnknownInputs: true,
|
|
21
|
+
};
|
|
22
|
+
function withArkOpts(opts) {
|
|
23
|
+
return { ...Transaction.ARK_TX_OPTS, ...opts };
|
|
24
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { p2tr } from "@scure/btc-signer";
|
|
2
2
|
import { getNetwork } from '../networks.js';
|
|
3
3
|
import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
4
4
|
import { findP2AOutput, P2A } from '../utils/anchor.js';
|
|
5
5
|
import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
|
|
6
|
+
import { Transaction } from '../utils/transaction.js';
|
|
6
7
|
/**
|
|
7
8
|
* Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
|
|
8
9
|
*
|
|
@@ -104,9 +105,8 @@ export class OnchainWallet {
|
|
|
104
105
|
async bumpP2A(parent) {
|
|
105
106
|
const parentVsize = parent.vsize;
|
|
106
107
|
let child = new Transaction({
|
|
107
|
-
allowUnknownInputs: true,
|
|
108
|
-
allowLegacyWitnessUtxo: true,
|
|
109
108
|
version: 3,
|
|
109
|
+
allowLegacyWitnessUtxo: true,
|
|
110
110
|
});
|
|
111
111
|
child.addInput(findP2AOutput(parent)); // throws if not found
|
|
112
112
|
const childVsize = TxWeightEstimator.create()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference lib="webworker" />
|
|
2
2
|
import { SingleKey } from '../../identity/singleKey.js';
|
|
3
|
-
import { isRecoverable, isSpendable, isSubdust } from '../index.js';
|
|
3
|
+
import { isRecoverable, isSpendable, isSubdust, } from '../index.js';
|
|
4
4
|
import { Wallet } from '../wallet.js';
|
|
5
5
|
import { Request } from './request.js';
|
|
6
6
|
import { Response } from './response.js';
|
|
@@ -10,7 +10,7 @@ import { RestIndexerProvider } from '../../providers/indexer.js';
|
|
|
10
10
|
import { hex } from "@scure/base";
|
|
11
11
|
import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
|
|
12
12
|
import { WalletRepositoryImpl, } from '../../repositories/walletRepository.js';
|
|
13
|
-
import { extendVirtualCoin } from '../utils.js';
|
|
13
|
+
import { extendCoin, extendVirtualCoin } from '../utils.js';
|
|
14
14
|
import { DEFAULT_DB_NAME } from './utils.js';
|
|
15
15
|
/**
|
|
16
16
|
* Worker is a class letting to interact with ServiceWorkerWallet from the client
|
|
@@ -57,6 +57,15 @@ export class Worker {
|
|
|
57
57
|
spent: allVtxos.filter((vtxo) => !isSpendable(vtxo)),
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Get all boarding utxos from wallet repository
|
|
62
|
+
*/
|
|
63
|
+
async getAllBoardingUtxos() {
|
|
64
|
+
if (!this.wallet)
|
|
65
|
+
return [];
|
|
66
|
+
const address = await this.wallet.getBoardingAddress();
|
|
67
|
+
return await this.walletRepository.getUtxos(address);
|
|
68
|
+
}
|
|
60
69
|
async start(withServiceWorkerUpdate = true) {
|
|
61
70
|
self.addEventListener("message", async (event) => {
|
|
62
71
|
await this.handleMessage(event);
|
|
@@ -130,6 +139,14 @@ export class Worker {
|
|
|
130
139
|
this.sendMessageToAllClients(Response.vtxoUpdate(newVtxos, spentVtxos));
|
|
131
140
|
}
|
|
132
141
|
if (funds.type === "utxo") {
|
|
142
|
+
const newUtxos = funds.coins.map((utxo) => extendCoin(this.wallet, utxo));
|
|
143
|
+
if (newUtxos.length === 0) {
|
|
144
|
+
this.sendMessageToAllClients(Response.utxoUpdate([]));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const boardingAddress = await this.wallet?.getBoardingAddress();
|
|
148
|
+
// save utxos using unified repository
|
|
149
|
+
await this.walletRepository.saveUtxos(boardingAddress, newUtxos);
|
|
133
150
|
// notify all clients about the utxo update
|
|
134
151
|
this.sendMessageToAllClients(Response.utxoUpdate(funds.coins));
|
|
135
152
|
}
|
|
@@ -288,7 +305,7 @@ export class Worker {
|
|
|
288
305
|
}
|
|
289
306
|
try {
|
|
290
307
|
const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
|
|
291
|
-
this.
|
|
308
|
+
this.getAllBoardingUtxos(),
|
|
292
309
|
this.getSpendableVtxos(),
|
|
293
310
|
this.getSweptVtxos(),
|
|
294
311
|
]);
|
|
@@ -393,7 +410,7 @@ export class Worker {
|
|
|
393
410
|
return;
|
|
394
411
|
}
|
|
395
412
|
try {
|
|
396
|
-
const boardingUtxos = await this.
|
|
413
|
+
const boardingUtxos = await this.getAllBoardingUtxos();
|
|
397
414
|
event.source?.postMessage(Response.boardingUtxos(message.id, boardingUtxos));
|
|
398
415
|
}
|
|
399
416
|
catch (error) {
|