@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.
- 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 +105 -22
- 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 }) {
|
|
@@ -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", {
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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
|
|
169
|
+
async function fetchUtxosBatch({
|
|
160
170
|
chain,
|
|
161
171
|
address,
|
|
162
172
|
apiKey,
|
|
163
173
|
offset = 0,
|
|
164
|
-
limit =
|
|
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=
|
|
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
|
-
|
|
173
|
-
|
|
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 =
|
|
191
|
-
}: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof
|
|
240
|
+
limit = 30,
|
|
241
|
+
}: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof fetchUtxosBatch>>> {
|
|
192
242
|
if (!address)
|
|
193
|
-
throw new SwapKitError("toolbox_utxo_invalid_params", {
|
|
243
|
+
throw new SwapKitError("toolbox_utxo_invalid_params", {
|
|
244
|
+
error: "Address is required",
|
|
245
|
+
});
|
|
194
246
|
|
|
195
247
|
try {
|
|
196
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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
|
|
289
|
+
async function getUtxos({
|
|
216
290
|
address,
|
|
217
291
|
chain,
|
|
218
292
|
apiKey,
|
|
219
293
|
fetchTxHex = true,
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
};
|