Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Eth bridge tx replacement #1350

Merged
merged 3 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions src/store/bridge/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import { subBridgeConnector } from '@/utils/bridge/sub/classes/adapter';
import { updateSubBridgeHistory } from '@/utils/bridge/sub/classes/history';
import ethersUtil from '@/utils/ethers-util';

import type { SignTxResult } from './types';
import type { SwapQuote } from '@sora-substrate/liquidity-proxy/build/types';
import type { IBridgeTransaction, CodecString } from '@sora-substrate/util';
import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types';
Expand Down Expand Up @@ -647,7 +646,7 @@ const actions = defineActions({
return bridgeHistory;
},

async signEthBridgeOutgoingEvm(context, id: string): Promise<SignTxResult> {
async signEthBridgeOutgoingEvm(context, id: string): Promise<ethers.TransactionResponse> {
const { rootState, rootGetters } = bridgeActionContext(context);
const tx = ethBridgeApi.getHistory(id) as Nullable<EthHistory>;

Expand Down Expand Up @@ -678,15 +677,11 @@ const actions = defineActions({
});

const transaction: ethers.TransactionResponse = await contract[method](...args);
const fee = transaction.gasPrice ? ethersUtil.calcEvmFee(transaction.gasPrice, transaction.gasLimit) : undefined;

return {
hash: transaction.hash,
fee,
};
return transaction;
},

async signEthBridgeIncomingEvm(context, id: string): Promise<SignTxResult> {
async signEthBridgeIncomingEvm(context, id: string): Promise<ethers.TransactionResponse> {
const { commit, rootState, rootGetters } = bridgeActionContext(context);
const tx = ethBridgeApi.getHistory(id);

Expand Down Expand Up @@ -717,14 +712,14 @@ const actions = defineActions({
MaxUint256, // uint256 amount
];

let transaction: any;
let transaction: ethers.TransactionResponse;
try {
checkEvmNetwork(context);
transaction = await tokenInstance.approve(...methodArgs);
} finally {
commit.removeTxIdFromApprove(tx.id); // change ui state after approve in client
}
await waitForEvmTransactionMined(transaction.hash); // wait for 1 confirm block
await waitForEvmTransactionMined(transaction); // wait for 1 confirm block
}

const { contract, method, args } = await getIncomingEvmTransactionData({
Expand All @@ -737,11 +732,8 @@ const actions = defineActions({
checkEvmNetwork(context);

const transaction: ethers.TransactionResponse = await contract[method](...args);
const fee = transaction.gasPrice ? ethersUtil.calcEvmFee(transaction.gasPrice, transaction.gasLimit) : undefined;
return {
hash: transaction.hash,
fee,
};

return transaction;
},
});

Expand Down
5 changes: 0 additions & 5 deletions src/store/bridge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,3 @@ export type BridgeState = {
inProgressIds: Record<string, boolean>;
notificationData: Nullable<IBridgeTransaction>;
};

export type SignTxResult = {
hash: string;
fee: Nullable<CodecString>;
};
6 changes: 2 additions & 4 deletions src/store/moonpay/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,15 @@ const actions = defineActions({
},
async getTransactionTranserData(_, hash: string): Promise<Nullable<MoonpayEVMTransferAssetData>> {
try {
const confirmations = 1;
const timeout = 0;
const ethersInstance = ethersUtil.getEthersInstance();

console.info(`Moonpay: found latest moonpay transaction.\nChecking ethereum transaction by hash:\n${hash}`);

// wait until transaction complete
// ISSUE: moonpay sending eth in ropsten, erc20 in rinkeby
await ethersInstance.waitForTransaction(hash, confirmations, timeout);
await ethersInstance.waitForTransaction(hash);

const tx = await ethersInstance.getTransaction(hash);
const tx = await ethersUtil.getEvmTransaction(hash);

if (!tx) throw new Error(`Transaction "${hash}" not found`);

Expand Down
3 changes: 2 additions & 1 deletion src/utils/bridge/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IBridgeTransaction } from '@sora-substrate/util';
import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types';
import type { TransactionResponse } from 'ethers';

export type AddAsset = (address: string) => Promise<void>;
export type GetAssetByAddress = (address: string) => Nullable<RegisteredAccountAsset>;
Expand All @@ -11,7 +12,7 @@ export type GetTransaction<T> = (id: string) => T;
export type UpdateTransaction<T> = (id: string, params: Partial<T>) => void;
export type ShowNotification<T> = (tx: T) => void;
export type BeforeTransactionSign = () => Promise<void>;
export type SignExternal = (id: string) => Promise<any>;
export type SignExternal = (id: string) => Promise<TransactionResponse>;
export type TransactionBoundaryStates<T extends IBridgeTransaction> = Partial<
Record<
T['type'],
Expand Down
53 changes: 21 additions & 32 deletions src/utils/bridge/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,29 @@ import type { EthHistory } from '@sora-substrate/util/build/bridgeProxy/eth/type
import type { EvmHistory } from '@sora-substrate/util/build/bridgeProxy/evm/types';
import type { SubHistory } from '@sora-substrate/util/build/bridgeProxy/sub/types';

const waitForEvmTransactionStatus = async (
hash: string,
replaceCallback: (hash: string) => any,
cancelCallback: (hash: string) => any
) => {
export const waitForEvmTransactionMined = async (
tx: ethers.TransactionResponse | null,
replaceCallback?: (tx: ethers.TransactionResponse | null) => void
): Promise<ethers.TransactionReceipt | null> => {
if (!tx) throw new Error('[waitForEvmTransactionMined]: tx cannot be empty!');

try {
const ethersInstance = ethersUtil.getEthersInstance();
await ethersInstance.waitForTransaction(hash);
} catch (error: any) {
const startBlock = tx.blockNumber ?? (await ethersUtil.getBlockNumber());
const replaceableTx = tx.replaceableTransaction(startBlock);
const txReceipt = await replaceableTx.wait();

return txReceipt;
} catch (error) {
if (ethers.isError(error, 'TRANSACTION_REPLACED')) {
if (error.reason === 'cancelled') {
cancelCallback(error.replacement.hash);
} else {
replaceCallback(error.replacement.hash);
}
}
}
};
const replacedTx = error.replacement;

export const waitForEvmTransactionMined = async (hash?: string, updatedCallback?: (hash: string) => void) => {
if (!hash) throw new Error('[waitForEvmTransactionMined]: hash cannot be empty!');

await waitForEvmTransactionStatus(
hash,
async (replaceHash: string) => {
updatedCallback?.(replaceHash);
await waitForEvmTransactionMined(replaceHash, updatedCallback);
},
(cancelHash) => {
throw new Error(`[waitForEvmTransactionMined]: The transaction was canceled by the user [${cancelHash}]`);
replaceCallback?.(replacedTx);

return await waitForEvmTransactionMined(replacedTx, replaceCallback);
}
);

throw error;
}
};

export const getEvmTransactionRecieptByHash = async (
Expand All @@ -55,11 +46,9 @@ export const getEvmTransactionRecieptByHash = async (

if (!receipt) throw new Error(`Transaction receipt "${transactionHash}" not found`);

const { from, gasPrice, gasUsed, blockNumber, blockHash } = receipt;

const fee = ethersUtil.calcEvmFee(gasPrice, gasUsed);
const { fee, from, blockNumber, blockHash } = receipt;

return { fee, blockHash, blockNumber, from };
return { fee: fee.toString(), blockHash, blockNumber, from };
} catch (error) {
return null;
}
Expand Down
41 changes: 26 additions & 15 deletions src/utils/bridge/eth/classes/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@

import { BridgeReducer } from '@/utils/bridge/common/classes';
import type { IBridgeReducerOptions, GetBridgeHistoryInstance, SignExternal } from '@/utils/bridge/common/types';
import {
getEvmTransactionRecieptByHash,
getTransactionEvents,
waitForEvmTransactionMined,
} from '@/utils/bridge/common/utils';
import { getTransactionEvents, waitForEvmTransactionMined } from '@/utils/bridge/common/utils';
import { ethBridgeApi } from '@/utils/bridge/eth/api';
import type { EthBridgeHistory } from '@/utils/bridge/eth/classes/history';
import { getTransaction, waitForApprovedRequest, waitForIncomingRequest } from '@/utils/bridge/eth/utils';
import {
getTransaction,
getTransactionFee,
waitForApprovedRequest,
waitForIncomingRequest,
} from '@/utils/bridge/eth/utils';
import ethersUtil from '@/utils/ethers-util';

import type { IBridgeTransaction } from '@sora-substrate/util';
import type { RegisteredAccountAsset } from '@sora-substrate/util/build/assets/types';
Expand Down Expand Up @@ -39,12 +41,21 @@

async onEvmPending(id: string): Promise<void> {
const tx = this.getTransaction(id);
const updatedCallback = (externalHash: string) => this.updateTransactionParams(id, { externalHash });
const hash = tx.externalHash;

await waitForEvmTransactionMined(tx.externalHash, updatedCallback);
if (!hash) throw new Error(`[${this.constructor.name}]: Ethereum transaction hash is empty`);

const txResponse = await ethersUtil.getEvmTransaction(hash);
const txReceipt = await waitForEvmTransactionMined(txResponse, (replacedTx) => {
if (replacedTx) {
this.updateTransactionParams(id, {
externalHash: replacedTx.hash,
externalNetworkFee: getTransactionFee(replacedTx),
});
}
});

const { fee, blockNumber, blockHash } =
(await getEvmTransactionRecieptByHash(this.getTransaction(id).externalHash as string)) || {};
const { fee, blockNumber, blockHash } = txReceipt || {};

Check warning on line 58 in src/utils/bridge/eth/classes/reducers.ts

View check run for this annotation

Soramitsu-Sonar-PR-decoration / polkaswap-exchange-web Sonarqube Results

src/utils/bridge/eth/classes/reducers.ts#L58

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.

if (!(fee && blockNumber && blockHash)) {
this.updateTransactionParams(id, { externalHash: undefined, externalNetworkFee: undefined });
Expand All @@ -55,7 +66,7 @@

// In EthHistory 'blockHeight' will store evm block number
this.updateTransactionParams(id, {
externalNetworkFee: fee,
externalNetworkFee: fee.toString(),
externalBlockHeight: blockNumber,
externalBlockId: blockHash,
});
Expand All @@ -68,11 +79,11 @@
this.beforeSubmit(id);

try {
const { hash: externalHash, fee } = await signExternal(id);

const signedTx = await signExternal(id);
// update after sign
this.updateTransactionParams(id, {
externalHash,
externalNetworkFee: fee ?? tx.externalNetworkFee,
externalHash: signedTx.hash,
externalNetworkFee: getTransactionFee(signedTx),
});
} catch (error: any) {
// maybe transaction already completed, try to restore ethereum transaction hash
Expand Down
7 changes: 7 additions & 0 deletions src/utils/bridge/eth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,10 @@ export async function getEthNetworkFee(

return ethersUtil.calcEvmFee(gasPrice, gasLimitTotal);
}

export const getTransactionFee = (tx: ethers.TransactionResponse | ethers.TransactionReceipt) => {
const gasPrice = tx.gasPrice;
const gasAmount = 'gasUsed' in tx ? tx.gasUsed : tx.gasLimit;

return ethersUtil.calcEvmFee(gasPrice, gasAmount);
};