@reservoir0x/relay-sdk
Version:
Relay is the Fastest and Cheapest Way to Bridge and Transact Across Chains.
427 lines • 22.4 kB
JavaScript
import { pollUntilHasData, pollUntilOk } from './pollApi.js';
import { axios } from '../utils/index.js';
import { getClient } from '../client.js';
import { LogLevel } from '../utils/logger.js';
import { sendTransactionSafely } from './transaction.js';
import { canBatchTransactions, prepareBatchTransaction } from './prepareBatchTransaction.js';
// /**
// * When attempting to perform actions, such as, bridging or performing a cross chain action
// * the user's account needs to meet certain requirements. For
// * example, if the user attempts to bridge currency you must check if the
// * user has enough balance, before providing the transaction to be signed by
// * the user. This function executes all transactions and signatures, in order, to complete the
// * action.
// * @param chainId matching the chain to execute on
// * @param request AxiosRequestConfig object with at least a url set
// * @param wallet ReservoirWallet object that adheres to the ReservoirWallet interface
// * @param setState Callback to update UI state has execution progresses
// * @param newJson Data passed around, which contains steps and items etc
// * @returns A promise you can await on
// */
export async function executeSteps(chainId, request = {}, wallet, setState, newJson, stepOptions) {
const client = getClient();
if (client?.baseApiUrl) {
request.baseURL = client.baseApiUrl;
}
const pollingInterval = client.pollingInterval ?? 5000;
const maximumAttempts = client.maxPollingAttemptsBeforeTimeout ??
(2.5 * 60 * 1000) / pollingInterval;
const chain = client.chains.find((chain) => chain.id === chainId);
if (!chain) {
throw `Unable to find chain: Chain id ${chainId}`;
}
let json = newJson;
let isAtomicBatchSupported = false;
try {
if (!json) {
client.log(['Execute Steps: Fetching Steps', request], LogLevel.Verbose);
const res = await axios.request(request);
json = res.data;
if (res.status !== 200)
throw json;
client.log(['Execute Steps: Steps retrieved', json], LogLevel.Verbose);
}
// Handle errors
if (json.error || !json.steps)
throw json;
// Check if step's transactions can be batched and if wallet supports atomic batch
// If so, manipulate steps to batch transactions
if (canBatchTransactions(json.steps)) {
isAtomicBatchSupported = Boolean(wallet?.supportsAtomicBatch &&
(await wallet?.supportsAtomicBatch(chainId)));
if (isAtomicBatchSupported) {
const batchedStep = prepareBatchTransaction(json.steps);
json.steps = [batchedStep];
}
}
// Update state on first call or recursion
setState({
steps: [...json?.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
let incompleteStepIndex = -1;
let incompleteStepItemIndex = -1;
json.steps.find((step, i) => {
if (!step.items) {
return false;
}
incompleteStepItemIndex = step.items.findIndex((item) => item.status == 'incomplete');
if (incompleteStepItemIndex !== -1) {
incompleteStepIndex = i;
return true;
}
});
// There are no more incomplete steps
if (incompleteStepIndex === -1) {
client.log(['Execute Steps: all steps complete'], LogLevel.Verbose);
return json;
}
const step = json.steps[incompleteStepIndex];
if (stepOptions && stepOptions[step.id]) {
const currentStepOptions = stepOptions[step.id];
step.items?.forEach((stepItem) => {
if (currentStepOptions.gasLimit) {
stepItem.data.gas = currentStepOptions.gasLimit;
}
});
}
let stepItems = json.steps[incompleteStepIndex].items;
if (!stepItems) {
client.log(['Execute Steps: skipping step, no items in step'], LogLevel.Verbose);
return json;
}
let { kind } = step;
let stepItem = stepItems[incompleteStepItemIndex];
// If step item is missing data, poll until it is ready
if (!stepItem.data) {
client.log(['Execute Steps: step item data is missing, begin polling'], LogLevel.Verbose);
json = (await pollUntilHasData(request, (json) => {
client.log(['Execute Steps: step item data is missing, polling', json], LogLevel.Verbose);
const data = json;
// An item is ready if:
// - data became available
// - the status changed to "completed"
return data?.steps?.[incompleteStepIndex].items?.[incompleteStepItemIndex].data ||
data?.steps?.[incompleteStepIndex].items?.[incompleteStepItemIndex]
.status === 'complete'
? true
: false;
}));
if (!json.steps || !json.steps[incompleteStepIndex].items)
throw json;
const items = json.steps[incompleteStepIndex].items;
if (!items ||
!items[incompleteStepItemIndex] ||
!items[incompleteStepItemIndex].data) {
throw json;
}
stepItems = items;
stepItem = items[incompleteStepItemIndex];
setState({
steps: [...json?.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
}
client.log([`Execute Steps: Begin processing step items for: ${step.action}`], LogLevel.Verbose);
const promises = stepItems
.filter((stepItem) => stepItem.status === 'incomplete')
.map((stepItem) => {
return new Promise(async (resolve, reject) => {
try {
const stepData = stepItem.data;
if (!json) {
return;
}
// Handle each step based on it's kind
switch (kind) {
// Make an on-chain transaction
case 'transaction': {
try {
client.log([
'Execute Steps: Begin transaction step for, sending transaction'
], LogLevel.Verbose);
// if chainId is present in the tx data field then you should relay the tx on that chain
// otherwise, it's assumed the chain id matched the network the api request was made on
const transactionChainId = stepItem?.data?.chainId ?? chainId;
const crossChainIntentChainId = chainId;
stepItem.progressState = 'confirming';
setState({
steps: [...json.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
// If atomic batch is supported and first item in step, batch all items in the step
const transactionStepItems = isAtomicBatchSupported && incompleteStepItemIndex === 0
? stepItems
: stepItem;
await sendTransactionSafely(transactionChainId, transactionStepItems, step, wallet, (txHashes) => {
client.log([
'Execute Steps: Transaction step, got transactions',
txHashes
], LogLevel.Verbose);
stepItem.txHashes = txHashes;
if (json) {
setState({
steps: [...json.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
}
}, (internalTxHashes) => {
stepItem.internalTxHashes = internalTxHashes;
if (json) {
setState({
steps: [...json.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
}
}, request, undefined, crossChainIntentChainId, (res) => {
if (res && res.data.status === 'delayed') {
stepItem.progressState = 'validating_delayed';
}
else {
stepItem.progressState = 'validating';
}
if (json) {
setState({
steps: [...json.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
}
}, json?.details);
}
catch (e) {
throw e;
}
break;
}
// Sign a message
case 'signature': {
let signature;
const signData = stepData['sign'];
const postData = stepData['post'];
client.log(['Execute Steps: Begin signature step'], LogLevel.Verbose);
if (signData) {
stepItem.progressState = 'signing';
setState({
steps: [...json.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
signature = await wallet.handleSignMessageStep(stepItem, step);
if (signature) {
request.params = {
...request.params,
signature
};
}
}
if (postData) {
client.log(['Execute Steps: Posting order'], LogLevel.Verbose);
stepItem.progressState = 'posting';
setState({
steps: [...json.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
const postOrderUrl = new URL(`${request.baseURL}${postData.endpoint}`);
const headers = {
'Content-Type': 'application/json'
};
try {
const getData = async function () {
let response = await axios.request({
url: postOrderUrl.href,
data: postData.body
? JSON.stringify(postData.body)
: undefined,
method: postData.method,
params: request.params,
headers
});
return response;
};
const res = await getData();
// Append new steps if returned in response
if (res.data &&
res.data.steps &&
Array.isArray(res.data.steps)) {
json.steps = [...json.steps, ...res.data.steps];
setState({
steps: [...json.steps, ...res.data.steps],
fees: { ...json.fees },
breakdown: json.breakdown,
details: json.details
});
client.log([
`Execute Steps: New steps appended from ${postData.endpoint}`,
res.data.steps
], LogLevel.Verbose);
break;
}
// If check, poll check until validated
if (stepItem?.check) {
stepItem.progressState = 'validating';
setState({
steps: [...json.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
stepItem.isValidatingSignature = true;
setState({
steps: [...json?.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
await pollUntilOk({
url: `${request.baseURL}${stepItem?.check.endpoint}`,
method: stepItem?.check.method,
headers
}, (res) => {
client.log([
`Execute Steps: Polling execute status to check if indexed`,
res
], LogLevel.Verbose);
//set status
if (res?.data?.status === 'success' &&
res?.data?.txHashes) {
const chainTxHashes = res.data?.txHashes?.map((hash) => {
return {
txHash: hash,
chainId: res.data.destinationChainId ?? chain?.id
};
});
if (res?.data?.inTxHashes) {
const chainInTxHashes = res.data?.inTxHashes?.map((hash) => {
return {
txHash: hash,
chainId: chain?.id ?? res.data.originChainId
};
});
stepItem.internalTxHashes = chainInTxHashes;
}
stepItem.txHashes = chainTxHashes;
return true;
}
else if (res?.data?.status === 'failure') {
throw Error(res?.data?.details || 'Transaction failed');
}
else if (res?.data?.status === 'delayed') {
stepItem.progressState = 'validating_delayed';
}
return false;
}, maximumAttempts, 0, pollingInterval);
}
if (res.status > 299 || res.status < 200)
throw res.data;
if (res.data.results) {
stepItem.orderData = res.data.results;
}
else if (res.data && res.data.orderId) {
stepItem.orderData = [
{
orderId: res.data.orderId,
crossPostingOrderId: res.data.crossPostingOrderId,
orderIndex: res.data.orderIndex || 0
}
];
}
setState({
steps: [...json?.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
}
catch (err) {
throw err;
}
}
break;
}
default:
break;
}
stepItem.status = 'complete';
stepItem.progressState = 'complete';
stepItem.isValidatingSignature = false;
setState({
steps: [...json?.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
resolve(stepItem);
}
catch (e) {
const error = e;
const errorMessage = error
? error.message
: 'Error: something went wrong';
if (error && json?.steps) {
json.steps[incompleteStepIndex].error = errorMessage;
stepItem.error = errorMessage;
stepItem.errorData = e?.response?.data || e;
stepItem.isValidatingSignature = false;
setState({
steps: [...json?.steps],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details
});
}
reject(error);
}
});
});
await Promise.all(promises);
// Recursively call executeSteps()
return await executeSteps(chainId, request, wallet, setState, json, stepOptions);
}
catch (err) {
client.log(['Execute Steps: An error occurred', err], LogLevel.Error);
const error = err && err?.response?.data ? err.response.data : err;
let refunded = false;
if (error && error.message) {
refunded = error.message.includes('Refunded');
}
else if (error && error.includes) {
refunded = error.includes('Refunded');
}
if (json) {
json.error = error;
setState({
steps: json.steps ? [...json.steps] : [{}],
fees: { ...json?.fees },
breakdown: json?.breakdown,
details: json?.details,
refunded: refunded,
error
});
}
else {
json = {
error,
steps: [],
refunded
};
setState(json);
}
throw err;
}
}
//# sourceMappingURL=executeSteps.js.map