Skip to content

Commit

Permalink
Fix Eth bridge tx replacement (#1350)
Browse files Browse the repository at this point in the history
* refactoring eth utils methods

* fix replaceable tx tracking

* refactoring
  • Loading branch information
Nikita-Polyakov authored Mar 15, 2024
1 parent bda74c6 commit 06bf20b
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 72 deletions.
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 first from 'lodash/fp/first';

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 @@ export class EthBridgeReducer extends BridgeReducer<EthHistory> {

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 || {};

if (!(fee && blockNumber && blockHash)) {
this.updateTransactionParams(id, { externalHash: undefined, externalNetworkFee: undefined });
Expand All @@ -55,7 +66,7 @@ export class EthBridgeReducer extends BridgeReducer<EthHistory> {

// 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 @@ export class EthBridgeReducer extends BridgeReducer<EthHistory> {
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);
};

0 comments on commit 06bf20b

Please sign in to comment.