From 85300bb9c1331e51e5752cbe56b9e1f5444f2a07 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:52:23 -0800 Subject: [PATCH] better error handling & some fixes --- frontend/src/components/RelayButton.tsx | 433 +++++++++++++----------- 1 file changed, 226 insertions(+), 207 deletions(-) diff --git a/frontend/src/components/RelayButton.tsx b/frontend/src/components/RelayButton.tsx index 48b4f04..01b8418 100644 --- a/frontend/src/components/RelayButton.tsx +++ b/frontend/src/components/RelayButton.tsx @@ -1,9 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + import React from 'react'; import MockOjo from '../artifacts/contracts/MockOjo.sol/MockOjo.json'; import IAxelarGateway from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/interfaces/IAxelarGateway.sol/IAxelarGateway.json' import { axelarChains, axelarGatewayAddresses, isAxelarChain, axelarChainIDs } from './lib/AxelarChains' -import { Squid, TokenData } from '@0xsquid/sdk' -import { ChainData, ChainType } from "@0xsquid/squid-types"; +import { Squid } from '@0xsquid/sdk' +import { ChainType, SquidCallType } from "@0xsquid/squid-types"; import { AxelarQueryAPI, Environment, @@ -11,6 +15,7 @@ import { } from "@axelar-network/axelarjs-sdk"; import { ethers } from 'ethers'; import { erc20ABI, useNetwork } from 'wagmi'; + const mockOjoAddress = import.meta.env.VITE_MOCK_OJO_ADDRESS as `0x${string}`; const environment = import.meta.env.VITE_ENVIRONMENT as Environment; @@ -30,13 +35,12 @@ const getSDK = (): Squid => { const getOjoGasEstimate = async (networkName: string): Promise => { try { - if(networkName == "mainnet") { + if(networkName === "mainnet") { networkName = "ethereum" } - if(networkName == "Arbitrum One") { + if(networkName === "Arbitrum One") { networkName = "Arbitrum" } - // log console.log("Fetching Ojo gas estimate for network:", networkName); const response = await fetch( @@ -45,7 +49,7 @@ const getOjoGasEstimate = async (networkName: string): Promise => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - const data = await response.json(); + const data = await response.json() as { gas_estimate: string }; return data.gas_estimate; } catch (error) { console.error("Failed to fetch Ojo gas estimate:", error); @@ -63,224 +67,239 @@ const RelayPricesButton: React.FC = ({ assetNames, symbol } if (typeof window.ethereum !== "undefined" && chain && isAxelarChain(chain.name)) { - const provider = new ethers.BrowserProvider(window.ethereum); - const signer = await provider.getSigner(); + try { + const provider = new ethers.BrowserProvider(window.ethereum); + const signer = await provider.getSigner(); + // Verify contract addresses + if (!mockOjoAddress) { + throw new Error("MockOjo contract address not configured"); + } - // check amount of assets requested to be relayed is not over limit - // const ojoContract = new ethers.Contract(ojoAddress, Ojo.abi, signer); - const assetLimit = 1; - if (assetNames.length > assetLimit) { - alert("Cannot relay more than " + assetLimit + " assets at one time") - return - } + // Check asset limit + const assetLimit = 1; + if (assetNames.length > assetLimit) { + alert("Cannot relay more than " + assetLimit + " assets at one time") + return + } + + // Get token addresses + let axelarTokenAddress: string; + if (symbol) { + const axelarGatewayAddress = axelarGatewayAddresses[chain.name]; + if (!axelarGatewayAddress) { + throw new Error(`No Axelar Gateway address found for chain ${chain.name}`); + } + console.log("Using Axelar Gateway:", axelarGatewayAddress); + + const axelarGatewayContract = new ethers.Contract( + axelarGatewayAddress, + IAxelarGateway.abi, + signer + ); + const formattedSymbol = symbol.toUpperCase().trim(); + + try { + axelarTokenAddress = await axelarGatewayContract.tokenAddresses(formattedSymbol); + if (!axelarTokenAddress) { + throw new Error(`No token address found for symbol ${formattedSymbol}`); + } + console.log(`Token address for ${formattedSymbol}: ${axelarTokenAddress}`); + } catch (error) { + console.error(`Error fetching token address for ${formattedSymbol}:`, error); + throw new Error(`Failed to fetch token address for ${formattedSymbol}`); + } + } else { + throw new Error("Symbol is required"); + } + + // Estimate Axelar GMP fee with higher gas limit + const api = new AxelarQueryAPI({ environment: environment }); + await api.estimateGasFee( + axelarChains[chain?.name], + "ojo", + GasToken.AXL, + 1000000, // Increased gas limit + 2, // Higher gas multiplier + ); + + const chainid = axelarChainIDs[chain.name as keyof typeof axelarChainIDs]; + if (!chainid) { + throw new Error(`No chain ID found for ${chain.name}`); + } - // fetch token address of fee token if selected - let axelarTokenAddress; - let ethTokenAddress; - if (symbol) { - const axelarGatewayAddress = axelarGatewayAddresses[chain.name]; - // log address - console.log("axelarGatewayAddress", axelarGatewayAddress); - const axelarGatewayContract = new ethers.Contract(axelarGatewayAddress, IAxelarGateway.abi, signer); + // Prepare contract interactions + const erc20Interface = new ethers.Interface(erc20ABI); + const approvalerc20 = erc20Interface.encodeFunctionData("approve", [ + mockOjoAddress, + ethers.MaxUint256, + ]); + + // Prepare relay transaction + const assetNamesArray = assetNames.map(name => ethers.encodeBytes32String(name)); + const ojoInterface = new ethers.Interface(MockOjo); + const ojoEncodedData = ojoInterface.encodeFunctionData( + "relayOjoPriceDataWithToken", + [ + assetNamesArray, + symbol, + ethers.parseUnits(amount, 6), // Using 6 decimals for USDC + axelarTokenAddress, + ] + ); + + // Calculate gas requirements + const gasEstimateUAxl = await getOjoGasEstimate(chain.name); + if (!gasEstimateUAxl) { + throw new Error("Failed to get gas estimate"); + } + console.log("Base gas estimate (uAXL):", gasEstimateUAxl); + // Safety multiplier of 2x + const increasedGasEstimateUAxl = Number(gasEstimateUAxl) * 2; + console.log("Increased gas estimate (uAXL):", increasedGasEstimateUAxl); - // Convert symbol to uppercase and trim any whitespace - const formattedSymbol = symbol.toUpperCase().trim(); + // Calculate total gas for 72 updates (3 months) + const totalGasUAxl = BigInt(increasedGasEstimateUAxl) * BigInt(72); + const totalGasAXL = totalGasUAxl / BigInt(10**6); - try { - axelarTokenAddress = await axelarGatewayContract.tokenAddresses(formattedSymbol); + // Get token prices for conversion + const [ethPriceResponse, axlPriceResponse] = await Promise.all([ + fetch(`https://api.agamotto-val-prod-0.ojo.network/ojo/oracle/v1/denoms/exchange_rates/ETH`), + fetch(`https://api.agamotto-val-prod-0.ojo.network/ojo/oracle/v1/denoms/exchange_rates/AXL`) + ]); + type ExchangeRateResponse = { + exchange_rates: Array<{ amount: string }>; + }; - ethTokenAddress = await axelarGatewayContract.tokenAddresses("ETH"); - console.log(`Token address for ${formattedSymbol}: ${axelarTokenAddress}`); + const ethPriceData = await ethPriceResponse.json() as ExchangeRateResponse; + const axlPriceData = await axlPriceResponse.json() as ExchangeRateResponse; - } catch (error) { - console.error(`Error fetching token address for ${formattedSymbol}:`, error); - alert(`Failed to fetch token address for ${formattedSymbol}. Please check the console for details.`); - return; + if (!ethPriceData.exchange_rates?.[0]?.amount || !axlPriceData.exchange_rates?.[0]?.amount) { + throw new Error("Failed to get token prices"); } - } + const ethPrice = Number(ethPriceData.exchange_rates[0].amount); + const axlPrice = Number(axlPriceData.exchange_rates[0].amount); + const ethToAxlRate = ethPrice / axlPrice; - // estimate axelar gmp fee - const api = new AxelarQueryAPI({ environment: environment }); - const gasFee = await api.estimateGasFee( - axelarChains[chain?.name], - "ojo", - GasToken.AXL, - 700000, - 2, - ); - - const chainid = axelarChainIDs[chain.name as keyof typeof axelarChainIDs] - // Set up parameters for swapping tokens and depositing into Radiant lending pool - - // erc20 approval interface - const erc20Interface = new ethers.Interface(erc20ABI) - const approvalerc20 = erc20Interface.encodeFunctionData("approve", [ - mockOjoAddress, - ethers.MaxUint256, - ]); - - // send relay price data tx - const assetNamesArray = assetNames.map(name => ethers.encodeBytes32String(name)); - const ojoInterface = new ethers.Interface( - MockOjo.abi - ); - const ojoEncodedData = ojoInterface.encodeFunctionData( - "relayOjoPriceDataWithToken", - [ - assetNamesArray, - symbol, // Placeholder for dynamic balance - ethers.parseUnits(amount, 6), - axelarTokenAddress, - ] - ); - - // Get the swap route using Squid SDK - const squid = getSDK() - await squid.init() - - // get chain id selected - const chainId = chain?.id - // print chainid - console.log("chainId", chainId); - - const fromToken = squid.tokens.find( - t => - t.symbol === "WETH" && - t.chainId === chainId.toString() - ); - // log fromToken - console.log("fromToken", fromToken); - - const postHooks = { - chainType: ChainType.EVM, - calls: [ - { - chainType: ChainType.EVM, - callType: 1,// SquidCallType.FULL_TOKEN_BALANCE - target: axelarTokenAddress, - value: "0", // this will be replaced by the full native balance of the multicall after the swap - callData: approvalerc20, - payload: { - tokenAddress: axelarTokenAddress, - inputPos: 1, - }, - estimatedGas: "50000", - }, - { + if (ethToAxlRate === 0) { + throw new Error("Invalid exchange rate"); + } + + // Calculate final ETH amount + const totalGasETH = Number(totalGasAXL) / ethToAxlRate; + const totalGasETHWei = Math.floor(totalGasETH * 10**18).toString(); + + console.log("Total gas cost:", { + ETH: totalGasETH, + USD: totalGasETH * ethPrice, + AXL: Number(totalGasAXL) + }); + + // Initialize Squid SDK + const squid = getSDK(); + await squid.init(); + + const fromToken = squid.tokens.find( + t => t.symbol === "WETH" && t.chainId === chainid + ); + + if (!fromToken) { + throw new Error("WETH token not found for chain"); + } + + // Configure post-hooks with improved gas estimates + const postHooks = { chainType: ChainType.EVM, - callType: 1, // SquidCallType.FULL_TOKEN_BALANCE - target: mockOjoAddress, - value: "0", - callData: ojoEncodedData, - payload: { - tokenAddress: axelarTokenAddress, - inputPos: 1, - }, - estimatedGas: "50000", - }, - ], - description: "ojo price data relay", - logoURI: "https://v2.app.squidrouter.com/images/icons/squid_logo.svg", - provider: signer.address, - } - - // Let's calculate how much gas we want for 3 months. ETH tends to deviate about 72 times every 3 months. - // Every deviation is a tx, so we want to have 72 * gasestimate = gas in uAXL. - // Then, we need to convert uAXL -> AXL -> ETH. - // Then, swap that amount of ETH and use it for the transaction. - - // Get Ojo gas estimate in uAXL - const gasEstimateUAxl = await getOjoGasEstimate(chain.name); - console.log("Gas estimate (uAXL):", gasEstimateUAxl); - // let's increase this by 2x for safety - const increasedGasEstimateUAxl = Number(gasEstimateUAxl) * 2; - console.log("Increased gas estimate (uAXL):", increasedGasEstimateUAxl); - - // Calculate total gas needed for 72 updates (3 months) - const totalGasUAxl = BigInt(increasedGasEstimateUAxl) * BigInt(72); - console.log("Total gas for 3 months (uAXL):", totalGasUAxl.toString()); - - // divide by 10^6 to get AXL - const totalGasAXL = totalGasUAxl / BigInt(10**6); - console.log("Total gas for 3 months (AXL):", totalGasAXL.toString()); - - // now convert to ETH. Get the price of ETH from Ojo's API - const ethPriceResponse = await fetch(`https://api.agamotto-val-prod-0.ojo.network/ojo/oracle/v1/denoms/exchange_rates/ETH`); - const ethPriceData = await ethPriceResponse.json(); - const ethPrice = ethPriceData.exchange_rates[0].amount; - console.log("ETH price:", ethPrice); - - // now get AXL price - const axlPriceResponse = await fetch(`https://api.agamotto-val-prod-0.ojo.network/ojo/oracle/v1/denoms/exchange_rates/AXL`); - const axlPriceData = await axlPriceResponse.json(); - const axlPrice = axlPriceData.exchange_rates[0].amount; - console.log("AXL price:", axlPrice); - // get ETH -> AXL exchange rate - const ethToAxlRate = ethPrice / axlPrice; - console.log("ETH -> AXL rate:", ethToAxlRate); - - // convert totalGasAXL to ETH - const totalGasETH = Number(totalGasAXL) / ethToAxlRate; - console.log("Total gas for 3 months (ETH):", totalGasETH.toString()); - - // now multiply ETH value by 10^18 - const totalGasETHWei = totalGasETH * 10**18; - // print totalGasETHWei - console.log("Total gas for 3 months (ETH Wei):", totalGasETHWei.toString()); - - // print in USD - const totalGasUSD = totalGasETH * ethPrice; - console.log("Total gas for 3 months (USD):", totalGasUSD.toString()); - - - // Now you can use totalGasUAxl to calculate the fromAmount - // Example: Convert this to the appropriate ETH amount for the swap - // For now, keeping the existing amount: - const params = { - fromAddress: signer.address, - fromChain: chainid, - fromToken: fromToken?.address || "", - fromAmount: totalGasETHWei.toString(), - toChain: chainid, - toToken: axelarTokenAddress, - toAddress: signer.address, - quoteOnly: false, - postHooks: postHooks - }; - - console.log("params", params); - - - const { route, requestId } = await squid!.getRoute(params) - console.log("route", route); - - - - // Execute the swap transaction - const tx = (await squid.executeRoute({ - signer, - route, - })) as unknown as ethers.TransactionResponse; - const txReceipt = await tx.wait(); - - // Show the transaction receipt with Axelarscan link - console.log("txReceipt", txReceipt); - - - // print route - console.log("route", route); - console.log("requestId", requestId); - alert("Relay tx sent succesfully, check status on https://testnet.axelarscan.io/gmp/search?chain=ojo") + calls: [ + { + chainType: ChainType.EVM, + callType: SquidCallType.FULL_TOKEN_BALANCE, + target: axelarTokenAddress, + value: "0", + callData: approvalerc20, + payload: { + tokenAddress: axelarTokenAddress, + inputPos: 1, + }, + estimatedGas: "100000", // Increased for approval + }, + { + chainType: ChainType.EVM, + callType: SquidCallType.FULL_TOKEN_BALANCE, + target: mockOjoAddress, + value: "0", + callData: ojoEncodedData, + payload: { + tokenAddress: axelarTokenAddress, + inputPos: 1, + }, + estimatedGas: "500000", // Increased for cross-chain call + }, + ], + description: "Ojo price data relay", + logoURI: "https://v2.app.squidrouter.com/images/icons/squid_logo.svg", + provider: signer.address, + }; + + // Prepare route parameters + const params = { + fromAddress: signer.address, + fromChain: chainid, + fromToken: fromToken.address, + fromAmount: totalGasETHWei, + toChain: chainid, + toToken: axelarTokenAddress, + toAddress: signer.address, + quoteOnly: false, + postHooks: postHooks + }; + + console.log("Requesting route with params:", params); + + // Get and execute route + const { route, requestId } = await squid.getRoute(params); + if (!route) { + throw new Error("Failed to get route"); + } + console.log("Route received:", { route, requestId }); + + const tx = await squid.executeRoute({ + signer, + route, + }) as unknown as ethers.TransactionResponse; + + if (!tx) { + throw new Error("Failed to execute transaction"); + } + + console.log("Transaction sent:", tx.hash); + + const receipt = await tx.wait(); + if (!receipt) { + throw new Error("Failed to get transaction receipt"); + } + console.log("Transaction confirmed:", receipt); + + if (receipt.status === 0) { + throw new Error("Transaction failed"); + } + + alert(`Transaction successful! Track status at https://${environment === Environment.TESTNET ? 'testnet.' : ''}axelarscan.io/gmp/${tx.hash}`); + + } catch (error: unknown) { + console.error("Error executing relay transaction:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + alert(`Transaction failed: ${errorMessage}`); + } } else { - alert("No wallet connected!") + alert("Please connect your wallet and select a supported network"); } } return ; }; -export default RelayPricesButton +export default RelayPricesButton;