@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.
@@ -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 fetchUnspentUtxoBatch({
161
+ async function fetchUtxosBatch({
160
162
  chain,
161
163
  address,
162
164
  apiKey,
165
+ targetValue,
163
166
  offset = 0,
164
- limit = 100,
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),recipient(${address})&limit=${limit}&offset=${offset}`,
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
- .filter(({ is_spent }) => !is_spent)
173
- .map(({ script_hex, block_id, transaction_hash, index, value, spending_signature_hex }) => ({
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 = 100,
191
- }: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof fetchUnspentUtxoBatch>>> {
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 txs = await fetchUnspentUtxoBatch({ chain, address, apiKey, offset, limit });
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
- if (txs.length <= limit) return txs;
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
- return [...txs, ...nextBatch];
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 scanUTXOs({
273
+ async function getUtxos({
216
274
  address,
217
275
  chain,
218
276
  apiKey,
219
277
  fetchTxHex = true,
220
- }: BlockchairParams<{ address: string; fetchTxHex?: boolean }>) {
221
- const utxos = await getUnspentUtxos({ chain, address, apiKey });
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
- scanUTXOs: (params: { address: string; fetchTxHex?: boolean }) =>
253
- scanUTXOs({ ...params, chain, apiKey }),
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
- 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
  };