Skip to content

Commit

Permalink
chore: extract repeated code into functions
Browse files Browse the repository at this point in the history
  • Loading branch information
slavastartsev committed Jan 8, 2025
1 parent b674a02 commit ef5cc0c
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 92 deletions.
147 changes: 57 additions & 90 deletions sdk/src/wallet/utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const checkUtxoNode = (node: TreeNode<OutputNodeData> | null): boolean => {
/**
* @ignore
*/
export const processUtxos = async (
export const _processUtxos = async (
utxos: UTXO[],
cardinalOutputsSet: Set<string>,
esploraClient: EsploraClient,
Expand All @@ -114,6 +114,51 @@ export const processUtxos = async (
return utxos.filter((_, index) => allowedList[index]);
};

const processUtxos = async (address: string, esploraClient: EsploraClient): Promise<UTXO[]> => {
const addressInfo = getAddressInfo(address);

const ordinalsClient = new OrdinalsClient(addressInfo.network);

const [utxos, cardinalOutputs] = await Promise.all([
esploraClient.getAddressUtxos(address),
// cardinal = return UTXOs not containing inscriptions or runes
ordinalsClient.getOutputsFromAddress(address, 'cardinal'),
]);

const cardinalOutputsSet = new Set(cardinalOutputs.map((output) => output.outpoint));

return _processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient);
};

const collectPossibleInputs = async (fromAddress: string, publicKey: string) => {
const addressInfo = getAddressInfo(fromAddress);

const esploraClient = new EsploraClient(addressInfo.network);

const allowedUtxos = await processUtxos(fromAddress, esploraClient);

if (allowedUtxos.length === 0) {
throw new Error('No confirmed UTXOs');
}

// To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs
return Promise.all(
allowedUtxos.map(async (utxo) => {
const hex = await esploraClient.getTransactionHex(utxo.txid);
const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true });
const input = getInputFromUtxoAndTx(
addressInfo.network as BitcoinNetworkName,
utxo,
transaction,
addressInfo.type,
publicKey
);

return input;
})
);
};

type OutputNodeData = Pick<UTXO, 'txid' | 'vout'> & { cardinal: boolean; indexed: boolean };

export interface Input {
Expand Down Expand Up @@ -187,44 +232,11 @@ export async function createBitcoinPsbt(
}

const esploraClient = new EsploraClient(addressInfo.network);
const ordinalsClient = new OrdinalsClient(addressInfo.network);

let utxos: UTXO[] = [];
// contains UTXOs which do not contain inscriptions
let cardinalOutputs: OutputJson[] = [];

[utxos, feeRate, cardinalOutputs] = await Promise.all([
esploraClient.getAddressUtxos(fromAddress),
const [possibleInputs, computedfeeRate] = await Promise.all([
collectPossibleInputs(fromAddress, publicKey),
feeRate === undefined ? esploraClient.getFeeEstimate(confirmationTarget) : feeRate,
// cardinal = return UTXOs not containing inscriptions or runes
ordinalsClient.getOutputsFromAddress(fromAddress, 'cardinal'),
]);

if (utxos.length === 0) {
throw new Error('No confirmed UTXOs');
}

const cardinalOutputsSet = new Set(cardinalOutputs.map((output) => output.outpoint));

const allowedUtxos = await processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient);

// To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs
const possibleInputs = await Promise.all(
allowedUtxos.map(async (utxo) => {
const hex = await esploraClient.getTransactionHex(utxo.txid);
const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true });
const input = getInputFromUtxoAndTx(
addressInfo.network as BitcoinNetworkName,
utxo,
transaction,
addressInfo.type,
publicKey
);

return input;
})
);

const outputs: Output[] = [
{
address: toAddress,
Expand All @@ -250,7 +262,7 @@ export async function createBitcoinPsbt(
// big outputs to small ones, which in the end will create a lot of outputs close to dust.
const transaction = selectUTXO(possibleInputs, outputs, utxoSelectionStrategy, {
changeAddress: fromAddress, // Refund surplus to the payment address
feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer
feePerByte: BigInt(Math.ceil(computedfeeRate)), // round up to the nearest integer
bip69: true, // Sort inputs and outputs according to BIP69
createTx: true, // Create the transaction
network: getBtcNetwork(addressInfo.network),
Expand All @@ -261,11 +273,10 @@ export async function createBitcoinPsbt(
});

if (!transaction || !transaction.tx) {
console.debug('utxos', utxos);
console.debug('outputsFromAddress', cardinalOutputs);
console.debug('possibleInputs', possibleInputs);
console.debug(`fromAddress: ${fromAddress}, toAddress: ${toAddress}, amount: ${amount}`);
console.debug(`publicKey: ${publicKey}, opReturnData: ${opReturnData}`);
console.debug(`feeRate: ${feeRate}, confirmationTarget: ${confirmationTarget}`);
console.debug(`feeRate: ${computedfeeRate}, confirmationTarget: ${confirmationTarget}`);
throw new Error('Failed to create transaction. Do you have enough funds?');
}

Expand Down Expand Up @@ -393,46 +404,12 @@ export async function estimateTxFee(
// Use the from address as the toAddress for the fee estimate
const toAddress = fromAddress;

// TODO: allow submitting the UTXOs, fee estimate and confirmed transactions
// to avoid fetching them again.
const esploraClient = new EsploraClient(addressInfo.network);
const ordinalsClient = new OrdinalsClient(addressInfo.network);

let utxos: UTXO[] = [];
// contains UTXOs which do not contain inscriptions
let cardinalOutputs: OutputJson[] = [];

[utxos, feeRate, cardinalOutputs] = await Promise.all([
esploraClient.getAddressUtxos(fromAddress),
const [possibleInputs, computedfeeRate] = await Promise.all([
collectPossibleInputs(fromAddress, publicKey),
feeRate === undefined ? esploraClient.getFeeEstimate(confirmationTarget) : feeRate,
// cardinal = return UTXOs not containing inscriptions or runes
ordinalsClient.getOutputsFromAddress(fromAddress, 'cardinal'),
]);

if (utxos.length === 0) {
throw new Error('No confirmed UTXOs');
}

const cardinalOutputsSet = new Set(cardinalOutputs.map((output) => output.outpoint));

const allowedUtxos = await processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient);

const possibleInputs = await Promise.all(
allowedUtxos.map(async (utxo) => {
const hex = await esploraClient.getTransactionHex(utxo.txid);
const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true });
const input = getInputFromUtxoAndTx(
addressInfo.network as BitcoinNetworkName,
utxo,
transaction,
addressInfo.type,
publicKey
);

return input;
})
);

// Create transaction without outputs
const outputs: Output[] = [
{
Expand Down Expand Up @@ -465,7 +442,7 @@ export async function estimateTxFee(
// big outputs to small ones, which in the end will create a lot of outputs close to dust.
const transaction = selectUTXO(possibleInputs, outputs, utxoSelectionStrategy, {
changeAddress: fromAddress, // Refund surplus to the payment address
feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer
feePerByte: BigInt(Math.ceil(computedfeeRate)), // round up to the nearest integer
bip69: true, // Sort inputs and outputs according to BIP69
createTx: true, // Create the transaction
network: getBtcNetwork(addressInfo.network),
Expand All @@ -476,11 +453,10 @@ export async function estimateTxFee(
});

if (!transaction || !transaction.tx) {
console.debug('utxos', utxos);
console.debug('cardinalOutputs', cardinalOutputs);
console.debug('possibleInputs', possibleInputs);
console.debug(`fromAddress: ${fromAddress}, amount: ${amount}`);
console.debug(`publicKey: ${publicKey}, opReturnData: ${opReturnData}`);
console.debug(`feeRate: ${feeRate}, confirmationTarget: ${confirmationTarget}`);
console.debug(`feeRate: ${computedfeeRate}, confirmationTarget: ${confirmationTarget}`);
throw new Error('Failed to create transaction. Do you have enough funds?');
}

Expand Down Expand Up @@ -513,17 +489,8 @@ export async function getBalance(address?: string) {
const addressInfo = getAddressInfo(address);

const esploraClient = new EsploraClient(addressInfo.network);
const ordinalsClient = new OrdinalsClient(addressInfo.network);

const [utxos, cardinalOutputs] = await Promise.all([
esploraClient.getAddressUtxos(address),
// cardinal = return UTXOs not containing inscriptions or runes
ordinalsClient.getOutputsFromAddress(address, 'cardinal'),
]);

const cardinalOutputsSet = new Set(cardinalOutputs.map((output) => output.outpoint));

const allowedUtxos = await processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient);
const allowedUtxos = await processUtxos(address, esploraClient);

const total = allowedUtxos.reduce((acc, utxo) => acc + utxo.value, 0);

Expand Down
4 changes: 2 additions & 2 deletions sdk/test/utxo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
estimateTxFee,
Input,
getBalance,
processUtxos,
_processUtxos,
} from '../src/wallet/utxo';
import { TransactionOutput } from '@scure/btc-signer/psbt';
import { OrdinalsClient, OutPoint } from '../src/ordinal-api';
Expand Down Expand Up @@ -658,7 +658,7 @@ describe('UTXO Tests', () => {
return result;
});

const allowedUtxos = await processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient);
const allowedUtxos = await _processUtxos(utxos, cardinalOutputsSet, esploraClient, ordinalsClient);

expect(allowedUtxos).toEqual([utxos[0], utxos[1]]);
});
Expand Down

0 comments on commit ef5cc0c

Please sign in to comment.