UNPKG

@reservoir0x/relay-sdk

Version:

Relay is the Fastest and Cheapest Way to Bridge and Transact Across Chains.

307 lines 12.1 kB
import {} from 'viem'; import { LogLevel } from './logger.js'; import { axios } from '../utils/axios.js'; import { getClient } from '../client.js'; import { DepositTransactionTimeoutError, SolverStatusTimeoutError, TransactionConfirmationError } from '../errors/index.js'; import { repeatUntilOk } from '../utils/repeatUntilOk.js'; import { getTenderlyDetails } from '../utils/getTenderlyDetails.js'; /** * Safe txhash.wait which handles replacements when users speed up the transaction * @param url an URL object * @returns A Promise to wait on */ export async function sendTransactionSafely(chainId, items, step, wallet, setTxHashes, setInternalTxHashes, request, headers, crossChainIntentChainId, isValidating, details) { const client = getClient(); try { //In some cases wallets can be delayed when switching chains, causing this check to fail. //To work around this we check the chain id of the active wallet a few times before declaring it a failure await repeatUntilOk(async () => { const walletChainId = await wallet.getChainId(); return walletChainId === chainId; }, 10, undefined, 250); } catch (e) { const walletChainId = await wallet.getChainId(); throw `Current chain id: ${walletChainId} does not match expected chain id: ${chainId} `; } let receipt; let transactionCancelled = false; let confirmationError = false; const pollingInterval = client.pollingInterval ?? 5000; const maximumAttempts = client.maxPollingAttemptsBeforeTimeout ?? (2.5 * 60 * 1000) / pollingInterval; // default to 2 minutes and 30 seconds worth of attempts let waitingForConfirmation = true; let attemptCount = 0; let txHash; // Check if batching txs is supported and if there are multiple items to batch const isBatchTransaction = Boolean(Array.isArray(items) && items.length > 1 && wallet.handleBatchTransactionStep); if (isBatchTransaction) { txHash = await wallet.handleBatchTransactionStep?.(chainId, items); } else { txHash = await wallet.handleSendTransactionStep(chainId, Array.isArray(items) ? items[0] : items, step); } if (txHash === 'null') { throw 'User rejected the request'; } // Find the first item with a check endpoint const check = Array.isArray(items) ? items.find((item) => item.check)?.check : items.check; // Post transaction to solver postTransactionToSolver({ txHash, chainId, step, request, headers }); if (!isBatchTransaction && !Array.isArray(items) && chainId === details?.currencyOut?.currency?.chainId) { postSameChainTransactionToSolver({ calldata: JSON.stringify({ ...items.data, txHash }), chainId, step, request, headers }); } if (!txHash) { throw Error('Transaction hash not returned from handleSendTransactionStep method'); } setTxHashes([ { txHash: txHash, chainId: chainId, isBatchTx: isBatchTransaction } ]); //Set up internal functions const validate = (res) => { getClient()?.log(['Execute Steps: Polling for confirmation', res], LogLevel.Verbose); if (res.status === 200 && res.data && res.data.status === 'failure') { throw Error('Transaction failed'); } if (res.status === 200 && res.data && res.data.status === 'fallback') { throw Error('Transaction failed: Refunded'); } if (res.status === 200 && res.data && res.data.status === 'success') { if (txHash) { setInternalTxHashes([ { txHash: txHash, chainId: chainId, isBatchTx: isBatchTransaction } ]); } const chainTxHashes = res.data?.txHashes?.map((hash) => { return { txHash: hash, chainId: res?.data?.destinationChainId ?? crossChainIntentChainId }; }); setTxHashes(chainTxHashes); return true; } return false; }; // Poll the confirmation url to confirm the transaction went through const pollForConfirmation = async () => { isValidating?.(); while (waitingForConfirmation && attemptCount < maximumAttempts && !transactionCancelled && !confirmationError) { let res; if (check?.endpoint && !request?.data?.useExternalLiquidity) { res = await axios.request({ url: `${request.baseURL}${check?.endpoint}`, method: check?.method, headers: headers }); } if (!res || validate(res)) { waitingForConfirmation = false; // transaction confirmed } else if (res) { if (res.data.status !== 'pending') { isValidating?.(res); attemptCount++; } await new Promise((resolve) => setTimeout(resolve, pollingInterval)); } } if (attemptCount >= maximumAttempts) { if (receipt) { throw new SolverStatusTimeoutError(txHash, attemptCount); } else { throw new DepositTransactionTimeoutError(txHash, attemptCount); } } if (transactionCancelled) { throw Error('Transaction was cancelled'); } return true; }; const waitForTransaction = () => { const controller = new AbortController(); const signal = controller.signal; // Handle transaction replacements and cancellations return { promise: wallet .handleConfirmTransactionStep(txHash, chainId, (replacementTxHash) => { if (signal.aborted) { return; } setTxHashes([{ txHash: replacementTxHash, chainId: chainId }]); txHash = replacementTxHash; attemptCount = 0; // reset attempt count getClient()?.log(['Transaction replaced', replacementTxHash], LogLevel.Verbose); postTransactionToSolver({ txHash: replacementTxHash, chainId, step, request, headers }); if (!isBatchTransaction && !Array.isArray(items) && chainId === details?.currencyOut?.currency?.chainId) { postSameChainTransactionToSolver({ calldata: JSON.stringify({ ...items.data, replacementTxHash }), chainId, step, request, headers }); } }, () => { if (signal.aborted) { return; } transactionCancelled = true; getClient()?.log(['Transaction cancelled'], LogLevel.Verbose); }) .then((data) => { if (signal.aborted) { return; } receipt = data; if (receipt && typeof receipt === 'object' && 'status' in receipt && receipt.status === 'reverted') { throw 'Transaction Reverted'; } getClient()?.log(['Transaction Receipt obtained', receipt], LogLevel.Verbose); }) .catch(async (error) => { if (signal.aborted) { return; } let tenderlyError = null; if (receipt && receipt.transactionHash) { tenderlyError = await getTenderlyDetails(receipt.transactionHash); } getClient()?.log(['Error in handleConfirmTransactionStep', error], LogLevel.Error); if (error.message === 'Transaction cancelled') { transactionCancelled = true; } else { confirmationError = true; throw new TransactionConfirmationError(error, receipt, tenderlyError); } }), controller }; }; //If the origin chain is bitcoin, skip polling for confirmation, because the deposit will take too long if (chainId === 8253038) { return true; } if (isBatchTransaction) { await pollForConfirmation(); // Rely on the solver to confirm batch transactions } else if ( //Sequence internal functions // We want synchronous execution in the following cases: // - Approval Signature step required first // - Bitcoin is the destination // - Canonical route used step.id === 'approve' || details?.currencyOut?.currency?.chainId === 8253038 || request?.data?.useExternalLiquidity) { await waitForTransaction().promise; //In the following cases we want to skip polling for confirmation: // - Bitcoin destination chain, we want to skip polling for confirmation as the block times are lengthy // - Canonical route, also lengthy fill time if (details?.currencyOut?.currency?.chainId !== 8253038 && !request?.data?.useExternalLiquidity) { await pollForConfirmation(); } } else { const { promise: receiptPromise, controller: receiptController } = waitForTransaction(); const confirmationPromise = pollForConfirmation(); await Promise.race([receiptPromise, confirmationPromise]); if (waitingForConfirmation) { await confirmationPromise; } if (!receipt) { if (!check) { await receiptPromise; } else { receiptController.abort(); } } } return true; } const postSameChainTransactionToSolver = async ({ calldata, chainId, request, headers, step }) => { if (calldata && step.requestId && chainId) { getClient()?.log(['Posting same chain transaction to notify the solver'], LogLevel.Verbose); try { const triggerData = { tx: calldata, chainId: chainId.toString(), requestId: step.requestId }; axios .request({ url: `${request.baseURL}/transactions/single`, method: 'POST', headers: headers, data: triggerData }) .then(() => { getClient()?.log(['Same chain transaction notified to the solver'], LogLevel.Verbose); }); } catch (e) { getClient()?.log(['Failed to post same chain transaction to solver', e], LogLevel.Warn); } } }; const postTransactionToSolver = async ({ txHash, chainId, request, headers, step }) => { if (step.id === 'deposit' && txHash) { getClient()?.log(['Posting transaction to notify the solver'], LogLevel.Verbose); try { const triggerData = { txHash, chainId: chainId.toString() }; axios .request({ url: `${request.baseURL}/transactions/index`, method: 'POST', headers: headers, data: triggerData }) .then(() => { getClient()?.log(['Transaction notified to the solver'], LogLevel.Verbose); }); } catch (e) { getClient()?.log(['Failed to post transaction to solver', e], LogLevel.Warn); } } }; //# sourceMappingURL=transaction.js.map