@swapkit/toolboxes 4.0.0-beta.43 → 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 }) {
@@ -104,23 +106,14 @@ async function getSuggestedTxFee(chain: Chain) {
104
106
  }
105
107
 
106
108
  async function blockchairRequest<T>(url: string, apiKey?: string): Promise<T> {
107
- try {
108
- const response = await RequestClient.get<BlockchairResponse<T>>(url);
109
- if (!response || response.context.code !== 200)
110
- throw new SwapKitError("toolbox_utxo_api_error", { error: `Failed to query ${url}` });
111
-
112
- return response.data as T;
113
- } catch (error) {
114
- if (!apiKey) throw error;
115
- const response = await RequestClient.get<BlockchairResponse<T>>(
116
- `${url}${apiKey ? `&key=${apiKey}` : ""}`,
117
- );
109
+ const response = await RequestClient.get<BlockchairResponse<T>>(
110
+ `${url}${apiKey ? `${url.includes("?") ? "&" : "?"}key=${apiKey}` : ""}`,
111
+ );
118
112
 
119
- if (!response || response.context.code !== 200)
120
- throw new SwapKitError("toolbox_utxo_api_error", { error: `Failed to query ${url}` });
113
+ if (!response || response.context.code !== 200)
114
+ throw new SwapKitError("toolbox_utxo_api_error", { error: `Failed to query ${url}` });
121
115
 
122
- return response.data as T;
123
- }
116
+ return response.data as T;
124
117
  }
125
118
 
126
119
  async function getAddressData({ address, chain, apiKey }: BlockchairParams<{ address?: string }>) {
@@ -165,46 +158,98 @@ async function getRawTx({ chain, apiKey, txHash }: BlockchairParams<{ txHash?: s
165
158
  }
166
159
  }
167
160
 
168
- async function fetchUnspentUtxoBatch({
161
+ async function fetchUtxosBatch({
169
162
  chain,
170
163
  address,
171
164
  apiKey,
165
+ targetValue,
172
166
  offset = 0,
173
- limit = 100,
167
+ limit = 30,
174
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
+
175
172
  const response = await blockchairRequest<BlockchairOutputsResponse[]>(
176
- `${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}`,
177
174
  apiKey,
178
175
  );
179
176
 
180
- const txs = response
181
- .filter(({ is_spent }) => !is_spent)
182
- .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
+ }) => ({
183
187
  hash: transaction_hash,
184
188
  index,
185
189
  value,
186
190
  txHex: spending_signature_hex,
187
191
  script_hex,
188
192
  is_confirmed: block_id !== -1,
189
- }));
193
+ is_spent,
194
+ }),
195
+ );
190
196
 
191
197
  return txs;
192
198
  }
193
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
+
194
226
  async function getUnspentUtxos({
195
227
  chain,
196
228
  address,
197
229
  apiKey,
230
+ targetValue,
231
+ accumulativeValue = 0,
198
232
  offset = 0,
199
- limit = 100,
200
- }: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof fetchUnspentUtxoBatch>>> {
233
+ limit = 30,
234
+ }: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof fetchUtxosBatch>>> {
201
235
  if (!address)
202
236
  throw new SwapKitError("toolbox_utxo_invalid_params", { error: "Address is required" });
203
237
 
204
238
  try {
205
- 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;
206
242
 
207
- if (txs.length <= limit) return txs;
243
+ const unspentUtxos = utxos.filter(({ is_spent }) => !is_spent);
244
+
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
+ }
208
253
 
209
254
  const nextBatch = await getUnspentUtxos({
210
255
  chain,
@@ -212,22 +257,28 @@ async function getUnspentUtxos({
212
257
  apiKey,
213
258
  offset: offset + limit,
214
259
  limit,
260
+ accumulativeValue: totalCurrentValue,
261
+ targetValue,
215
262
  });
216
263
 
217
- return [...txs, ...nextBatch];
264
+ const allUtxos = [...unspentUtxos, ...nextBatch];
265
+
266
+ return pickMostValuableTxs(allUtxos, targetValue);
218
267
  } catch (error) {
219
268
  console.error("Failed to fetch unspent UTXOs:", error);
220
269
  return [];
221
270
  }
222
271
  }
223
272
 
224
- async function scanUTXOs({
273
+ async function getUtxos({
225
274
  address,
226
275
  chain,
227
276
  apiKey,
228
277
  fetchTxHex = true,
229
- }: BlockchairParams<{ address: string; fetchTxHex?: boolean }>) {
230
- 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
+
231
282
  const results = [];
232
283
 
233
284
  for (const { hash, index, script_hex, value } of utxos) {
@@ -258,8 +309,8 @@ function utxoApi(chain: UTXOChain) {
258
309
  getSuggestedTxFee: () => getSuggestedTxFee(chain),
259
310
  getBalance: (address: string) => getUnconfirmedBalance({ address, chain, apiKey }),
260
311
  getAddressData: (address: string) => getAddressData({ address, chain, apiKey }),
261
- scanUTXOs: (params: { address: string; fetchTxHex?: boolean }) =>
262
- scanUTXOs({ ...params, chain, apiKey }),
312
+ getUtxos: (params: { address: string; fetchTxHex?: boolean; targetValue?: number }) =>
313
+ getUtxos({ ...params, chain, apiKey }),
263
314
  };
264
315
  }
265
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
  };