@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
329 lines (273 loc) • 10.7 kB
text/typescript
import { Network } from '@btc-vision/bitcoin';
import { IFundingTransactionParameters, TransactionFactory, Wallet } from '../opnet.js';
import { BroadcastResponse } from './interfaces/BroadcastResponse.js';
import {
FetchUTXOParams,
FetchUTXOParamsMultiAddress,
RawUTXOResponse,
UTXO,
} from './interfaces/IUTXO.js';
export interface WalletUTXOs {
readonly confirmed: RawUTXOResponse[];
readonly pending: RawUTXOResponse[];
readonly spentTransactions: RawUTXOResponse[];
}
/**
* Allows to fetch UTXO data from any OPNET node
*/
export class OPNetLimitedProvider {
private readonly utxoPath: string = 'address/utxos';
private readonly rpc: string = 'json-rpc';
public constructor(private readonly opnetAPIUrl: string) {}
/**
* Fetches UTXO data from the OPNET node
* @param {FetchUTXOParams} settings - The settings to fetch UTXO data
* @returns {Promise<UTXO[]>} - The UTXOs fetched
* @throws {Error} - If UTXOs could not be fetched
*/
public async fetchUTXO(settings: FetchUTXOParams): Promise<UTXO[]> {
if (settings.usePendingUTXO === undefined) {
settings.usePendingUTXO = true;
}
if (settings.optimized === undefined) {
settings.optimized = true;
}
const params = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
const url: string = `${this.opnetAPIUrl}/api/v1/${this.utxoPath}?address=${settings.address}&optimize=${settings.optimized ?? false}`;
const resp: Response = await fetch(url, params);
if (!resp.ok) {
throw new Error(`Failed to fetch UTXO data: ${resp.statusText}`);
}
const fetchedData: WalletUTXOs = (await resp.json()) as WalletUTXOs;
const allUtxos = settings.usePendingUTXO
? [...fetchedData.confirmed, ...fetchedData.pending]
: fetchedData.confirmed;
const unspentUTXOs: RawUTXOResponse[] = [];
for (const utxo of allUtxos) {
if (
fetchedData.spentTransactions.some(
(spent) =>
spent.transactionId === utxo.transactionId &&
spent.outputIndex === utxo.outputIndex,
)
) {
continue;
}
unspentUTXOs.push(utxo);
}
if (unspentUTXOs.length === 0) {
throw new Error('No UTXO found');
}
const meetCriteria: RawUTXOResponse[] = unspentUTXOs.filter((utxo: RawUTXOResponse) => {
return BigInt(utxo.value) >= settings.minAmount;
});
if (meetCriteria.length === 0) {
throw new Error('No UTXO found (minAmount)');
}
const finalUTXOs: UTXO[] = [];
let currentAmount: bigint = 0n;
const amountRequested: bigint = settings.requestedAmount;
for (const utxo of meetCriteria) {
const utxoValue: bigint = BigInt(utxo.value);
// check if value is greater than 0
if (utxoValue <= 0n) {
continue;
}
currentAmount += utxoValue;
finalUTXOs.push({
transactionId: utxo.transactionId,
outputIndex: utxo.outputIndex,
value: utxoValue,
scriptPubKey: utxo.scriptPubKey,
nonWitnessUtxo: Buffer.from(utxo.raw, 'base64'),
});
if (currentAmount > amountRequested) {
break;
}
}
return finalUTXOs;
}
/**
* Fetches UTXO data from the OPNET node for multiple addresses
* @param {FetchUTXOParamsMultiAddress} settings - The settings to fetch UTXO data
* @returns {Promise<UTXO[]>} - The UTXOs fetched
* @throws {Error} - If UTXOs could not be fetched
*/
public async fetchUTXOMultiAddr(settings: FetchUTXOParamsMultiAddress): Promise<UTXO[]> {
const promises: Promise<UTXO[]>[] = [];
for (const address of settings.addresses) {
const params: FetchUTXOParams = {
address: address,
minAmount: settings.minAmount,
requestedAmount: settings.requestedAmount,
optimized: settings.optimized,
usePendingUTXO: settings.usePendingUTXO,
};
const promise = this.fetchUTXO(params).catch(() => {
return [];
});
promises.push(promise);
}
const utxos: UTXO[][] = await Promise.all(promises);
const all = utxos.flat();
const finalUTXOs: UTXO[] = [];
let currentAmount = 0n;
for (let i = 0; i < all.length; i++) {
const utxo = all[i];
if (currentAmount >= settings.requestedAmount) {
break;
}
currentAmount += utxo.value;
finalUTXOs.push(utxo);
}
return finalUTXOs;
}
/**
* Broadcasts a transaction to the OPNET node
* @param {string} transaction - The transaction to broadcast
* @param {boolean} psbt - Whether the transaction is a PSBT
* @returns {Promise<BroadcastResponse>} - The response from the OPNET node
*/
public async broadcastTransaction(
transaction: string,
psbt: boolean,
): Promise<BroadcastResponse | undefined> {
const params = [transaction, psbt];
const result = await this.rpcMethod('btc_sendRawTransaction', params);
if (!result) {
return;
}
return result as BroadcastResponse;
}
/**
* Splits UTXOs into smaller UTXOs
* @param {Wallet} wallet - The wallet to split UTXOs
* @param {Network} network - The network to split UTXOs
* @param {number} splitInputsInto - The number of UTXOs to split into
* @param {bigint} amountPerUTXO - The amount per UTXO
* @returns {Promise<BroadcastResponse | { error: string }>} - The response from the OPNET node or an error
*/
public async splitUTXOs(
wallet: Wallet,
network: Network,
splitInputsInto: number,
amountPerUTXO: bigint,
): Promise<BroadcastResponse | { error: string }> {
const utxoSetting: FetchUTXOParamsMultiAddress = {
addresses: [wallet.p2wpkh, wallet.p2tr],
minAmount: 330n,
requestedAmount: 1_000_000_000_000_000n,
};
const utxos: UTXO[] = await this.fetchUTXOMultiAddr(utxoSetting);
if (!utxos || !utxos.length) return { error: 'No UTXOs found' };
const amount = BigInt(splitInputsInto) * amountPerUTXO;
const fundingTransactionParameters: IFundingTransactionParameters = {
amount: amount,
feeRate: 500,
from: wallet.p2tr,
utxos: utxos,
signer: wallet.keypair,
network,
to: wallet.p2tr,
splitInputsInto,
priorityFee: 0n,
gasSatFee: 330n,
};
const transactionFactory = new TransactionFactory();
const fundingTx = await transactionFactory.createBTCTransfer(fundingTransactionParameters);
const broadcastResponse = await this.broadcastTransaction(fundingTx.tx, false);
if (!broadcastResponse) return { error: 'Could not broadcast transaction' };
return broadcastResponse;
}
/**
* Fetches to the OPNET node
* @param {string} method
* @param {unknown[]} paramsMethod
* @returns {Promise<unknown>}
*/
public async rpcMethod(method: string, paramsMethod: unknown[]): Promise<unknown> {
const params = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: method,
params: paramsMethod,
id: 1,
}),
};
const url: string = `${this.opnetAPIUrl}/api/v1/${this.rpc}`;
const resp: Response = await fetch(url, params);
if (!resp.ok) {
throw new Error(`Failed to fetch to rpc: ${resp.statusText}`);
}
const fetchedData = (await resp.json()) as {
result: {
error?: string;
};
};
if (!fetchedData) {
throw new Error('No data fetched');
}
const result = fetchedData.result;
if (!result) {
throw new Error('No rpc parameters found');
}
if ('error' in result) {
throw new Error(`Error in fetching to rpc ${result.error}`);
}
return result;
}
/**
* Fetches the wrap parameters from the OPNET node
* @param {bigint} amount - The amount to wrap
* @returns {Promise<WrappedGeneration | undefined>} - The wrap parameters fetched
* @throws {Error} - If wrap parameters could not be fetched
*/
/*public async fetchWrapParameters(amount: bigint): Promise<WrappedGeneration | undefined> {
if (amount < currentConsensusConfig.VAULT_MINIMUM_AMOUNT) {
throw new Error(
`Amount must be greater than the minimum consolidation amount ${currentConsensusConfig.VAULT_MINIMUM_AMOUNT}sat.`,
);
}
const params = [0, amount.toString()];
const result = await this.rpcMethod('btc_generate', params);
if (!result) {
return;
}
return new WrappedGeneration(result as WrappedGenerationParameters);
}*/
/**
* Fetches the wrap parameters from the OPNET node
* @param {bigint} amount - The amount to wrap
* @param {string} receiver - The receiver address
* @returns {Promise<UnwrapGeneration | undefined>} - The wrap parameters fetched
* @throws {Error} - If wrap parameters could not be fetched
*/
/*public async fetchUnWrapParameters(
amount: bigint,
receiver: Address,
): Promise<UnwrapGeneration | undefined> {
if (amount < 330n) {
throw new Error(
`Amount must be greater than the minimum consolidation amount ${currentConsensusConfig.VAULT_MINIMUM_AMOUNT}sat.`,
);
}
if (receiver.length < 50) {
throw new Error('Invalid receiver address');
}
const params = [1, amount.toString(), receiver.toHex()];
const result = await this.rpcMethod('btc_generate', params);
if (!result) {
return;
}
return new UnwrapGeneration(result as UnwrappedGenerationParameters);
}*/
}