@reservoir0x/relay-sdk
Version:
Relay is the Fastest and Cheapest Way to Bridge and Transact Across Chains.
307 lines • 12.1 kB
JavaScript
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