diff --git a/packages/app-portal/src/systems/Bridge/events.tsx b/packages/app-portal/src/systems/Bridge/events.tsx index 6513f81a..09a995dd 100644 --- a/packages/app-portal/src/systems/Bridge/events.tsx +++ b/packages/app-portal/src/systems/Bridge/events.tsx @@ -32,7 +32,10 @@ export function bridgeEvents(store: Store) { store.send(Services.bridgeTxs, { type: 'FETCH_NEXT_PAGE' }); }, addTxEthToFuel( - input?: { ethTxId?: HexAddress } & BridgeInputs['fetchTxs'], + input?: { + ethTxId?: HexAddress; + inputEthTxNonce?: BigInt; + } & BridgeInputs['fetchTxs'], ) { if (!input) return; diff --git a/packages/app-portal/src/systems/Bridge/machines/bridgeTxsMachine.tsx b/packages/app-portal/src/systems/Bridge/machines/bridgeTxsMachine.tsx index c6a5f20e..04ab23ef 100644 --- a/packages/app-portal/src/systems/Bridge/machines/bridgeTxsMachine.tsx +++ b/packages/app-portal/src/systems/Bridge/machines/bridgeTxsMachine.tsx @@ -50,6 +50,7 @@ export type BridgeTxsMachineEvents = type: 'ADD_TX_ETH_TO_FUEL'; input: { ethTxId?: HexAddress; + inputEthTxNonce?: BigInt; } & BridgeInputs['fetchTxs']; } | { @@ -203,16 +204,25 @@ export const bridgeTxsMachine = createMachine( }), assignTxEthToFuel: assign({ ethToFuelTxRefs: (ctx, ev) => { - const { ethTxId, fuelAddress, fuelProvider, ethPublicClient } = - ev.input || {}; + const { + ethTxId, + fuelAddress, + fuelProvider, + ethPublicClient, + inputEthTxNonce, + } = ev.input || {}; if (!ethTxId || ctx.ethToFuelTxRefs?.[ethTxId]) return ctx.ethToFuelTxRefs; + const key = `${ethTxId}-${inputEthTxNonce}`; + console.log(`NEW: creating machine Fuel To Eth: ${ethTxId}`); const newRef = { - [ethTxId]: spawn( + [key]: spawn( txEthToFuelMachine.withContext({ ethTxId: ethTxId as HexAddress, + inputEthTxNonce: inputEthTxNonce, + machineId: key, fuelAddress: fuelAddress, fuelProvider: fuelProvider, ethPublicClient: ethPublicClient, diff --git a/packages/app-portal/src/systems/Bridge/services/bridge.tsx b/packages/app-portal/src/systems/Bridge/services/bridge.tsx index d0594b53..9341a0a8 100644 --- a/packages/app-portal/src/systems/Bridge/services/bridge.tsx +++ b/packages/app-portal/src/systems/Bridge/services/bridge.tsx @@ -79,7 +79,7 @@ export class BridgeService { const assetEth = getAssetEth(asset); const amountEthUnits = bn.parseUnits(amountFormatted, assetEth.decimals); - const txId = await TxEthToFuelService.start({ + const startedTxEthToFuel = await TxEthToFuelService.start({ amount: amountEthUnits.toHex(), ethWalletClient, fuelAddress, @@ -87,18 +87,22 @@ export class BridgeService { ethPublicClient, }); - if (txId) { + const { txHash, nonce } = startedTxEthToFuel || {}; + + if (txHash && nonce) { if (fuelWallet) { store.addTxEthToFuel({ - ethTxId: txId, + ethTxId: txHash, + inputEthTxNonce: nonce, fuelProvider, ethPublicClient, fuelAddress, }); store.openTxEthToFuel({ - txId, + txId: txHash, + messageSentEventNonce: nonce, }); - EthTxCache.setTxIsCreated(txId); + EthTxCache.setTxIsCreated(txHash); } } diff --git a/packages/app-portal/src/systems/Chains/eth/machines/txEthToFuelMachine.tsx b/packages/app-portal/src/systems/Chains/eth/machines/txEthToFuelMachine.tsx index 31eb676f..1e32f6b5 100644 --- a/packages/app-portal/src/systems/Chains/eth/machines/txEthToFuelMachine.tsx +++ b/packages/app-portal/src/systems/Chains/eth/machines/txEthToFuelMachine.tsx @@ -359,8 +359,8 @@ export const txEthToFuelMachine = createMachine( fuelMessageStatus: (_, ev) => ev.data, }), clearTxCreated: (ctx) => { - if (ctx.machineId && EthTxCache.getTxIsCreated(ctx.machineId)) { - EthTxCache.removeTxCreated(ctx.machineId); + if (ctx.ethTxId && EthTxCache.getTxIsCreated(ctx.ethTxId)) { + EthTxCache.removeTxCreated(ctx.ethTxId); } }, setEthToFuelTxReceiptCached: (ctx) => { @@ -400,7 +400,8 @@ export const txEthToFuelMachine = createMachine( hasEthTxNonce: (ctx, ev) => !!ctx.ethTxNonce || !!ev?.data?.nonce, hasAnalyzeTxInput: (ctx) => !!ctx.ethTxId && - !!ctx.inputEthTxNonce && + // inputEthTxNonce can be zero + ctx.inputEthTxNonce != null && !!ctx.machineId && !!ctx.fuelAddress && !!ctx.fuelProvider && diff --git a/packages/app-portal/src/systems/Chains/eth/services/txEthToFuel.ts b/packages/app-portal/src/systems/Chains/eth/services/txEthToFuel.ts index b51338cd..d5cf7245 100644 --- a/packages/app-portal/src/systems/Chains/eth/services/txEthToFuel.ts +++ b/packages/app-portal/src/systems/Chains/eth/services/txEthToFuel.ts @@ -24,7 +24,7 @@ import { FUEL_MESSAGE_PORTAL, decodeMessageSentData, } from '../contracts/FuelMessagePortal'; -import { getBlockDate, isErc20Address } from '../utils'; +import { getBlockDate, getTransactionReceipt, isErc20Address } from '../utils'; import { type HexAddress, @@ -126,8 +126,8 @@ export class TxEthToFuelService { TxEthToFuelService.assertStartEth(input); try { - const { ethWalletClient, fuelAddress, amount } = input; - if (fuelAddress && ethWalletClient) { + const { ethWalletClient, fuelAddress, amount, ethPublicClient } = input; + if (fuelAddress && ethWalletClient && ethPublicClient) { const bridgeSolidityContracts = await getBridgeSolidityContracts(); const fuelPortal = EthConnectorService.connectToFuelMessagePortal({ walletClient: ethWalletClient, @@ -142,7 +142,34 @@ export class TxEthToFuelService { }, ); - return txHash; + const receipt = await getTransactionReceipt({ + ethPublicClient, + txHash, + }); + + if (receipt.status !== 'success') { + throw new Error('Failed to deposit ETH'); + } + + const nonce = receipt.logs.map((log) => { + try { + const messageSentEvent = decodeEventLog({ + abi: FUEL_MESSAGE_PORTAL.abi, + data: log.data, + topics: log.topics, + }) as unknown as { args: FuelMessagePortalArgs['MessageSent'] }; + + return messageSentEvent?.args?.nonce; + } catch (_) { + /* empty */ + } + })[0]; + + if (nonce == null) { + throw new Error('Failed to get nonce of ETH deposit'); + } + + return { txHash, nonce }; } } catch (e) { // biome-ignore lint/suspicious/noExplicitAny: @@ -185,19 +212,11 @@ export class TxEthToFuelService { amount, ]); - let approveTxHashReceipt: TransactionReceipt; - try { - approveTxHashReceipt = await ethPublicClient.getTransactionReceipt({ - hash: approveTxHash, - }); - } catch (_err: unknown) { - // workaround in place because waitForTransactionReceipt stop working after first time using it - approveTxHashReceipt = - await ethPublicClient.waitForTransactionReceipt({ - hash: approveTxHash, - confirmations: 2, - }); - } + const approveTxHashReceipt = await getTransactionReceipt({ + ethPublicClient, + txHash: approveTxHash, + waitOptions: { confirmations: 2 }, + }); if (approveTxHashReceipt.status !== 'success') { throw new Error('Failed to approve Token for transfer'); @@ -213,7 +232,34 @@ export class TxEthToFuelService { amount, ]); - return depositTxHash; + const receipt = await getTransactionReceipt({ + ethPublicClient, + txHash: depositTxHash, + }); + + if (receipt.status !== 'success') { + throw new Error('Failed to deposit ETH'); + } + + const nonce = receipt.logs.map((log) => { + try { + const messageSentEvent = decodeEventLog({ + abi: FUEL_MESSAGE_PORTAL.abi, + data: log.data, + topics: log.topics, + }) as unknown as { args: FuelMessagePortalArgs['MessageSent'] }; + + return messageSentEvent?.args?.nonce; + } catch (_) { + /* empty */ + } + })[0]; + + if (!nonce) { + throw new Error('Failed to get nonce of ETH deposit'); + } + + return { txHash: depositTxHash, nonce }; } } catch (e) { // biome-ignore lint/suspicious/noExplicitAny: diff --git a/packages/app-portal/src/systems/Chains/eth/utils/index.tsx b/packages/app-portal/src/systems/Chains/eth/utils/index.tsx index 8ccb1f23..b2bfd3a8 100644 --- a/packages/app-portal/src/systems/Chains/eth/utils/index.tsx +++ b/packages/app-portal/src/systems/Chains/eth/utils/index.tsx @@ -2,3 +2,4 @@ export * from './block'; export * from './chain'; export * from './address'; export * from './txCache'; +export * from './transaction'; diff --git a/packages/app-portal/src/systems/Chains/eth/utils/transaction.ts b/packages/app-portal/src/systems/Chains/eth/utils/transaction.ts new file mode 100644 index 00000000..99081b81 --- /dev/null +++ b/packages/app-portal/src/systems/Chains/eth/utils/transaction.ts @@ -0,0 +1,29 @@ +import { + PublicClient, + TransactionReceipt, + WaitForTransactionReceiptParameters, +} from 'viem'; + +export async function getTransactionReceipt({ + ethPublicClient, + txHash, + waitOptions, +}: { + ethPublicClient: PublicClient; + txHash: `0x${string}`; + waitOptions?: Omit; +}): Promise { + let receipt: TransactionReceipt; + try { + receipt = await ethPublicClient.getTransactionReceipt({ + hash: txHash, + }); + } catch (_err: unknown) { + // workaround in place because waitForTransactionReceipt stop working after first time using it + receipt = await ethPublicClient.waitForTransactionReceipt({ + ...(waitOptions ? waitOptions : {}), + hash: txHash, + }); + } + return receipt; +} diff --git a/packages/app-portal/src/systems/Chains/fuel/services/txFuelToEth.ts b/packages/app-portal/src/systems/Chains/fuel/services/txFuelToEth.ts index 76e366b7..312a21ed 100644 --- a/packages/app-portal/src/systems/Chains/fuel/services/txFuelToEth.ts +++ b/packages/app-portal/src/systems/Chains/fuel/services/txFuelToEth.ts @@ -26,6 +26,7 @@ import { FUEL_MESSAGE_PORTAL } from '../../eth/contracts/FuelMessagePortal'; import { EthConnectorService } from '../../eth/services'; import { parseEthAddressToFuel } from '../../eth/utils/address'; import { createRelayMessageParams } from '../../eth/utils/relayMessage'; +import { getTransactionReceipt } from '../../eth/utils/transaction'; import { getBlock } from '../utils'; export type TxFuelToEthInputs = { @@ -439,28 +440,16 @@ export class TxFuelToEthService { const { ethPublicClient, txHash } = input; - let txReceipts: Awaited< - ReturnType< - | typeof ethPublicClient.getTransactionReceipt - | typeof ethPublicClient.waitForTransactionReceipt - > - >; - try { - txReceipts = await ethPublicClient.getTransactionReceipt({ - hash: txHash, - }); - } catch (_err: unknown) { - // workaround in place because waitForTransactionReceipt stop working after first time using it - txReceipts = await ethPublicClient.waitForTransactionReceipt({ - hash: txHash, - }); - } + const txReceipt = await getTransactionReceipt({ + ethPublicClient, + txHash, + }); - if (txReceipts.status !== 'success') { + if (txReceipt.status !== 'success') { throw new Error('Failed to relay message (transaction reverted)'); } - return !!txReceipts; + return !!txReceipt; } static async fetchTxs(input: TxFuelToEthInputs['fetchTxs']) {