UNPKG

@lifi/sdk

Version:

LI.FI Any-to-Any Cross-Chain-Swap SDK

207 lines 11.3 kB
import { AddressType, getAddressInfo, hexToUnit8Array, signPsbt, waitForTransaction, withTimeout, } from '@bigmi/core'; import * as ecc from '@bitcoinerlab/secp256k1'; import { ChainId } from '@lifi/types'; import { address, initEccLib, networks, Psbt } from 'bitcoinjs-lib'; import { config } from '../../config.js'; import { LiFiErrorCode } from '../../errors/constants.js'; import { TransactionError } from '../../errors/errors.js'; import { getStepTransaction } from '../../services/api.js'; import { BaseStepExecutor } from '../BaseStepExecutor.js'; import { checkBalance } from '../checkBalance.js'; import { stepComparison } from '../stepComparison.js'; import { waitForDestinationChainTransaction } from '../waitForDestinationChainTransaction.js'; import { getUTXOPublicClient } from './getUTXOPublicClient.js'; import { parseUTXOErrors } from './parseUTXOErrors.js'; import { isPsbtFinalized, toXOnly } from './utils.js'; export class UTXOStepExecutor extends BaseStepExecutor { client; constructor(options) { super(options); this.client = options.client; } checkClient = (step) => { // TODO: check chain and possibly implement chain switch? // Prevent execution of the quote by wallet different from the one which requested the quote if (this.client.account?.address !== step.action.fromAddress) { throw new TransactionError(LiFiErrorCode.WalletChangedDuringExecution, 'The wallet address that requested the quote does not match the wallet address attempting to sign the transaction.'); } }; executeStep = async (step) => { step.execution = this.statusManager.initExecutionObject(step); const fromChain = await config.getChainById(step.action.fromChainId); const toChain = await config.getChainById(step.action.toChainId); const isBridgeExecution = fromChain.id !== toChain.id; const currentProcessType = isBridgeExecution ? 'CROSS_CHAIN' : 'SWAP'; let process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, chainId: fromChain.id, }); const publicClient = await getUTXOPublicClient(ChainId.BTC); if (process.status !== 'DONE') { try { let txHash; let txHex; if (process.txHash) { // Make sure that the chain is still correct this.checkClient(step); // Wait for exiting transaction txHash = process.txHash; txHex = process.txHex; } else { process = this.statusManager.updateProcess(step, process.type, 'STARTED'); // Check balance await checkBalance(this.client.account.address, step); // Create new transaction if (!step.transactionRequest) { // biome-ignore lint/correctness/noUnusedVariables: destructuring const { execution, ...stepBase } = step; const updatedStep = await getStepTransaction(stepBase); const comparedStep = await stepComparison(this.statusManager, step, updatedStep, this.allowUserInteraction, this.executionOptions); Object.assign(step, { ...comparedStep, execution: step.execution, }); } if (!step.transactionRequest?.data) { throw new TransactionError(LiFiErrorCode.TransactionUnprepared, 'Unable to prepare transaction.'); } process = this.statusManager.updateProcess(step, process.type, 'ACTION_REQUIRED'); if (!this.allowUserInteraction) { return step; } let transactionRequest = { data: step.transactionRequest.data, }; if (this.executionOptions?.updateTransactionRequestHook) { const customizedTransactionRequest = await this.executionOptions.updateTransactionRequestHook({ requestType: 'transaction', ...transactionRequest, }); transactionRequest = { ...transactionRequest, ...customizedTransactionRequest, }; } if (!transactionRequest.data) { throw new TransactionError(LiFiErrorCode.TransactionUnprepared, 'Unable to prepare transaction.'); } this.checkClient(step); const psbtHex = transactionRequest.data; // Initialize ECC library required for Taproot operations // https://github.com/bitcoinjs/bitcoinjs-lib?tab=readme-ov-file#using-taproot initEccLib(ecc); const psbt = Psbt.fromHex(psbtHex, { network: networks.bitcoin }); psbt.data.inputs.forEach((input, index) => { const accountAddress = input.witnessUtxo ? address.fromOutputScript(input.witnessUtxo.script, networks.bitcoin) : this.client.account?.address; const addressInfo = getAddressInfo(accountAddress); if (addressInfo.type === AddressType.p2tr) { // Taproot (P2TR) addresses require specific PSBT fields for proper signing // tapInternalKey: Required for Taproot key-path spending // Most wallets / libraries usually handle this already if (!input.tapInternalKey) { const pubKey = this.client.account?.publicKey; if (pubKey) { const tapInternalKey = toXOnly(hexToUnit8Array(pubKey)); psbt.updateInput(index, { tapInternalKey, }); } } // sighashType: Required by bitcoinjs-lib even though the bitcoin protocol allows defaults // check if sighashType is default (0) or not set (undefined) if (!input.sighashType) { psbt.updateInput(index, { sighashType: 1, // Default to Transaction.SIGHASH_ALL - 1 }); } } }); const inputsToSign = Array.from(psbt.data.inputs .reduce((map, input, index) => { const accountAddress = input.witnessUtxo ? address.fromOutputScript(input.witnessUtxo.script, networks.bitcoin) : this.client.account?.address; if (map.has(accountAddress)) { map.get(accountAddress).signingIndexes.push(index); } else { map.set(accountAddress, { address: accountAddress, sigHash: 1, // Default to Transaction.SIGHASH_ALL - 1 signingIndexes: [index], }); } return map; }, new Map()) .values()); // We give users 10 minutes to sign the transaction or it should be considered expired const signedPsbtHex = await withTimeout(() => signPsbt(this.client, { psbt: psbt.toHex(), inputsToSign: inputsToSign, finalize: false, }), { timeout: 600_000, errorInstance: new TransactionError(LiFiErrorCode.TransactionExpired, 'Transaction has expired.'), }); const signedPsbt = Psbt.fromHex(signedPsbtHex); if (!isPsbtFinalized(signedPsbt)) { signedPsbt.finalizeAllInputs(); } txHex = signedPsbt.extractTransaction().toHex(); txHash = await publicClient.sendUTXOTransaction({ hex: txHex, }); process = this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash: txHash, txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}`, txHex, }); } let replacementReason; const transaction = await waitForTransaction(publicClient, { txId: txHash, txHex, senderAddress: this.client.account?.address, onReplaced: (response) => { replacementReason = response.reason; process = this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash: response.transaction.txid, txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${response.transaction.txid}`, }); }, }); if (replacementReason === 'cancelled') { throw new TransactionError(LiFiErrorCode.TransactionCanceled, 'User canceled transaction.'); } if (transaction.txid !== txHash) { process = this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash: transaction.txid, txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${transaction.txid}`, }); } if (isBridgeExecution) { process = this.statusManager.updateProcess(step, process.type, 'DONE'); } } catch (e) { const error = await parseUTXOErrors(e, step, process); process = this.statusManager.updateProcess(step, process.type, 'FAILED', { error: { message: error.cause.message, code: error.code, }, }); this.statusManager.updateExecution(step, 'FAILED'); throw error; } } await waitForDestinationChainTransaction(step, process, fromChain, toChain, this.statusManager, 10_000); // DONE return step; }; } //# sourceMappingURL=UTXOStepExecutor.js.map