diff --git a/contracts/IOjo.sol b/contracts/IOjo.sol index f882b52..c3ea9b0 100644 --- a/contracts/IOjo.sol +++ b/contracts/IOjo.sol @@ -29,6 +29,26 @@ interface IOjo { bytes calldata commandParams ) external payable; + /// @notice Triggers the relaying of price data from the Ojo Network to the Ojo contract and uses said price data + /// when calling the specified contract method at the specified contract address. Uses ERC-20 token for gas payment + /// instead of native gas. + /// @dev Reverts if contract method call does not succeed. + /// @param assetNames List of assets to be relayed from the Ojo Network and used by the contract method. + /// @param contractAddress Address of contract containing the contract method to be called. + /// @param commandSelector First four bytes of the Keccak-256 hash of the contract method to be called. + /// @param commandParams Abi encoded parameters to be used when calling the contract method (excluding assetNames + /// parameter). + /// @param gasToken The address of the ERC-20 token used to pay for gas. + /// @param gasFeeAmount The amount of tokens to pay for gas. + function callContractMethodWithOjoPriceDataNonNativeGas( + bytes32[] calldata assetNames, + address contractAddress, + bytes4 commandSelector, + bytes calldata commandParams, + address gasToken, + uint256 gasFeeAmount + ) external; + /// @notice Triggers the relaying of price data from the Ojo Network to the Ojo contract with an ERC-20 token for /// and uses said price data when calling the specified contract method at the specified contract address. /// @dev Reverts if contract method call does not succeed. @@ -48,6 +68,30 @@ interface IOjo { uint256 amount ) external payable; + /// @notice Triggers the relaying of price data from the Ojo Network to the Ojo contract with an ERC-20 token for + /// and uses said price data when calling the specified contract method at the specified contract address. Uses + /// ERC-20 token for gas payment instead of native gas. + /// @dev Reverts if contract method call does not succeed. + /// @param assetNames List of assets to be relayed from the Ojo Network and used by the contract method. + /// @param contractAddress Address of contract containing the contract method to be called. + /// @param commandSelector First four bytes of the Keccak-256 hash of the contract method to be called. + /// @param commandParams Abi encoded parameters to be used when calling the contract method (excluding assetNames + /// parameter). + /// @param symbol The symbol of the token to be sent with the call. + /// @param amount The amount of tokens to be sent with the call. + /// @param gasToken The address of the ERC-20 token used to pay for gas. + /// @param gasFeeAmount The amount of tokens to pay for gas. + function callContractMethodWithOjoPriceDataAndTokenNonNativeGas( + bytes32[] calldata assetNames, + address contractAddress, + bytes4 commandSelector, + bytes calldata commandParams, + string memory symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount + ) external; + /// @notice Returns the price data of a specified asset. /// @dev Price data is stored in a mapping, so requesting the price data of a non existent asset will return the /// zero byte representation of OjoTypes.PriceData. diff --git a/contracts/MockOjo.sol b/contracts/MockOjo.sol index 57ad1f3..ca4f36b 100644 --- a/contracts/MockOjo.sol +++ b/contracts/MockOjo.sol @@ -35,6 +35,26 @@ contract MockOjo { ); } + function relayOjoPriceDataNonNativeGas( + bytes32[] calldata assetNames, + address gasToken, + uint256 gasFeeAmount + ) external payable { + IERC20(gasToken).transferFrom(msg.sender, address(this), gasFeeAmount); + IERC20(gasToken).approve(address(ojo), gasFeeAmount); + + bytes memory commandParams = "0x"; + + ojo.callContractMethodWithOjoPriceDataNonNativeGas( + assetNames, + address(this), + OjoTypes.EMPTY_COMMAND_SELECTOR, + commandParams, + gasToken, + gasFeeAmount + ); + } + function relayOjoPriceDataWithToken( bytes32[] calldata assetNames, string memory symbol, @@ -56,6 +76,34 @@ contract MockOjo { ); } + function relayOjoPriceDataWithTokenNonNativeGas( + bytes32[] calldata assetNames, + string memory symbol, + uint256 amount, + address tokenAddress, + address gasToken, + uint256 gasFeeAmount + ) external payable { + IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + IERC20(tokenAddress).approve(address(ojo), amount); + + IERC20(gasToken).transferFrom(msg.sender, address(this), gasFeeAmount); + IERC20(gasToken).approve(address(ojo), gasFeeAmount); + + bytes memory commandParams = "0x"; + + ojo.callContractMethodWithOjoPriceDataAndTokenNonNativeGas( + assetNames, + address(this), + OjoTypes.EMPTY_COMMAND_SELECTOR, + commandParams, + symbol, + amount, + gasToken, + gasFeeAmount + ); + } + function setBalanceWithOjoPriceData( bytes32[] calldata assetNames, uint256 multiplier diff --git a/contracts/Ojo.sol b/contracts/Ojo.sol index fe4c147..df09346 100644 --- a/contracts/Ojo.sol +++ b/contracts/Ojo.sol @@ -52,6 +52,37 @@ contract Ojo is IOjo, AxelarExpressExecutable, Upgradable { gateway.callContract(ojoChain, ojoAddress, payloadWithVersion); } + function callContractMethodWithOjoPriceDataNonNativeGas( + bytes32[] calldata assetNames, + address contractAddress, + bytes4 commandSelector, + bytes calldata commandParams, + address gasToken, + uint256 gasFeeAmount + ) external { + require(assetNames.length <= assetLimit, "Number of assets requested is over limit"); + + bytes memory payloadWithVersion = abi.encodePacked( + bytes4(uint32(0)), // version number + abi.encode(assetNames, contractAddress, commandSelector, commandParams, block.timestamp) // payload + ); + + IERC20(gasToken).transferFrom(msg.sender, address(this), gasFeeAmount); + IERC20(gasToken).approve(address(gasReceiver), gasFeeAmount); + + gasReceiver.payGasForContractCall( + address(this), + ojoChain, + ojoAddress, + payloadWithVersion, + gasToken, + gasFeeAmount, + msg.sender + ); + + gateway.callContract(ojoChain, ojoAddress, payloadWithVersion); + } + function callContractMethodWithOjoPriceDataAndToken( bytes32[] calldata assetNames, address contractAddress, @@ -84,6 +115,45 @@ contract Ojo is IOjo, AxelarExpressExecutable, Upgradable { gateway.callContractWithToken(ojoChain, ojoAddress, payloadWithVersion, symbol, amount); } + function callContractMethodWithOjoPriceDataAndTokenNonNativeGas( + bytes32[] calldata assetNames, + address contractAddress, + bytes4 commandSelector, + bytes calldata commandParams, + string memory symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount + ) external { + require(assetNames.length <= assetLimit, "Number of assets requested is over limit"); + + address tokenAddress = gateway.tokenAddresses(symbol); + IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + IERC20(tokenAddress).approve(address(gateway), amount); + + IERC20(gasToken).transferFrom(msg.sender, address(this), gasFeeAmount); + IERC20(gasToken).approve(address(gasReceiver), gasFeeAmount); + + bytes memory payloadWithVersion = abi.encodePacked( + bytes4(uint32(0)), // version number + abi.encode(assetNames, contractAddress, commandSelector, commandParams, block.timestamp) // payload + ); + + gasReceiver.payGasForContractCallWithToken( + address(this), + ojoChain, + ojoAddress, + payloadWithVersion, + symbol, + amount, + gasToken, + gasFeeAmount, + msg.sender + ); + + gateway.callContractWithToken(ojoChain, ojoAddress, payloadWithVersion, symbol, amount); + } + function _setup(bytes calldata data) internal override { (string memory ojoChain_, string memory ojoAddress_, uint256 resolveWindow_, uint16 assetLimit_) = abi.decode( data, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2f38e03..a198b60 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,13 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@0xsquid/sdk": "^2.8.20", + "@0xsquid/squid-types": "^0.1.113", "@axelar-network/axelar-gmp-sdk-solidity": "^5.6.4", "@axelar-network/axelarjs-sdk": "^0.13.9", "@rainbow-me/rainbowkit": "^1.3.3", "bootstrap": "^5.3.2", - "ethers": "^6.10.0", + "ethers": "^6.8.1", "react": "^18.2.0", "react-bootstrap": "^2.10.0", "react-dom": "^18.2.0", @@ -32,6 +34,208 @@ "vite": "^5.0.8" } }, + "node_modules/@0xsquid/sdk": { + "version": "2.8.20", + "resolved": "https://registry.npmjs.org/@0xsquid/sdk/-/sdk-2.8.20.tgz", + "integrity": "sha512-KR5XOvImJmSnveFgxjNrW+SRRlE/+JxSPKUWhmEy+c07386YwoA9AQBP4tcdlWS6u/SDf9EtPZ2yrgkNuTkDRw==", + "license": "MIT", + "dependencies": { + "@cosmjs/encoding": "^0.31.0", + "@cosmjs/stargate": "^0.31.3", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/wallet": "^5.7.0", + "axios": "^1.5.0", + "cosmjs-types": "^0.8.0", + "ethers": "6.8.1", + "ethers-multicall-provider": "^5.0.0", + "lodash": "^4.17.21", + "long": "^5.2.3" + } + }, + "node_modules/@0xsquid/sdk/node_modules/@cosmjs/stargate": { + "version": "0.31.3", + "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.31.3.tgz", + "integrity": "sha512-53NxnzmB9FfXpG4KjOUAYAvWLYKdEmZKsutcat/u2BrDXNZ7BN8jim/ENcpwXfs9/Og0K24lEIdvA4gsq3JDQw==", + "license": "Apache-2.0", + "dependencies": { + "@confio/ics23": "^0.6.8", + "@cosmjs/amino": "^0.31.3", + "@cosmjs/encoding": "^0.31.3", + "@cosmjs/math": "^0.31.3", + "@cosmjs/proto-signing": "^0.31.3", + "@cosmjs/stream": "^0.31.3", + "@cosmjs/tendermint-rpc": "^0.31.3", + "@cosmjs/utils": "^0.31.3", + "cosmjs-types": "^0.8.0", + "long": "^4.0.0", + "protobufjs": "~6.11.3", + "xstream": "^11.14.0" + } + }, + "node_modules/@0xsquid/sdk/node_modules/@cosmjs/stargate/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/@0xsquid/sdk/node_modules/@cosmjs/stream": { + "version": "0.31.3", + "resolved": "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.31.3.tgz", + "integrity": "sha512-8keYyI7X0RjsLyVcZuBeNjSv5FA4IHwbFKx7H60NHFXszN8/MvXL6aZbNIvxtcIHHsW7K9QSQos26eoEWlAd+w==", + "license": "Apache-2.0", + "dependencies": { + "xstream": "^11.14.0" + } + }, + "node_modules/@0xsquid/sdk/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", + "license": "MIT" + }, + "node_modules/@0xsquid/sdk/node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@0xsquid/sdk/node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/@0xsquid/sdk/node_modules/protobufjs/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/@0xsquid/squid-types": { + "version": "0.1.113", + "resolved": "https://registry.npmjs.org/@0xsquid/squid-types/-/squid-types-0.1.113.tgz", + "integrity": "sha512-F+3Dr/MkiZdemrugn1ljRWTK6pOwYxiu8nn03ctQgQYYqcqjz+l7o6Ozm+VkNIdlLhgKmLlzJ0M1fGMiNKzjNg==", + "license": "Apache-2.0", + "dependencies": { + "@axelar-network/axelarjs-sdk": "^0.16.1", + "@ethersproject/providers": "^5.7.2", + "typescript": "*" + }, + "peerDependencies": { + "typescript": "^5.3.3" + } + }, + "node_modules/@0xsquid/squid-types/node_modules/@axelar-network/axelar-cgp-solidity": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@axelar-network/axelar-cgp-solidity/-/axelar-cgp-solidity-6.4.0.tgz", + "integrity": "sha512-Xnw5xi234B1cmTCzgudV8zq+DDjJ1d1U362CM0vKH1FWmZprKIdqgmOYkiRyu+QiVhnznKiBURiSEHVrNjtYpw==", + "license": "MIT", + "dependencies": { + "@axelar-network/axelar-gmp-sdk-solidity": "5.10.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@0xsquid/squid-types/node_modules/@axelar-network/axelarjs-sdk": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@axelar-network/axelarjs-sdk/-/axelarjs-sdk-0.16.2.tgz", + "integrity": "sha512-Fz/fovSzJRDNg9WrAzDffWzROe2Cm/e/f2OmwUossFbwm9xzZvoOHoJAEHd4T3ZK5SELqpcCBDvpe9wWtZDokg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@axelar-network/axelar-cgp-solidity": "^6.3.0", + "@axelar-network/axelarjs-types": "^0.33.0", + "@cosmjs/json-rpc": "^0.30.1", + "@cosmjs/stargate": "0.31.0-alpha.1", + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/networks": "^5.7.1", + "@ethersproject/providers": "^5.7.2", + "@types/uuid": "^8.3.1", + "bech32": "^2.0.0", + "clone-deep": "^4.0.1", + "cross-fetch": "^3.1.5", + "ethers": "^5.7.2", + "socket.io-client": "^4.6.1", + "standard-http-error": "^2.0.1", + "string-similarity-js": "^2.1.4", + "uuid": "^8.3.2", + "ws": "^8.13.0" + } + }, + "node_modules/@0xsquid/squid-types/node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -68,11 +272,12 @@ } }, "node_modules/@axelar-network/axelar-gmp-sdk-solidity": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.6.4.tgz", - "integrity": "sha512-PQjV+HeJynmSRMhyM3SexwnbFNruSaiRUeNCWjV8/7CkdPsDqypoqIXVRVU8Zk92DUUHeqZZzL/3qP2LYuvlnA==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.10.0.tgz", + "integrity": "sha512-s8SImALvYB+5AeiT3tbfWNBI2Mhqw1x91i/zM3DNpVUCnAR2HKtsB9T84KnUn/OJjOVgb4h0lv7q9smeYniRPw==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@axelar-network/axelarjs-sdk": { @@ -3282,11 +3487,12 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.11.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", - "integrity": "sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/prop-types": { @@ -4417,6 +4623,12 @@ "tslib": "^2.0.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -4841,6 +5053,18 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -5062,6 +5286,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -5645,9 +5878,9 @@ } }, "node_modules/ethers": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.10.0.tgz", - "integrity": "sha512-nMNwYHzs6V1FR3Y4cdfxSQmNgZsRj1RiTU25JwvnJLmyzw9z3SKxNc2XKDuiXXo/v9ds5Mp9m6HBabgYQQ26tA==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.8.1.tgz", + "integrity": "sha512-iEKm6zox5h1lDn6scuRWdIdFJUCGg3+/aQWu0F4K0GVyEZiktFkqrJbRjTn1FlYEPz7RKA707D6g5Kdk6j7Ljg==", "funding": [ { "type": "individual", @@ -5658,6 +5891,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { "@adraffy/ens-normalize": "1.10.0", "@noble/curves": "1.2.0", @@ -5671,6 +5905,22 @@ "node": ">=14.0.0" } }, + "node_modules/ethers-multicall-provider": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ethers-multicall-provider/-/ethers-multicall-provider-5.0.0.tgz", + "integrity": "sha512-dsfIwBSbr8yG+F0o87uoMFje1k5w988883MMJvK7R66mYT6NApQhQ7sMH/cxKGXTRf3at+nGt/4QIYHbYhe/8A==", + "license": "MIT", + "dependencies": { + "ethers": "^6.0.0", + "lodash": "^4.17.0" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "lodash": "^4.17.0" + } + }, "node_modules/ethers/node_modules/@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", @@ -5685,17 +5935,20 @@ "node_modules/ethers/node_modules/@types/node": { "version": "18.15.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", + "license": "MIT" }, "node_modules/ethers/node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "license": "0BSD" }, "node_modules/ethers/node_modules/ws": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -5901,15 +6154,16 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -5927,6 +6181,20 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7022,6 +7290,27 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -7571,6 +7860,12 @@ "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.5.1.tgz", "integrity": "sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8449,9 +8744,10 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -8552,9 +8848,10 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" }, "node_modules/unenv": { "version": "1.9.0", @@ -9089,9 +9386,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/frontend/package.json b/frontend/package.json index 6312ffc..82c7905 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,11 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "@0xsquid/sdk": "^2.8.20", + "@0xsquid/squid-types": "^0.1.113", "@axelar-network/axelar-gmp-sdk-solidity": "^5.6.4", "@axelar-network/axelarjs-sdk": "^0.13.9", "@rainbow-me/rainbowkit": "^1.3.3", "bootstrap": "^5.3.2", - "ethers": "^6.10.0", + "ethers": "^6.8.1", "react": "^18.2.0", "react-bootstrap": "^2.10.0", "react-dom": "^18.2.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2dcc685..41b5528 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,19 +1,44 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { useNetwork } from 'wagmi'; import AssetForm from './components/AssetForm'; -import SymbolDropdown from './components/SymbolDropdown'; -import PriceTable from './components/PriceTable'; -import DisplayPricesButton from './components/DisplayPricesButton'; import RelayButton from './components/RelayButton'; +import { isAxelarChain } from './components/lib/AxelarChains'; import 'bootstrap/dist/css/bootstrap.min.css'; import './App.css'; import { ConnectButton } from '@rainbow-me/rainbowkit'; +import axios from 'axios'; // Import axios for making HTTP requests -function App () { - const [assetNames, setAssetNames] = useState([] as string[]); - const [symbol, setSymbol] = useState(''); +function App() { + const [selectedAsset, setSelectedAsset] = useState(null); const [amount, setAmount] = useState(''); - const [priceData, setPriceData] = useState([] as any[]); - const [selectAll, setSelectAll] = useState(false); + const { chain } = useNetwork(); + + // Constant fee token + const feeToken = "AXL"; + + useEffect(() => { + const updateGasFee = async () => { + if (chain && isAxelarChain(chain.name)) { + try { + // Fetch gas estimate from the API + const response = await axios.get('https://api.agamotto-val-prod-0.ojo.network/ojo/gasestimate/v1/gasestimate?network=Arbitrum'); + let gasEstimate = response.data.gas_estimate; + // divide gas estimate by 1000000 + gasEstimate = (gasEstimate * 10 / 1000000).toString(); + + // Set the amount with the fetched gas estimate + setAmount(gasEstimate); + } catch (error) { + console.error("Error fetching gas estimate:", error); + setAmount('10'); + } + } else { + setAmount(''); + } + }; + + updateGasFee(); + }, [chain]); return (
@@ -25,32 +50,30 @@ function App () {
+ + +
+

Fee Token: {feeToken}

+ setAmount(e.target.value)} + placeholder="Estimated fee (editable)" + /> +
-
- - - setAmount(e.target.value)} placeholder="Enter fee token amount" /> -
); } -export default App +export default App; diff --git a/frontend/src/components/AssetForm.tsx b/frontend/src/components/AssetForm.tsx index 9ccb772..f128783 100644 --- a/frontend/src/components/AssetForm.tsx +++ b/frontend/src/components/AssetForm.tsx @@ -1,61 +1,78 @@ -import React from 'react'; -import { Form } from 'react-bootstrap'; - -type AssetFormParameters = { - assetNames: string[]; - setAssetNames: React.Dispatch>; - selectAll: boolean; - setSelectAll: React.Dispatch>; +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; + +interface AssetFormProps { + selectedAsset: string | null; + setSelectedAsset: React.Dispatch>; +} + +interface ExchangeRate { + denom: string; + amount: string; } -const AssetForm: React.FC = ({ assetNames, setAssetNames, selectAll, setSelectAll }) => { - const availableAssets = [ - "ARCH", "ATOM","AXL","BNB","BTC","CMDX","CMST","CRV","DAI","DOT","ETH", - "INJ","IST","JUNO","KUJI","LINK","LUNA","MATIC","MKR","MNTA","ORDI", - "OSMO","RETH","SATS","SCRT","SEI","STARS","STATOM","STJUNO","STOSMO", - "SUSHI","USDC","USDT","USK","WBTC","WETH","XRP" - ]; - - const handleSelectAllChange = () => { - if (selectAll) { - setAssetNames([]); - } else { - setAssetNames(availableAssets); - } - setSelectAll(!selectAll); - }; - - const handleSwitchChange = (asset: string) => { - if (assetNames.includes(asset)) { - setAssetNames(assetNames.filter(a => a !== asset)); - } else { - setAssetNames([...assetNames, asset]); - } - }; - - return ( -
- - {availableAssets.map((asset, index) => ( - handleSwitchChange(asset)} - checked={assetNames.includes(asset)} - /> - ))} - - ); +const AssetForm: React.FC = ({ selectedAsset, setSelectedAsset }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [exchangeRates, setExchangeRates] = useState([]); + const [filteredRates, setFilteredRates] = useState([]); + + useEffect(() => { + const fetchExchangeRates = async () => { + try { + const response = await axios.get('https://api.agamotto-val-prod-0.ojo.network/ojo/oracle/v1/denoms/exchange_rates/'); + setExchangeRates(response.data.exchange_rates); + } catch (error) { + console.error('Error fetching exchange rates:', error); + } + }; + + fetchExchangeRates(); + }, []); + + useEffect(() => { + const filtered = exchangeRates.filter(rate => + rate.denom.toLowerCase().includes(searchTerm.toLowerCase()) + ); + setFilteredRates(filtered); + }, [searchTerm, exchangeRates]); + + const handleAssetSelect = (denom: string) => { + setSelectedAsset(denom); + setSearchTerm(''); + }; + + const handleRemoveAsset = () => { + setSelectedAsset(null); + }; + + return ( +
+ {selectedAsset ? ( +
+ Selected Asset: {selectedAsset} + +
+ ) : ( + <> + setSearchTerm(e.target.value)} + placeholder="Search for assets..." + /> + {searchTerm && ( +
    + {filteredRates.map((rate) => ( +
  • handleAssetSelect(rate.denom)}> + {rate.denom} +
  • + ))} +
+ )} + + )} +
+ ); }; export default AssetForm; diff --git a/frontend/src/components/EstimateGasFeeButton.tsx b/frontend/src/components/EstimateGasFeeButton.tsx new file mode 100644 index 0000000..5f4f553 --- /dev/null +++ b/frontend/src/components/EstimateGasFeeButton.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useNetwork } from 'wagmi'; +import { estimateGasFee } from '../utils/gasEstimate'; +import { isAxelarChain } from './lib/AxelarChains'; + +interface EstimateGasFeeButtonProps { + setAmount: React.Dispatch>; +} + +const EstimateGasFeeButton: React.FC = ({ setAmount }) => { + const { chain } = useNetwork(); + + const handleEstimateGasFee = async () => { + if (!chain || !isAxelarChain(chain.name)) { + alert("Please connect to a supported network"); + return; + } + + try { + const gasFee = await estimateGasFee(chain.name, "0x0000000000000000000000000000000000000000", "1000000", "0.40"); + setAmount(gasFee); + } catch (error) { + console.error("Error estimating gas fee:", error); + setAmount(''); + } + }; + + return ( +
+ +
+ ); +}; + +export default EstimateGasFeeButton; diff --git a/frontend/src/components/RelayButton.tsx b/frontend/src/components/RelayButton.tsx index 359c431..01b8418 100644 --- a/frontend/src/components/RelayButton.tsx +++ b/frontend/src/components/RelayButton.tsx @@ -1,17 +1,21 @@ +/* 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 Ojo from '../artifacts/contracts/Ojo.sol/Ojo.json'; import MockOjo from '../artifacts/contracts/MockOjo.sol/MockOjo.json'; -import IERC20 from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/interfaces/IERC20.sol/IERC20.json' import IAxelarGateway from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/interfaces/IAxelarGateway.sol/IAxelarGateway.json' -import { axelarChains, axelarGatewayAddresses, isAxelarChain } from './lib/AxelarChains' +import { axelarChains, axelarGatewayAddresses, isAxelarChain, axelarChainIDs } from './lib/AxelarChains' +import { Squid } from '@0xsquid/sdk' +import { ChainType, SquidCallType } from "@0xsquid/squid-types"; import { AxelarQueryAPI, Environment, GasToken, } from "@axelar-network/axelarjs-sdk"; import { ethers } from 'ethers'; -import { useNetwork } from 'wagmi'; -const ojoAddress = import.meta.env.VITE_OJO_ADDRESS as `0x${string}`; +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; @@ -21,6 +25,38 @@ type RelayPricesParameters = { amount: string; } +const getSDK = (): Squid => { + const squid = new Squid({ + baseUrl: "https://apiplus.squidrouter.com", + integratorId: "ojo-5bc051db-c688-4182-9b52-ba8c7557d041", + }) + return squid +} + +const getOjoGasEstimate = async (networkName: string): Promise => { + try { + if(networkName === "mainnet") { + networkName = "ethereum" + } + if(networkName === "Arbitrum One") { + networkName = "Arbitrum" + } + console.log("Fetching Ojo gas estimate for network:", networkName); + + const response = await fetch( + `https://api.agamotto-val-prod-0.ojo.network/ojo/gasestimate/v1/gasestimate?network=${networkName}` + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json() as { gas_estimate: string }; + return data.gas_estimate; + } catch (error) { + console.error("Failed to fetch Ojo gas estimate:", error); + throw error; + } +}; + const RelayPricesButton: React.FC = ({ assetNames, symbol, amount }) => { const { chain } = useNetwork(); @@ -31,69 +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(); - - // check amount of assets requested to be relayed is not over limit - const ojoContract = new ethers.Contract(ojoAddress, Ojo.abi, signer); - const assetLimit = await ojoContract.assetLimit(); - if (assetNames.length > assetLimit) { - alert("Cannot relay more than " + assetLimit + " assets at one time") - return - } + try { + const provider = new ethers.BrowserProvider(window.ethereum); + const signer = await provider.getSigner(); - // fetch token address of fee token if selected - let tokenAddress; - if (symbol) { - const axelarGatewayAddress = axelarGatewayAddresses[chain.name]; - const axelarGatewayContract = new ethers.Contract(axelarGatewayAddress, IAxelarGateway.abi, signer); - tokenAddress = await axelarGatewayContract.tokenAddresses(symbol); - } + // Verify contract addresses + if (!mockOjoAddress) { + throw new Error("MockOjo contract address not configured"); + } - // estimate axelar gmp fee - - const api = new AxelarQueryAPI({ environment: environment }); - const gasFee = await api.estimateGasFee( - axelarChains[chain?.name], - "ojo", - GasToken.ETH, - 700000, - 2, - ); - - // approve MockOjo Contract to spend fee token if selected - if (symbol) { - const tokenContract = new ethers.Contract(tokenAddress, IERC20.abi, signer); - const tx1 = await tokenContract.approve(mockOjoAddress, ethers.parseUnits(amount, 6)); - await tx1.wait(); - } + // 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}`); + } + + // Prepare contract interactions + 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 mockOjoContract = new ethers.Contract(mockOjoAddress, MockOjo.abi, signer); - let tx2; - if (symbol) { - tx2 = await mockOjoContract.relayOjoPriceDataWithToken( - assetNamesArray, - symbol, - ethers.parseUnits(amount, 6), - tokenAddress, - { value: gasFee } + // 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, + ] ); - } else { - tx2 = await mockOjoContract.relayOjoPriceData( - assetNamesArray, - { value: gasFee } - ) + + // 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); + + // Calculate total gas for 72 updates (3 months) + const totalGasUAxl = BigInt(increasedGasEstimateUAxl) * BigInt(72); + const totalGasAXL = totalGasUAxl / BigInt(10**6); + + // 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 }>; + }; + + const ethPriceData = await ethPriceResponse.json() as ExchangeRateResponse; + const axlPriceData = await axlPriceResponse.json() as ExchangeRateResponse; + + 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; + + 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, + 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}`); } - await tx2.wait(); - alert("Relay tx sent succesfully, check status on https://testnet.axelarscan.io/gmp/search?chain=ojo") } else { - alert("No wallet connected!") + alert("Please connect your wallet and select a supported network"); } } return ; }; -export default RelayPricesButton +export default RelayPricesButton; diff --git a/frontend/src/components/SymbolDropdown.tsx b/frontend/src/components/SymbolDropdown.tsx index 008564e..b70b8dd 100644 --- a/frontend/src/components/SymbolDropdown.tsx +++ b/frontend/src/components/SymbolDropdown.tsx @@ -7,7 +7,7 @@ type AssetFormParameters = { } const SymbolDropdown: React.FC = ({ symbol, setSymbol }) => { - const feeTokens = ["aUSDC"]; + const feeTokens = ["AXL"]; const handleSelectSymbol = (key: any) => { setSymbol(key); diff --git a/frontend/src/components/lib/AxelarChains.ts b/frontend/src/components/lib/AxelarChains.ts index e6b0895..ce324c3 100644 --- a/frontend/src/components/lib/AxelarChains.ts +++ b/frontend/src/components/lib/AxelarChains.ts @@ -19,7 +19,7 @@ export const axelarChains = { "Kava EVM Testnet": "kava", "Filecoin Calibration": "filecoin-2", "Linea Goerli Testnet": "linea", - "Ethereum": "mainnet", + "Ethereum": "ethereum", "Arbitrum One": "arbitrum", "Base": "base", "OP Mainnet": "optimism" @@ -45,12 +45,20 @@ export const axelarGatewayAddresses = { "Kava EVM Testnet": "0xC8D18F85cB0Cee5C95eC29c69DeaF6cea972349c", "Filecoin Calibration": "0x999117D44220F33e0441fbAb2A5aDB8FF485c54D", "Linea Goerli Testnet": "0xe432150cce91c13a887f7D836923d5597adD8E31", - "Ethereum": "0xe432150cce91c13a887f7D836923d5597adD8E31", + "Ethereum": "0x4F4495243837681061C4743b74B3eEdf548D56A5", "Arbitrum One": "0xe432150cce91c13a887f7D836923d5597adD8E31", "Base": "0xe432150cce91c13a887f7D836923d5597adD8E31", "OP Mainnet": "0xe432150cce91c13a887f7D836923d5597adD8E31" } + +export const axelarChainIDs = { + "Arbitrum One": "42161", + "Ethereum": "1", + "OP Mainnet": "10", + "Base": "8453", +} + export function isAxelarChain(key: any): key is keyof typeof axelarChains { return key in axelarChains; } diff --git a/frontend/src/utils/gasEstimate.ts b/frontend/src/utils/gasEstimate.ts new file mode 100644 index 0000000..d498b77 --- /dev/null +++ b/frontend/src/utils/gasEstimate.ts @@ -0,0 +1,54 @@ +import axios from 'axios'; + +const endpointURL = "https://api.axelarscan.io/gmp/estimateGasFee"; +const sourceChain = "ojo"; + +// This is a placeholder. In a real-world scenario, this would be dynamically generated. +const executeData = "0x00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000b20000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b60000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000205245374c52540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a2642918000000000000000000000000000000000000000000000000000000000066960af100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000006a0000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000005eebff00000000000000000000000000000000000000000000000000000000005ee4f700000000000000000000000000000000000000000000000000000000005eddef00000000000000000000000000000000000000000000000000000000005ed6e700000000000000000000000000000000000000000000000000000000005ecfdf00000000000000000000000000000000000000000000000000000000005ec8d700000000000000000000000000000000000000000000000000000000005ec1cf00000000000000000000000000000000000000000000000000000000005ebac700000000000000000000000000000000000000000000000000000000005eb3bf00000000000000000000000000000000000000000000000000000000005eacb700000000000000000000000000000000000000000000000000000000005ea5af00000000000000000000000000000000000000000000000000000000005e9ea700000000000000000000000000000000000000000000000000000000005e979f00000000000000000000000000000000000000000000000000000000005e909700000000000000000000000000000000000000000000000000000000005e898f00000000000000000000000000000000000000000000000000000000005e828700000000000000000000000000000000000000000000000000000000005e7b7f00000000000000000000000000000000000000000000000000000000005e747700000000000000000000000000000000000000000000000000000000005e6d6f00000000000000000000000000000000000000000000000000000000005f0f2700000000000000000000000000000000000000000000000000000000005f081f00000000000000000000000000000000000000000000000000000000005f011700000000000000000000000000000000000000000000000000000000005efa0f00000000000000000000000000000000000000000000000000000000005ef307000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000396fb886e0000000000000000000000000000000000000000000000000000000390b235200000000000000000000000000000000000000000000000000000000392e8739f000000000000000000000000000000000000000000000000000000039341dbce000000000000000000000000000000000000000000000000000000038de6f3a8000000000000000000000000000000000000000000000000000000037fb311860000000000000000000000000000000000000000000000000000000376da178a00000000000000000000000000000000000000000000000000000003677c2f7600000000000000000000000000000000000000000000000000000003674094ac0000000000000000000000000000000000000000000000000000000365daf3f000000000000000000000000000000000000000000000000000000003693b3861000000000000000000000000000000000000000000000000000000036cd7179c0000000000000000000000000000000000000000000000000000000368e1d032000000000000000000000000000000000000000000000000000000036386e80c0000000000000000000000000000000000000000000000000000000364b0edfe00000000000000000000000000000000000000000000000000000003632d7fdd000000000000000000000000000000000000000000000000000000035f383873000000000000000000000000000000000000000000000000000000035d01f9f4000000000000000000000000000000000000000000000000000000035cc65f2a00000000000000000000000000000000000000000000000000000003b1dfde9100000000000000000000000000000000000000000000000000000003b46f853f00000000000000000000000000000000000000000000000000000003b522559d00000000000000000000000000000000000000000000000000000003a61dd5b8000000000000000000000000000000000000000000000000000000039e15797f000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000029baff41400000000000000000000000000000000000000000000000000000001b79f446d000000000000000000000000000000000000000000000000000000025c7afe17000000000000000000000000000000000000000000000000000000026b4946bc000000000000000000000000000000000000000000000000000000016f821e43000000000000000000000000000000000000000000000000000000062b0deeee00000000000000000000000000000000000000000000000000000002882b577e00000000000000000000000000000000000000000000000000000003cf8ec3f800000000000000000000000000000000000000000000000000000000da9873a400000000000000000000000000000000000000000000000000000000fe66049e000000000000000000000000000000000000000000000000000000014fadc3c300000000000000000000000000000000000000000000000000000001912acb0e00000000000000000000000000000000000000000000000000000000ecec7ca600000000000000000000000000000000000000000000000000000002ffe0015500000000000000000000000000000000000000000000000000000001919cfdd300000000000000000000000000000000000000000000000000000003b249cbd0000000000000000000000000000000000000000000000000000000011c81159200000000000000000000000000000000000000000000000000000000d5a49cb800000000000000000000000000000000000000000000000000000000f1dedade00000000000000000000000000000000000000000000000000000001e84f443f00000000000000000000000000000000000000000000000000000001b9921fc6000000000000000000000000000000000000000000000000000000071e341559000000000000000000000000000000000000000000000000000000043b0ba83e000000000000000000000000000000000000000000000000000000021ec0616700000000000000000000000000000000000000000000000000000000000000015245374c525400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; // truncated for brevity + +interface RequestBody { + destinationChain: string; + executeData: string; + destinationAddress: string; + gasLimit: string; + sourceChain: string; + showDetailedFees: boolean; +} + +interface ResponseBody { + totalFee: string; + message: string; + error: boolean; +} + +export async function estimateGasFee( + destinationChain: string, + destinationAddress: string, + gasLimit: string, + multiplier: string +): Promise { + const body: RequestBody = { + destinationChain, + executeData, + destinationAddress, + gasLimit, + sourceChain, + showDetailedFees: true, + }; + + try { + const response = await axios.post(endpointURL, body); + const { data } = response; + + if (data.error) { + throw new Error(data.message); + } + + const fee = parseFloat(data.totalFee); + const mul = parseFloat(multiplier); + return (fee * mul).toFixed(0); + } catch (error) { + console.error('Error estimating gas fee:', error); + throw error; + } +} diff --git a/package.json b/package.json index 2a08115..49663e0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@types/mocha": ">=9.1.0", "@types/node": ">=16.0.0", "chai": "^4.2.0", - "ethers": "^6.4.0", + "ethers": "^6.8.1", "hardhat-gas-reporter": "^1.0.8", "solidity-coverage": "^0.8.0", "ts-node": ">=8.0.0",