@swapkit/toolboxes 4.0.0-beta.44 → 4.0.0-beta.45
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/dist/src/utxo/index.cjs +4 -4
- package/dist/src/utxo/index.cjs.map +7 -7
- package/dist/src/utxo/index.js +4 -4
- package/dist/src/utxo/index.js.map +7 -7
- package/package.json +1 -1
- package/src/utxo/helpers/api.ts +77 -17
- package/src/utxo/helpers/bchaddrjs.ts +13 -7
- package/src/utxo/toolbox/bitcoinCash.ts +13 -4
- package/src/utxo/toolbox/index.ts +3 -1
- package/src/utxo/toolbox/utxo.ts +7 -4
package/src/utxo/helpers/api.ts
CHANGED
|
@@ -17,6 +17,8 @@ type BlockchairFetchUnspentUtxoParams = BlockchairParams<{
|
|
|
17
17
|
offset?: number;
|
|
18
18
|
limit?: number;
|
|
19
19
|
address: string;
|
|
20
|
+
targetValue?: number;
|
|
21
|
+
accumulativeValue?: number;
|
|
20
22
|
}>;
|
|
21
23
|
|
|
22
24
|
async function broadcastUTXOTx({ chain, txHash }: { chain: Chain; txHash: string }) {
|
|
@@ -156,46 +158,98 @@ async function getRawTx({ chain, apiKey, txHash }: BlockchairParams<{ txHash?: s
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
async function
|
|
161
|
+
async function fetchUtxosBatch({
|
|
160
162
|
chain,
|
|
161
163
|
address,
|
|
162
164
|
apiKey,
|
|
165
|
+
targetValue,
|
|
163
166
|
offset = 0,
|
|
164
|
-
limit =
|
|
167
|
+
limit = 30,
|
|
165
168
|
}: BlockchairFetchUnspentUtxoParams) {
|
|
169
|
+
// Only fetch the fields we need to reduce payload size
|
|
170
|
+
const fields = "is_spent,transaction_hash,index,value,script_hex,block_id,spending_signature_hex";
|
|
171
|
+
|
|
166
172
|
const response = await blockchairRequest<BlockchairOutputsResponse[]>(
|
|
167
|
-
`${baseUrl(chain)}/outputs?q=is_spent(false)
|
|
173
|
+
`${baseUrl(chain)}/outputs?q=recipient(${address}),is_spent(false)&s=value(desc)${targetValue ? `&value(..${targetValue * 5})` : ""}&fields=${fields}&limit=${limit}&offset=${offset}`,
|
|
168
174
|
apiKey,
|
|
169
175
|
);
|
|
170
176
|
|
|
171
|
-
const txs = response
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
const txs = response.map(
|
|
178
|
+
({
|
|
179
|
+
is_spent,
|
|
180
|
+
script_hex,
|
|
181
|
+
block_id,
|
|
182
|
+
transaction_hash,
|
|
183
|
+
index,
|
|
184
|
+
value,
|
|
185
|
+
spending_signature_hex,
|
|
186
|
+
}) => ({
|
|
174
187
|
hash: transaction_hash,
|
|
175
188
|
index,
|
|
176
189
|
value,
|
|
177
190
|
txHex: spending_signature_hex,
|
|
178
191
|
script_hex,
|
|
179
192
|
is_confirmed: block_id !== -1,
|
|
180
|
-
|
|
193
|
+
is_spent,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
181
196
|
|
|
182
197
|
return txs;
|
|
183
198
|
}
|
|
184
199
|
|
|
200
|
+
function getTxsValue(txs: Awaited<ReturnType<typeof fetchUtxosBatch>>) {
|
|
201
|
+
return txs.reduce((total, tx) => total + tx.value, 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function pickMostValuableTxs(
|
|
205
|
+
txs: Awaited<ReturnType<typeof fetchUtxosBatch>>,
|
|
206
|
+
targetValue?: number,
|
|
207
|
+
): Awaited<ReturnType<typeof fetchUtxosBatch>> {
|
|
208
|
+
const sortedTxs = [...txs].sort((a, b) => b.value - a.value);
|
|
209
|
+
|
|
210
|
+
if (targetValue) {
|
|
211
|
+
const result = [];
|
|
212
|
+
let accumulated = 0;
|
|
213
|
+
|
|
214
|
+
for (const utxo of sortedTxs) {
|
|
215
|
+
result.push(utxo);
|
|
216
|
+
accumulated += utxo.value;
|
|
217
|
+
if (accumulated >= targetValue) break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return sortedTxs;
|
|
224
|
+
}
|
|
225
|
+
|
|
185
226
|
async function getUnspentUtxos({
|
|
186
227
|
chain,
|
|
187
228
|
address,
|
|
188
229
|
apiKey,
|
|
230
|
+
targetValue,
|
|
231
|
+
accumulativeValue = 0,
|
|
189
232
|
offset = 0,
|
|
190
|
-
limit =
|
|
191
|
-
}: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof
|
|
233
|
+
limit = 30,
|
|
234
|
+
}: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof fetchUtxosBatch>>> {
|
|
192
235
|
if (!address)
|
|
193
236
|
throw new SwapKitError("toolbox_utxo_invalid_params", { error: "Address is required" });
|
|
194
237
|
|
|
195
238
|
try {
|
|
196
|
-
const
|
|
239
|
+
const utxos = await fetchUtxosBatch({ targetValue, chain, address, apiKey, offset, limit });
|
|
240
|
+
const utxosCount = utxos.length;
|
|
241
|
+
const isComplete = utxosCount < limit;
|
|
242
|
+
|
|
243
|
+
const unspentUtxos = utxos.filter(({ is_spent }) => !is_spent);
|
|
197
244
|
|
|
198
|
-
|
|
245
|
+
const unspentUtxosValue = getTxsValue(unspentUtxos);
|
|
246
|
+
const totalCurrentValue = accumulativeValue + unspentUtxosValue;
|
|
247
|
+
|
|
248
|
+
const limitReached = targetValue && totalCurrentValue >= targetValue;
|
|
249
|
+
|
|
250
|
+
if (isComplete || limitReached) {
|
|
251
|
+
return pickMostValuableTxs(unspentUtxos, targetValue);
|
|
252
|
+
}
|
|
199
253
|
|
|
200
254
|
const nextBatch = await getUnspentUtxos({
|
|
201
255
|
chain,
|
|
@@ -203,22 +257,28 @@ async function getUnspentUtxos({
|
|
|
203
257
|
apiKey,
|
|
204
258
|
offset: offset + limit,
|
|
205
259
|
limit,
|
|
260
|
+
accumulativeValue: totalCurrentValue,
|
|
261
|
+
targetValue,
|
|
206
262
|
});
|
|
207
263
|
|
|
208
|
-
|
|
264
|
+
const allUtxos = [...unspentUtxos, ...nextBatch];
|
|
265
|
+
|
|
266
|
+
return pickMostValuableTxs(allUtxos, targetValue);
|
|
209
267
|
} catch (error) {
|
|
210
268
|
console.error("Failed to fetch unspent UTXOs:", error);
|
|
211
269
|
return [];
|
|
212
270
|
}
|
|
213
271
|
}
|
|
214
272
|
|
|
215
|
-
async function
|
|
273
|
+
async function getUtxos({
|
|
216
274
|
address,
|
|
217
275
|
chain,
|
|
218
276
|
apiKey,
|
|
219
277
|
fetchTxHex = true,
|
|
220
|
-
|
|
221
|
-
|
|
278
|
+
targetValue,
|
|
279
|
+
}: BlockchairParams<{ address: string; fetchTxHex?: boolean; targetValue?: number }>) {
|
|
280
|
+
const utxos = await getUnspentUtxos({ chain, address, apiKey, targetValue });
|
|
281
|
+
|
|
222
282
|
const results = [];
|
|
223
283
|
|
|
224
284
|
for (const { hash, index, script_hex, value } of utxos) {
|
|
@@ -249,8 +309,8 @@ function utxoApi(chain: UTXOChain) {
|
|
|
249
309
|
getSuggestedTxFee: () => getSuggestedTxFee(chain),
|
|
250
310
|
getBalance: (address: string) => getUnconfirmedBalance({ address, chain, apiKey }),
|
|
251
311
|
getAddressData: (address: string) => getAddressData({ address, chain, apiKey }),
|
|
252
|
-
|
|
253
|
-
|
|
312
|
+
getUtxos: (params: { address: string; fetchTxHex?: boolean; targetValue?: number }) =>
|
|
313
|
+
getUtxos({ ...params, chain, apiKey }),
|
|
254
314
|
};
|
|
255
315
|
}
|
|
256
316
|
|
|
@@ -75,12 +75,18 @@ function toCashAddress(address: string): string {
|
|
|
75
75
|
|
|
76
76
|
function decodeAddress(address: string) {
|
|
77
77
|
try {
|
|
78
|
-
|
|
78
|
+
const decoded = decodeBase58Address(address);
|
|
79
|
+
if (decoded) {
|
|
80
|
+
return decoded;
|
|
81
|
+
}
|
|
79
82
|
} catch (_error) {
|
|
80
83
|
// Try to decode as cashaddr if base58 decoding fails.
|
|
81
84
|
}
|
|
82
85
|
try {
|
|
83
|
-
|
|
86
|
+
const decoded = decodeCashAddress(address);
|
|
87
|
+
if (decoded) {
|
|
88
|
+
return decoded;
|
|
89
|
+
}
|
|
84
90
|
} catch (_error) {
|
|
85
91
|
// Try to decode as bitpay if cashaddr decoding fails.
|
|
86
92
|
}
|
|
@@ -116,10 +122,10 @@ function decodeBase58Address(address: string) {
|
|
|
116
122
|
return { hash, format: Format.Bitpay, network: UtxoNetwork.Mainnet, type: Type.P2SH };
|
|
117
123
|
|
|
118
124
|
default:
|
|
119
|
-
|
|
125
|
+
return;
|
|
120
126
|
}
|
|
121
127
|
} catch (_error) {
|
|
122
|
-
|
|
128
|
+
return;
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
|
|
@@ -141,10 +147,10 @@ function decodeCashAddress(address: string) {
|
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
|
|
150
|
+
return;
|
|
145
151
|
}
|
|
146
152
|
|
|
147
|
-
function decodeCashAddressWithPrefix(address: string)
|
|
153
|
+
function decodeCashAddressWithPrefix(address: string) {
|
|
148
154
|
try {
|
|
149
155
|
const { hash, prefix, type } = cashaddr.decode(address);
|
|
150
156
|
|
|
@@ -155,7 +161,7 @@ function decodeCashAddressWithPrefix(address: string): DecodedType {
|
|
|
155
161
|
type: type === "P2PKH" ? Type.P2PKH : Type.P2SH,
|
|
156
162
|
};
|
|
157
163
|
} catch (_error) {
|
|
158
|
-
|
|
164
|
+
return;
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
|
|
@@ -97,7 +97,7 @@ export async function createBCHToolbox<T extends Chain.BitcoinCash>(
|
|
|
97
97
|
: updateDerivationPath(NetworkDerivationPath[chain], { index }),
|
|
98
98
|
);
|
|
99
99
|
|
|
100
|
-
const keys = (await getCreateKeysForPath(chain))({ phrase, derivationPath });
|
|
100
|
+
const keys = phrase ? (await getCreateKeysForPath(chain))({ phrase, derivationPath }) : undefined;
|
|
101
101
|
|
|
102
102
|
const signer = keys
|
|
103
103
|
? await createSignerWithKeys(keys)
|
|
@@ -140,9 +140,14 @@ async function createTransaction({
|
|
|
140
140
|
}: UTXOBuildTxParams) {
|
|
141
141
|
if (!bchValidateAddress(recipient))
|
|
142
142
|
throw new SwapKitError("toolbox_utxo_invalid_address", { address: recipient });
|
|
143
|
-
|
|
143
|
+
|
|
144
|
+
// Overestimate by 7500 byte * feeRate to ensure we have enough UTXOs for fees and change
|
|
145
|
+
const targetValue = Math.ceil(assetValue.getBaseValue("number") + feeRate * 7500);
|
|
146
|
+
|
|
147
|
+
const utxos = await getUtxoApi(chain).getUtxos({
|
|
144
148
|
address: stripToCashAddress(sender),
|
|
145
149
|
fetchTxHex: true,
|
|
150
|
+
targetValue,
|
|
146
151
|
});
|
|
147
152
|
|
|
148
153
|
const compiledMemo = memo ? await compileMemo(memo) : null;
|
|
@@ -237,9 +242,13 @@ async function buildTx({ assetValue, recipient, memo, feeRate, sender }: UTXOBui
|
|
|
237
242
|
if (!bchValidateAddress(recipientCashAddress))
|
|
238
243
|
throw new SwapKitError("toolbox_utxo_invalid_address", { address: recipientCashAddress });
|
|
239
244
|
|
|
240
|
-
|
|
245
|
+
// Overestimate by 7500 byte * feeRate to ensure we have enough UTXOs for fees and change
|
|
246
|
+
const targetValue = Math.ceil(assetValue.getBaseValue("number") + feeRate * 7500);
|
|
247
|
+
|
|
248
|
+
const utxos = await getUtxoApi(chain).getUtxos({
|
|
241
249
|
address: stripToCashAddress(sender),
|
|
242
|
-
fetchTxHex:
|
|
250
|
+
fetchTxHex: false,
|
|
251
|
+
targetValue,
|
|
243
252
|
});
|
|
244
253
|
|
|
245
254
|
const feeRateWhole = Number(feeRate.toFixed(0));
|
|
@@ -56,7 +56,9 @@ export async function getUtxoToolbox<T extends keyof UTXOToolboxes>(
|
|
|
56
56
|
): Promise<UTXOToolboxes[T]> {
|
|
57
57
|
switch (chain) {
|
|
58
58
|
case Chain.BitcoinCash: {
|
|
59
|
-
const toolbox = await createBCHToolbox(
|
|
59
|
+
const toolbox = await createBCHToolbox(
|
|
60
|
+
(params as UtxoToolboxParams[Chain.BitcoinCash]) || {},
|
|
61
|
+
);
|
|
60
62
|
return toolbox as UTXOToolboxes[T];
|
|
61
63
|
}
|
|
62
64
|
|
package/src/utxo/toolbox/utxo.ts
CHANGED
|
@@ -504,18 +504,21 @@ async function getInputsAndTargetOutputs({
|
|
|
504
504
|
fetchTxHex: fetchTxOverwrite = false,
|
|
505
505
|
}: Omit<UTXOBuildTxParams, "feeRate">) {
|
|
506
506
|
const chain = assetValue.chain as UTXOChain;
|
|
507
|
+
const feeRate = (await getFeeRates(chain))[FeeOption.Fastest];
|
|
507
508
|
|
|
508
509
|
const fetchTxHex = fetchTxOverwrite || nonSegwitChains.includes(chain);
|
|
509
510
|
|
|
510
|
-
const
|
|
511
|
+
const amountToSend = assetValue.getBaseValue("number");
|
|
511
512
|
|
|
512
|
-
//
|
|
513
|
-
|
|
513
|
+
// Overestimate by 5000 byte * highest feeRate to ensure we have enough UTXOs for fees and change
|
|
514
|
+
const targetValue = Math.ceil(amountToSend + feeRate * 5000);
|
|
515
|
+
|
|
516
|
+
const inputs = await getUtxoApi(chain).getUtxos({ address: sender, fetchTxHex, targetValue });
|
|
514
517
|
|
|
515
518
|
return {
|
|
516
519
|
inputs,
|
|
517
520
|
outputs: [
|
|
518
|
-
{ address: recipient, value:
|
|
521
|
+
{ address: recipient, value: amountToSend },
|
|
519
522
|
...(memo ? [{ address: "", script: await compileMemo(memo), value: 0 }] : []),
|
|
520
523
|
],
|
|
521
524
|
};
|