Skip to content

Commit

Permalink
fix: utxo unspent offset calculation (#1167)
Browse files Browse the repository at this point in the history
* fix: utxo unspent offset calculation

* chore: add recursion to full list fetching

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
towanTG and github-actions[bot] authored Jan 23, 2025
1 parent 87cca0e commit 35dfd61
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-doors-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@swapkit/toolbox-utxo": patch
---

Fixes offset calculation for UTXO unspent tx
71 changes: 44 additions & 27 deletions packages/toolboxes/utxo/src/api/blockchairApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import type {
BlockchairOutputsResponse,
BlockchairRawTransactionResponse,
BlockchairResponse,
UTXOType,
} from "../types/index";
type BlockchairParams<T> = T & { chain: Chain; apiKey?: string };
type BlockchairFetchUnspentUtxoParams = BlockchairParams<{
offset?: number;
limit?: number;
address: string;
}>;

const baseUrl = (chain: Chain) => `https://api.blockchair.com/${mapChainToBlockchairChain(chain)}`;

Expand Down Expand Up @@ -141,44 +145,57 @@ const getRawTx = async ({ chain, apiKey, txHash }: BlockchairParams<{ txHash?: s
}
};

const getUnspentTxs = async ({
const fetchUnspentUtxoBatch = async ({
chain,
address,
apiKey,
offset = 0,
}: BlockchairParams<{ offset?: number; address: string }>): Promise<
(UTXOType & { script_hex: string; is_confirmed: boolean })[]
limit = 100,
}: BlockchairFetchUnspentUtxoParams) => {
const response = await blockchairRequest<BlockchairOutputsResponse[]>(
`${baseUrl(chain)}/outputs?q=is_spent(false),recipient(${address})&limit=${limit}&offset=${offset}`,
apiKey,
);

const txs = response
.filter(({ is_spent }) => !is_spent)
.map(({ script_hex, block_id, transaction_hash, index, value, spending_signature_hex }) => ({
hash: transaction_hash,
index,
value,
txHex: spending_signature_hex,
script_hex,
is_confirmed: block_id !== -1,
}));

return txs;
};

const getUnspentUtxos = async ({
chain,
address,
apiKey,
offset = 0,
limit = 100,
}: BlockchairFetchUnspentUtxoParams): Promise<
Awaited<ReturnType<typeof fetchUnspentUtxoBatch>>
> => {
if (!address) throw new Error("address is required");
try {
const response = await blockchairRequest<BlockchairOutputsResponse[]>(
`${baseUrl(
chain,
)}/outputs?q=is_spent(false),recipient(${address})&limit=100&offset=${offset}`,
apiKey,
);

const txs = response
.filter(({ is_spent }) => !is_spent)
.map(({ script_hex, block_id, transaction_hash, index, value, spending_signature_hex }) => ({
hash: transaction_hash,
index,
value,
txHex: spending_signature_hex,
script_hex,
is_confirmed: block_id !== -1,
})) as (UTXOType & { script_hex: string; is_confirmed: boolean })[];
try {
const txs = await fetchUnspentUtxoBatch({ chain, address, apiKey, offset, limit });

if (response.length !== 100) return txs;
if (txs.length <= limit) return txs;

const nextBatch = await getUnspentTxs({
address,
const nextBatch = await getUnspentUtxos({
chain,
address,
apiKey,
offset: response?.[99]?.transaction_id,
offset: offset + limit,
limit,
});

return txs.concat(nextBatch);
return [...txs, ...nextBatch];
} catch (error) {
console.error(error);
return [];
Expand All @@ -191,7 +208,7 @@ const scanUTXOs = async ({
apiKey,
fetchTxHex = true,
}: BlockchairParams<{ address: string; fetchTxHex?: boolean }>) => {
const utxos = await getUnspentTxs({ chain, address, apiKey });
const utxos = await getUnspentUtxos({ chain, address, apiKey });
const results = [];

for (const { hash, index, script_hex, value } of utxos) {
Expand Down

0 comments on commit 35dfd61

Please sign in to comment.