@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.
- 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 +83 -32
- 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 }) {
|
|
@@ -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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
120
|
-
|
|
113
|
+
if (!response || response.context.code !== 200)
|
|
114
|
+
throw new SwapKitError("toolbox_utxo_api_error", { error: `Failed to query ${url}` });
|
|
121
115
|
|
|
122
|
-
|
|
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
|
|
161
|
+
async function fetchUtxosBatch({
|
|
169
162
|
chain,
|
|
170
163
|
address,
|
|
171
164
|
apiKey,
|
|
165
|
+
targetValue,
|
|
172
166
|
offset = 0,
|
|
173
|
-
limit =
|
|
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)
|
|
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
|
-
|
|
182
|
-
|
|
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 =
|
|
200
|
-
}: BlockchairFetchUnspentUtxoParams): Promise<Awaited<ReturnType<typeof
|
|
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
|
|
239
|
+
const utxos = await fetchUtxosBatch({ targetValue, chain, address, apiKey, offset, limit });
|
|
240
|
+
const utxosCount = utxos.length;
|
|
241
|
+
const isComplete = utxosCount < limit;
|
|
206
242
|
|
|
207
|
-
|
|
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
|
-
|
|
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
|
|
273
|
+
async function getUtxos({
|
|
225
274
|
address,
|
|
226
275
|
chain,
|
|
227
276
|
apiKey,
|
|
228
277
|
fetchTxHex = true,
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
};
|