@swapkit/toolboxes 4.0.0-beta.44 → 4.0.0-beta.46

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.
@@ -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 }) {
@@ -35,7 +37,9 @@ async function broadcastUTXOTx({ chain, txHash }: { chain: Chain; txHash: string
35
37
  }>(rpcUrl, { headers: { "Content-Type": "application/json" }, body });
36
38
 
37
39
  if (response.error) {
38
- throw new SwapKitError("toolbox_utxo_broadcast_failed", { error: response.error?.message });
40
+ throw new SwapKitError("toolbox_utxo_broadcast_failed", {
41
+ error: response.error?.message,
42
+ });
39
43
  }
40
44
 
41
45
  if (response.result.includes('"code":-26')) {
@@ -109,14 +113,18 @@ async function blockchairRequest<T>(url: string, apiKey?: string): Promise<T> {
109
113
  );
110
114
 
111
115
  if (!response || response.context.code !== 200)
112
- throw new SwapKitError("toolbox_utxo_api_error", { error: `Failed to query ${url}` });
116
+ throw new SwapKitError("toolbox_utxo_api_error", {
117
+ error: `Failed to query ${url}`,
118
+ });
113
119
 
114
120
  return response.data as T;
115
121
  }
116
122
 
117
123
  async function getAddressData({ address, chain, apiKey }: BlockchairParams<{ address?: string }>) {
118
124
  if (!address)
119
- throw new SwapKitError("toolbox_utxo_invalid_params", { error: "Address is required" });
125
+ throw new SwapKitError("toolbox_utxo_invalid_params", {
126
+ error: "Address is required",
127
+ });
120
128
 
121
129
  try {
122
130
  const response = await blockchairRequest<BlockchairAddressResponse>(
@@ -142,7 +150,9 @@ async function getUnconfirmedBalance({
142
150
 
143
151
  async function getRawTx({ chain, apiKey, txHash }: BlockchairParams<{ txHash?: string }>) {
144
152
  if (!txHash)
145
- throw new SwapKitError("toolbox_utxo_invalid_params", { error: "TxHash is required" });
153
+ throw new SwapKitError("toolbox_utxo_invalid_params", {
154
+ error: "TxHash is required",
155
+ });
146
156
 
147
157
  try {
148
158
  const rawTxResponse = await blockchairRequest<BlockchairRawTransactionResponse>(
@@ -156,46 +166,106 @@ async function getRawTx({ chain, apiKey, txHash }: BlockchairParams<{ txHash?: s
156
166
  }
157
167
  }
158
168
 
159
- async function fetchUnspentUtxoBatch({
169
+ async function fetchUtxosBatch({
160
170
  chain,
161
171
  address,
162
172
  apiKey,
163
173
  offset = 0,
164
- limit = 100,
174
+ limit = 30,
165
175
  }: BlockchairFetchUnspentUtxoParams) {
176
+ // Only fetch the fields we need to reduce payload size
177
+ const fields = "is_spent,transaction_hash,index,value,script_hex,block_id,spending_signature_hex";
178
+
166
179
  const response = await blockchairRequest<BlockchairOutputsResponse[]>(
167
- `${baseUrl(chain)}/outputs?q=is_spent(false),recipient(${address})&limit=${limit}&offset=${offset}`,
180
+ `${baseUrl(chain)}/outputs?q=recipient(${address}),is_spent(false)&s=value(desc)&fields=${fields}&limit=${limit}&offset=${offset}`,
168
181
  apiKey,
169
182
  );
170
183
 
171
- const txs = response
172
- .filter(({ is_spent }) => !is_spent)
173
- .map(({ script_hex, block_id, transaction_hash, index, value, spending_signature_hex }) => ({
184
+ const txs = response.map(
185
+ ({
186
+ is_spent,
187
+ script_hex,
188
+ block_id,
189
+ transaction_hash,
190
+ index,
191
+ value,
192
+ spending_signature_hex,
193
+ }) => ({
174
194
  hash: transaction_hash,
175
195
  index,
176
196
  value,
177
197
  txHex: spending_signature_hex,
178
198
  script_hex,
179
199
  is_confirmed: block_id !== -1,
180
- }));
200
+ is_spent,
201
+ }),
202
+ );
181
203
 
182
204
  return txs;
183
205
  }
184
206
 
207
+ function getTxsValue(txs: Awaited<ReturnType<typeof fetchUtxosBatch>>) {
208
+ return txs.reduce((total, tx) => total + tx.value, 0);
209
+ }
210
+
211
+ function pickMostValuableTxs(
212
+ txs: Awaited<ReturnType<typeof fetchUtxosBatch>>,
213
+ targetValue?: number,
214
+ ): Awaited<ReturnType<typeof fetchUtxosBatch>> {
215
+ const sortedTxs = [...txs].sort((a, b) => b.value - a.value);
216
+
217
+ if (targetValue) {
218
+ const result = [];
219
+ let accumulated = 0;
220
+
221
+ for (const utxo of sortedTxs) {
222
+ result.push(utxo);
223
+ accumulated += utxo.value;
224
+ if (accumulated >= targetValue) break;
225
+ }
226
+
227
+ return result;
228
+ }
229
+
230
+ return sortedTxs;
231
+ }
232
+
185
233
  async function getUnspentUtxos({
186
234
  chain,
187
235
  address,
188
236
  apiKey,
237
+ targetValue,
238
+ accumulativeValue = 0,
189
239
  offset = 0,
190
- limit = 100,
191
- }: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof fetchUnspentUtxoBatch>>> {
240
+ limit = 30,
241
+ }: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof fetchUtxosBatch>>> {
192
242
  if (!address)
193
- throw new SwapKitError("toolbox_utxo_invalid_params", { error: "Address is required" });
243
+ throw new SwapKitError("toolbox_utxo_invalid_params", {
244
+ error: "Address is required",
245
+ });
194
246
 
195
247
  try {
196
- const txs = await fetchUnspentUtxoBatch({ chain, address, apiKey, offset, limit });
248
+ const utxos = await fetchUtxosBatch({
249
+ targetValue,
250
+ chain,
251
+ address,
252
+ apiKey,
253
+ offset,
254
+ limit,
255
+ });
256
+ const utxosCount = utxos.length;
257
+ const isComplete = utxosCount < limit;
258
+
259
+ const unspentUtxos = utxos.filter(({ is_spent }) => !is_spent);
260
+
261
+ const unspentUtxosValue = getTxsValue(unspentUtxos);
262
+ const totalCurrentValue = accumulativeValue + unspentUtxosValue;
263
+
264
+ const limitReached = targetValue && totalCurrentValue >= targetValue;
197
265
 
198
- if (txs.length <= limit) return txs;
266
+ if (isComplete || limitReached) {
267
+ return pickMostValuableTxs(unspentUtxos, targetValue);
268
+ }
199
269
 
200
270
  const nextBatch = await getUnspentUtxos({
201
271
  chain,
@@ -203,22 +273,32 @@ async function getUnspentUtxos({
203
273
  apiKey,
204
274
  offset: offset + limit,
205
275
  limit,
276
+ accumulativeValue: totalCurrentValue,
277
+ targetValue,
206
278
  });
207
279
 
208
- return [...txs, ...nextBatch];
280
+ const allUtxos = [...unspentUtxos, ...nextBatch];
281
+
282
+ return pickMostValuableTxs(allUtxos, targetValue);
209
283
  } catch (error) {
210
284
  console.error("Failed to fetch unspent UTXOs:", error);
211
285
  return [];
212
286
  }
213
287
  }
214
288
 
215
- async function scanUTXOs({
289
+ async function getUtxos({
216
290
  address,
217
291
  chain,
218
292
  apiKey,
219
293
  fetchTxHex = true,
220
- }: BlockchairParams<{ address: string; fetchTxHex?: boolean }>) {
221
- const utxos = await getUnspentUtxos({ chain, address, apiKey });
294
+ targetValue,
295
+ }: BlockchairParams<{
296
+ address: string;
297
+ fetchTxHex?: boolean;
298
+ targetValue?: number;
299
+ }>) {
300
+ const utxos = await getUnspentUtxos({ chain, address, apiKey, targetValue });
301
+
222
302
  const results = [];
223
303
 
224
304
  for (const { hash, index, script_hex, value } of utxos) {
@@ -249,8 +329,11 @@ function utxoApi(chain: UTXOChain) {
249
329
  getSuggestedTxFee: () => getSuggestedTxFee(chain),
250
330
  getBalance: (address: string) => getUnconfirmedBalance({ address, chain, apiKey }),
251
331
  getAddressData: (address: string) => getAddressData({ address, chain, apiKey }),
252
- scanUTXOs: (params: { address: string; fetchTxHex?: boolean }) =>
253
- scanUTXOs({ ...params, chain, apiKey }),
332
+ getUtxos: (params: {
333
+ address: string;
334
+ fetchTxHex?: boolean;
335
+ targetValue?: number;
336
+ }) => getUtxos({ ...params, chain, apiKey }),
254
337
  };
255
338
  }
256
339
 
@@ -75,12 +75,18 @@ function toCashAddress(address: string): string {
75
75
 
76
76
  function decodeAddress(address: string) {
77
77
  try {
78
- return decodeBase58Address(address);
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
- return decodeCashAddress(address);
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
- throw new SwapKitError("toolbox_utxo_invalid_address", { address });
125
+ return;
120
126
  }
121
127
  } catch (_error) {
122
- throw new SwapKitError("toolbox_utxo_invalid_address", { address });
128
+ return;
123
129
  }
124
130
  }
125
131
 
@@ -141,10 +147,10 @@ function decodeCashAddress(address: string) {
141
147
  }
142
148
  }
143
149
 
144
- throw new SwapKitError("toolbox_utxo_invalid_address", { address });
150
+ return;
145
151
  }
146
152
 
147
- function decodeCashAddressWithPrefix(address: string): DecodedType {
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
- throw new SwapKitError("toolbox_utxo_invalid_address", { address });
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
- const utxos = await getUtxoApi(chain).scanUTXOs({
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
- const utxos = await getUtxoApi(chain).scanUTXOs({
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: true,
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(params as UtxoToolboxParams[Chain.BitcoinCash]);
59
+ const toolbox = await createBCHToolbox(
60
+ (params as UtxoToolboxParams[Chain.BitcoinCash]) || {},
61
+ );
60
62
  return toolbox as UTXOToolboxes[T];
61
63
  }
62
64
 
@@ -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 inputs = await getUtxoApi(chain).scanUTXOs({ address: sender, fetchTxHex });
511
+ const amountToSend = assetValue.getBaseValue("number");
511
512
 
512
- //1. add output amount and recipient to targets
513
- //2. add output memo to targets (optional)
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: Number(assetValue.bigIntValue) },
521
+ { address: recipient, value: amountToSend },
519
522
  ...(memo ? [{ address: "", script: await compileMemo(memo), value: 0 }] : []),
520
523
  ],
521
524
  };