@arkade-os/sdk 0.3.11 → 0.3.13
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 +2 -3
- package/dist/cjs/providers/onchain.js +2 -2
- package/dist/cjs/utils/transactionHistory.js +9 -7
- package/dist/cjs/utils/txSizeEstimator.js +39 -14
- package/dist/cjs/wallet/onchain.js +57 -12
- package/dist/cjs/wallet/unroll.js +7 -2
- package/dist/cjs/wallet/utils.js +2 -0
- package/dist/cjs/wallet/wallet.js +4 -1
- package/dist/esm/providers/onchain.js +2 -2
- package/dist/esm/utils/transactionHistory.js +9 -7
- package/dist/esm/utils/txSizeEstimator.js +39 -14
- package/dist/esm/wallet/onchain.js +57 -12
- package/dist/esm/wallet/unroll.js +7 -2
- package/dist/esm/wallet/utils.js +1 -0
- package/dist/esm/wallet/wallet.js +4 -1
- package/dist/types/utils/transactionHistory.d.ts +1 -1
- package/dist/types/utils/txSizeEstimator.d.ts +12 -2
- package/dist/types/wallet/onchain.d.ts +22 -1
- package/dist/types/wallet/utils.d.ts +1 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -164,9 +164,8 @@ const boardingUtxos = await wallet.getBoardingUtxos()
|
|
|
164
164
|
```typescript
|
|
165
165
|
// Send bitcoin via Ark
|
|
166
166
|
const txid = await wallet.sendBitcoin({
|
|
167
|
-
address: '
|
|
168
|
-
amount: 50000,
|
|
169
|
-
feeRate: 1 // optional, in sats/vbyte
|
|
167
|
+
address: 'ark1qq4...', // ark address
|
|
168
|
+
amount: 50000, // in satoshis
|
|
170
169
|
})
|
|
171
170
|
```
|
|
172
171
|
|
|
@@ -235,14 +235,14 @@ exports.EsploraProvider = EsploraProvider;
|
|
|
235
235
|
function isValidBlocksTip(tip) {
|
|
236
236
|
return (Array.isArray(tip) &&
|
|
237
237
|
tip.every((t) => {
|
|
238
|
-
t &&
|
|
238
|
+
return (t &&
|
|
239
239
|
typeof t === "object" &&
|
|
240
240
|
typeof t.id === "string" &&
|
|
241
241
|
t.id.length > 0 &&
|
|
242
242
|
typeof t.height === "number" &&
|
|
243
243
|
t.height >= 0 &&
|
|
244
244
|
typeof t.mediantime === "number" &&
|
|
245
|
-
t.mediantime > 0;
|
|
245
|
+
t.mediantime > 0);
|
|
246
246
|
}));
|
|
247
247
|
}
|
|
248
248
|
const isExplorerTransaction = (tx) => {
|
|
@@ -16,7 +16,7 @@ const txKey = {
|
|
|
16
16
|
* @param {Set<string>} commitmentsToIgnore - A set of commitment IDs that should be excluded from processing.
|
|
17
17
|
* @return {ExtendedArkTransaction[]} A sorted array of extended Ark transactions, representing the transaction history.
|
|
18
18
|
*/
|
|
19
|
-
function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnore) {
|
|
19
|
+
async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnore, getTxCreatedAt) {
|
|
20
20
|
const fromOldestVtxo = [...vtxos].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
21
21
|
const sent = [];
|
|
22
22
|
let received = [];
|
|
@@ -59,21 +59,23 @@ function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnore) {
|
|
|
59
59
|
if (vtxo.arkTxId &&
|
|
60
60
|
!sent.some((s) => s.key.arkTxid === vtxo.arkTxId)) {
|
|
61
61
|
const changes = fromOldestVtxo.filter((_) => _.txid === vtxo.arkTxId);
|
|
62
|
+
// We want to find all the other VTXOs spent by the same transaction to
|
|
63
|
+
// calculate the full amount of the change.
|
|
64
|
+
const allSpent = fromOldestVtxo.filter((v) => v.arkTxId === vtxo.arkTxId);
|
|
65
|
+
const spentAmount = allSpent.reduce((acc, v) => acc + v.value, 0);
|
|
62
66
|
let txAmount = 0;
|
|
63
67
|
let txTime = 0;
|
|
64
68
|
if (changes.length > 0) {
|
|
65
69
|
const changeAmount = changes.reduce((acc, v) => acc + v.value, 0);
|
|
66
|
-
// We want to find all the other VTXOs spent by the same transaction to
|
|
67
|
-
// calculate the full amount of the change.
|
|
68
|
-
const allSpent = fromOldestVtxo.filter((v) => v.arkTxId === vtxo.arkTxId);
|
|
69
|
-
const spentAmount = allSpent.reduce((acc, v) => acc + v.value, 0);
|
|
70
70
|
txAmount = spentAmount - changeAmount;
|
|
71
71
|
txTime = changes[0].createdAt.getTime();
|
|
72
72
|
}
|
|
73
73
|
else {
|
|
74
|
-
txAmount =
|
|
74
|
+
txAmount = spentAmount;
|
|
75
75
|
// TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
|
|
76
|
-
txTime =
|
|
76
|
+
txTime = getTxCreatedAt
|
|
77
|
+
? await getTxCreatedAt(vtxo.arkTxId)
|
|
78
|
+
: vtxo.createdAt.getTime() + 1;
|
|
77
79
|
}
|
|
78
80
|
sent.push({
|
|
79
81
|
key: { ...txKey, arkTxid: vtxo.arkTxId },
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TxWeightEstimator = void 0;
|
|
4
|
+
const btc_signer_1 = require("@scure/btc-signer");
|
|
5
|
+
/**
|
|
6
|
+
* Calculates the byte size required to store a variable-length integer (VarInt).
|
|
7
|
+
* Bitcoin uses VarInts to compact integer data (like array lengths).
|
|
8
|
+
*
|
|
9
|
+
* @param n - The integer value to check
|
|
10
|
+
* @returns The size in bytes (1, 3, 5, or 9)
|
|
11
|
+
*/
|
|
12
|
+
const getVarIntSize = (n) => {
|
|
13
|
+
if (n < 0xfd)
|
|
14
|
+
return 1;
|
|
15
|
+
if (n <= 0xffff)
|
|
16
|
+
return 3;
|
|
17
|
+
if (n <= 0xffffffff)
|
|
18
|
+
return 5;
|
|
19
|
+
return 9;
|
|
20
|
+
};
|
|
4
21
|
class TxWeightEstimator {
|
|
5
22
|
constructor(hasWitness, inputCount, outputCount, inputSize, inputWitnessSize, outputSize) {
|
|
6
23
|
this.hasWitness = hasWitness;
|
|
@@ -41,16 +58,16 @@ class TxWeightEstimator {
|
|
|
41
58
|
1 +
|
|
42
59
|
leafControlBlockSize;
|
|
43
60
|
this.inputCount++;
|
|
44
|
-
this.inputWitnessSize += leafWitnessSize + controlBlockWitnessSize;
|
|
61
|
+
this.inputWitnessSize += leafWitnessSize + 1 + controlBlockWitnessSize;
|
|
45
62
|
this.inputSize += TxWeightEstimator.INPUT_SIZE;
|
|
46
63
|
this.hasWitness = true;
|
|
47
|
-
this.inputCount++;
|
|
48
64
|
return this;
|
|
49
65
|
}
|
|
50
|
-
|
|
66
|
+
addP2WPKHOutput() {
|
|
51
67
|
this.outputCount++;
|
|
52
68
|
this.outputSize +=
|
|
53
|
-
TxWeightEstimator.OUTPUT_SIZE +
|
|
69
|
+
TxWeightEstimator.OUTPUT_SIZE +
|
|
70
|
+
TxWeightEstimator.P2WPKH_OUTPUT_SIZE;
|
|
54
71
|
return this;
|
|
55
72
|
}
|
|
56
73
|
addP2TROutput() {
|
|
@@ -59,16 +76,24 @@ class TxWeightEstimator {
|
|
|
59
76
|
TxWeightEstimator.OUTPUT_SIZE + TxWeightEstimator.P2TR_OUTPUT_SIZE;
|
|
60
77
|
return this;
|
|
61
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Adds an output given a raw script.
|
|
81
|
+
* Cost = 8 bytes (amount) + varint(scriptLen) + scriptLen
|
|
82
|
+
*/
|
|
83
|
+
addOutputScript(script) {
|
|
84
|
+
this.outputCount++;
|
|
85
|
+
this.outputSize += 8 + getVarIntSize(script.length) + script.length;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Adds an output by decoding the address to get the exact script size.
|
|
90
|
+
*/
|
|
91
|
+
addOutputAddress(address, network) {
|
|
92
|
+
const payment = (0, btc_signer_1.Address)(network).decode(address);
|
|
93
|
+
const script = btc_signer_1.OutScript.encode(payment);
|
|
94
|
+
return this.addOutputScript(script);
|
|
95
|
+
}
|
|
62
96
|
vsize() {
|
|
63
|
-
const getVarIntSize = (n) => {
|
|
64
|
-
if (n < 0xfd)
|
|
65
|
-
return 1;
|
|
66
|
-
if (n < 0xffff)
|
|
67
|
-
return 3;
|
|
68
|
-
if (n < 0xffffffff)
|
|
69
|
-
return 5;
|
|
70
|
-
return 9;
|
|
71
|
-
};
|
|
72
97
|
const inputCount = getVarIntSize(this.inputCount);
|
|
73
98
|
const outputCount = getVarIntSize(this.outputCount);
|
|
74
99
|
// Calculate the size of the transaction without witness data
|
|
@@ -93,7 +118,7 @@ TxWeightEstimator.P2PKH_SCRIPT_SIG_SIZE = 1 + 73 + 1 + 33;
|
|
|
93
118
|
TxWeightEstimator.INPUT_SIZE = 32 + 4 + 1 + 4;
|
|
94
119
|
TxWeightEstimator.BASE_CONTROL_BLOCK_SIZE = 1 + 32;
|
|
95
120
|
TxWeightEstimator.OUTPUT_SIZE = 8 + 1;
|
|
96
|
-
TxWeightEstimator.
|
|
121
|
+
TxWeightEstimator.P2WPKH_OUTPUT_SIZE = 1 + 1 + 20;
|
|
97
122
|
TxWeightEstimator.BASE_TX_SIZE = 8 + 2; // Version + LockTime
|
|
98
123
|
TxWeightEstimator.WITNESS_HEADER_SIZE = 2; // Flag + Marker
|
|
99
124
|
TxWeightEstimator.WITNESS_SCALE_FACTOR = 4;
|
|
@@ -8,6 +8,7 @@ const onchain_1 = require("../providers/onchain");
|
|
|
8
8
|
const anchor_1 = require("../utils/anchor");
|
|
9
9
|
const txSizeEstimator_1 = require("../utils/txSizeEstimator");
|
|
10
10
|
const transaction_1 = require("../utils/transaction");
|
|
11
|
+
const utils_1 = require("./utils");
|
|
11
12
|
/**
|
|
12
13
|
* Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
|
|
13
14
|
*
|
|
@@ -59,11 +60,58 @@ class OnchainWallet {
|
|
|
59
60
|
const onchainTotal = onchainConfirmed + onchainUnconfirmed;
|
|
60
61
|
return onchainTotal;
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Iteratively selects coins and estimates transaction fees until convergence.
|
|
65
|
+
*
|
|
66
|
+
* This method handles the circular dependency between coin selection and fee
|
|
67
|
+
* estimation: the fee depends on transaction size, which depends on the number
|
|
68
|
+
* of inputs (selected coins) and whether a change output is needed.
|
|
69
|
+
*
|
|
70
|
+
* The algorithm iterates up to 10 times, refining the fee estimate based on
|
|
71
|
+
* the actual transaction structure. It resolves dust oscillation loops that
|
|
72
|
+
* occur when the change amount hovers near the dust threshold—adding/removing
|
|
73
|
+
* the change output causes the fee to fluctuate, preventing convergence.
|
|
74
|
+
* When a lower fee is computed (indicating the change output was dropped),
|
|
75
|
+
* the function accepts this state to guarantee termination.
|
|
76
|
+
*
|
|
77
|
+
* @param coins - Available coins to select from
|
|
78
|
+
* @param amount - Target send amount in satoshis
|
|
79
|
+
* @param feeRate - Fee rate in sat/vbyte
|
|
80
|
+
* @param recipientAddress - Destination address for size estimation
|
|
81
|
+
* @returns Selected inputs, change amount, and calculated fee
|
|
82
|
+
* @throws Error if fee estimation fails to converge within max iterations
|
|
83
|
+
*/
|
|
84
|
+
estimateFeesAndSelectCoins(coins, amount, feeRate, recipientAddress) {
|
|
85
|
+
const MAX_ITERATIONS = 10;
|
|
86
|
+
let fee = 0;
|
|
87
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
88
|
+
const totalNeeded = amount + fee;
|
|
89
|
+
const selected = selectCoins(coins, totalNeeded);
|
|
90
|
+
const estimator = txSizeEstimator_1.TxWeightEstimator.create();
|
|
91
|
+
for (const _ of selected.inputs) {
|
|
92
|
+
estimator.addKeySpendInput();
|
|
93
|
+
}
|
|
94
|
+
estimator.addOutputAddress(recipientAddress, this.network);
|
|
95
|
+
if (selected.changeAmount >= BigInt(utils_1.DUST_AMOUNT)) {
|
|
96
|
+
estimator.addOutputAddress(this.address, this.network);
|
|
97
|
+
}
|
|
98
|
+
const newFee = Number(estimator.vsize().value) * feeRate;
|
|
99
|
+
const roundedNewFee = Math.ceil(newFee);
|
|
100
|
+
// Prevent oscillation loops when change falls just below the dust limit.
|
|
101
|
+
// If removing the change output reduces the fee below our budget,
|
|
102
|
+
// we accept the valid transaction state to guarantee convergence.
|
|
103
|
+
if (roundedNewFee <= fee) {
|
|
104
|
+
return { ...selected, fee: roundedNewFee };
|
|
105
|
+
}
|
|
106
|
+
fee = roundedNewFee;
|
|
107
|
+
}
|
|
108
|
+
throw new Error("Fee estimation failed: could not converge");
|
|
109
|
+
}
|
|
62
110
|
async send(params) {
|
|
63
111
|
if (params.amount <= 0) {
|
|
64
112
|
throw new Error("Amount must be positive");
|
|
65
113
|
}
|
|
66
|
-
if (params.amount <
|
|
114
|
+
if (params.amount < utils_1.DUST_AMOUNT) {
|
|
67
115
|
throw new Error("Amount is below dust limit");
|
|
68
116
|
}
|
|
69
117
|
const coins = await this.getCoins();
|
|
@@ -74,15 +122,14 @@ class OnchainWallet {
|
|
|
74
122
|
if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) {
|
|
75
123
|
feeRate = OnchainWallet.MIN_FEE_RATE;
|
|
76
124
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const selected = selectCoins(coins, totalNeeded);
|
|
125
|
+
const { inputs, changeAmount } = this.estimateFeesAndSelectCoins(coins, params.amount, feeRate, params.address);
|
|
126
|
+
if (!inputs) {
|
|
127
|
+
throw new Error("Fee estimation failed");
|
|
128
|
+
}
|
|
82
129
|
// Create transaction
|
|
83
130
|
let tx = new transaction_1.Transaction();
|
|
84
131
|
// Add inputs
|
|
85
|
-
for (const input of
|
|
132
|
+
for (const input of inputs) {
|
|
86
133
|
tx.addInput({
|
|
87
134
|
txid: input.txid,
|
|
88
135
|
index: input.vout,
|
|
@@ -95,9 +142,8 @@ class OnchainWallet {
|
|
|
95
142
|
}
|
|
96
143
|
// Add payment output
|
|
97
144
|
tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
tx.addOutputAddress(this.address, selected.changeAmount, this.network);
|
|
145
|
+
if (changeAmount >= BigInt(utils_1.DUST_AMOUNT)) {
|
|
146
|
+
tx.addOutputAddress(this.address, changeAmount, this.network);
|
|
101
147
|
}
|
|
102
148
|
// Sign inputs and Finalize
|
|
103
149
|
tx = await this.identity.sign(tx);
|
|
@@ -116,7 +162,7 @@ class OnchainWallet {
|
|
|
116
162
|
const childVsize = txSizeEstimator_1.TxWeightEstimator.create()
|
|
117
163
|
.addKeySpendInput(true)
|
|
118
164
|
.addP2AInput()
|
|
119
|
-
.
|
|
165
|
+
.addOutputAddress(this.address, this.network)
|
|
120
166
|
.vsize().value;
|
|
121
167
|
const packageVSize = parentVsize + Number(childVsize);
|
|
122
168
|
let feeRate = await this.provider.getFeeRate();
|
|
@@ -160,7 +206,6 @@ class OnchainWallet {
|
|
|
160
206
|
}
|
|
161
207
|
exports.OnchainWallet = OnchainWallet;
|
|
162
208
|
OnchainWallet.MIN_FEE_RATE = 1; // sat/vbyte
|
|
163
|
-
OnchainWallet.DUST_AMOUNT = 546; // sats
|
|
164
209
|
/**
|
|
165
210
|
* Select coins to reach a target amount, prioritizing those closer to expiry
|
|
166
211
|
* @param coins List of coins to select from
|
|
@@ -8,6 +8,7 @@ const base_2 = require("../script/base");
|
|
|
8
8
|
const txSizeEstimator_1 = require("../utils/txSizeEstimator");
|
|
9
9
|
const wallet_1 = require("./wallet");
|
|
10
10
|
const transaction_1 = require("../utils/transaction");
|
|
11
|
+
const utils_1 = require("./utils");
|
|
11
12
|
var Unroll;
|
|
12
13
|
(function (Unroll) {
|
|
13
14
|
let StepType;
|
|
@@ -203,7 +204,7 @@ var Unroll;
|
|
|
203
204
|
for (const input of inputs) {
|
|
204
205
|
tx.addInput(input);
|
|
205
206
|
}
|
|
206
|
-
txWeightEstimator.
|
|
207
|
+
txWeightEstimator.addOutputAddress(outputAddress, wallet.network);
|
|
207
208
|
let feeRate = await wallet.onchainProvider.getFeeRate();
|
|
208
209
|
if (!feeRate || feeRate < wallet_1.Wallet.MIN_FEE_RATE) {
|
|
209
210
|
feeRate = wallet_1.Wallet.MIN_FEE_RATE;
|
|
@@ -212,7 +213,11 @@ var Unroll;
|
|
|
212
213
|
if (feeAmount > totalAmount) {
|
|
213
214
|
throw new Error("fee amount is greater than the total amount");
|
|
214
215
|
}
|
|
215
|
-
|
|
216
|
+
const sendAmount = totalAmount - feeAmount;
|
|
217
|
+
if (sendAmount < BigInt(utils_1.DUST_AMOUNT)) {
|
|
218
|
+
throw new Error("send amount is less than dust amount");
|
|
219
|
+
}
|
|
220
|
+
tx.addOutputAddress(outputAddress, sendAmount);
|
|
216
221
|
const signedTx = await wallet.identity.sign(tx);
|
|
217
222
|
signedTx.finalize();
|
|
218
223
|
await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
|
package/dist/cjs/wallet/utils.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DUST_AMOUNT = void 0;
|
|
3
4
|
exports.extendVirtualCoin = extendVirtualCoin;
|
|
4
5
|
exports.extendCoin = extendCoin;
|
|
6
|
+
exports.DUST_AMOUNT = 546; // sats
|
|
5
7
|
function extendVirtualCoin(wallet, vtxo) {
|
|
6
8
|
return {
|
|
7
9
|
...vtxo,
|
|
@@ -268,7 +268,10 @@ class ReadonlyWallet {
|
|
|
268
268
|
scripts: [base_1.hex.encode(this.offchainTapscript.pkScript)],
|
|
269
269
|
});
|
|
270
270
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
271
|
-
|
|
271
|
+
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
272
|
+
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
273
|
+
.then((res) => res.vtxos[0]?.createdAt.getTime() || 0);
|
|
274
|
+
return (0, transactionHistory_1.buildTransactionHistory)(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
272
275
|
}
|
|
273
276
|
async getBoardingTxs() {
|
|
274
277
|
const utxos = [];
|
|
@@ -231,14 +231,14 @@ export class EsploraProvider {
|
|
|
231
231
|
function isValidBlocksTip(tip) {
|
|
232
232
|
return (Array.isArray(tip) &&
|
|
233
233
|
tip.every((t) => {
|
|
234
|
-
t &&
|
|
234
|
+
return (t &&
|
|
235
235
|
typeof t === "object" &&
|
|
236
236
|
typeof t.id === "string" &&
|
|
237
237
|
t.id.length > 0 &&
|
|
238
238
|
typeof t.height === "number" &&
|
|
239
239
|
t.height >= 0 &&
|
|
240
240
|
typeof t.mediantime === "number" &&
|
|
241
|
-
t.mediantime > 0;
|
|
241
|
+
t.mediantime > 0);
|
|
242
242
|
}));
|
|
243
243
|
}
|
|
244
244
|
const isExplorerTransaction = (tx) => {
|
|
@@ -13,7 +13,7 @@ const txKey = {
|
|
|
13
13
|
* @param {Set<string>} commitmentsToIgnore - A set of commitment IDs that should be excluded from processing.
|
|
14
14
|
* @return {ExtendedArkTransaction[]} A sorted array of extended Ark transactions, representing the transaction history.
|
|
15
15
|
*/
|
|
16
|
-
export function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnore) {
|
|
16
|
+
export async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnore, getTxCreatedAt) {
|
|
17
17
|
const fromOldestVtxo = [...vtxos].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
18
18
|
const sent = [];
|
|
19
19
|
let received = [];
|
|
@@ -56,21 +56,23 @@ export function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgno
|
|
|
56
56
|
if (vtxo.arkTxId &&
|
|
57
57
|
!sent.some((s) => s.key.arkTxid === vtxo.arkTxId)) {
|
|
58
58
|
const changes = fromOldestVtxo.filter((_) => _.txid === vtxo.arkTxId);
|
|
59
|
+
// We want to find all the other VTXOs spent by the same transaction to
|
|
60
|
+
// calculate the full amount of the change.
|
|
61
|
+
const allSpent = fromOldestVtxo.filter((v) => v.arkTxId === vtxo.arkTxId);
|
|
62
|
+
const spentAmount = allSpent.reduce((acc, v) => acc + v.value, 0);
|
|
59
63
|
let txAmount = 0;
|
|
60
64
|
let txTime = 0;
|
|
61
65
|
if (changes.length > 0) {
|
|
62
66
|
const changeAmount = changes.reduce((acc, v) => acc + v.value, 0);
|
|
63
|
-
// We want to find all the other VTXOs spent by the same transaction to
|
|
64
|
-
// calculate the full amount of the change.
|
|
65
|
-
const allSpent = fromOldestVtxo.filter((v) => v.arkTxId === vtxo.arkTxId);
|
|
66
|
-
const spentAmount = allSpent.reduce((acc, v) => acc + v.value, 0);
|
|
67
67
|
txAmount = spentAmount - changeAmount;
|
|
68
68
|
txTime = changes[0].createdAt.getTime();
|
|
69
69
|
}
|
|
70
70
|
else {
|
|
71
|
-
txAmount =
|
|
71
|
+
txAmount = spentAmount;
|
|
72
72
|
// TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
|
|
73
|
-
txTime =
|
|
73
|
+
txTime = getTxCreatedAt
|
|
74
|
+
? await getTxCreatedAt(vtxo.arkTxId)
|
|
75
|
+
: vtxo.createdAt.getTime() + 1;
|
|
74
76
|
}
|
|
75
77
|
sent.push({
|
|
76
78
|
key: { ...txKey, arkTxid: vtxo.arkTxId },
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
import { Address, OutScript } from "@scure/btc-signer";
|
|
2
|
+
/**
|
|
3
|
+
* Calculates the byte size required to store a variable-length integer (VarInt).
|
|
4
|
+
* Bitcoin uses VarInts to compact integer data (like array lengths).
|
|
5
|
+
*
|
|
6
|
+
* @param n - The integer value to check
|
|
7
|
+
* @returns The size in bytes (1, 3, 5, or 9)
|
|
8
|
+
*/
|
|
9
|
+
const getVarIntSize = (n) => {
|
|
10
|
+
if (n < 0xfd)
|
|
11
|
+
return 1;
|
|
12
|
+
if (n <= 0xffff)
|
|
13
|
+
return 3;
|
|
14
|
+
if (n <= 0xffffffff)
|
|
15
|
+
return 5;
|
|
16
|
+
return 9;
|
|
17
|
+
};
|
|
1
18
|
export class TxWeightEstimator {
|
|
2
19
|
constructor(hasWitness, inputCount, outputCount, inputSize, inputWitnessSize, outputSize) {
|
|
3
20
|
this.hasWitness = hasWitness;
|
|
@@ -38,16 +55,16 @@ export class TxWeightEstimator {
|
|
|
38
55
|
1 +
|
|
39
56
|
leafControlBlockSize;
|
|
40
57
|
this.inputCount++;
|
|
41
|
-
this.inputWitnessSize += leafWitnessSize + controlBlockWitnessSize;
|
|
58
|
+
this.inputWitnessSize += leafWitnessSize + 1 + controlBlockWitnessSize;
|
|
42
59
|
this.inputSize += TxWeightEstimator.INPUT_SIZE;
|
|
43
60
|
this.hasWitness = true;
|
|
44
|
-
this.inputCount++;
|
|
45
61
|
return this;
|
|
46
62
|
}
|
|
47
|
-
|
|
63
|
+
addP2WPKHOutput() {
|
|
48
64
|
this.outputCount++;
|
|
49
65
|
this.outputSize +=
|
|
50
|
-
TxWeightEstimator.OUTPUT_SIZE +
|
|
66
|
+
TxWeightEstimator.OUTPUT_SIZE +
|
|
67
|
+
TxWeightEstimator.P2WPKH_OUTPUT_SIZE;
|
|
51
68
|
return this;
|
|
52
69
|
}
|
|
53
70
|
addP2TROutput() {
|
|
@@ -56,16 +73,24 @@ export class TxWeightEstimator {
|
|
|
56
73
|
TxWeightEstimator.OUTPUT_SIZE + TxWeightEstimator.P2TR_OUTPUT_SIZE;
|
|
57
74
|
return this;
|
|
58
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Adds an output given a raw script.
|
|
78
|
+
* Cost = 8 bytes (amount) + varint(scriptLen) + scriptLen
|
|
79
|
+
*/
|
|
80
|
+
addOutputScript(script) {
|
|
81
|
+
this.outputCount++;
|
|
82
|
+
this.outputSize += 8 + getVarIntSize(script.length) + script.length;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Adds an output by decoding the address to get the exact script size.
|
|
87
|
+
*/
|
|
88
|
+
addOutputAddress(address, network) {
|
|
89
|
+
const payment = Address(network).decode(address);
|
|
90
|
+
const script = OutScript.encode(payment);
|
|
91
|
+
return this.addOutputScript(script);
|
|
92
|
+
}
|
|
59
93
|
vsize() {
|
|
60
|
-
const getVarIntSize = (n) => {
|
|
61
|
-
if (n < 0xfd)
|
|
62
|
-
return 1;
|
|
63
|
-
if (n < 0xffff)
|
|
64
|
-
return 3;
|
|
65
|
-
if (n < 0xffffffff)
|
|
66
|
-
return 5;
|
|
67
|
-
return 9;
|
|
68
|
-
};
|
|
69
94
|
const inputCount = getVarIntSize(this.inputCount);
|
|
70
95
|
const outputCount = getVarIntSize(this.outputCount);
|
|
71
96
|
// Calculate the size of the transaction without witness data
|
|
@@ -89,7 +114,7 @@ TxWeightEstimator.P2PKH_SCRIPT_SIG_SIZE = 1 + 73 + 1 + 33;
|
|
|
89
114
|
TxWeightEstimator.INPUT_SIZE = 32 + 4 + 1 + 4;
|
|
90
115
|
TxWeightEstimator.BASE_CONTROL_BLOCK_SIZE = 1 + 32;
|
|
91
116
|
TxWeightEstimator.OUTPUT_SIZE = 8 + 1;
|
|
92
|
-
TxWeightEstimator.
|
|
117
|
+
TxWeightEstimator.P2WPKH_OUTPUT_SIZE = 1 + 1 + 20;
|
|
93
118
|
TxWeightEstimator.BASE_TX_SIZE = 8 + 2; // Version + LockTime
|
|
94
119
|
TxWeightEstimator.WITNESS_HEADER_SIZE = 2; // Flag + Marker
|
|
95
120
|
TxWeightEstimator.WITNESS_SCALE_FACTOR = 4;
|
|
@@ -4,6 +4,7 @@ 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
6
|
import { Transaction } from '../utils/transaction.js';
|
|
7
|
+
import { DUST_AMOUNT } from './utils.js';
|
|
7
8
|
/**
|
|
8
9
|
* Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
|
|
9
10
|
*
|
|
@@ -55,11 +56,58 @@ export class OnchainWallet {
|
|
|
55
56
|
const onchainTotal = onchainConfirmed + onchainUnconfirmed;
|
|
56
57
|
return onchainTotal;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Iteratively selects coins and estimates transaction fees until convergence.
|
|
61
|
+
*
|
|
62
|
+
* This method handles the circular dependency between coin selection and fee
|
|
63
|
+
* estimation: the fee depends on transaction size, which depends on the number
|
|
64
|
+
* of inputs (selected coins) and whether a change output is needed.
|
|
65
|
+
*
|
|
66
|
+
* The algorithm iterates up to 10 times, refining the fee estimate based on
|
|
67
|
+
* the actual transaction structure. It resolves dust oscillation loops that
|
|
68
|
+
* occur when the change amount hovers near the dust threshold—adding/removing
|
|
69
|
+
* the change output causes the fee to fluctuate, preventing convergence.
|
|
70
|
+
* When a lower fee is computed (indicating the change output was dropped),
|
|
71
|
+
* the function accepts this state to guarantee termination.
|
|
72
|
+
*
|
|
73
|
+
* @param coins - Available coins to select from
|
|
74
|
+
* @param amount - Target send amount in satoshis
|
|
75
|
+
* @param feeRate - Fee rate in sat/vbyte
|
|
76
|
+
* @param recipientAddress - Destination address for size estimation
|
|
77
|
+
* @returns Selected inputs, change amount, and calculated fee
|
|
78
|
+
* @throws Error if fee estimation fails to converge within max iterations
|
|
79
|
+
*/
|
|
80
|
+
estimateFeesAndSelectCoins(coins, amount, feeRate, recipientAddress) {
|
|
81
|
+
const MAX_ITERATIONS = 10;
|
|
82
|
+
let fee = 0;
|
|
83
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
84
|
+
const totalNeeded = amount + fee;
|
|
85
|
+
const selected = selectCoins(coins, totalNeeded);
|
|
86
|
+
const estimator = TxWeightEstimator.create();
|
|
87
|
+
for (const _ of selected.inputs) {
|
|
88
|
+
estimator.addKeySpendInput();
|
|
89
|
+
}
|
|
90
|
+
estimator.addOutputAddress(recipientAddress, this.network);
|
|
91
|
+
if (selected.changeAmount >= BigInt(DUST_AMOUNT)) {
|
|
92
|
+
estimator.addOutputAddress(this.address, this.network);
|
|
93
|
+
}
|
|
94
|
+
const newFee = Number(estimator.vsize().value) * feeRate;
|
|
95
|
+
const roundedNewFee = Math.ceil(newFee);
|
|
96
|
+
// Prevent oscillation loops when change falls just below the dust limit.
|
|
97
|
+
// If removing the change output reduces the fee below our budget,
|
|
98
|
+
// we accept the valid transaction state to guarantee convergence.
|
|
99
|
+
if (roundedNewFee <= fee) {
|
|
100
|
+
return { ...selected, fee: roundedNewFee };
|
|
101
|
+
}
|
|
102
|
+
fee = roundedNewFee;
|
|
103
|
+
}
|
|
104
|
+
throw new Error("Fee estimation failed: could not converge");
|
|
105
|
+
}
|
|
58
106
|
async send(params) {
|
|
59
107
|
if (params.amount <= 0) {
|
|
60
108
|
throw new Error("Amount must be positive");
|
|
61
109
|
}
|
|
62
|
-
if (params.amount <
|
|
110
|
+
if (params.amount < DUST_AMOUNT) {
|
|
63
111
|
throw new Error("Amount is below dust limit");
|
|
64
112
|
}
|
|
65
113
|
const coins = await this.getCoins();
|
|
@@ -70,15 +118,14 @@ export class OnchainWallet {
|
|
|
70
118
|
if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) {
|
|
71
119
|
feeRate = OnchainWallet.MIN_FEE_RATE;
|
|
72
120
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const selected = selectCoins(coins, totalNeeded);
|
|
121
|
+
const { inputs, changeAmount } = this.estimateFeesAndSelectCoins(coins, params.amount, feeRate, params.address);
|
|
122
|
+
if (!inputs) {
|
|
123
|
+
throw new Error("Fee estimation failed");
|
|
124
|
+
}
|
|
78
125
|
// Create transaction
|
|
79
126
|
let tx = new Transaction();
|
|
80
127
|
// Add inputs
|
|
81
|
-
for (const input of
|
|
128
|
+
for (const input of inputs) {
|
|
82
129
|
tx.addInput({
|
|
83
130
|
txid: input.txid,
|
|
84
131
|
index: input.vout,
|
|
@@ -91,9 +138,8 @@ export class OnchainWallet {
|
|
|
91
138
|
}
|
|
92
139
|
// Add payment output
|
|
93
140
|
tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
tx.addOutputAddress(this.address, selected.changeAmount, this.network);
|
|
141
|
+
if (changeAmount >= BigInt(DUST_AMOUNT)) {
|
|
142
|
+
tx.addOutputAddress(this.address, changeAmount, this.network);
|
|
97
143
|
}
|
|
98
144
|
// Sign inputs and Finalize
|
|
99
145
|
tx = await this.identity.sign(tx);
|
|
@@ -112,7 +158,7 @@ export class OnchainWallet {
|
|
|
112
158
|
const childVsize = TxWeightEstimator.create()
|
|
113
159
|
.addKeySpendInput(true)
|
|
114
160
|
.addP2AInput()
|
|
115
|
-
.
|
|
161
|
+
.addOutputAddress(this.address, this.network)
|
|
116
162
|
.vsize().value;
|
|
117
163
|
const packageVSize = parentVsize + Number(childVsize);
|
|
118
164
|
let feeRate = await this.provider.getFeeRate();
|
|
@@ -155,7 +201,6 @@ export class OnchainWallet {
|
|
|
155
201
|
}
|
|
156
202
|
}
|
|
157
203
|
OnchainWallet.MIN_FEE_RATE = 1; // sat/vbyte
|
|
158
|
-
OnchainWallet.DUST_AMOUNT = 546; // sats
|
|
159
204
|
/**
|
|
160
205
|
* Select coins to reach a target amount, prioritizing those closer to expiry
|
|
161
206
|
* @param coins List of coins to select from
|
|
@@ -5,6 +5,7 @@ import { VtxoScript } from '../script/base.js';
|
|
|
5
5
|
import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
|
|
6
6
|
import { Wallet } from './wallet.js';
|
|
7
7
|
import { Transaction } from '../utils/transaction.js';
|
|
8
|
+
import { DUST_AMOUNT } from './utils.js';
|
|
8
9
|
export var Unroll;
|
|
9
10
|
(function (Unroll) {
|
|
10
11
|
let StepType;
|
|
@@ -200,7 +201,7 @@ export var Unroll;
|
|
|
200
201
|
for (const input of inputs) {
|
|
201
202
|
tx.addInput(input);
|
|
202
203
|
}
|
|
203
|
-
txWeightEstimator.
|
|
204
|
+
txWeightEstimator.addOutputAddress(outputAddress, wallet.network);
|
|
204
205
|
let feeRate = await wallet.onchainProvider.getFeeRate();
|
|
205
206
|
if (!feeRate || feeRate < Wallet.MIN_FEE_RATE) {
|
|
206
207
|
feeRate = Wallet.MIN_FEE_RATE;
|
|
@@ -209,7 +210,11 @@ export var Unroll;
|
|
|
209
210
|
if (feeAmount > totalAmount) {
|
|
210
211
|
throw new Error("fee amount is greater than the total amount");
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
+
const sendAmount = totalAmount - feeAmount;
|
|
214
|
+
if (sendAmount < BigInt(DUST_AMOUNT)) {
|
|
215
|
+
throw new Error("send amount is less than dust amount");
|
|
216
|
+
}
|
|
217
|
+
tx.addOutputAddress(outputAddress, sendAmount);
|
|
213
218
|
const signedTx = await wallet.identity.sign(tx);
|
|
214
219
|
signedTx.finalize();
|
|
215
220
|
await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
|
package/dist/esm/wallet/utils.js
CHANGED
|
@@ -230,7 +230,10 @@ export class ReadonlyWallet {
|
|
|
230
230
|
scripts: [hex.encode(this.offchainTapscript.pkScript)],
|
|
231
231
|
});
|
|
232
232
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
233
|
-
|
|
233
|
+
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
234
|
+
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
235
|
+
.then((res) => res.vtxos[0]?.createdAt.getTime() || 0);
|
|
236
|
+
return buildTransactionHistory(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
234
237
|
}
|
|
235
238
|
async getBoardingTxs() {
|
|
236
239
|
const utxos = [];
|
|
@@ -11,5 +11,5 @@ type ExtendedArkTransaction = ArkTransaction & {
|
|
|
11
11
|
* @param {Set<string>} commitmentsToIgnore - A set of commitment IDs that should be excluded from processing.
|
|
12
12
|
* @return {ExtendedArkTransaction[]} A sorted array of extended Ark transactions, representing the transaction history.
|
|
13
13
|
*/
|
|
14
|
-
export declare function buildTransactionHistory(vtxos: VirtualCoin[], allBoardingTxs: ArkTransaction[], commitmentsToIgnore: Set<string>): ExtendedArkTransaction[]
|
|
14
|
+
export declare function buildTransactionHistory(vtxos: VirtualCoin[], allBoardingTxs: ArkTransaction[], commitmentsToIgnore: Set<string>, getTxCreatedAt?: (txid: string) => Promise<number>): Promise<ExtendedArkTransaction[]>;
|
|
15
15
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Network } from "../networks";
|
|
1
2
|
export type VSize = {
|
|
2
3
|
value: bigint;
|
|
3
4
|
fee(feeRate: bigint): bigint;
|
|
@@ -7,7 +8,7 @@ export declare class TxWeightEstimator {
|
|
|
7
8
|
static readonly INPUT_SIZE: number;
|
|
8
9
|
static readonly BASE_CONTROL_BLOCK_SIZE: number;
|
|
9
10
|
static readonly OUTPUT_SIZE: number;
|
|
10
|
-
static readonly
|
|
11
|
+
static readonly P2WPKH_OUTPUT_SIZE: number;
|
|
11
12
|
static readonly BASE_TX_SIZE: number;
|
|
12
13
|
static readonly WITNESS_HEADER_SIZE = 2;
|
|
13
14
|
static readonly WITNESS_SCALE_FACTOR = 4;
|
|
@@ -24,7 +25,16 @@ export declare class TxWeightEstimator {
|
|
|
24
25
|
addKeySpendInput(isDefault?: boolean): TxWeightEstimator;
|
|
25
26
|
addP2PKHInput(): TxWeightEstimator;
|
|
26
27
|
addTapscriptInput(leafWitnessSize: number, leafScriptSize: number, leafControlBlockSize: number): TxWeightEstimator;
|
|
27
|
-
|
|
28
|
+
addP2WPKHOutput(): TxWeightEstimator;
|
|
28
29
|
addP2TROutput(): TxWeightEstimator;
|
|
30
|
+
/**
|
|
31
|
+
* Adds an output given a raw script.
|
|
32
|
+
* Cost = 8 bytes (amount) + varint(scriptLen) + scriptLen
|
|
33
|
+
*/
|
|
34
|
+
addOutputScript(script: Uint8Array): TxWeightEstimator;
|
|
35
|
+
/**
|
|
36
|
+
* Adds an output by decoding the address to get the exact script size.
|
|
37
|
+
*/
|
|
38
|
+
addOutputAddress(address: string, network: Network): TxWeightEstimator;
|
|
29
39
|
vsize(): VSize;
|
|
30
40
|
}
|
|
@@ -25,7 +25,6 @@ import { Transaction } from "../utils/transaction";
|
|
|
25
25
|
export declare class OnchainWallet implements AnchorBumper {
|
|
26
26
|
private identity;
|
|
27
27
|
static MIN_FEE_RATE: number;
|
|
28
|
-
static DUST_AMOUNT: number;
|
|
29
28
|
readonly onchainP2TR: P2TR;
|
|
30
29
|
readonly provider: OnchainProvider;
|
|
31
30
|
readonly network: Network;
|
|
@@ -34,6 +33,28 @@ export declare class OnchainWallet implements AnchorBumper {
|
|
|
34
33
|
get address(): string;
|
|
35
34
|
getCoins(): Promise<Coin[]>;
|
|
36
35
|
getBalance(): Promise<number>;
|
|
36
|
+
/**
|
|
37
|
+
* Iteratively selects coins and estimates transaction fees until convergence.
|
|
38
|
+
*
|
|
39
|
+
* This method handles the circular dependency between coin selection and fee
|
|
40
|
+
* estimation: the fee depends on transaction size, which depends on the number
|
|
41
|
+
* of inputs (selected coins) and whether a change output is needed.
|
|
42
|
+
*
|
|
43
|
+
* The algorithm iterates up to 10 times, refining the fee estimate based on
|
|
44
|
+
* the actual transaction structure. It resolves dust oscillation loops that
|
|
45
|
+
* occur when the change amount hovers near the dust threshold—adding/removing
|
|
46
|
+
* the change output causes the fee to fluctuate, preventing convergence.
|
|
47
|
+
* When a lower fee is computed (indicating the change output was dropped),
|
|
48
|
+
* the function accepts this state to guarantee termination.
|
|
49
|
+
*
|
|
50
|
+
* @param coins - Available coins to select from
|
|
51
|
+
* @param amount - Target send amount in satoshis
|
|
52
|
+
* @param feeRate - Fee rate in sat/vbyte
|
|
53
|
+
* @param recipientAddress - Destination address for size estimation
|
|
54
|
+
* @returns Selected inputs, change amount, and calculated fee
|
|
55
|
+
* @throws Error if fee estimation fails to converge within max iterations
|
|
56
|
+
*/
|
|
57
|
+
private estimateFeesAndSelectCoins;
|
|
37
58
|
send(params: SendBitcoinParams): Promise<string>;
|
|
38
59
|
bumpP2A(parent: Transaction): Promise<[string, string]>;
|
|
39
60
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Coin, ExtendedCoin, ExtendedVirtualCoin, VirtualCoin } from "..";
|
|
2
2
|
import { ReadonlyWallet } from "./wallet";
|
|
3
|
+
export declare const DUST_AMOUNT = 546;
|
|
3
4
|
export declare function extendVirtualCoin(wallet: {
|
|
4
5
|
offchainTapscript: ReadonlyWallet["offchainTapscript"];
|
|
5
6
|
}, vtxo: VirtualCoin): ExtendedVirtualCoin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkade-os/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "Bitcoin wallet SDK with Taproot and Ark integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -53,24 +53,24 @@
|
|
|
53
53
|
"registry": "https://registry.npmjs.org/"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"@marcbachmann/cel-js": "7.3.1",
|
|
56
57
|
"@noble/curves": "2.0.0",
|
|
57
58
|
"@noble/secp256k1": "3.0.0",
|
|
58
59
|
"@scure/base": "2.0.0",
|
|
59
60
|
"@scure/btc-signer": "2.0.1",
|
|
60
|
-
"bip68": "1.0.4"
|
|
61
|
-
"@marcbachmann/cel-js": "7.0.0"
|
|
61
|
+
"bip68": "1.0.4"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/node": "24.3.1",
|
|
65
65
|
"@vitest/coverage-v8": "3.2.4",
|
|
66
|
-
"esbuild": "^0.
|
|
67
|
-
"expo": "~52.0.47",
|
|
66
|
+
"esbuild": "^0.27.3",
|
|
68
67
|
"eventsource": "4.0.0",
|
|
69
|
-
"
|
|
68
|
+
"expo": "~52.0.49",
|
|
69
|
+
"glob": "11.1.0",
|
|
70
70
|
"husky": "9.1.7",
|
|
71
71
|
"prettier": "3.6.2",
|
|
72
|
-
"rimraf": "^6.
|
|
73
|
-
"typedoc": "^0.28.
|
|
72
|
+
"rimraf": "^6.1.2",
|
|
73
|
+
"typedoc": "^0.28.16",
|
|
74
74
|
"typescript": "5.9.2",
|
|
75
75
|
"vitest": "3.2.4"
|
|
76
76
|
},
|