diff --git a/README.md b/README.md index f4d0938..804d81b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Multi-chain native-to-native token swap using existing DEXes. ### Details -Using liquidity of native vs UST (i.e. the UST highway), one can swap from native A on chain A to native B on chain B. For this specific example, we demonstrate a swap between Polygon (Mumbai testnet) and Ethereum (Goerli testnet) between MATIC and ETH. We wrote example smart contracts to interact with Uniswap V3 and Uniswap V2 forks (QuickSwap in this specific example for Polygon). Any DEX can be used to replace our example as long as the swap for a particular DEX has all of its parameters to perform the swap(s). +Using liquidity of native vs UST (i.e. the UST highway), one can swap from native A on chain A to native B on chain B. For this specific example, we demonstrate a swap between any combination of ETH (Goerli testnet), AVAX (Fuji testnet), MATIC (Mumbai testnet) and BNB (BSC testnet). We wrote example smart contracts to interact with Uniswap V3 and Uniswap V2 forks. Any DEX can be used to replace our example as long as the swap for a particular DEX has all of its parameters to perform the swap(s). A protocol that hosts NativeSwap is expected to run its own relayer to enhance its user experience by only requiring a one-click transaction to perform the complete swap. Otherwise the user will have to perform an extra transaction to manually allow the final swap. @@ -49,8 +49,8 @@ cp .env.sample .env Then deploy the example contracts: ``` -./deploy_to_goerli.sh -./deploy_to_mumbai.sh +./deploy_v2.sh +./deploy_v3.sh ``` Then change into the react directory, copy sample.env to .env and replace YOUR-PROJECT-ID with your Infura Goerli and Mumbai Project IDs diff --git a/contracts/.env.sample b/contracts/.env.sample index 0ad9e3c..31193a7 100644 --- a/contracts/.env.sample +++ b/contracts/.env.sample @@ -1,5 +1,5 @@ -GOERLI_PROVIDER=https://goerli.infura.io/v3/YOUR-PROJECT-ID -MUMBAI_PROVIDER=https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID -BSC_PROVIDER=https://data-seed-prebsc-1-s1.binance.org:8545 -FUJI_PROVIDER=https://api.avax-test.network/ext/bc/C/rpc +GOERLI_PROVIDER="https://goerli.infura.io/v3/YOUR-PROJECT-ID" +MUMBAI_PROVIDER="https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID" +BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545" +FUJI_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc" ETH_PRIVATE_KEY= diff --git a/contracts/cfg/truffle-config.avalanche.js b/contracts/cfg/truffle-config.avalanche.js new file mode 100644 index 0000000..5e32b35 --- /dev/null +++ b/contracts/cfg/truffle-config.avalanche.js @@ -0,0 +1,123 @@ +const HDWalletProvider = require('@truffle/hdwallet-provider'); + +require('dotenv').config({path:'.env'}); + +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * trufflesuite.com/docs/advanced/configuration + * + * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) + * to sign your transactions before they're sent to a remote public node. Infura accounts + * are available for free at: infura.io/register. + * + * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. If you're publishing your code to GitHub make sure you load this + * phrase from a file you've .gitignored so it doesn't accidentally become public. + * + */ + +// const HDWalletProvider = require('@truffle/hdwallet-provider'); +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync('.secret').toString().trim(); + +module.exports = { + contracts_directory: './contracts', + contracts_build_directory: './build/contracts', + migrations_directory: './migrations/avalanche', + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a development blockchain for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache-cli, geth or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + development: { + host: '127.0.0.1', // Localhost (default: none) + port: 8545, // Standard Ethereum port (default: none) + network_id: '*', // Any network (default: none) + }, + // Another network with more advanced options... + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send txs from (default: accounts[0]) + // websocket: true // Enable EventEmitter interface for web3 (default: false) + // }, + // Useful for deploying to a public network. + // NB: It's important to wrap the provider as a function. + fuji: { + provider: () => new HDWalletProvider( + process.env.ETH_PRIVATE_KEY, + process.env.FUJI_PROVIDER + ), + network_id: 43113, + skipDryRun: true + }, + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) + // settings: { // See the solidity docs for advice about optimization and evmVersion + optimizer: { + enabled: false, + runs: 200 + }, + // evmVersion: 'byzantium' + // } + } + }, + + // Truffle DB is currently disabled by default; to enable it, change enabled: + // false to enabled: true. The default storage location can also be + // overridden by specifying the adapter settings, as shown in the commented code below. + // + // NOTE: It is not possible to migrate your contracts to truffle DB and you should + // make a backup of your artifacts to a safe location before enabling this feature. + // + // After you backed up your artifacts you can utilize db by running migrate as follows: + // $ truffle migrate --reset --compile-all + // + // db: { + // enabled: false, + // host: '127.0.0.1', + // adapter: { + // name: 'sqlite', + // settings: { + // directory: '.db' + // } + // } + // } +}; diff --git a/contracts/cfg/truffle-config.bsc.js b/contracts/cfg/truffle-config.bsc.js new file mode 100644 index 0000000..5f7526e --- /dev/null +++ b/contracts/cfg/truffle-config.bsc.js @@ -0,0 +1,124 @@ +const HDWalletProvider = require('@truffle/hdwallet-provider'); + +require('dotenv').config({path:'.env'}); + +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * trufflesuite.com/docs/advanced/configuration + * + * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) + * to sign your transactions before they're sent to a remote public node. Infura accounts + * are available for free at: infura.io/register. + * + * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. If you're publishing your code to GitHub make sure you load this + * phrase from a file you've .gitignored so it doesn't accidentally become public. + * + */ + +// const HDWalletProvider = require('@truffle/hdwallet-provider'); +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync('.secret').toString().trim(); + +module.exports = { + contracts_directory: './contracts', + contracts_build_directory: './build/contracts', + migrations_directory: './migrations/bsc', + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a development blockchain for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache-cli, geth or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + development: { + host: '127.0.0.1', // Localhost (default: none) + port: 8545, // Standard Ethereum port (default: none) + network_id: '*', // Any network (default: none) + }, + // Another network with more advanced options... + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send txs from (default: accounts[0]) + // websocket: true // Enable EventEmitter interface for web3 (default: false) + // }, + // Useful for deploying to a public network. + // NB: It's important to wrap the provider as a function. + bsc: { + provider: () => new HDWalletProvider( + process.env.ETH_PRIVATE_KEY, + process.env.BSC_PROVIDER + ), + network_id: 97, + skipDryRun: true, + timeoutBlocks: 200 + }, + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) + // settings: { // See the solidity docs for advice about optimization and evmVersion + optimizer: { + enabled: false, + runs: 200 + }, + // evmVersion: 'byzantium' + // } + } + }, + + // Truffle DB is currently disabled by default; to enable it, change enabled: + // false to enabled: true. The default storage location can also be + // overridden by specifying the adapter settings, as shown in the commented code below. + // + // NOTE: It is not possible to migrate your contracts to truffle DB and you should + // make a backup of your artifacts to a safe location before enabling this feature. + // + // After you backed up your artifacts you can utilize db by running migrate as follows: + // $ truffle migrate --reset --compile-all + // + // db: { + // enabled: false, + // host: '127.0.0.1', + // adapter: { + // name: 'sqlite', + // settings: { + // directory: '.db' + // } + // } + // } +}; diff --git a/contracts/truffle-config.ethereum.js b/contracts/cfg/truffle-config.ethereum.js similarity index 100% rename from contracts/truffle-config.ethereum.js rename to contracts/cfg/truffle-config.ethereum.js diff --git a/contracts/truffle-config.polygon.js b/contracts/cfg/truffle-config.polygon.js similarity index 100% rename from contracts/truffle-config.polygon.js rename to contracts/cfg/truffle-config.polygon.js diff --git a/contracts/compile_contracts.sh b/contracts/compile_contracts.sh index 929d2ac..9311782 100755 --- a/contracts/compile_contracts.sh +++ b/contracts/compile_contracts.sh @@ -2,8 +2,8 @@ set -euo pipefail -npx truffle compile --config truffle-config.ethereum.js -npx truffle compile --config truffle-config.polygon.js +npx truffle compile --config cfg/truffle-config.ethereum.js +npx truffle compile --config cfg/truffle-config.polygon.js CONTRACTS="../react/src/abi/contracts" diff --git a/contracts/contracts/CrossChainSwapV2.sol b/contracts/contracts/CrossChainSwapV2.sol index 1d53e0c..0edd059 100644 --- a/contracts/contracts/CrossChainSwapV2.sol +++ b/contracts/contracts/CrossChainSwapV2.sol @@ -44,7 +44,8 @@ contract CrossChainSwapV2 { uint8 public immutable typeExactOut = 2; uint8 public immutable typeNativeSwap = 1; uint8 public immutable typeTokenSwap = 2; - uint16 public immutable expectedVaaLength = 262; + uint16 public immutable expectedVaaLength = 274; + uint8 public immutable terraChainId = 3; IUniswapV2Router02 public immutable swapRouter; address public immutable feeTokenAddress; address public immutable tokenBridgeAddress; @@ -385,17 +386,26 @@ contract CrossChainSwapV2 { swapParams.deadline ); - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOutMinimum, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactIn, - typeNativeSwap - ); + // create payload variable + bytes memory payload; + + // UST is native to Terra - no need for swap instructions + if (targetChainId == terraChainId) { + payload = abi.encodePacked( + swapParams.targetChainRecipient + ); + } else { + payload = abi.encodePacked( + swapParams.targetAmountOutMinimum, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + typeExactIn, + typeNativeSwap + ); + } // approve token bridge to spend feeTokens (UST) TransferHelper.safeApprove( @@ -761,17 +771,26 @@ contract CrossChainSwapV2 { typeNativeSwap ); - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOut, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactOut, - typeNativeSwap - ); + // create payload variable + bytes memory payload; + + // UST is native to Terra - no need for swap instructions + if (targetChainId == terraChainId) { + payload = abi.encodePacked( + swapParams.targetChainRecipient + ); + } else { + payload = abi.encodePacked( + swapParams.targetAmountOut, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + typeExactOut, + typeNativeSwap + ); + } // approve token bridge to spend feeTokens (UST) TransferHelper.safeApprove( diff --git a/contracts/contracts/CrossChainSwapV3.sol b/contracts/contracts/CrossChainSwapV3.sol index 98510aa..373e119 100644 --- a/contracts/contracts/CrossChainSwapV3.sol +++ b/contracts/contracts/CrossChainSwapV3.sol @@ -48,7 +48,8 @@ contract CrossChainSwapV3 { uint8 public immutable typeExactOut = 2; uint8 public immutable typeNativeSwap = 1; uint8 public immutable typeTokenSwap = 2; - uint16 public immutable expectedVaaLength = 262; + uint16 public immutable expectedVaaLength = 274; + uint8 public immutable terraChainId = 3; IUniswapRouter public immutable swapRouter; address public immutable feeTokenAddress; address public immutable tokenBridgeAddress; @@ -403,17 +404,26 @@ contract CrossChainSwapV3 { typeNativeSwap ); - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOutMinimum, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactIn, - typeNativeSwap - ); + // create payload variable + bytes memory payload; + + // UST is native to Terra - no need for swap instructions + if (targetChainId == terraChainId) { + payload = abi.encodePacked( + swapParams.targetChainRecipient + ); + } else { + payload = abi.encodePacked( + swapParams.targetAmountOutMinimum, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + typeExactIn, + typeNativeSwap + ); + } // approve token bridge to spend feeTokens (UST) TransferHelper.safeApprove( @@ -788,17 +798,26 @@ contract CrossChainSwapV3 { typeNativeSwap ); - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOut, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactOut, - typeNativeSwap - ); + // create payload variable + bytes memory payload; + + // UST is native to Terra - no need for swap instructions + if (targetChainId == terraChainId) { + payload = abi.encodePacked( + swapParams.targetChainRecipient + ); + } else { + payload = abi.encodePacked( + swapParams.targetAmountOut, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + typeExactOut, + typeNativeSwap + ); + } // approve token bridge to spend feeTokens (UST) TransferHelper.safeApprove( diff --git a/contracts/contracts/SwapHelper.sol b/contracts/contracts/SwapHelper.sol index 4b20654..c9e925c 100644 --- a/contracts/contracts/SwapHelper.sol +++ b/contracts/contracts/SwapHelper.sol @@ -18,7 +18,7 @@ library SwapHelper { uint256 amountIn; uint256 amountOutMinimum; uint256 targetAmountOutMinimum; - address targetChainRecipient; + bytes32 targetChainRecipient; uint256 deadline; uint24 poolFee; } @@ -28,7 +28,7 @@ library SwapHelper { uint256 amountOut; uint256 amountInMaximum; uint256 targetAmountOut; - address targetChainRecipient; + bytes32 targetChainRecipient; uint256 deadline; uint24 poolFee; } @@ -75,7 +75,7 @@ library SwapHelper { index += 32; decoded.estimatedAmount = encodedVm.payload.toUint256(index); - index += 32; + index += 44; decoded.recipientAddress = encodedVm.payload.toAddress(index); index += 20; diff --git a/contracts/deploy_to_mumbai.sh b/contracts/deploy_to_mumbai.sh deleted file mode 100755 index 68ed2b5..0000000 --- a/contracts/deploy_to_mumbai.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -npx truffle migrate --config truffle-config.polygon.js --network mumbai --reset diff --git a/contracts/deploy_v2.sh b/contracts/deploy_v2.sh new file mode 100755 index 0000000..eedc682 --- /dev/null +++ b/contracts/deploy_v2.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +npx truffle migrate --config cfg/truffle-config.avalanche.js --network fuji --reset +npx truffle migrate --config cfg/truffle-config.bsc.js --network bsc --reset +npx truffle migrate --config cfg/truffle-config.polygon.js --network mumbai --reset \ No newline at end of file diff --git a/contracts/deploy_to_goerli.sh b/contracts/deploy_v3.sh similarity index 68% rename from contracts/deploy_to_goerli.sh rename to contracts/deploy_v3.sh index 42c498d..53fc86a 100755 --- a/contracts/deploy_to_goerli.sh +++ b/contracts/deploy_v3.sh @@ -1,2 +1,4 @@ #!/bin/bash -npx truffle migrate --config truffle-config.ethereum.js --network goerli --reset +set -euo pipefail + +npx truffle migrate --config truffle-config.ethereum.js --network goerli --reset \ No newline at end of file diff --git a/contracts/migrations/avalanche/1_initial_migration.js b/contracts/migrations/avalanche/1_initial_migration.js new file mode 100644 index 0000000..16a7ba5 --- /dev/null +++ b/contracts/migrations/avalanche/1_initial_migration.js @@ -0,0 +1,5 @@ +const Migrations = artifacts.require("Migrations"); + +module.exports = function (deployer) { + deployer.deploy(Migrations); +}; diff --git a/contracts/migrations/avalanche/2_deploy_contracts.js b/contracts/migrations/avalanche/2_deploy_contracts.js new file mode 100644 index 0000000..09d6a26 --- /dev/null +++ b/contracts/migrations/avalanche/2_deploy_contracts.js @@ -0,0 +1,34 @@ +const fsp = require("fs/promises"); + +const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2"); +const SwapHelper = artifacts.require("SwapHelper"); + +const scriptsAddressPath = "../react/src/addresses"; + +module.exports = async function(deployer, network) { + const routerAddress = "0x7e3411b04766089cfaa52db688855356a12f05d1"; // hurricaneswap + const feeTokenAddress = "0xe09ed38e5cd1014444846f62376ac88c5232cde9"; // wUST + const tokenBridgeAddress = "0x61E44E506Ca5659E6c0bba9b678586fA2d729756"; + const wrappedAvaxAddress = "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3"; + + await deployer.deploy(SwapHelper); + await deployer.link(SwapHelper, CrossChainSwapV2); + await deployer.deploy( + CrossChainSwapV2, + routerAddress, + feeTokenAddress, + tokenBridgeAddress, + wrappedAvaxAddress + ); + + // save the contract address somewhere + await fsp.mkdir(scriptsAddressPath, { recursive: true }); + + await fsp.writeFile( + `${scriptsAddressPath}/${network}.ts`, + `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` + ); + + //deployer.link(ConvertLib, MetaCoin); + //deployer.deploy(MetaCoin); +}; diff --git a/contracts/migrations/bsc/1_initial_migration.js b/contracts/migrations/bsc/1_initial_migration.js new file mode 100644 index 0000000..16a7ba5 --- /dev/null +++ b/contracts/migrations/bsc/1_initial_migration.js @@ -0,0 +1,5 @@ +const Migrations = artifacts.require("Migrations"); + +module.exports = function (deployer) { + deployer.deploy(Migrations); +}; diff --git a/contracts/migrations/bsc/2_deploy_contracts.js b/contracts/migrations/bsc/2_deploy_contracts.js new file mode 100644 index 0000000..5219f85 --- /dev/null +++ b/contracts/migrations/bsc/2_deploy_contracts.js @@ -0,0 +1,34 @@ +const fsp = require("fs/promises"); + +const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2"); +const SwapHelper = artifacts.require("SwapHelper"); + +const scriptsAddressPath = "../react/src/addresses"; + +module.exports = async function(deployer, network) { + const routerAddress = "0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3"; // pancakeswap + const feeTokenAddress = "0x7b8eae1e85c8b189ee653d3f78733f4f788bb2c1"; // wUST + const tokenBridgeAddress = "0x9dcF9D205C9De35334D646BeE44b2D2859712A09"; + const wrappedBnbAddress = "0xae13d989dac2f0debff460ac112a837c89baa7cd"; + + await deployer.deploy(SwapHelper); + await deployer.link(SwapHelper, CrossChainSwapV2); + await deployer.deploy( + CrossChainSwapV2, + routerAddress, + feeTokenAddress, + tokenBridgeAddress, + wrappedBnbAddress + ); + + // save the contract address somewhere + await fsp.mkdir(scriptsAddressPath, { recursive: true }); + + await fsp.writeFile( + `${scriptsAddressPath}/${network}.ts`, + `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` + ); + + //deployer.link(ConvertLib, MetaCoin); + //deployer.deploy(MetaCoin); +}; diff --git a/misc/.gitignore b/misc/.gitignore new file mode 100644 index 0000000..eead6f2 --- /dev/null +++ b/misc/.gitignore @@ -0,0 +1,6 @@ +.env +node_modules/ +scripts/*.js +scripts/src/*.js +src +package-lock.json diff --git a/misc/package.json b/misc/package.json new file mode 100644 index 0000000..0ef1c3c --- /dev/null +++ b/misc/package.json @@ -0,0 +1,41 @@ +{ + "homepage": "https://certusone.github.io/wormhole-nativeswap-example", + "name": "NativeSwap", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc", + "clean": "rm_js_in_src.sh" + }, + "dependencies": { + "@certusone/wormhole-sdk": "^0.1.6", + "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.60", + "@metamask/detect-provider": "^1.2.0", + "@terra-money/terra.js": "^2.0.14", + "@terra-money/wallet-provider": "^2.2.0", + "@types/node": "^16.11.19", + "@types/react": "^17.0.38", + "@types/react-dom": "^17.0.11", + "@uniswap/smart-order-router": "^2.1.1", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v2-sdk": "^3.0.1", + "@uniswap/v3-periphery": "1.3", + "@uniswap/v3-sdk": "^3.8.1", + "ethers": "^5.5.3", + "jsbi": "^3.2.5", + "notistack": "^1.0.10", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "4.0.3", + "typescript": "^4.4.2", + "use-debounce": "^7.0.1" + }, + "devDependencies": { + "@craco/craco": "^6.3.0", + "gh-pages": "^3.2.3", + "wasm-loader": "^1.3.0" + } +} diff --git a/misc/rm_js_in_src.sh b/misc/rm_js_in_src.sh new file mode 100755 index 0000000..25d1862 --- /dev/null +++ b/misc/rm_js_in_src.sh @@ -0,0 +1,4 @@ +rm src/addresses/*.js +rm src/route/*.js +rm src/swapper/*.js +rm src/utils/*.js diff --git a/misc/scripts/src/provider.ts b/misc/scripts/src/provider.ts new file mode 100644 index 0000000..54aaf49 --- /dev/null +++ b/misc/scripts/src/provider.ts @@ -0,0 +1,36 @@ +import { ethers } from "ethers"; + +import { + ETH_TOKEN_INFO, + MATIC_TOKEN_INFO, + AVAX_TOKEN_INFO, + BNB_TOKEN_INFO, +} from "../../src/utils/consts"; + +export function makeProvider(tokenAddress: string) { + switch (tokenAddress) { + case ETH_TOKEN_INFO.address: { + return new ethers.providers.StaticJsonRpcProvider( + process.env.GOERLI_PROVIDER + ); + } + case MATIC_TOKEN_INFO.address: { + return new ethers.providers.StaticJsonRpcProvider( + process.env.MUMBAI_PROVIDER + ); + } + case AVAX_TOKEN_INFO.address: { + return new ethers.providers.StaticJsonRpcProvider( + process.env.FUJI_PROVIDER + ); + } + case BNB_TOKEN_INFO.address: { + return new ethers.providers.StaticJsonRpcProvider( + process.env.BSC_PROVIDER + ); + } + default: { + throw Error("unrecognized token address"); + } + } +} diff --git a/misc/scripts/swap-everything.sh b/misc/scripts/swap-everything.sh new file mode 100644 index 0000000..36607d9 --- /dev/null +++ b/misc/scripts/swap-everything.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euo pipefail + +root=$(dirname $0) +script="${root}/swap-with-vaa.js" + +echo `which node` + +node $script --in ETH --out MATIC +node $script --in ETH --out BNB +node $script --in ETH --out AVAX +node $script --in MATIC --out BNB +node $script --in MATIC --out AVAX +node $script --in BNB --out MATIC + +echo "done" \ No newline at end of file diff --git a/misc/scripts/swap-with-vaa.ts b/misc/scripts/swap-with-vaa.ts new file mode 100644 index 0000000..f1f0832 --- /dev/null +++ b/misc/scripts/swap-with-vaa.ts @@ -0,0 +1,470 @@ +import yargs from "yargs"; +import { ethers } from "ethers"; + +import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport"; + +import { + ExactInCrossParameters, + ExactOutCrossParameters, + UniswapToUniswapQuoter, +} from "../src/route/cross-quote"; +import { UniswapToUniswapExecutor } from "../src/swapper/swapper"; +import { + ETH_TOKEN_INFO, + MATIC_TOKEN_INFO, + AVAX_TOKEN_INFO, + BNB_TOKEN_INFO, + UST_TOKEN_INFO, +} from "../src/utils/consts"; + +import { makeProvider } from "./src/provider"; + +require("dotenv").config({ path: ".env" }); + +// swap related parameters (configurable in UI) +const SWAP_AMOUNT_IN_MATIC = "0.0069"; +const SWAP_AMOUNT_IN_ETH = "0.000907"; +const SWAP_AMOUNT_IN_AVAX = "0.0075"; +const SWAP_AMOUNT_IN_BNB = "0.0015"; +const SWAP_AMOUNT_IN_UST = "3.40"; + +const SWAP_DEADLINE = "1800"; +const SWAP_SLIPPAGE = "0.01"; + +// token bridge things +const BRIDGE_RELAYER_FEE_UST = "0.25"; + +interface Arguments { + in: string; + out: string; +} + +function parseArgs(): Arguments { + const parsed = yargs(process.argv.slice(2)) + .option("in", { + string: true, + description: "Name of inbound token", + required: true, + }) + .option("out", { + string: true, + description: "Name of outbound token", + required: true, + }) + .help("h") + .alias("h", "help").argv; + + const args: Arguments = { + in: parsed.in, + out: parsed.out, + }; + + return args; +} + +export function makeEvmWallet( + provider: ethers.providers.Provider +): ethers.Wallet { + return new ethers.Wallet(process.env.ETH_PRIVATE_KEY, provider); +} + +/* +async function fetchTokenBalance(signer, contract) { + const decimals = await contract.decimals(); + const balanceBeforeDecimals = (await contract.balanceOf(signer.address)).toString(); + const balance = ethers.utils.formatUnits(balanceBeforeDecimals, decimals); + return balance; +} +*/ + +// only exist as placeholder for actual wallet connection +function determineWalletFromToken(tokenAddress: string): ethers.Wallet { + return makeEvmWallet(makeProvider(tokenAddress)); +} + +function determineAmountFromToken(tokenAddress: string): string { + switch (tokenAddress) { + case ETH_TOKEN_INFO.address: { + return SWAP_AMOUNT_IN_ETH; + } + case MATIC_TOKEN_INFO.address: { + return SWAP_AMOUNT_IN_MATIC; + } + case AVAX_TOKEN_INFO.address: { + return SWAP_AMOUNT_IN_AVAX; + } + case BNB_TOKEN_INFO.address: { + return SWAP_AMOUNT_IN_BNB; + } + case UST_TOKEN_INFO.address: { + return SWAP_AMOUNT_IN_UST; + } + default: { + throw Error("you suck"); + } + } +} + +function logExactInParameters( + quoter: UniswapToUniswapQuoter, + params: ExactInCrossParameters +): void { + console.info(`amountIn: ${params.amountIn}`); + console.info(`minAmountOut: ${params.minAmountOut}`); + + const src = params.src; + if (src === undefined) { + console.warn(` src is undefined (ust?)`); + } else { + console.info(`src`); + console.info(` protocol: ${src.protocol}`); + //console.info(` amountIn: ${quoter.srcTokenIn.formatAmount(src.amountIn)}`); + console.info( + ` amountIn: ${quoter.srcRouter.formatAmountIn( + src.amountIn.toString() + )}` + ); + console.info( + // ` minAmountOut: ${quoter.srcTokenOut.formatAmount(src.minAmountOut)}` + ` minAmountOut: ${quoter.srcRouter.formatAmountOut( + src.minAmountOut.toString() + )}` + ); + console.info(` poolFee: ${src.poolFee}`); + console.info(` deadline: ${src.deadline.toString()}`); + console.info(` path: ${src.path}`); + } + + const dst = params.dst; + console.info(`dst`); + if (dst === undefined) { + console.warn(` dst is undefined (ust?)`); + } else { + console.info(` protocol: ${dst.protocol}`); + //console.info(` amountIn: ${quoter.dstTokenIn.formatAmount(dst.amountIn)}`); + console.info( + ` amountIn: ${quoter.dstRouter.formatAmountIn( + dst.amountIn.toString() + )}` + ); + console.info( + // ` minAmountOut: ${quoter.dstTokenOut.formatAmount(dst.minAmountOut)}` + ` minAmountOut: ${quoter.dstRouter.formatAmountOut( + dst.minAmountOut.toString() + )}` + ); + console.info(` poolFee: ${dst.poolFee}`); + console.info(` deadline: ${dst.deadline.toString()}`); + console.info(` path: ${dst.path}`); + + const relayerFee = params.relayerFee; + console.info(`relayerFee`); + console.info(` tokenAddress: ${relayerFee.tokenAddress}`); + console.info( + ` amount: ${quoter.dstRouter.formatAmountIn(relayerFee.amount)}` + ); + } + + return; +} + +async function swapEverythingExactIn( + swapper: UniswapToUniswapExecutor, + tokenInAddress: string, + tokenOutAddress: string, + isNative: boolean, + amountIn: string, + recipientAddress: string +): Promise { + const isTerraSrc = tokenInAddress === UST_TOKEN_INFO.address; + + if (isTerraSrc) { + throw Error("cannot use terra source yet"); + } + // connect src wallet + const srcWallet = determineWalletFromToken(tokenInAddress); + console.info(`sender: ${await srcWallet.getAddress()}`); + console.info(`recipient: ${recipientAddress}`); + + // tokens selected, let's initialize + await swapper.initialize(tokenInAddress, tokenOutAddress, isNative); + console.info(`quoter initialized`); + + // verify pool address on src and dst + await swapper + .computeAndVerifySrcPoolAddress() + .then((address) => { + console.info(`srcPool: ${address}`); + return address; + }) + .catch((response) => { + console.error( + `failed to find a pool address for src. how to handle in the front-end?` + ); + process.exit(1); + }); + + await swapper + .computeAndVerifyDstPoolAddress() + .then((address) => { + console.info(`dstPool: ${address}`); + return address; + }) + .catch((response) => { + console.error( + `failed to find a pool address for dst. how to handle in the front-end?` + ); + process.exit(1); + }); + + // set deadline + swapper.setDeadlines(SWAP_DEADLINE); + swapper.setSlippage(SWAP_SLIPPAGE); + swapper.setRelayerFee(BRIDGE_RELAYER_FEE_UST); + + const exactInParameters: ExactInCrossParameters = + await swapper.computeQuoteExactIn(amountIn); + + console.info("exactInParameters"); + logExactInParameters(swapper.quoter, exactInParameters); + + // do the src swap + if (isTerraSrc) { + // do terra method + throw Error("terra src not implemented yet"); + } else { + console.info("approveAndSwap"); + const srcSwapReceipt = await swapper.evmApproveAndSwap( + srcWallet, + recipientAddress + ); + console.info(`src transaction: ${srcSwapReceipt.transactionHash}`); + } + + // do the dst swap after fetching vaa + // connect dst wallet + const dstWallet = determineWalletFromToken(tokenOutAddress); + + console.info("fetchVaaAndSwap"); + //const dstSwapReceipt = await swapper.fetchVaaAndSwap(dstWallet); + //console.info(`dst transaction: ${dstSwapReceipt.transactionHash}`); + console.warn("jk"); + + return; +} + +function logExactOutParameters( + quoter: UniswapToUniswapQuoter, + params: ExactOutCrossParameters +): void { + const src = params.src; + console.info(`src`); + console.info(` protocol: ${src.protocol}`); + console.info( + ` amountOut: ${quoter.srcRouter.formatAmountOut( + src.amountOut.toString() + )}` + ); + console.info( + ` maxAmountIn: ${quoter.srcRouter.formatAmountIn( + src.maxAmountIn.toString() + )}` + ); + console.info(` poolFee: ${src.poolFee}`); + console.info(` deadline: ${src.deadline.toString()}`); + console.info(` path: ${src.path}`); + + const dst = params.dst; + console.info(`dst`); + console.info(` protocol: ${dst.protocol}`); + console.info( + ` amountOut: ${quoter.dstRouter.formatAmountOut( + dst.amountOut.toString() + )}` + ); + console.info( + ` maxAmountIn: ${quoter.dstRouter.formatAmountIn( + dst.maxAmountIn.toString() + )}` + ); + console.info(` poolFee: ${dst.poolFee}`); + console.info(` deadline: ${dst.deadline.toString()}`); + console.info(` path: ${dst.path}`); + + const relayerFee = params.relayerFee; + console.info(`relayerFee`); + console.info(` tokenAddress: ${relayerFee.tokenAddress}`); + console.info( + ` amount: ${quoter.dstRouter.formatAmountIn( + relayerFee.amount.toString() + )}` + ); + return; +} + +async function swapEverythingExactOut( + swapper: UniswapToUniswapExecutor, + tokenInAddress: string, + tokenOutAddress: string, + isNative: boolean, + amountOut: string, + recipientAddress: string +): Promise { + // connect src wallet + const srcWallet = determineWalletFromToken(tokenInAddress); + console.info(`wallet pubkey: ${await srcWallet.getAddress()}`); + + // tokens selected, let's initialize + await swapper.initialize(tokenInAddress, tokenOutAddress, isNative); + console.info(`quoter initialized`); + + // verify pool address on src and dst + await swapper + .computeAndVerifySrcPoolAddress() + .then((address) => { + console.info(`srcPool: ${address}`); + return address; + }) + .catch((response) => { + console.error( + `failed to find a pool address for src. how to handle in the front-end?` + ); + process.exit(1); + }); + + await swapper + .computeAndVerifyDstPoolAddress() + .then((address) => { + console.info(`dstPool: ${address}`); + return address; + }) + .catch((response) => { + console.error( + `failed to find a pool address for dst. how to handle in the front-end?` + ); + process.exit(1); + }); + + // set deadline + swapper.setDeadlines(SWAP_DEADLINE); + swapper.setSlippage(SWAP_SLIPPAGE); + swapper.setRelayerFee(BRIDGE_RELAYER_FEE_UST); + + const exactOutParameters: ExactOutCrossParameters = + await swapper.computeQuoteExactOut(amountOut); + + console.info("exactOutParameters"); + logExactOutParameters(swapper.quoter, exactOutParameters); + + // do the src swap + console.info("approveAndSwap"); + const srcSwapReceipt = await swapper.evmApproveAndSwap( + srcWallet, + recipientAddress + ); + console.info(`src transaction: ${srcSwapReceipt.transactionHash}`); + + // do the dst swap after fetching vaa + // connect dst wallet + const dstWallet = determineWalletFromToken(tokenOutAddress); + + console.info("fetchVaaAndSwap"); + //const dstSwapReceipt = await swapper.fetchVaaAndSwap(dstWallet); + //console.info(`dst transaction: ${dstSwapReceipt.transactionHash}`); + console.warn("jk"); + + return; +} + +function getTokenInfo(name: string) { + switch (name) { + case "ETH": { + return ETH_TOKEN_INFO; + } + case "MATIC": { + return MATIC_TOKEN_INFO; + } + case "UST": { + return UST_TOKEN_INFO; + } + case "AVAX": { + return AVAX_TOKEN_INFO; + } + case "BNB": { + return BNB_TOKEN_INFO; + } + default: { + throw Error("invalid token name"); + } + } +} + +async function main() { + const args = parseArgs(); + + const testExactIn = true; + const isNative = true; + + const swapper = new UniswapToUniswapExecutor(); + swapper.setTransport(NodeHttpTransport()); + + const tokenIn = getTokenInfo(args.in); + const tokenOut = getTokenInfo(args.out); + //const tokenOut = UST_TOKEN_INFO; + + const recipientAddress = "0x4e2dfAD7D7d0076b5A0A41223E4Bee390C33251C"; + //const recipientAddress = "terra1vewnsxcy5fqjslyyy409cw8js550esen38n8ey"; + + if (testExactIn) { + console.info(`testing exact in. native=${isNative}`); + + console.info(`${tokenIn.name} -> ${tokenOut.name}`); + await swapEverythingExactIn( + swapper, + tokenIn.address, + tokenOut.address, + isNative, + determineAmountFromToken(tokenIn.address), + recipientAddress + ); + + if (tokenOut.address === UST_TOKEN_INFO.address) { + console.warn("not pinging back"); + } else { + console.info(`${tokenOut.name} -> ${tokenIn.name}`); + await swapEverythingExactIn( + swapper, + tokenOut.address, + tokenIn.address, + isNative, + determineAmountFromToken(tokenOut.address), + recipientAddress + ); + } + } else { + console.info(`testing exact out. native=${isNative}`); + + console.info(`${tokenIn.name} -> ${tokenOut.name}`); + await swapEverythingExactOut( + swapper, + tokenIn.address, + tokenOut.address, + isNative, + determineAmountFromToken(tokenOut.address), + recipientAddress + ); + + console.info(`${tokenOut.name} -> ${tokenIn.name}`); + await swapEverythingExactOut( + swapper, + tokenOut.address, + tokenIn.address, + isNative, + determineAmountFromToken(tokenIn.address), + recipientAddress + ); + } + + return; +} +main(); diff --git a/misc/tsconfig.json b/misc/tsconfig.json new file mode 100644 index 0000000..3773eb2 --- /dev/null +++ b/misc/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "esModuleInterop": true + }, + "files": [ + "scripts/swap-with-vaa.ts" + ] +} diff --git a/react/.env.sample b/react/.env.sample index 1324637..5d6c4ec 100644 --- a/react/.env.sample +++ b/react/.env.sample @@ -1,2 +1,4 @@ -REACT_APP_GOERLI_PROVIDER=https://goerli.infura.io/v3/YOUR-PROJECT-ID -REACT_APP_MUMBAI_PROVIDER=https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID \ No newline at end of file +REACT_APP_GOERLI_PROVIDER="https://goerli.infura.io/v3/YOUR-PROJECT-ID" +REACT_APP_MUMBAI_PROVIDER="https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID" +REACT_APP_FUJI_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc" +REACT_APP_BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545" diff --git a/react/package-lock.json b/react/package-lock.json index e6c20d5..e394d26 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -14,6 +14,8 @@ "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", "@metamask/detect-provider": "^1.2.0", + "@terra-money/terra.js": "^2.0.14", + "@terra-money/wallet-provider": "^2.2.0", "@types/node": "^16.11.19", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", @@ -1893,6 +1895,36 @@ "rxjs": "^7.3.0" } }, + "node_modules/@certusone/wormhole-sdk/node_modules/@terra-money/wallet-provider": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz", + "integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==", + "dependencies": { + "@terra-dev/browser-check": "^2.5.3", + "@terra-dev/chrome-extension": "^2.5.3", + "@terra-dev/readonly-wallet": "^2.5.3", + "@terra-dev/readonly-wallet-modal": "^2.5.3", + "@terra-dev/use-wallet": "^2.5.3", + "@terra-dev/wallet-types": "^2.5.3", + "@terra-dev/walletconnect": "^2.5.3", + "@terra-dev/web-connector-controller": "^0.8.1", + "@terra-dev/web-connector-interface": "^0.8.1", + "fast-deep-equal": "^3.1.3", + "rxjs": "^7.4.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@terra-money/terra.js": "^2.0.0", + "react": "^17.0.0" + }, + "peerDependenciesMeta": { + "react-router-dom": { + "optional": true + } + } + }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -4494,12 +4526,24 @@ "@terra-money/terra.js": "^2.0.0" } }, - "node_modules/@terra-money/terra.js": { - "version": "2.1.23", - "resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.1.23.tgz", - "integrity": "sha512-nSAR35zqjKUn1Jzqevf30s47XRlW/VXU01YgK3n9ndmX15lkdlgFvqaV7UezK0xAmCpm+7xWIrtBTMmZpVBkMQ==", + "node_modules/@terra-dev/web-extension": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@terra-dev/web-extension/-/web-extension-0.6.0.tgz", + "integrity": "sha512-IyIWHLfweZCb5nHuMyzavnMYposnZMvpsA/89zZPIgIooxhxE//uZD+Ty+ptt4nvkbOgEFKdKIKe5rIHqgVLpA==", + "dependencies": { + "@terra-money/terra.js": "^1.8.0 || ^2.0.0", + "bowser": "^2.11.0", + "rxjs": "^7.3.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@terra-money/terra.js": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.0.14.tgz", + "integrity": "sha512-GeMadRIPaOedODa5a0pJ2+76l7MeFSIfSJZ2vvWPRco6MRIQLw/k0cZpPKMLm2Zo54li/oY1mrR+r3uxLM7q3Q==", "dependencies": { - "@terra-money/terra.proto": "^0.1.7", "axios": "^0.21.1", "bech32": "^2.0.0", "bip32": "^2.0.6", @@ -4514,7 +4558,7 @@ "ws": "^7.4.2" }, "engines": { - "node": ">=14" + "node": ">=12" } }, "node_modules/@terra-money/terra.js/node_modules/axios": { @@ -4525,38 +4569,27 @@ "follow-redirects": "^1.14.0" } }, - "node_modules/@terra-money/terra.proto": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz", - "integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==", - "dependencies": { - "google-protobuf": "^3.17.3", - "long": "^4.0.0", - "protobufjs": "~6.11.2" - } - }, "node_modules/@terra-money/wallet-provider": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz", - "integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.2.0.tgz", + "integrity": "sha512-K8NLpJ/yak8Pq6jQpjVr7yWDIbxjTp42OXaAS+xlTufqQwWbCR7coAGbm2FpYX43j4uymuSAICZvsOS1qrFeYA==", "dependencies": { - "@terra-dev/browser-check": "^2.5.3", - "@terra-dev/chrome-extension": "^2.5.3", - "@terra-dev/readonly-wallet": "^2.5.3", - "@terra-dev/readonly-wallet-modal": "^2.5.3", - "@terra-dev/use-wallet": "^2.5.3", - "@terra-dev/wallet-types": "^2.5.3", - "@terra-dev/walletconnect": "^2.5.3", - "@terra-dev/web-connector-controller": "^0.8.1", - "@terra-dev/web-connector-interface": "^0.8.1", + "@terra-dev/browser-check": "^2.2.0", + "@terra-dev/chrome-extension": "^2.2.0", + "@terra-dev/readonly-wallet": "^2.2.0", + "@terra-dev/readonly-wallet-modal": "^2.2.0", + "@terra-dev/use-wallet": "^2.2.0", + "@terra-dev/wallet-types": "^2.2.0", + "@terra-dev/walletconnect": "^2.2.0", + "@terra-dev/web-extension": "^0.6.0", + "@terra-money/terra.js": "^2.0.0", "fast-deep-equal": "^3.1.3", - "rxjs": "^7.4.0" + "rxjs": "^7.3.0" }, "engines": { "node": ">=12" }, "peerDependencies": { - "@terra-money/terra.js": "^2.0.0", "react": "^17.0.0" }, "peerDependenciesMeta": { @@ -12324,7 +12357,8 @@ "node_modules/google-protobuf": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.19.3.tgz", - "integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A==" + "integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A==", + "peer": true }, "node_modules/graceful-fs": { "version": "4.2.9", @@ -27356,6 +27390,26 @@ "js-base64": "^3.6.1", "protobufjs": "^6.11.2", "rxjs": "^7.3.0" + }, + "dependencies": { + "@terra-money/wallet-provider": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz", + "integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==", + "requires": { + "@terra-dev/browser-check": "^2.5.3", + "@terra-dev/chrome-extension": "^2.5.3", + "@terra-dev/readonly-wallet": "^2.5.3", + "@terra-dev/readonly-wallet-modal": "^2.5.3", + "@terra-dev/use-wallet": "^2.5.3", + "@terra-dev/wallet-types": "^2.5.3", + "@terra-dev/walletconnect": "^2.5.3", + "@terra-dev/web-connector-controller": "^0.8.1", + "@terra-dev/web-connector-interface": "^0.8.1", + "fast-deep-equal": "^3.1.3", + "rxjs": "^7.4.0" + } + } } }, "@cnakazawa/watch": { @@ -29148,12 +29202,21 @@ "rxjs": "^7.4.0" } }, - "@terra-money/terra.js": { - "version": "2.1.23", - "resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.1.23.tgz", - "integrity": "sha512-nSAR35zqjKUn1Jzqevf30s47XRlW/VXU01YgK3n9ndmX15lkdlgFvqaV7UezK0xAmCpm+7xWIrtBTMmZpVBkMQ==", + "@terra-dev/web-extension": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@terra-dev/web-extension/-/web-extension-0.6.0.tgz", + "integrity": "sha512-IyIWHLfweZCb5nHuMyzavnMYposnZMvpsA/89zZPIgIooxhxE//uZD+Ty+ptt4nvkbOgEFKdKIKe5rIHqgVLpA==", + "requires": { + "@terra-money/terra.js": "^1.8.0 || ^2.0.0", + "bowser": "^2.11.0", + "rxjs": "^7.3.0" + } + }, + "@terra-money/terra.js": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.0.14.tgz", + "integrity": "sha512-GeMadRIPaOedODa5a0pJ2+76l7MeFSIfSJZ2vvWPRco6MRIQLw/k0cZpPKMLm2Zo54li/oY1mrR+r3uxLM7q3Q==", "requires": { - "@terra-money/terra.proto": "^0.1.7", "axios": "^0.21.1", "bech32": "^2.0.0", "bip32": "^2.0.6", @@ -29178,32 +29241,22 @@ } } }, - "@terra-money/terra.proto": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz", - "integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==", - "requires": { - "google-protobuf": "^3.17.3", - "long": "^4.0.0", - "protobufjs": "~6.11.2" - } - }, "@terra-money/wallet-provider": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz", - "integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.2.0.tgz", + "integrity": "sha512-K8NLpJ/yak8Pq6jQpjVr7yWDIbxjTp42OXaAS+xlTufqQwWbCR7coAGbm2FpYX43j4uymuSAICZvsOS1qrFeYA==", "requires": { - "@terra-dev/browser-check": "^2.5.3", - "@terra-dev/chrome-extension": "^2.5.3", - "@terra-dev/readonly-wallet": "^2.5.3", - "@terra-dev/readonly-wallet-modal": "^2.5.3", - "@terra-dev/use-wallet": "^2.5.3", - "@terra-dev/wallet-types": "^2.5.3", - "@terra-dev/walletconnect": "^2.5.3", - "@terra-dev/web-connector-controller": "^0.8.1", - "@terra-dev/web-connector-interface": "^0.8.1", + "@terra-dev/browser-check": "^2.2.0", + "@terra-dev/chrome-extension": "^2.2.0", + "@terra-dev/readonly-wallet": "^2.2.0", + "@terra-dev/readonly-wallet-modal": "^2.2.0", + "@terra-dev/use-wallet": "^2.2.0", + "@terra-dev/wallet-types": "^2.2.0", + "@terra-dev/walletconnect": "^2.2.0", + "@terra-dev/web-extension": "^0.6.0", + "@terra-money/terra.js": "^2.0.0", "fast-deep-equal": "^3.1.3", - "rxjs": "^7.4.0" + "rxjs": "^7.3.0" } }, "@tootallnate/once": { @@ -35422,7 +35475,8 @@ "google-protobuf": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.19.3.tgz", - "integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A==" + "integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A==", + "peer": true }, "graceful-fs": { "version": "4.2.9", diff --git a/react/package.json b/react/package.json index 639f0ef..4dd492c 100644 --- a/react/package.json +++ b/react/package.json @@ -10,6 +10,8 @@ "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", "@metamask/detect-provider": "^1.2.0", + "@terra-money/terra.js": "^2.0.14", + "@terra-money/wallet-provider": "^2.2.0", "@types/node": "^16.11.19", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", diff --git a/react/src/addresses/.gitignore b/react/src/addresses/.gitignore index b44d3a4..4fa566d 100644 --- a/react/src/addresses/.gitignore +++ b/react/src/addresses/.gitignore @@ -1,2 +1,2 @@ -goerli.ts -mumbai.ts +*.ts +*.js diff --git a/react/src/components/Footer.tsx b/react/src/components/Footer.tsx index ba8afef..f42fc68 100644 --- a/react/src/components/Footer.tsx +++ b/react/src/components/Footer.tsx @@ -5,7 +5,7 @@ import Github from "../icons/Github.svg"; import Medium from "../icons/Medium.svg"; import Telegram from "../icons/Telegram.svg"; import Twitter from "../icons/Twitter.svg"; -import Wormhole from "../icons/wormhole-network.svg"; +import Wormhole from "../icons/wormhole_logo.svg"; const useStyles = makeStyles((theme) => ({ footer: { @@ -19,7 +19,6 @@ const useStyles = makeStyles((theme) => ({ }, }, builtWithContainer: { - display: "flex", alignItems: "center", justifyContent: "center", opacity: 0.5, @@ -27,7 +26,7 @@ const useStyles = makeStyles((theme) => ({ }, wormholeIcon: { height: 48, - width: 48, + width: 192, filter: "contrast(0)", transition: "filter 0.5s", "&:hover": { @@ -92,24 +91,18 @@ export default function Footer() { Twitter +
+ + Wormhole + +
-
- - Wormhole - -
-
- Open Source - Built with ❤ -
+ Open Source + Built with ❤
); diff --git a/react/src/components/SwapProgress.tsx b/react/src/components/SwapProgress.tsx index 8087fab..0b50bf9 100644 --- a/react/src/components/SwapProgress.tsx +++ b/react/src/components/SwapProgress.tsx @@ -1,7 +1,8 @@ -import { ChainId, CHAIN_ID_POLYGON, isEVMChain } from "@certusone/wormhole-sdk"; +import { ChainId, isEVMChain } from "@certusone/wormhole-sdk"; import { LinearProgress, makeStyles, Typography } from "@material-ui/core"; import { useEffect, useState } from "react"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { getChainName } from "../utils/consts"; const useStyles = makeStyles((theme) => ({ root: { @@ -16,17 +17,19 @@ const useStyles = makeStyles((theme) => ({ export default function TransactionProgress({ chainId, txBlockNumber, - step, + hasSignedVAA, + isTargetSwapComplete, }: { chainId: ChainId; txBlockNumber: number | undefined; - step: number; + hasSignedVAA: boolean; + isTargetSwapComplete: boolean; }) { const classes = useStyles(); const { provider } = useEthereumProvider(); const [currentBlock, setCurrentBlock] = useState(0); useEffect(() => { - if (step !== 1 || !txBlockNumber) return; + if (hasSignedVAA || !txBlockNumber) return; if (isEVMChain(chainId) && provider) { let cancelled = false; (async () => { @@ -46,33 +49,30 @@ export default function TransactionProgress({ cancelled = true; }; } - }, [step, chainId, provider, txBlockNumber]); - const blockDiff = + }, [hasSignedVAA, chainId, provider, txBlockNumber]); + let blockDiff = txBlockNumber !== undefined && txBlockNumber && currentBlock ? currentBlock - txBlockNumber : 0; const expectedBlocks = 15; + blockDiff = Math.min(Math.max(blockDiff, 0), expectedBlocks); let value; let valueBuffer; let message; - switch (step) { - case 1: - value = (blockDiff / expectedBlocks) * 50; - valueBuffer = 50; - message = `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${ - chainId === CHAIN_ID_POLYGON ? "Polygon" : "Ethereum" - }...`; - break; - case 2: - value = 50; - valueBuffer = 100; - message = "Waiting for relayer to complete swap..."; - break; - case 3: - value = 100; - valueBuffer = 100; - message = ""; - break; + if (!hasSignedVAA) { + value = (blockDiff / expectedBlocks) * 50; + valueBuffer = 50; + message = `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${getChainName( + chainId + )}...`; + } else if (!isTargetSwapComplete) { + value = 50; + valueBuffer = 100; + message = "Waiting for relayer to complete swap..."; + } else { + value = 100; + valueBuffer = 100; + message = "Success!"; } return (
diff --git a/react/src/components/TerraWalletKey.tsx b/react/src/components/TerraWalletKey.tsx new file mode 100644 index 0000000..38456c3 --- /dev/null +++ b/react/src/components/TerraWalletKey.tsx @@ -0,0 +1,22 @@ +import { useTerraWallet } from "../contexts/TerraWalletContext"; +import ToggleConnectedButton from "./ToggleConnectedButton"; + +const TerraWalletKey = () => { + const { connect, disconnect, connected, wallet } = useTerraWallet(); + const pk = + (wallet && + wallet.wallets && + wallet.wallets.length > 0 && + wallet.wallets[0].terraAddress) || + ""; + return ( + + ); +}; + +export default TerraWalletKey; diff --git a/react/src/components/TokenSelect.tsx b/react/src/components/TokenSelect.tsx index f13c15f..300a1f4 100644 --- a/react/src/components/TokenSelect.tsx +++ b/react/src/components/TokenSelect.tsx @@ -5,7 +5,20 @@ import { MenuItem, TextField, } from "@material-ui/core"; -import { TokenInfo } from "../utils/consts"; +import { + AVAX_TOKEN_INFO, + BNB_TOKEN_INFO, + ETH_TOKEN_INFO, + MATIC_TOKEN_INFO, + TokenInfo, + UST_TOKEN_INFO, +} from "../utils/consts"; + +import ethIcon from "../icons/eth.svg"; +import polygonIcon from "../icons/polygon.svg"; +import terraIcon from "../icons/terra.svg"; +import bscIcon from "../icons/bsc.svg"; +import avaxIcon from "../icons/avax.svg"; const useStyles = makeStyles((theme) => ({ select: { @@ -23,10 +36,27 @@ const useStyles = makeStyles((theme) => ({ }, })); -const createTokenMenuItem = ({ name, logo }: TokenInfo, classes: any) => ( +const getLogo = (name: string) => { + switch (name) { + case ETH_TOKEN_INFO.name: + return ethIcon; + case MATIC_TOKEN_INFO.name: + return polygonIcon; + case UST_TOKEN_INFO.name: + return terraIcon; + case AVAX_TOKEN_INFO.name: + return avaxIcon; + case BNB_TOKEN_INFO.name: + return bscIcon; + default: + return ""; + } +}; + +const createTokenMenuItem = ({ name }: TokenInfo, classes: any) => ( - {name} + {name} {name} diff --git a/react/src/contexts/TerraWalletContext.tsx b/react/src/contexts/TerraWalletContext.tsx new file mode 100644 index 0000000..5787d59 --- /dev/null +++ b/react/src/contexts/TerraWalletContext.tsx @@ -0,0 +1,97 @@ +import { + NetworkInfo, + Wallet, + WalletProvider, + useWallet, +} from "@terra-money/wallet-provider"; +import React, { + ReactChildren, + useCallback, + useContext, + useMemo, + useState, +} from "react"; + +const testnet: NetworkInfo = { + name: "testnet", + chainID: "bombay-12", + lcd: "https://bombay-lcd.terra.dev", +}; + +const walletConnectChainIds: Record = { + 0: testnet, +}; + +interface ITerraWalletContext { + connect(): void; + disconnect(): void; + connected: boolean; + wallet: any; +} + +const TerraWalletContext = React.createContext({ + connect: () => {}, + disconnect: () => {}, + connected: false, + wallet: null, +}); + +export const TerraWalletWrapper = ({ + children, +}: { + children: ReactChildren; +}) => { + // TODO: Use wallet instead of useConnectedWallet. + const terraWallet = useWallet(); + const [, setWallet] = useState(undefined); + const [connected, setConnected] = useState(false); + + const connect = useCallback(() => { + const CHROME_EXTENSION = 1; + if (terraWallet) { + terraWallet.connect(terraWallet.availableConnectTypes[CHROME_EXTENSION]); + setWallet(terraWallet); + setConnected(true); + } + }, [terraWallet]); + + const disconnect = useCallback(() => { + setConnected(false); + setWallet(undefined); + }, []); + + const contextValue = useMemo( + () => ({ + connect, + disconnect, + connected, + wallet: terraWallet, + }), + [connect, disconnect, connected, terraWallet] + ); + + return ( + + {children} + + ); +}; + +export const TerraWalletProvider = ({ + children, +}: { + children: ReactChildren; +}) => { + return ( + + {children} + + ); +}; + +export const useTerraWallet = () => { + return useContext(TerraWalletContext); +}; diff --git a/react/src/hooks/useIsWalletReady.ts b/react/src/hooks/useIsWalletReady.ts index f722dee..085bf3f 100644 --- a/react/src/hooks/useIsWalletReady.ts +++ b/react/src/hooks/useIsWalletReady.ts @@ -1,12 +1,10 @@ -import { ChainId, CHAIN_ID_SOLANA, isEVMChain } from "@certusone/wormhole-sdk"; +import { ChainId, CHAIN_ID_TERRA, isEVMChain } from "@certusone/wormhole-sdk"; import { hexlify, hexStripZeros } from "@ethersproject/bytes"; +import { useConnectedWallet } from "@terra-money/wallet-provider"; import { useCallback, useMemo } from "react"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; -// import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import { getEvmChainId } from "../utils/consts"; -const CLUSTER = "testnet"; // TODO: change this - const createWalletStatus = ( isReady: boolean, statusMessage: string = "", @@ -29,8 +27,8 @@ function useIsWalletReady( forceNetworkSwitch: () => void; } { const autoSwitch = enableNetworkAutoswitch; - // const solanaWallet = useSolanaWallet(); - // const solPK = solanaWallet?.publicKey; + const terraWallet = useConnectedWallet(); + const hasTerraWallet = !!terraWallet; const { provider, signerAddress, @@ -54,14 +52,19 @@ function useIsWalletReady( }, [provider, correctEvmNetwork, chainId]); return useMemo(() => { - //if (chainId === CHAIN_ID_SOLANA && solPK) { - // return createWalletStatus( - // true, - // undefined, - // forceNetworkSwitch, - // solPK.toString() - // ); - //} + if ( + chainId === CHAIN_ID_TERRA && + hasTerraWallet && + terraWallet?.walletAddress + ) { + // TODO: terraWallet does not update on wallet changes + return createWalletStatus( + true, + undefined, + forceNetworkSwitch, + terraWallet.walletAddress + ); + } if (isEVMChain(chainId) && hasEthInfo && signerAddress) { if (hasCorrectEvmNetwork) { return createWalletStatus( @@ -76,7 +79,7 @@ function useIsWalletReady( } return createWalletStatus( false, - `Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${correctEvmNetwork}`, + `Wallet is not connected to testnet. Expected Chain ID: ${correctEvmNetwork}`, forceNetworkSwitch, undefined ); @@ -93,12 +96,13 @@ function useIsWalletReady( chainId, autoSwitch, forceNetworkSwitch, - // solPK, + hasTerraWallet, hasEthInfo, correctEvmNetwork, hasCorrectEvmNetwork, provider, signerAddress, + terraWallet, ]); } diff --git a/react/src/icons/avax.svg b/react/src/icons/avax.svg new file mode 100644 index 0000000..a5787ea --- /dev/null +++ b/react/src/icons/avax.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/react/src/icons/bsc.svg b/react/src/icons/bsc.svg new file mode 100644 index 0000000..61c684d --- /dev/null +++ b/react/src/icons/bsc.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/react/src/icons/terra.svg b/react/src/icons/terra.svg new file mode 100644 index 0000000..f1c7602 --- /dev/null +++ b/react/src/icons/terra.svg @@ -0,0 +1,23 @@ + + + + + + + diff --git a/react/src/icons/wormhole-network.svg b/react/src/icons/wormhole-network.svg deleted file mode 100644 index cd0e70d..0000000 --- a/react/src/icons/wormhole-network.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/react/src/icons/wormhole_logo.svg b/react/src/icons/wormhole_logo.svg new file mode 100644 index 0000000..ec6231e --- /dev/null +++ b/react/src/icons/wormhole_logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/react/src/index.js b/react/src/index.js index ef588ed..2c4cdf8 100644 --- a/react/src/index.js +++ b/react/src/index.js @@ -5,6 +5,7 @@ import ReactDOM from "react-dom"; import App from "./App"; import ErrorBoundary from "./components/ErrorBoundary"; import { EthereumProviderProvider } from "./contexts/EthereumProviderContext"; +import { TerraWalletProvider } from "./contexts/TerraWalletContext"; import { theme } from "./muiTheme"; ReactDOM.render( @@ -12,9 +13,11 @@ ReactDOM.render( - - - + + + + + diff --git a/react/src/route/.gitignore b/react/src/route/.gitignore new file mode 100644 index 0000000..a6c7c28 --- /dev/null +++ b/react/src/route/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/react/src/route/cross-quote.ts b/react/src/route/cross-quote.ts index b9db9c6..310b3cd 100644 --- a/react/src/route/cross-quote.ts +++ b/react/src/route/cross-quote.ts @@ -1,41 +1,132 @@ import { ethers } from "ethers"; -import { UniEvmToken } from "./uniswap-core"; -import { QuickswapRouter } from "./quickswap"; -import { SingleAmmSwapRouter as UniswapV3Router } from "./uniswap-v3"; + +import { QuickswapRouter as MaticRouter } from "./quickswap"; +import { UniswapV3Router as EthRouter } from "./uniswap-v3"; +import { TerraUstTransfer as UstRouter } from "./terra-ust-transfer"; +import { HurricaneswapRouter as AvaxRouter } from "./hurricaneswap"; +import { PancakeswapRouter as BnbRouter } from "./pancakeswap"; import { - ETH_NETWORK_CHAIN_ID, - POLYGON_NETWORK_CHAIN_ID, + ETH_TOKEN_INFO, + MATIC_TOKEN_INFO, + AVAX_TOKEN_INFO, + BNB_TOKEN_INFO, + UST_TOKEN_INFO, } from "../utils/consts"; +import { addFixedAmounts, subtractFixedAmounts } from "../utils/math"; +import { UstLocation } from "./generic"; +import { + ExactInParameters, + ExactOutParameters, + makeExactInParameters, + makeExactOutParameters, +} from "./uniswap-core"; +import { + ChainId, + CHAIN_ID_ETH, + CHAIN_ID_POLYGON, + CHAIN_ID_AVAX, + CHAIN_ID_BSC, + CHAIN_ID_TERRA, +} from "@certusone/wormhole-sdk"; export { PROTOCOL as PROTOCOL_UNISWAP_V2 } from "./uniswap-v2"; export { PROTOCOL as PROTOCOL_UNISWAP_V3 } from "./uniswap-v3"; +export { PROTOCOL as PROTOCOL_TERRA_UST_TRANSFER } from "./terra-ust-transfer"; + +export const TERRA_UST = UST_TOKEN_INFO.address; export enum QuoteType { ExactIn = 1, ExactOut, } -function makeRouter(provider: ethers.providers.Provider, id: number) { - switch (id) { - case ETH_NETWORK_CHAIN_ID: { - return new UniswapV3Router(provider); +export function makeEvmProviderFromAddress(tokenAddress: string) { + switch (tokenAddress) { + case ETH_TOKEN_INFO.address: { + const url = process.env.REACT_APP_GOERLI_PROVIDER; + if (!url) { + throw new Error("Could not find REACT_APP_GOERLI_PROVIDER"); + } + return new ethers.providers.StaticJsonRpcProvider(url); } - case POLYGON_NETWORK_CHAIN_ID: { - return new QuickswapRouter(provider); + case MATIC_TOKEN_INFO.address: { + const url = process.env.REACT_APP_MUMBAI_PROVIDER; + if (!url) { + throw new Error("Could not find REACT_APP_MUMBAI_PROVIDER"); + } + return new ethers.providers.StaticJsonRpcProvider(url); + } + case AVAX_TOKEN_INFO.address: { + const url = process.env.REACT_APP_FUJI_PROVIDER; + if (!url) { + throw new Error("Could not find REACT_APP_FUJI_PROVIDER"); + } + return new ethers.providers.StaticJsonRpcProvider(url); + } + case BNB_TOKEN_INFO.address: { + const url = process.env.REACT_APP_BSC_PROVIDER; + if (!url) { + throw new Error("Could not find REACT_APP_BSC_PROVIDER"); + } + return new ethers.providers.StaticJsonRpcProvider(url); } default: { - throw Error("unrecognized chain id"); + throw Error("unrecognized evm token address"); } } } -export function getUstAddress(id: number): string { - switch (id) { - case ETH_NETWORK_CHAIN_ID: { - return "0x36Ed51Afc79619b299b238898E72ce482600568a"; +export function getChainIdFromAddress(tokenAddress: string) { + switch (tokenAddress) { + case ETH_TOKEN_INFO.address: { + return CHAIN_ID_ETH; } - case POLYGON_NETWORK_CHAIN_ID: { - return "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c"; + case MATIC_TOKEN_INFO.address: { + return CHAIN_ID_POLYGON; + } + case AVAX_TOKEN_INFO.address: { + return CHAIN_ID_AVAX; + } + case BNB_TOKEN_INFO.address: { + return CHAIN_ID_BSC; + } + case UST_TOKEN_INFO.address: { + return CHAIN_ID_TERRA; + } + default: { + throw Error("unrecognized evm token address"); + } + } +} + +async function makeRouter(tokenAddress: string, loc: UstLocation) { + switch (tokenAddress) { + case ETH_TOKEN_INFO.address: { + const provider = makeEvmProviderFromAddress(tokenAddress); + const router = new EthRouter(provider); + await router.initialize(loc); + return router; + } + case MATIC_TOKEN_INFO.address: { + const provider = makeEvmProviderFromAddress(tokenAddress); + const router = new MaticRouter(provider); + await router.initialize(loc); + return router; + } + case AVAX_TOKEN_INFO.address: { + const provider = makeEvmProviderFromAddress(tokenAddress); + const router = new AvaxRouter(provider); + await router.initialize(loc); + return router; + } + case BNB_TOKEN_INFO.address: { + const provider = makeEvmProviderFromAddress(tokenAddress); + const router = new BnbRouter(provider); + await router.initialize(loc); + return router; + } + case UST_TOKEN_INFO.address: { + return new UstRouter(); } default: { throw Error("unrecognized chain id"); @@ -51,123 +142,101 @@ function splitSlippageInHalf(totalSlippage: string): string { .toString(); } -interface RelayerFee { - amount: ethers.BigNumber; +export interface RelayerFee { + amount: string; tokenAddress: string; } -export interface ExactInParameters { - protocol: string; - amountIn: ethers.BigNumber; - minAmountOut: ethers.BigNumber; - deadline: ethers.BigNumber; - poolFee: string; - path: [string, string]; -} - export interface ExactInCrossParameters { - src: ExactInParameters; - dst: ExactInParameters; + amountIn: string; + ustAmountIn: string; + minAmountOut: string; + src: ExactInParameters | undefined; + dst: ExactInParameters | undefined; relayerFee: RelayerFee; } -export interface ExactOutParameters { - protocol: string; - amountOut: ethers.BigNumber; - maxAmountIn: ethers.BigNumber; - deadline: ethers.BigNumber; - poolFee: string; - path: [string, string]; -} - export interface ExactOutCrossParameters { - src: ExactOutParameters; - dst: ExactOutParameters; + amountOut: string; + ustAmountIn: string; + maxAmountIn: string; + src: ExactOutParameters | undefined; + dst: ExactOutParameters | undefined; relayerFee: RelayerFee; } export class UniswapToUniswapQuoter { - // providers - srcProvider: ethers.providers.Provider; - dstProvider: ethers.providers.Provider; - - // networks - srcNetwork: ethers.providers.Network; - dstNetwork: ethers.providers.Network; + // tokens + tokenInAddress: string; + tokenOutAddress: string; // routers - srcRouter: UniswapV3Router | QuickswapRouter; - dstRouter: UniswapV3Router | QuickswapRouter; + srcRouter: UstRouter | EthRouter | MaticRouter | AvaxRouter | BnbRouter; + dstRouter: UstRouter | EthRouter | MaticRouter | AvaxRouter | BnbRouter; - // tokens - srcTokenIn: UniEvmToken; - srcTokenOut: UniEvmToken; - dstTokenIn: UniEvmToken; - dstTokenOut: UniEvmToken; + async initialize(tokenInAddress: string, tokenOutAddress: string) { + if (tokenInAddress !== this.tokenInAddress) { + this.tokenInAddress = tokenInAddress; + this.srcRouter = await makeRouter(tokenInAddress, UstLocation.Out); + } - constructor( - srcProvider: ethers.providers.Provider, - dstProvider: ethers.providers.Provider - ) { - this.srcProvider = srcProvider; - this.dstProvider = dstProvider; - } - - async initialize(): Promise { - [this.srcNetwork, this.dstNetwork] = await Promise.all([ - this.srcProvider.getNetwork(), - this.dstProvider.getNetwork(), - ]); - - this.srcRouter = makeRouter(this.srcProvider, this.srcNetwork.chainId); - this.dstRouter = makeRouter(this.dstProvider, this.dstNetwork.chainId); - return; - } - - sameChain(): boolean { - return this.srcNetwork.chainId === this.dstNetwork.chainId; - } - - async makeSrcTokens( - tokenInAddress: string - ): Promise<[UniEvmToken, UniEvmToken]> { - const ustOutAddress = getUstAddress(this.srcNetwork.chainId); - - const router = this.srcRouter; - - [this.srcTokenIn, this.srcTokenOut] = await Promise.all([ - router.makeToken(tokenInAddress), - router.makeToken(ustOutAddress), - ]); - return [this.srcTokenIn, this.srcTokenOut]; - } - - async makeDstTokens( - tokenOutAddress: string - ): Promise<[UniEvmToken, UniEvmToken]> { - const ustInAddress = getUstAddress(this.dstNetwork.chainId); - - const router = this.dstRouter; - - [this.dstTokenIn, this.dstTokenOut] = await Promise.all([ - router.makeToken(ustInAddress), - router.makeToken(tokenOutAddress), - ]); - return [this.dstTokenIn, this.dstTokenOut]; + if (tokenOutAddress !== this.tokenOutAddress) { + this.tokenOutAddress = tokenOutAddress; + this.dstRouter = await makeRouter(tokenOutAddress, UstLocation.In); + } } async computeAndVerifySrcPoolAddress(): Promise { - return this.srcRouter.computeAndVerifyPoolAddress( - this.srcTokenIn, - this.srcTokenOut - ); + return this.srcRouter.computeAndVerifyPoolAddress(); } async computeAndVerifyDstPoolAddress(): Promise { - return this.dstRouter.computeAndVerifyPoolAddress( - this.dstTokenIn, - this.dstTokenOut - ); + return this.dstRouter.computeAndVerifyPoolAddress(); + } + + computeSwapSlippage(slippage: string): string { + if (this.isSrcUst() || this.isDstUst()) { + return slippage; + } + + return splitSlippageInHalf(slippage); + } + + getRelayerFee(amount: string): RelayerFee { + if (this.isSrcUst()) { + return { + amount: this.srcRouter.computeUnitAmountOut(amount), + tokenAddress: TERRA_UST, // TODO: make sure this is the right address for bridge transfer? + }; + } + + const relayerFee: RelayerFee = { + amount: this.srcRouter.computeUnitAmountOut(amount), + tokenAddress: this.srcRouter.getTokenOutAddress(), + }; + return relayerFee; + } + + makeSrcExactInParameters( + amountIn: string, + minAmountOut: string + ): ExactInParameters | undefined { + if (this.isSrcUst()) { + return undefined; + } + // @ts-ignore + return makeExactInParameters(this.srcRouter, amountIn, minAmountOut); + } + + makeDstExactInParameters( + amountIn: string, + minAmountOut: string + ): ExactInParameters | undefined { + if (this.isDstUst()) { + return undefined; + } + // @ts-ignore + return makeExactInParameters(this.dstRouter, amountIn, minAmountOut); } async computeExactInParameters( @@ -175,71 +244,69 @@ export class UniswapToUniswapQuoter { slippage: string, relayerFeeUst: string ): Promise { - const singleSlippage = splitSlippageInHalf(slippage); + const singleSlippage = this.computeSwapSlippage(slippage); // src quote const srcRouter = this.srcRouter; - const srcTokenIn = this.srcTokenIn; - const srcTokenOut = this.srcTokenOut; - const srcMinAmountOut = await srcRouter.fetchQuoteAmountOut( - srcTokenIn, - srcTokenOut, + const srcMinAmountOut = await srcRouter.fetchExactInQuote( amountIn, singleSlippage ); // dst quote const dstRouter = this.dstRouter; - const dstAmountIn = this.srcTokenOut.formatAmount(srcMinAmountOut); + const dstAmountIn = srcMinAmountOut; //srcRouter.formatAmountOut(srcMinAmountOut); if (Number(dstAmountIn) < Number(relayerFeeUst)) { throw Error( `srcAmountOut <= relayerFeeUst. ${dstAmountIn} vs ${relayerFeeUst}` ); } - const dstTokenIn = this.dstTokenIn; - const dstTokenOut = this.dstTokenOut; - const dstAmountInAfterFee = dstTokenIn.subtractAmounts( + const dstAmountInAfterFee = subtractFixedAmounts( dstAmountIn, - relayerFeeUst + relayerFeeUst, + dstRouter.getTokenInDecimals() ); - const dstMinAmountOut = await dstRouter.fetchQuoteAmountOut( - dstTokenIn, - dstTokenOut, + const dstMinAmountOut = await dstRouter.fetchExactInQuote( dstAmountInAfterFee, singleSlippage ); - const srcParameters: ExactInParameters = { - protocol: srcRouter.getProtocol(), - amountIn: srcTokenIn.computeUnitAmount(amountIn), - minAmountOut: srcMinAmountOut, - poolFee: srcRouter.getPoolFee(), - deadline: srcRouter.getTradeDeadline(), - path: [srcTokenIn.getAddress(), srcTokenOut.getAddress()], - }; - - const dstParameters: ExactInParameters = { - protocol: dstRouter.getProtocol(), - amountIn: dstTokenIn.computeUnitAmount(dstAmountInAfterFee), - minAmountOut: dstMinAmountOut, - poolFee: dstRouter.getPoolFee(), - deadline: dstRouter.getTradeDeadline(), - path: [dstTokenIn.getAddress(), dstTokenOut.getAddress()], - }; - + // organize parameters const params: ExactInCrossParameters = { - src: srcParameters, - dst: dstParameters, - relayerFee: { - amount: dstTokenIn.computeUnitAmount(relayerFeeUst), - tokenAddress: this.dstTokenIn.getAddress(), - }, + amountIn: amountIn, + ustAmountIn: dstAmountInAfterFee, + minAmountOut: dstMinAmountOut, + src: this.makeSrcExactInParameters(amountIn, srcMinAmountOut), + dst: this.makeDstExactInParameters(dstAmountInAfterFee, dstMinAmountOut), + relayerFee: this.getRelayerFee(relayerFeeUst), }; return params; } + makeSrcExactOutParameters( + amountOut: string, + maxAmountIn: string + ): ExactOutParameters | undefined { + if (this.isSrcUst()) { + return undefined; + } + // @ts-ignore + return makeExactOutParameters(this.srcRouter, amountOut, maxAmountIn); + } + + makeDstExactOutParameters( + amountOut: string, + maxAmountIn: string + ): ExactOutParameters | undefined { + if (this.isDstUst()) { + return undefined; + } + // @ts-ignore + return makeExactOutParameters(this.dstRouter, amountOut, maxAmountIn); + } + async computeExactOutParameters( amountOut: string, slippage: string, @@ -249,69 +316,86 @@ export class UniswapToUniswapQuoter { // dst quote first const dstRouter = this.dstRouter; - const dstTokenIn = this.dstTokenIn; - const dstTokenOut = this.dstTokenOut; - const dstMaxAmountIn = await dstRouter.fetchQuoteAmountIn( - dstTokenIn, - dstTokenOut, + const dstMaxAmountIn = await dstRouter.fetchExactOutQuote( amountOut, singleSlippage ); // src quote const srcRouter = this.srcRouter; - const srcAmountOut = this.dstTokenIn.formatAmount(dstMaxAmountIn); + const srcAmountOut = dstMaxAmountIn; if (Number(srcAmountOut) < Number(relayerFeeUst)) { throw Error( `dstAmountIn <= relayerFeeUst. ${srcAmountOut} vs ${relayerFeeUst}` ); } - const srcTokenIn = this.srcTokenIn; - const srcTokenOut = this.srcTokenOut; - const srcAmountOutBeforeFee = srcTokenOut.addAmounts( + const srcAmountOutBeforeFee = addFixedAmounts( srcAmountOut, - relayerFeeUst + relayerFeeUst, + srcRouter.getTokenOutDecimals() ); - const srcMaxAmountIn = await srcRouter.fetchQuoteAmountIn( - srcTokenIn, - srcTokenOut, + const srcMaxAmountIn = await srcRouter.fetchExactOutQuote( srcAmountOutBeforeFee, singleSlippage ); - const srcParameters: ExactOutParameters = { - protocol: srcRouter.getProtocol(), - amountOut: srcTokenOut.computeUnitAmount(srcAmountOutBeforeFee), - maxAmountIn: srcMaxAmountIn, - poolFee: srcRouter.getPoolFee(), - deadline: srcRouter.getTradeDeadline(), - path: [srcTokenIn.getAddress(), srcTokenOut.getAddress()], - }; - - const dstParameters: ExactOutParameters = { - protocol: dstRouter.getProtocol(), - amountOut: dstTokenOut.computeUnitAmount(amountOut), - maxAmountIn: dstMaxAmountIn, - poolFee: dstRouter.getPoolFee(), - deadline: dstRouter.getTradeDeadline(), - path: [dstTokenIn.getAddress(), dstTokenOut.getAddress()], - }; - + // organize parameters const params: ExactOutCrossParameters = { - src: srcParameters, - dst: dstParameters, - relayerFee: { - amount: dstTokenIn.computeUnitAmount(relayerFeeUst), - tokenAddress: this.dstTokenIn.getAddress(), - }, + amountOut: amountOut, + ustAmountIn: dstMaxAmountIn, + maxAmountIn: srcMaxAmountIn, + src: this.makeSrcExactOutParameters( + srcAmountOutBeforeFee, + srcMaxAmountIn + ), + dst: this.makeDstExactOutParameters(amountOut, dstMaxAmountIn), + relayerFee: this.getRelayerFee(relayerFeeUst), }; return params; } setDeadlines(deadline: string): void { - this.srcRouter.setDeadline(deadline); - this.dstRouter.setDeadline(deadline); + if (!this.isSrcUst()) { + // @ts-ignore + this.srcRouter.setDeadline(deadline); + } + if (!this.isDstUst()) { + // @ts-ignore + this.dstRouter.setDeadline(deadline); + } + } + + isSrcUst(): boolean { + return this.tokenInAddress === TERRA_UST; + } + + isDstUst(): boolean { + return this.tokenOutAddress === TERRA_UST; + } + + getSrcEvmProvider(): ethers.providers.Provider | undefined { + if (this.isSrcUst()) { + return undefined; + } + // @ts-ignore + return this.srcRouter.getProvider(); + } + + getDstEvmProvider(): ethers.providers.Provider | undefined { + if (this.isDstUst()) { + return undefined; + } + // @ts-ignore + return this.dstRouter.getProvider(); + } + + getSrcChainId(): ChainId { + return getChainIdFromAddress(this.tokenInAddress); + } + + getDstChainId(): ChainId { + return getChainIdFromAddress(this.tokenOutAddress); } } diff --git a/react/src/route/generic.ts b/react/src/route/generic.ts index 151edf5..0a33a2c 100644 --- a/react/src/route/generic.ts +++ b/react/src/route/generic.ts @@ -1,7 +1,38 @@ -export abstract class DexRouter { - abstract makeToken(tokenAddress: string): any; - abstract quoteLot(tokenA: any, tokenB: any, amount: string): Promise; - abstract setSlippage(slippage: string): void; +export enum UstLocation { + In = 1, + Out, +} + +export abstract class RouterCore { + abstract computeAndVerifyPoolAddress(): Promise; + + abstract computePoolAddress(): string; + + //abstract computeUnitAmountIn(amount: string): string; + + abstract computeUnitAmountOut(amount: string): string; + + abstract fetchExactInQuote( + amountOut: string, + slippage: string + ): Promise; + + abstract fetchExactOutQuote( + amountOut: string, + slippage: string + ): Promise; + + abstract formatAmountIn(amount: string): string; + + abstract formatAmountOut(amount: string): string; + + abstract getProtocol(): string; + + abstract getTokenInDecimals(): number; + + abstract getTokenOutDecimals(): number; + + abstract getTokenOutAddress(): string; } export abstract class GenericToken { @@ -9,16 +40,3 @@ export abstract class GenericToken { abstract getDecimals(): number; } - -// TODO: wrap SwapRoute and other routes -export class GenericRoute { - route: any; - - constructor(route: any) { - this.route = route; - } - - getRoute(): any { - return this.route; - } -} diff --git a/react/src/route/hurricaneswap.ts b/react/src/route/hurricaneswap.ts new file mode 100644 index 0000000..f1b946b --- /dev/null +++ b/react/src/route/hurricaneswap.ts @@ -0,0 +1,26 @@ +import { ethers } from "ethers"; + +import { AVAX_TOKEN_INFO } from "../utils/consts"; +import { UstLocation } from "./generic"; +import { UniswapV2Router } from "./uniswap-v2"; + +export { PROTOCOL } from "./uniswap-v2"; + +const HURRICANESWAP_FACTORY_ADDRESS = ""; + +export class HurricaneswapRouter extends UniswapV2Router { + constructor(provider: ethers.providers.Provider) { + super(provider); + super.setFactoryAddress(HURRICANESWAP_FACTORY_ADDRESS); + } + + async initialize(ustLocation: UstLocation): Promise { + await super.initializeTokens(AVAX_TOKEN_INFO, ustLocation); + return; + } + + computePoolAddress(): string { + // cannot find factory address on testnet + return "0xD8087870E8869e45154189d434DF61C19e77ae30"; + } +} diff --git a/react/src/route/pancakeswap.ts b/react/src/route/pancakeswap.ts new file mode 100644 index 0000000..36e1f52 --- /dev/null +++ b/react/src/route/pancakeswap.ts @@ -0,0 +1,26 @@ +import { ethers } from "ethers"; + +import { BNB_TOKEN_INFO } from "../utils/consts"; +import { UstLocation } from "./generic"; +import { UniswapV2Router } from "./uniswap-v2"; + +export { PROTOCOL } from "./uniswap-v2"; + +const PANCAKESWAP_FACTORY_ADDRESS = ""; + +export class PancakeswapRouter extends UniswapV2Router { + constructor(provider: ethers.providers.Provider) { + super(provider); + super.setFactoryAddress(PANCAKESWAP_FACTORY_ADDRESS); + } + + async initialize(ustLocation: UstLocation): Promise { + await super.initializeTokens(BNB_TOKEN_INFO, ustLocation); + return; + } + + computePoolAddress(): string { + // cannot find factory address on testnet + return "0x8682096d4A2a2f3cd63147D05e4BAB47634e2AD1"; + } +} diff --git a/react/src/route/quickswap.ts b/react/src/route/quickswap.ts index a36c4c1..b31d5cc 100644 --- a/react/src/route/quickswap.ts +++ b/react/src/route/quickswap.ts @@ -1,12 +1,21 @@ import { ethers } from "ethers"; -import { QUICKSWAP_FACTORY_ADDRESS } from "../utils/consts"; -import { SingleAmmSwapRouter } from "./uniswap-v2"; + +import { MATIC_TOKEN_INFO } from "../utils/consts"; +import { UstLocation } from "./generic"; +import { UniswapV2Router } from "./uniswap-v2"; export { PROTOCOL } from "./uniswap-v2"; -export class QuickswapRouter extends SingleAmmSwapRouter { +const QUICKSWAP_FACTORY_ADDRESS = "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32"; + +export class QuickswapRouter extends UniswapV2Router { constructor(provider: ethers.providers.Provider) { super(provider); super.setFactoryAddress(QUICKSWAP_FACTORY_ADDRESS); } + + async initialize(ustLocation: UstLocation): Promise { + await super.initializeTokens(MATIC_TOKEN_INFO, ustLocation); + return; + } } diff --git a/react/src/route/terra-ust-transfer.ts b/react/src/route/terra-ust-transfer.ts new file mode 100644 index 0000000..687d2df --- /dev/null +++ b/react/src/route/terra-ust-transfer.ts @@ -0,0 +1,67 @@ +import { Dec, Int } from "@terra-money/terra.js"; + +import { UST_TOKEN_INFO } from "../utils/consts"; +import { RouterCore } from "./generic"; + +export const PROTOCOL = "TerraUstTransfer"; + +const UST_DECIMALS = 6; + +const UST_AMOUNT_MULTIPLIER = "1000000"; + +export class TerraUstTransfer extends RouterCore { + computePoolAddress(): string { + return UST_TOKEN_INFO.address; + } + + computeAndVerifyPoolAddress(): Promise { + return new Promise((resolve) => { + return resolve(this.computePoolAddress()); + }); + } + + formatAmountIn(amount: string): string { + const formatted = new Dec(amount).div(UST_AMOUNT_MULTIPLIER); + return formatted.toString(); + } + + formatAmountOut(amount: string): string { + return this.formatAmountIn(amount); + } + + computeUnitAmountIn(amount: string): string { + const unitified = new Dec(amount).mul(UST_AMOUNT_MULTIPLIER); + return new Int(unitified.toString()).toString(); + } + + computeUnitAmountOut(amount: string): string { + return this.computeUnitAmountIn(amount); + } + + getProtocol(): string { + return PROTOCOL; + } + + async fetchExactInQuote(amountIn: string, slippage: string): Promise { + return amountIn; + } + + async fetchExactOutQuote( + amountOut: string, + slippage: string + ): Promise { + return amountOut; + } + + getTokenInDecimals(): number { + return UST_DECIMALS; + } + + getTokenOutDecimals(): number { + return UST_DECIMALS; + } + + getTokenOutAddress(): string { + return this.computePoolAddress(); + } +} diff --git a/react/src/route/uniswap-core.ts b/react/src/route/uniswap-core.ts index 54e63a7..e85e435 100644 --- a/react/src/route/uniswap-core.ts +++ b/react/src/route/uniswap-core.ts @@ -1,7 +1,10 @@ +//@ts-nocheck import { ethers } from "ethers"; import { CurrencyAmount, Token } from "@uniswap/sdk-core"; import { EvmToken } from "./evm"; +import { RouterCore, UstLocation } from "./generic"; +import { TokenInfo } from "../utils/consts"; export function computeTradeDeadline(deadline: string): ethers.BigNumber { return ethers.BigNumber.from(Math.floor(Date.now() / 1000)).add(deadline); @@ -78,46 +81,112 @@ export async function makeUniEvmToken( return new UniEvmToken(chainId, erc20); } -export abstract class UniswapRouterCore { +function stringToBigNumber(value: string): ethers.BigNumber { + return ethers.BigNumber.from(value); +} + +export interface ExactInParameters { + protocol: string; + amountIn: ethers.BigNumber; + minAmountOut: ethers.BigNumber; + deadline: ethers.BigNumber; + poolFee: string; + path: [string, string]; +} + +export interface ExactOutParameters { + protocol: string; + amountOut: ethers.BigNumber; + maxAmountIn: ethers.BigNumber; + deadline: ethers.BigNumber; + poolFee: string; + path: [string, string]; +} + +export function makeExactInParameters( + router: UniswapRouterCore, + amountIn: string, + minAmountOut: string +): ExactInParameters { + const params: ExactInParameters = { + protocol: router.getProtocol(), + amountIn: router.tokenIn.computeUnitAmount(amountIn), + minAmountOut: router.tokenOut.computeUnitAmount(minAmountOut), + poolFee: router.getPoolFee(), + deadline: router.getTradeDeadline(), + path: [router.tokenIn.getAddress(), router.tokenOut.getAddress()], + }; + return params; +} + +export function makeExactOutParameters( + router: UniswapRouterCore, + amountOut: string, + maxAmountIn: string +): ExactOutParameters { + const params: ExactOutParameters = { + protocol: router.getProtocol(), + amountOut: router.tokenOut.computeUnitAmount(amountOut), + maxAmountIn: router.tokenIn.computeUnitAmount(maxAmountIn), + poolFee: router.getPoolFee(), + deadline: router.getTradeDeadline(), + path: [router.tokenIn.getAddress(), router.tokenOut.getAddress()], + }; + return params; +} + +export abstract class UniswapRouterCore extends RouterCore { provider: ethers.providers.Provider; + network: ethers.providers.Network; + + // wormhole + chainId: number; + + // tokens + tokenIn: UniEvmToken; + tokenOut: UniEvmToken; // params deadline: string = ""; constructor(provider: ethers.providers.Provider) { + super(); this.provider = provider; } - public async makeToken(tokenAddress: string): Promise { - const network = await this.provider.getNetwork(); - return makeUniEvmToken(this.provider, network.chainId, tokenAddress); + public getProvider(): ethers.providers.Provider { + return this.provider; } - abstract computePoolAddress( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken - ): string; + public async initializeTokens( + tokenInfo: TokenInfo, + ustLocation: UstLocation + ): Promise { + this.network = await this.provider.getNetwork(); - abstract computeAndVerifyPoolAddress( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken - ): Promise; + const network = this.network; - abstract fetchQuoteAmountOut( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, - amountOut: string, - slippage: string - ): Promise; - - abstract fetchQuoteAmountIn( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, - amountOut: string, - slippage: string - ): Promise; - - abstract getProtocol(): string; + if (ustLocation === UstLocation.Out) { + [this.tokenIn, this.tokenOut] = await Promise.all([ + makeUniEvmToken(this.provider, network.chainId, tokenInfo.address), + makeUniEvmToken( + this.provider, + network.chainId, + tokenInfo.ustPairedAddress + ), + ]); + } else { + [this.tokenIn, this.tokenOut] = await Promise.all([ + makeUniEvmToken( + this.provider, + network.chainId, + tokenInfo.ustPairedAddress + ), + makeUniEvmToken(this.provider, network.chainId, tokenInfo.address), + ]); + } + return; + } public getPoolFee(): string { return ""; @@ -130,4 +199,36 @@ export abstract class UniswapRouterCore { public getTradeDeadline(): ethers.BigNumber { return computeTradeDeadline(this.deadline); } + + /* + public computeUnitAmountIn(amount: string): string { + return this.tokenIn.computeUnitAmount(amount).toString(); + } + */ + + public computeUnitAmountOut(amount: string): string { + return this.tokenOut.computeUnitAmount(amount).toString(); + } + + public formatAmountIn(amount: string): string { + return this.tokenIn.formatAmount(stringToBigNumber(amount)); + } + + public formatAmountOut(amount: string): string { + return this.tokenOut.formatAmount(stringToBigNumber(amount)); + } + + public getTokenInDecimals(): number { + return this.tokenIn.getDecimals(); + } + + public getTokenOutDecimals(): number { + return this.tokenOut.getDecimals(); + } + + public getTokenOutAddress(): string { + return this.tokenOut.getAddress(); + } + + abstract getProtocol(): string; } diff --git a/react/src/route/uniswap-v2.ts b/react/src/route/uniswap-v2.ts index 77d3d90..19bbb8a 100644 --- a/react/src/route/uniswap-v2.ts +++ b/react/src/route/uniswap-v2.ts @@ -3,11 +3,19 @@ import { CurrencyAmount, TradeType } from "@uniswap/sdk-core"; import { abi as IUniswapV2PairABI } from "@uniswap/v2-core/build/UniswapV2Pair.json"; import { computePairAddress, Pair, Route, Trade } from "@uniswap/v2-sdk"; -import { UniEvmToken, UniswapRouterCore } from "./uniswap-core"; +import { UniswapRouterCore } from "./uniswap-core"; export const PROTOCOL = "UniswapV2"; -export class SingleAmmSwapRouter extends UniswapRouterCore { +// uniswap v3 (ethereum) +//export const UNISWAP_V3_FACTORY_ADDRESS = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; +//export const UNISWAP_V3_ROUTER_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564'; + +// quickswap (polygon) +export const QUICKSWAP_V2_ROUTER_ADDRESS = + "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff"; + +export class UniswapV2Router extends UniswapRouterCore { factoryAddress: string; pairContract: ethers.Contract; pair: Pair; @@ -17,23 +25,20 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { return; } - computePoolAddress(tokenIn: UniEvmToken, tokenOut: UniEvmToken): string { + computePoolAddress(): string { if (this.factoryAddress === undefined) { throw Error("factoryAddress is undefined. use setFactoryAddress"); } return computePairAddress({ factoryAddress: this.factoryAddress, - tokenA: tokenIn.getUniToken(), - tokenB: tokenOut.getUniToken(), + tokenA: this.tokenIn.getUniToken(), + tokenB: this.tokenOut.getUniToken(), }); } - async computeAndVerifyPoolAddress( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken - ): Promise { - const pairAddress = this.computePoolAddress(tokenIn, tokenOut); + async computeAndVerifyPoolAddress(): Promise { + const pairAddress = this.computePoolAddress(); // verify by attempting to call factory() const poolContract = new ethers.Contract( @@ -46,8 +51,8 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { return pairAddress; } - async createPool(tokenIn: UniEvmToken, tokenOut: UniEvmToken): Promise { - const pairAddress = this.computePoolAddress(tokenIn, tokenOut); + async createPool(): Promise { + const pairAddress = this.computePoolAddress(); const pairContract = new ethers.Contract( pairAddress, @@ -63,6 +68,9 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { const reserve0 = reserves._reserve0.toString(); const reserve1 = reserves._reserve1.toString(); + const tokenIn = this.tokenIn; + const tokenOut = this.tokenOut; + if (token0.toLowerCase() === tokenIn.getAddress().toLowerCase()) { return new Pair( CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), reserve0), @@ -76,15 +84,13 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { ); } - async fetchQuoteAmountOut( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, - amountIn: string, - slippage: string - ): Promise { + async fetchExactInQuote(amountIn: string, slippage: string): Promise { // create pool - const pair = await this.createPool(tokenIn, tokenOut); + const pair = await this.createPool(); + // let's get that quote + const tokenIn = this.tokenIn; + const tokenOut = this.tokenOut; const route = new Route( [pair], @@ -108,18 +114,24 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { .mulUnsafe(slippageMultiplier) .round(decimals); - return tokenOut.computeUnitAmount(minAmountOutWithSlippage.toString()); + /* + return tokenOut + .computeUnitAmount(minAmountOutWithSlippage.toString()) + .toString(); + */ + return minAmountOutWithSlippage.toString(); } - async fetchQuoteAmountIn( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, + async fetchExactOutQuote( amountOut: string, slippage: string - ): Promise { + ): Promise { // create pool - const pair = await this.createPool(tokenIn, tokenOut); + const pair = await this.createPool(); + // let's get that quote + const tokenIn = this.tokenIn; + const tokenOut = this.tokenOut; const route = new Route( [pair], @@ -142,7 +154,12 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { .divUnsafe(slippageDivisor) .round(decimals); - return tokenIn.computeUnitAmount(maxAmountInWithSlippage.toString()); + /* + return tokenIn + .computeUnitAmount(maxAmountInWithSlippage.toString()) + .toString(); + */ + return maxAmountInWithSlippage.toString(); } getProtocol(): string { diff --git a/react/src/route/uniswap-v3.ts b/react/src/route/uniswap-v3.ts index 8855300..46f5f44 100644 --- a/react/src/route/uniswap-v3.ts +++ b/react/src/route/uniswap-v3.ts @@ -13,12 +13,15 @@ import { Trade, } from "@uniswap/v3-sdk"; -import { UniEvmToken, UniswapRouterCore } from "./uniswap-core"; -import { UNISWAP_V3_FACTORY_ADDRESS } from "../utils/consts"; +import { UniswapRouterCore } from "./uniswap-core"; +import { ETH_TOKEN_INFO } from "../utils/consts"; +import { UstLocation } from "./generic"; export const PROTOCOL = "UniswapV3"; -export class SingleAmmSwapRouter extends UniswapRouterCore { +const UNISWAP_V3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; + +export class UniswapV3Router extends UniswapRouterCore { poolContract: ethers.Contract; pool: Pool; poolFee: FeeAmount; @@ -30,24 +33,26 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { this.poolFee = FeeAmount.MEDIUM; } + async initialize(ustLocation: UstLocation): Promise { + await this.initializeTokens(ETH_TOKEN_INFO, ustLocation); + return; + } + getPoolFee(): string { return this.poolFee.toString(); } - computePoolAddress(tokenIn: UniEvmToken, tokenOut: UniEvmToken): string { + computePoolAddress(): string { return computePoolAddress({ factoryAddress: UNISWAP_V3_FACTORY_ADDRESS, fee: this.poolFee, - tokenA: tokenIn.getUniToken(), - tokenB: tokenOut.getUniToken(), + tokenA: this.tokenIn.getUniToken(), + tokenB: this.tokenOut.getUniToken(), }); } - async computeAndVerifyPoolAddress( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken - ): Promise { - const pairAddress = this.computePoolAddress(tokenIn, tokenOut); + async computeAndVerifyPoolAddress(): Promise { + const pairAddress = this.computePoolAddress(); // verify by attempting to call factory() const poolContract = new ethers.Contract( @@ -60,8 +65,8 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { return pairAddress; } - async createPool(tokenIn: UniEvmToken, tokenOut: UniEvmToken): Promise { - const poolAddress = this.computePoolAddress(tokenIn, tokenOut); + async createPool(): Promise { + const poolAddress = this.computePoolAddress(); const poolContract = new ethers.Contract( poolAddress, @@ -103,8 +108,8 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { ]; return new Pool( - tokenIn.getUniToken(), - tokenOut.getUniToken(), + this.tokenIn.getUniToken(), + this.tokenOut.getUniToken(), this.poolFee, sqrtPriceX96.toString(), //note the description discrepancy - sqrtPriceX96 and sqrtRatioX96 are interchangable values liquidity, @@ -114,13 +119,15 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { } async computeTradeExactIn( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, amount: string ): Promise> { // create pool - const pool = await this.createPool(tokenIn, tokenOut); + const pool = await this.createPool(); + // let's get that quote + const tokenIn = this.tokenIn; + const tokenOut = this.tokenOut; + const amountIn = tokenIn.computeUnitAmount(amount); const route = new Route( @@ -136,13 +143,15 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { } async computeTradeExactOut( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, amount: string ): Promise> { // create pool - const pool = await this.createPool(tokenIn, tokenOut); + const pool = await this.createPool(); + // let's get that quote + const tokenIn = this.tokenIn; + const tokenOut = this.tokenOut; + const amountOut = tokenOut.computeUnitAmount(amount); const route = new Route( @@ -160,15 +169,11 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { ); } - async fetchQuoteAmountOut( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, - amountIn: string, - slippage: string - ): Promise { + async fetchExactInQuote(amountIn: string, slippage: string): Promise { // get the quote - const trade = await this.computeTradeExactIn(tokenIn, tokenOut, amountIn); + const trade = await this.computeTradeExactIn(amountIn); + const tokenOut = this.tokenOut; const decimals = tokenOut.getDecimals(); // calculate output amount with slippage @@ -183,18 +188,22 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { .mulUnsafe(slippageMultiplier) .round(decimals); - return tokenOut.computeUnitAmount(minAmountOutWithSlippage.toString()); + /* + return tokenOut + .computeUnitAmount(minAmountOutWithSlippage.toString()) + .toString(); + */ + return minAmountOutWithSlippage.toString(); } - async fetchQuoteAmountIn( - tokenIn: UniEvmToken, - tokenOut: UniEvmToken, + async fetchExactOutQuote( amountOut: string, slippage: string - ): Promise { + ): Promise { // get the quote - const trade = await this.computeTradeExactOut(tokenIn, tokenOut, amountOut); + const trade = await this.computeTradeExactOut(amountOut); + const tokenIn = this.tokenIn; const decimals = tokenIn.getDecimals(); // calculate output amount with slippage @@ -209,7 +218,12 @@ export class SingleAmmSwapRouter extends UniswapRouterCore { .divUnsafe(slippageDivisor) .round(decimals); - return tokenIn.computeUnitAmount(maxAmountInWithSlippage.toString()); + /* + return tokenIn + .computeUnitAmount(maxAmountInWithSlippage.toString()) + .toString(); + */ + return maxAmountInWithSlippage.toString(); } getProtocol(): string { diff --git a/react/src/swapper/.gitignore b/react/src/swapper/.gitignore new file mode 100644 index 0000000..a6c7c28 --- /dev/null +++ b/react/src/swapper/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/react/src/swapper/helpers.ts b/react/src/swapper/helpers.ts new file mode 100644 index 0000000..65cf840 --- /dev/null +++ b/react/src/swapper/helpers.ts @@ -0,0 +1,111 @@ +import { ethers } from "ethers"; +import { TransactionReceipt } from "@ethersproject/abstract-provider"; + +import { + EVM_ETH_NETWORK_CHAIN_ID, + EVM_POLYGON_NETWORK_CHAIN_ID, + EVM_AVAX_NETWORK_CHAIN_ID, + //EVM_BSC_NETWORK_CHAIN_ID, +} from "../utils/consts"; + +export const CROSSCHAINSWAP_GAS_PARAMETERS_EIP1559 = { + gasLimit: "694200", + //maxFeePerGas: "250000000000", + maxFeePerGas: "100420690000", + maxPriorityFeePerGas: "1690000000", +}; + +export const CROSSCHAINSWAP_GAS_PARAMETERS_EVM = { + gasLimit: "694200", + //gasPrice: "250000000000", + gasPrice: "20420690000", +}; + +export const EVM_EIP1559_CHAIN_IDS = [ + EVM_ETH_NETWORK_CHAIN_ID, + EVM_POLYGON_NETWORK_CHAIN_ID, + EVM_AVAX_NETWORK_CHAIN_ID, +]; + +export async function getEvmGasParametersForContract( + contract: ethers.Contract +): Promise { + const chainId = await getChainIdFromContract(contract); + + if (EVM_EIP1559_CHAIN_IDS.indexOf(chainId) >= 0) { + return CROSSCHAINSWAP_GAS_PARAMETERS_EIP1559; + } + + return CROSSCHAINSWAP_GAS_PARAMETERS_EVM; +} + +async function getChainIdFromContract( + contract: ethers.Contract +): Promise { + const network = await contract.provider.getNetwork(); + return network.chainId; +} + +// exact in +// +export async function evmSwapExactInFromVaaNative( + swapContractWithSigner: ethers.Contract, + signedVaa: Uint8Array +): Promise { + const gasParams = await getEvmGasParametersForContract( + swapContractWithSigner + ); + + const tx = await swapContractWithSigner.recvAndSwapExactNativeIn( + signedVaa, + gasParams + ); + return tx.wait(); +} + +export async function evmSwapExactInFromVaaToken( + swapContractWithSigner: ethers.Contract, + signedVaa: Uint8Array +): Promise { + const gasParams = await getEvmGasParametersForContract( + swapContractWithSigner + ); + + const tx = await swapContractWithSigner.recvAndSwapExactIn( + signedVaa, + gasParams + ); + return tx.wait(); +} + +// exact out +// +export async function evmSwapExactOutFromVaaNative( + swapContractWithSigner: ethers.Contract, + signedVaa: Uint8Array +): Promise { + const gasParams = await getEvmGasParametersForContract( + swapContractWithSigner + ); + + const tx = await swapContractWithSigner.recvAndSwapExactNativeOut( + signedVaa, + gasParams + ); + return tx.wait(); +} + +export async function evmSwapExactOutFromVaaToken( + swapContractWithSigner: ethers.Contract, + signedVaa: Uint8Array +): Promise { + const gasParams = await getEvmGasParametersForContract( + swapContractWithSigner + ); + + const tx = await swapContractWithSigner.recvAndSwapExactOut( + signedVaa, + gasParams + ); + return tx.wait(); +} diff --git a/react/src/swapper/swapper.ts b/react/src/swapper/swapper.ts index 7bc639d..d4102ca 100644 --- a/react/src/swapper/swapper.ts +++ b/react/src/swapper/swapper.ts @@ -1,9 +1,13 @@ +//@ts-nocheck import { ethers } from "ethers"; import { TransactionReceipt } from "@ethersproject/abstract-provider"; import { - CHAIN_ID_POLYGON as WORMHOLE_CHAIN_ID_POLYGON, - CHAIN_ID_ETH as WORMHOLE_CHAIN_ID_ETHEREUM, ChainId, + CHAIN_ID_ETH, + CHAIN_ID_POLYGON, + CHAIN_ID_AVAX, + CHAIN_ID_BSC, + CHAIN_ID_TERRA, getEmitterAddressEth, hexToUint8Array, nativeToHexString, @@ -11,7 +15,6 @@ import { getSignedVAAWithRetry, } from "@certusone/wormhole-sdk"; import { grpc } from "@improbable-eng/grpc-web"; -import { UniEvmToken } from "../route/uniswap-core"; import { PROTOCOL_UNISWAP_V2, // PROTOCOL_UNISWAP_V3, @@ -21,27 +24,48 @@ import { UniswapToUniswapQuoter, } from "../route/cross-quote"; import { + TOKEN_BRIDGE_ADDRESS_ETHEREUM, TOKEN_BRIDGE_ADDRESS_POLYGON, + TOKEN_BRIDGE_ADDRESS_TERRA, + TOKEN_BRIDGE_ADDRESS_AVALANCHE, + TOKEN_BRIDGE_ADDRESS_BSC, CORE_BRIDGE_ADDRESS_ETHEREUM, CORE_BRIDGE_ADDRESS_POLYGON, - TOKEN_BRIDGE_ADDRESS_ETHEREUM, + CORE_BRIDGE_ADDRESS_TERRA, + CORE_BRIDGE_ADDRESS_AVALANCHE, + CORE_BRIDGE_ADDRESS_BSC, WORMHOLE_RPC_HOSTS, - POLYGON_NETWORK_CHAIN_ID, - ETH_NETWORK_CHAIN_ID, - WETH_TOKEN_INFO, - WMATIC_TOKEN_INFO, + //ETH_NETWORK_CHAIN_ID, + //POLYGON_NETWORK_CHAIN_ID, + //TERRA_NETWORK_CHAIN_ID, + UST_TOKEN_INFO, } from "../utils/consts"; import { - CROSSCHAINSWAP_GAS_PARAMETERS, - swapExactInFromVaaNative, - swapExactInFromVaaToken, - swapExactOutFromVaaNative, - swapExactOutFromVaaToken, -} from "./util"; + evmSwapExactInFromVaaNative, + evmSwapExactInFromVaaToken, + evmSwapExactOutFromVaaNative, + evmSwapExactOutFromVaaToken, + getEvmGasParametersForContract, +} from "./helpers"; import { abi as SWAP_CONTRACT_V2_ABI } from "../abi/contracts/CrossChainSwapV2.json"; import { abi as SWAP_CONTRACT_V3_ABI } from "../abi/contracts/CrossChainSwapV3.json"; import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM } from "../addresses/goerli"; import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON } from "../addresses/mumbai"; +import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_AVALANCHE } from "../addresses/fuji"; +import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_BSC } from "../addresses/bsc"; +import { makeErc20Contract } from "../route/evm"; + +// placeholders +const CROSSCHAINSWAP_CONTRACT_ADDRESS_TERRA = + "terra163shc8unyqrndgcldaj2q9kgnqs82v0kgkhynf"; + +function makeNullSwapPath(): any[] { + const zeroBuffer = Buffer.alloc(20); + const nullAddress = "0x" + zeroBuffer.toString("hex"); + return [nullAddress, nullAddress]; +} + +const NULL_SWAP_PATH = makeNullSwapPath(); interface SwapContractParameters { address: string; @@ -63,7 +87,7 @@ const EXECUTION_PARAMETERS_ETHEREUM: ExecutionParameters = { address: CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM, }, wormhole: { - chainId: WORMHOLE_CHAIN_ID_ETHEREUM, + chainId: CHAIN_ID_ETH, coreBridgeAddress: CORE_BRIDGE_ADDRESS_ETHEREUM, tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_ETHEREUM, }, @@ -74,34 +98,77 @@ const EXECUTION_PARAMETERS_POLYGON: ExecutionParameters = { address: CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON, }, wormhole: { - chainId: WORMHOLE_CHAIN_ID_POLYGON, + chainId: CHAIN_ID_POLYGON, coreBridgeAddress: CORE_BRIDGE_ADDRESS_POLYGON, tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_POLYGON, }, }; -function makeExecutionParameters(id: number): ExecutionParameters { - switch (id) { - case ETH_NETWORK_CHAIN_ID: { +const EXECUTION_PARAMETERS_AVALANCHE: ExecutionParameters = { + crossChainSwap: { + address: CROSSCHAINSWAP_CONTRACT_ADDRESS_AVALANCHE, + }, + wormhole: { + chainId: CHAIN_ID_AVAX, + coreBridgeAddress: CORE_BRIDGE_ADDRESS_AVALANCHE, + tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_AVALANCHE, + }, +}; + +const EXECUTION_PARAMETERS_BSC: ExecutionParameters = { + crossChainSwap: { + address: CROSSCHAINSWAP_CONTRACT_ADDRESS_BSC, + }, + wormhole: { + chainId: CHAIN_ID_BSC, + coreBridgeAddress: CORE_BRIDGE_ADDRESS_BSC, + tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_BSC, + }, +}; + +const EXECUTION_PARAMETERS_TERRA: ExecutionParameters = { + crossChainSwap: { + address: CROSSCHAINSWAP_CONTRACT_ADDRESS_TERRA, + }, + wormhole: { + chainId: CHAIN_ID_TERRA, + coreBridgeAddress: CORE_BRIDGE_ADDRESS_TERRA, + tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_TERRA, + }, +}; + +function makeExecutionParameters(chainId: ChainId): ExecutionParameters { + switch (chainId) { + case CHAIN_ID_ETH: { return EXECUTION_PARAMETERS_ETHEREUM; } - case POLYGON_NETWORK_CHAIN_ID: { + case CHAIN_ID_POLYGON: { return EXECUTION_PARAMETERS_POLYGON; } + case CHAIN_ID_AVAX: { + return EXECUTION_PARAMETERS_AVALANCHE; + } + case CHAIN_ID_BSC: { + return EXECUTION_PARAMETERS_BSC; + } + case CHAIN_ID_TERRA: { + return EXECUTION_PARAMETERS_TERRA; + } default: { throw Error("unrecognized chain id"); } } } -async function approveContractTokenSpend( +async function evmApproveContractTokenSpend( provider: ethers.providers.Provider, signer: ethers.Signer, - tokenContract: ethers.Contract, + tokenAddress: string, //ethers.Contract, swapContractAddress: string, amount: ethers.BigNumber ): Promise { // build transaction for token spending + const tokenContract = await makeErc20Contract(provider, tokenAddress); const unsignedTx = await tokenContract.populateTransaction.approve( swapContractAddress, amount @@ -140,7 +207,7 @@ function makeCrossChainSwapV2Contract( return new ethers.Contract(contractAddress, SWAP_CONTRACT_V2_ABI, provider); } -function makeCrossChainSwapContract( +function makeCrossChainSwapEvmContract( provider: ethers.providers.Provider, protocol: string, contractAddress: string @@ -163,19 +230,62 @@ function addressToBytes32( return hexToUint8Array(hexString); } -async function approveAndSwapExactIn( +function evmMakeExactInSwapParameters( + amountIn: ethers.BigNumber, + recipientAddress: string, + dstWormholeChainId: ChainId, + quoteParams: ExactInCrossParameters +): any[] { + const src = quoteParams.src; + const dst = quoteParams.dst; + + if (dst === undefined) { + return [ + amountIn, + src.minAmountOut, + 0, + addressToBytes32(recipientAddress, dstWormholeChainId), + src.deadline, + src.poolFee || 0, + ]; + } + + return [ + amountIn, + src.minAmountOut, + dst.minAmountOut, + addressToBytes32(recipientAddress, dstWormholeChainId), + src.deadline, + dst.poolFee || src.poolFee || 0, + ]; +} + +function makePathArray( + quoteParams: ExactInCrossParameters | ExactOutCrossParameters +): any[] { + if (quoteParams.src === undefined) { + return NULL_SWAP_PATH.concat(quoteParams.dst.path); + } else if (quoteParams.dst === undefined) { + return quoteParams.src.path.concat(NULL_SWAP_PATH); + } else { + return quoteParams.src.path.concat(quoteParams.dst.path); + } +} + +async function evmApproveAndSwapExactIn( srcProvider: ethers.providers.Provider, srcWallet: ethers.Signer, - srcTokenIn: UniEvmToken, + tokenInAddress: string, quoteParams: ExactInCrossParameters, srcExecutionParams: ExecutionParameters, dstExecutionParams: ExecutionParameters, - isNative: boolean + isNative: boolean, + recipientAddress: string ): Promise { const swapContractParams = srcExecutionParams.crossChainSwap; const protocol = quoteParams.src.protocol; - const swapContract = makeCrossChainSwapContract( + const swapContract = makeCrossChainSwapEvmContract( srcProvider, protocol, swapContractParams.address @@ -184,35 +294,27 @@ async function approveAndSwapExactIn( // approve and swap this amount const amountIn = quoteParams.src.amountIn; - - const address = await srcWallet.getAddress(); - - const swapParams = [ - amountIn, - quoteParams.src.minAmountOut, - quoteParams.dst.minAmountOut, - address, - quoteParams.src.deadline, - quoteParams.dst.poolFee || quoteParams.src.poolFee, - ]; - - const pathArray = quoteParams.src.path.concat(quoteParams.dst.path); - const dstWormholeChainId = dstExecutionParams.wormhole.chainId; + + const swapParams = evmMakeExactInSwapParameters( + amountIn, + recipientAddress, + dstWormholeChainId, + quoteParams + ); + + const pathArray = makePathArray(quoteParams); + const dstContractAddress = addressToBytes32( dstExecutionParams.crossChainSwap.address, dstWormholeChainId ); const bridgeNonce = 69; + const gasParams = getEvmGasParametersForContract(swapContract); // do the swap if (isNative) { - const gasPlusValue = { - value: amountIn, - gasLimit: CROSSCHAINSWAP_GAS_PARAMETERS.gasLimit, - maxFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxFeePerGas, - maxPriorityFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxPriorityFeePerGas, - }; + const transactionParams = { value: amountIn, ...gasParams }; console.info("swapExactNativeInAndTransfer"); const tx = await contractWithSigner.swapExactNativeInAndTransfer( @@ -222,15 +324,15 @@ async function approveAndSwapExactIn( dstWormholeChainId, dstContractAddress, bridgeNonce, - gasPlusValue + transactionParams ); return tx.wait(); } else { console.info("approving contract to spend token in"); - await approveContractTokenSpend( + await evmApproveContractTokenSpend( srcProvider, srcWallet, - srcTokenIn.getContract(), + tokenInAddress, swapContract.address, amountIn ); @@ -243,25 +345,27 @@ async function approveAndSwapExactIn( dstWormholeChainId, dstContractAddress, bridgeNonce, - CROSSCHAINSWAP_GAS_PARAMETERS + gasParams ); return tx.wait(); } } -async function approveAndSwapExactOut( +// TODO: fix to resemble ExactIn +async function evmApproveAndSwapExactOut( srcProvider: ethers.providers.Provider, srcWallet: ethers.Signer, - srcTokenIn: UniEvmToken, + tokenInAddress: string, quoteParams: ExactOutCrossParameters, srcExecutionParams: ExecutionParameters, dstExecutionParams: ExecutionParameters, - isNative: boolean + isNative: boolean, + recipientAddress: string ): Promise { const swapContractParams = srcExecutionParams.crossChainSwap; - const protocol = quoteParams.src.protocol; - const swapContract = makeCrossChainSwapContract( + const protocol = quoteParams.src?.protocol; + const swapContract = makeCrossChainSwapEvmContract( srcProvider, protocol, swapContractParams.address @@ -269,36 +373,30 @@ async function approveAndSwapExactOut( const contractWithSigner = swapContract.connect(srcWallet); // approve and swap this amount - const amountOut = quoteParams.src.amountOut; - const maxAmountIn = quoteParams.src.maxAmountIn; - - const address = await srcWallet.getAddress(); + const amountOut = quoteParams.src?.amountOut; + const maxAmountIn = quoteParams.src?.maxAmountIn; + const dstWormholeChainId = dstExecutionParams.wormhole.chainId; const swapParams = [ amountOut, maxAmountIn, quoteParams.dst.amountOut, - address, + addressToBytes32(recipientAddress, dstWormholeChainId), quoteParams.src.deadline, - quoteParams.dst.poolFee || quoteParams.src.poolFee, + quoteParams.dst.poolFee || quoteParams.src.poolFee || 0, ]; - const pathArray = quoteParams.src.path.concat(quoteParams.dst.path); + const pathArray = makePathArray(quoteParams); - const dstWormholeChainId = dstExecutionParams.wormhole.chainId; const dstContractAddress = addressToBytes32( dstExecutionParams.crossChainSwap.address, dstWormholeChainId ); const bridgeNonce = 69; + const gasParams = getEvmGasParametersForContract(swapContract); // do the swap if (isNative) { - const gasPlusValue = { - value: maxAmountIn, - gasLimit: CROSSCHAINSWAP_GAS_PARAMETERS.gasLimit, - maxFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxFeePerGas, - maxPriorityFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxPriorityFeePerGas, - }; + const gasPlusValue = { value: maxAmountIn, ...gasParams }; console.info("swapExactNativeOutAndTransfer"); const tx = await contractWithSigner.swapExactNativeOutAndTransfer( @@ -313,10 +411,10 @@ async function approveAndSwapExactOut( return tx.wait(); } else { console.info("approving contract to spend token in"); - await approveContractTokenSpend( + await evmApproveContractTokenSpend( srcProvider, srcWallet, - srcTokenIn.getContract(), + tokenInAddress, swapContract.address, maxAmountIn ); @@ -329,7 +427,7 @@ async function approveAndSwapExactOut( dstWormholeChainId, dstContractAddress, bridgeNonce, - CROSSCHAINSWAP_GAS_PARAMETERS + gasParams ); return tx.wait(); } @@ -345,7 +443,7 @@ async function swapExactInFromVaa( ): Promise { const swapContractParams = dstExecutionParams.crossChainSwap; - const swapContract = makeCrossChainSwapContract( + const swapContract = makeCrossChainSwapEvmContract( dstProvider, dstProtocol, swapContractParams.address @@ -353,11 +451,11 @@ async function swapExactInFromVaa( const contractWithSigner = swapContract.connect(dstWallet); if (isNative) { - console.info("swapExactInFromVaaNative"); - return swapExactInFromVaaNative(contractWithSigner, signedVaa); + console.info("evmSwapExactInFromVaaNative"); + return evmSwapExactInFromVaaNative(contractWithSigner, signedVaa); } else { - console.info("swapExactInFromVaaToken"); - return swapExactInFromVaaToken(contractWithSigner, signedVaa); + console.info("evmSwapExactInFromVaaToken"); + return evmSwapExactInFromVaaToken(contractWithSigner, signedVaa); } } @@ -371,7 +469,7 @@ async function swapExactOutFromVaa( ): Promise { const swapContractParams = dstExecutionParams.crossChainSwap; - const swapContract = makeCrossChainSwapContract( + const swapContract = makeCrossChainSwapEvmContract( dstProvider, dstProtocol, swapContractParams.address @@ -379,46 +477,42 @@ async function swapExactOutFromVaa( const contractWithSigner = swapContract.connect(dstWallet); if (isNative) { - console.info("swapExactOutFromVaaNative"); - return swapExactOutFromVaaNative(contractWithSigner, signedVaa); + console.info("evmSwapExactOutFromVaaNative"); + return evmSwapExactOutFromVaaNative(contractWithSigner, signedVaa); } else { - console.info("swapExactOutFromVaaToken"); - return swapExactOutFromVaaToken(contractWithSigner, signedVaa); + console.info("evmSwapExactOutFromVaaToken"); + return evmSwapExactOutFromVaaToken(contractWithSigner, signedVaa); } } -interface CrossChainSwapTokens { - srcIn: UniEvmToken; - srcOut: UniEvmToken; - dstIn: UniEvmToken; - dstOut: UniEvmToken; -} - interface VaaSearchParams { sequence: string; emitterAddress: string; } -export function makeProvider(tokenAddress: string) { +export function makeEvmProvider(tokenAddress: string) { + let url; switch (tokenAddress) { - case WETH_TOKEN_INFO.address: { - const url = process.env.REACT_APP_GOERLI_PROVIDER; - if (!url) { - throw new Error("Could not find REACT_APP_GOERLI_PROVIDER"); - } - return new ethers.providers.StaticJsonRpcProvider(url); - } - case WMATIC_TOKEN_INFO.address: { - const url = process.env.REACT_APP_MUMBAI_PROVIDER; - if (!url) { - throw new Error("Could not find REACT_APP_MUMBAI_PROVIDER"); - } - return new ethers.providers.StaticJsonRpcProvider(url); - } - default: { + case ETH_TOKEN_INFO.address: + url = process.env.REACT_APP_GOERLI_PROVIDER; + if (!url) throw new Error("REACT_APP_GOERLI_PROVIDER not set"); + break; + case MATIC_TOKEN_INFO.address: + url = process.env.REACT_APP_MUMBAI_PROVIDER; + if (!url) throw new Error("REACT_APP_MUMBAI_PROVIDER not set"); + break; + case AVAX_TOKEN_INFO.address: + url = process.env.REACT_APP_FUJI_PROVIDER; + if (!url) throw new Error("REACT_APP_FUJI_PROVIDER not set"); + break; + case BSC_TOKEN_INFO.address: + url = process.env.REACT_APP_BSC_PROVIDER; + if (!url) throw new Error("REACT_APP_BSC_PROVIDER not set"); + break; + default: throw Error("unrecognized token address"); - } } + return new ethers.providers.StaticJsonRpcProvider(url); } export class UniswapToUniswapExecutor { @@ -427,7 +521,6 @@ export class UniswapToUniswapExecutor { cachedExactInParams: ExactInCrossParameters; cachedExactOutParams: ExactOutCrossParameters; quoteType: QuoteType; - tokens: CrossChainSwapTokens; // swapping isNative: boolean; @@ -440,8 +533,16 @@ export class UniswapToUniswapExecutor { transportFactory: grpc.TransportFactory; vaaSearchParams: VaaSearchParams; vaaBytes: Uint8Array; - srcReceipt: TransactionReceipt; - dstReceipt: TransactionReceipt; + + // receipts + srcEvmReceipt: TransactionReceipt; + dstEvmReceipt: TransactionReceipt; + srcTerraReceipt: any; + dstTerraReceipt: any; + + constructor() { + this.quoter = new UniswapToUniswapQuoter(); + } async initialize( tokenInAddress: string, @@ -450,20 +551,14 @@ export class UniswapToUniswapExecutor { ): Promise { this.isNative = isNative; - const srcProvider = makeProvider(tokenInAddress); - const dstProvider = makeProvider(tokenOutAddress); - - this.quoter = new UniswapToUniswapQuoter(srcProvider, dstProvider); - await this.quoter.initialize(); - - await this.makeTokens(tokenInAddress, tokenOutAddress); + await this.quoter.initialize(tokenInAddress, tokenOutAddress); // now that we have a chain id for each network, get contract info for each chain this.srcExecutionParams = makeExecutionParameters( - this.quoter.srcNetwork.chainId + this.quoter.getSrcChainId() ); this.dstExecutionParams = makeExecutionParameters( - this.quoter.dstNetwork.chainId + this.quoter.getDstChainId() ); } @@ -483,6 +578,7 @@ export class UniswapToUniswapExecutor { this.quoter.setDeadlines(deadline); } + /* async makeTokens( tokenInAddress: string, tokenOutAddress: string @@ -507,7 +603,7 @@ export class UniswapToUniswapExecutor { getTokens(): CrossChainSwapTokens { return this.tokens; } - +*/ async computeAndVerifySrcPoolAddress(): Promise { return this.quoter.computeAndVerifySrcPoolAddress(); } @@ -546,59 +642,97 @@ export class UniswapToUniswapExecutor { return this.cachedExactOutParams; } - getSrcProvider(): ethers.providers.Provider { - return this.quoter.srcProvider; + getSrcEvmProvider(): ethers.providers.Provider { + return this.quoter.getSrcEvmProvider(); } - getDstProvider(): ethers.providers.Provider { - return this.quoter.dstProvider; + getDstEvmProvider(): ethers.providers.Provider { + return this.quoter.getDstEvmProvider(); } - async approveAndSwapExactIn( - wallet: ethers.Signer + getTokenInAddress(): string { + return this.quoter.tokenInAddress; + } + + getTokenOutAddress(): string { + return this.quoter.tokenOutAddress; + } + + async evmApproveAndSwapExactIn( + srcWallet: ethers.Signer, + recipientAddress: string ): Promise { - return approveAndSwapExactIn( - this.getSrcProvider(), - wallet, - this.tokens.srcIn, + return evmApproveAndSwapExactIn( + this.getSrcEvmProvider(), + srcWallet, + this.getTokenInAddress(), this.cachedExactInParams, this.srcExecutionParams, this.dstExecutionParams, - this.isNative + this.isNative, + recipientAddress ); } - async approveAndSwapExactOut( - wallet: ethers.Signer + async evmApproveAndSwapExactOut( + srcWallet: ethers.Signer, + recipientAddress: string ): Promise { - return approveAndSwapExactOut( - this.getSrcProvider(), - wallet, - this.tokens.srcIn, + return evmApproveAndSwapExactOut( + this.getSrcEvmProvider(), + srcWallet, + this.getTokenInAddress(), this.cachedExactOutParams, this.srcExecutionParams, this.dstExecutionParams, - this.isNative + this.isNative, + recipientAddress ); } - async approveAndSwap(wallet: ethers.Signer): Promise { + srcIsUst(): boolean { + return ( + this.quoter.tokenInAddress === UST_TOKEN_INFO.address && + this.cachedExactInParams.src === undefined + ); + } + + async evmApproveAndSwap( + wallet: ethers.Signer, + recipientAddress: string + ): Promise { const quoteType = this.quoteType; if (quoteType === QuoteType.ExactIn) { - this.srcReceipt = await this.approveAndSwapExactIn(wallet); + this.srcEvmReceipt = await this.evmApproveAndSwapExactIn( + wallet, + recipientAddress + ); } else if (quoteType === QuoteType.ExactOut) { - this.srcReceipt = await this.approveAndSwapExactOut(wallet); + this.srcEvmReceipt = await this.evmApproveAndSwapExactOut( + wallet, + recipientAddress + ); } else { throw Error("no quote found"); } - this.fetchAndSetEmitterAndSequence(); - return this.srcReceipt; + this.fetchAndSetEvmEmitterAndSequence(); + return this.srcEvmReceipt; } fetchAndSetEmitterAndSequence(): void { - const receipt = this.srcReceipt; + // TODO + return; + } + + fetchAndSetTerraEmitterAndSequence(): void { + // TODO + return; + } + + fetchAndSetEvmEmitterAndSequence(): void { + const receipt = this.srcEvmReceipt; if (receipt === undefined) { throw Error("no swap receipt found"); } @@ -623,11 +757,15 @@ export class UniswapToUniswapExecutor { const emitterAddress = vaaSearchParams.emitterAddress; console.info(`sequence: ${sequence}, emitterAddress: ${emitterAddress}`); // wait for VAA to be signed + const vaaResponse = await getSignedVAAWithRetry( WORMHOLE_RPC_HOSTS, this.srcExecutionParams.wormhole.chainId, vaaSearchParams.emitterAddress, - vaaSearchParams.sequence + vaaSearchParams.sequence, + { + transport: this.transportFactory, + } ); // grab vaaBytes this.vaaBytes = vaaResponse.vaaBytes; @@ -636,22 +774,27 @@ export class UniswapToUniswapExecutor { async fetchVaaAndSwap(wallet: ethers.Signer): Promise { await this.fetchSignedVaaFromSwap(); + // check if Terra transaction + // TODO: change return as something else (not evm TransactionReceipt) + const quoteType = this.quoteType; if (quoteType === QuoteType.ExactIn) { - this.dstReceipt = await this.swapExactInFromVaa(wallet); + this.dstEvmReceipt = await this.evmSwapExactInFromVaa(wallet); } else if (quoteType === QuoteType.ExactOut) { - this.dstReceipt = await this.swapExactOutFromVaa(wallet); + this.dstEvmReceipt = await this.evmSwapExactOutFromVaa(wallet); } else { throw Error("no quote found"); } - return this.dstReceipt; + return this.dstEvmReceipt; } - async swapExactInFromVaa(wallet: ethers.Signer): Promise { + async evmSwapExactInFromVaa( + wallet: ethers.Signer + ): Promise { return swapExactInFromVaa( - this.getDstProvider(), + this.getDstEvmProvider(), wallet, this.dstExecutionParams, this.cachedExactInParams.dst.protocol, @@ -660,11 +803,11 @@ export class UniswapToUniswapExecutor { ); } - async swapExactOutFromVaa( + async evmSwapExactOutFromVaa( wallet: ethers.Signer ): Promise { return swapExactOutFromVaa( - this.getDstProvider(), + this.getDstEvmProvider(), wallet, this.dstExecutionParams, this.cachedExactOutParams.dst.protocol, @@ -673,6 +816,10 @@ export class UniswapToUniswapExecutor { ); } + setTransport(transportFactory: grpc.TransportFactory) { + this.transportFactory = transportFactory; + } + //getSwapResult( // walletAddress: string, // onSwapResult: (result: boolean) => void @@ -680,7 +827,7 @@ export class UniswapToUniswapExecutor { // console.log(this.cachedExactInParams.dst.protocol); // console.log(this.dstExecutionParams.crossChainSwap.address); // const contract = makeCrossChainSwapContract( - // this.getDstProvider(), + // this.getDstEvmProvider(), // this.quoteType === QuoteType.ExactIn // ? this.cachedExactInParams.dst.protocol // : this.cachedExactOutParams.dst.protocol, diff --git a/react/src/swapper/util.ts b/react/src/swapper/util.ts deleted file mode 100644 index 09b9edd..0000000 --- a/react/src/swapper/util.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ethers } from "ethers"; -import { TransactionReceipt } from "@ethersproject/abstract-provider"; - -export const CROSSCHAINSWAP_GAS_PARAMETERS = { - gasLimit: "550000", - maxFeePerGas: "250000000000", - maxPriorityFeePerGas: "1690000000", -}; - -// exact in -// -export async function swapExactInFromVaaNative( - swapContractWithSigner: ethers.Contract, - signedVaa: Uint8Array -): Promise { - const tx = await swapContractWithSigner.recvAndSwapExactNativeIn( - signedVaa, - CROSSCHAINSWAP_GAS_PARAMETERS - ); - return tx.wait(); -} - -export async function swapExactInFromVaaToken( - swapContractWithSigner: ethers.Contract, - signedVaa: Uint8Array -): Promise { - const tx = await swapContractWithSigner.recvAndSwapExactIn( - signedVaa, - CROSSCHAINSWAP_GAS_PARAMETERS - ); - return tx.wait(); -} - -// exact out (TODO: add to util) -// -export async function swapExactOutFromVaaNative( - swapContractWithSigner: ethers.Contract, - signedVaa: Uint8Array -): Promise { - const tx = await swapContractWithSigner.recvAndSwapExactNativeOut( - signedVaa, - CROSSCHAINSWAP_GAS_PARAMETERS - ); - return tx.wait(); -} - -export async function swapExactOutFromVaaToken( - swapContractWithSigner: ethers.Contract, - signedVaa: Uint8Array -): Promise { - const tx = await swapContractWithSigner.recvAndSwapExactOut( - signedVaa, - CROSSCHAINSWAP_GAS_PARAMETERS - ); - return tx.wait(); -} diff --git a/react/src/utils/.gitignore b/react/src/utils/.gitignore new file mode 100644 index 0000000..a6c7c28 --- /dev/null +++ b/react/src/utils/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/react/src/utils/consts.ts b/react/src/utils/consts.ts index e9b818f..c0ced83 100644 --- a/react/src/utils/consts.ts +++ b/react/src/utils/consts.ts @@ -2,72 +2,117 @@ import { ChainId, CHAIN_ID_ETH, CHAIN_ID_POLYGON, + CHAIN_ID_TERRA, + CHAIN_ID_AVAX, + CHAIN_ID_BSC, } from "@certusone/wormhole-sdk"; -import ethIcon from "../icons/eth.svg"; -import polygonIcon from "../icons/polygon.svg"; + +export const EVM_POLYGON_NETWORK_CHAIN_ID = 80001; +export const EVM_ETH_NETWORK_CHAIN_ID = 5; +export const EVM_AVAX_NETWORK_CHAIN_ID = 43113; +export const EVM_BSC_NETWORK_CHAIN_ID = 97; export interface TokenInfo { name: string; address: string; chainId: ChainId; - logo: string; - isNative: boolean; + evmChainId: number | undefined; maxAmount: number; + ustPairedAddress: string | undefined; } export const MATIC_TOKEN_INFO: TokenInfo = { name: "MATIC", - address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889", // used to compute quote - chainId: CHAIN_ID_POLYGON, - logo: polygonIcon, - isNative: true, - maxAmount: 0.1, -}; - -export const WMATIC_TOKEN_INFO: TokenInfo = { - name: "WMATIC", address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889", chainId: CHAIN_ID_POLYGON, - logo: polygonIcon, - isNative: false, + evmChainId: EVM_POLYGON_NETWORK_CHAIN_ID, + //logo: polygonIcon, maxAmount: 0.1, + ustPairedAddress: "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c", }; export const ETH_TOKEN_INFO: TokenInfo = { name: "ETH", - address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", // used to compute quote - chainId: CHAIN_ID_ETH, - logo: ethIcon, - isNative: true, - maxAmount: 0.01, -}; - -export const WETH_TOKEN_INFO: TokenInfo = { - name: "WETH", address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", chainId: CHAIN_ID_ETH, - logo: ethIcon, - isNative: false, + evmChainId: EVM_ETH_NETWORK_CHAIN_ID, + //logo: ethIcon, maxAmount: 0.01, + ustPairedAddress: "0x36Ed51Afc79619b299b238898E72ce482600568a", +}; + +export const AVAX_TOKEN_INFO: TokenInfo = { + name: "AVAX", + address: "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3", + chainId: CHAIN_ID_AVAX, + evmChainId: EVM_AVAX_NETWORK_CHAIN_ID, + //logo: avaxIcon, + maxAmount: 0.01, + ustPairedAddress: "0xe09ed38e5cd1014444846f62376ac88c5232cde9", +}; + +export const BNB_TOKEN_INFO: TokenInfo = { + name: "BNB", + address: "0xae13d989dac2f0debff460ac112a837c89baa7cd", + chainId: CHAIN_ID_BSC, + evmChainId: EVM_BSC_NETWORK_CHAIN_ID, + //logo: bscIcon, + maxAmount: 0.01, + ustPairedAddress: "0x7b8eae1e85c8b189ee653d3f78733f4f788bb2c1", +}; + +export const UST_TOKEN_INFO: TokenInfo = { + name: "UST", + address: "uusd", + chainId: CHAIN_ID_TERRA, + evmChainId: undefined, + //logo: terraIcon, + maxAmount: 10.0, + ustPairedAddress: undefined, }; export const TOKEN_INFOS = [ MATIC_TOKEN_INFO, - WMATIC_TOKEN_INFO, ETH_TOKEN_INFO, - WETH_TOKEN_INFO, + AVAX_TOKEN_INFO, + BNB_TOKEN_INFO, + // TODO: support swaps from/to terra + // UST_TOKEN_INFO, ]; -export const ETH_NETWORK_CHAIN_ID = 5; +export const getSupportedSwaps = (tokenInfo: TokenInfo) => { + return TOKEN_INFOS.filter((x) => x !== tokenInfo); +}; -export const POLYGON_NETWORK_CHAIN_ID = 80001; +export const getEvmChainId = (chainId: ChainId): number | undefined => { + switch (chainId) { + case CHAIN_ID_ETH: + return EVM_ETH_NETWORK_CHAIN_ID; + case CHAIN_ID_POLYGON: + return EVM_POLYGON_NETWORK_CHAIN_ID; + case CHAIN_ID_AVAX: + return EVM_AVAX_NETWORK_CHAIN_ID; + case CHAIN_ID_BSC: + return EVM_BSC_NETWORK_CHAIN_ID; + default: + return undefined; + } +}; -export const getEvmChainId = (chainId: ChainId) => - chainId === CHAIN_ID_ETH - ? ETH_NETWORK_CHAIN_ID - : chainId === CHAIN_ID_POLYGON - ? POLYGON_NETWORK_CHAIN_ID - : undefined; +export const getChainName = (chainId: ChainId) => { + switch (chainId) { + case CHAIN_ID_ETH: + return "Ethereum"; + case CHAIN_ID_POLYGON: + return "Polygon"; + case CHAIN_ID_AVAX: + return "Avalanche"; + case CHAIN_ID_BSC: + return "BSC"; + default: + return ""; + } +}; export const RELAYER_FEE_UST = "0.25"; @@ -75,22 +120,37 @@ export const WORMHOLE_RPC_HOSTS = [ "https://wormhole-v2-testnet-api.certus.one", ]; +// core bridge export const CORE_BRIDGE_ADDRESS_ETHEREUM = "0x706abc4E45D419950511e474C7B9Ed348A4a716c"; export const CORE_BRIDGE_ADDRESS_POLYGON = "0x0CBE91CF822c73C2315FB05100C2F714765d5c20"; +export const CORE_BRIDGE_ADDRESS_AVALANCHE = + "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"; + +export const CORE_BRIDGE_ADDRESS_BSC = + "0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D"; + +export const CORE_BRIDGE_ADDRESS_TERRA = + "terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v"; + +// token bridge export const TOKEN_BRIDGE_ADDRESS_ETHEREUM = "0xF890982f9310df57d00f659cf4fd87e65adEd8d7"; export const TOKEN_BRIDGE_ADDRESS_POLYGON = "0x377D55a7928c046E18eEbb61977e714d2a76472a"; -export const QUICKSWAP_FACTORY_ADDRESS = - "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32"; +export const TOKEN_BRIDGE_ADDRESS_BSC = + "0x9dcF9D205C9De35334D646BeE44b2D2859712A09"; -export const UNISWAP_V3_FACTORY_ADDRESS = - "0x1F98431c8aD98523631AE4a59f267346ea31F984"; +export const TOKEN_BRIDGE_ADDRESS_AVALANCHE = + "0x61E44E506Ca5659E6c0bba9b678586fA2d729756"; +export const TOKEN_BRIDGE_ADDRESS_TERRA = + "terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a"; + +// gas export const APPROVAL_GAS_LIMIT = "100000"; diff --git a/react/src/utils/math.ts b/react/src/utils/math.ts new file mode 100644 index 0000000..7cadd4f --- /dev/null +++ b/react/src/utils/math.ts @@ -0,0 +1,19 @@ +import { FixedNumber } from "ethers"; + +export function addFixedAmounts( + left: string, + right: string, + decimals: number +): string { + const sum = FixedNumber.from(left).addUnsafe(FixedNumber.from(right)); + return sum.round(decimals).toString(); +} + +export function subtractFixedAmounts( + left: string, + right: string, + decimals: number +): string { + const diff = FixedNumber.from(left).subUnsafe(FixedNumber.from(right)); + return diff.round(decimals).toString(); +} diff --git a/react/src/views/Home.tsx b/react/src/views/Home.tsx index 63db034..7d1a188 100644 --- a/react/src/views/Home.tsx +++ b/react/src/views/Home.tsx @@ -7,7 +7,12 @@ import { TextField, Typography, } from "@material-ui/core"; -import { ChainId, getSignedVAAWithRetry } from "@certusone/wormhole-sdk"; +import { + ChainId, + CHAIN_ID_TERRA, + getSignedVAAWithRetry, + isEVMChain, +} from "@certusone/wormhole-sdk"; import { useCallback, useEffect, useState } from "react"; import ButtonWithLoader from "../components/ButtonWithLoader"; import EthereumSignerKey from "../components/EthereumSignerKey"; @@ -16,11 +21,11 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { ETH_TOKEN_INFO, getEvmChainId, + getSupportedSwaps, MATIC_TOKEN_INFO, RELAYER_FEE_UST, + TokenInfo, TOKEN_INFOS, - WETH_TOKEN_INFO, - WMATIC_TOKEN_INFO, WORMHOLE_RPC_HOSTS, } from "../utils/consts"; import { COLORS } from "../muiTheme"; @@ -37,6 +42,8 @@ import CircleLoader from "../components/CircleLoader"; import { ArrowForward, CheckCircleOutlineRounded } from "@material-ui/icons"; import SwapProgress from "../components/SwapProgress"; import Footer from "../components/Footer"; +import TerraWalletKey from "../components/TerraWalletKey"; +import useIsWalletReady from "../hooks/useIsWalletReady"; const useStyles = makeStyles((theme) => ({ bg: { @@ -135,7 +142,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const switchProviderNetwork = async ( +const switchEvmProviderNetwork = async ( provider: Web3Provider, chainId: ChainId ) => { @@ -152,6 +159,78 @@ const switchProviderNetwork = async ( } }; +const ConnectedWalletAddress = ({ + chainId, + prefix, +}: { + chainId: ChainId; + prefix: string; +}) => { + const { walletAddress } = useIsWalletReady(chainId, false); + if (walletAddress) { + const is0x = walletAddress.startsWith("0x"); + return ( + + {prefix} {walletAddress?.substring(0, is0x ? 6 : 3)}... + {walletAddress?.substring(walletAddress.length - (is0x ? 4 : 3))} + + ); + } + return null; +}; + +const SwapButton = ({ + source, + target, + disabled, + showLoader, + onClick, +}: { + source: TokenInfo; + target: TokenInfo; + disabled: boolean; + showLoader: boolean; + onClick: () => void; +}) => { + const { isReady: isSourceWalletReady } = useIsWalletReady( + source.chainId, + !disabled + ); + const { isReady: isTargetWalletReady } = useIsWalletReady( + target.chainId, + !isEVMChain(source.chainId) + ); + + if (!isSourceWalletReady) { + return isEVMChain(source.chainId) ? ( + + ) : source.chainId === CHAIN_ID_TERRA ? ( + + ) : null; + } + + if ( + !isTargetWalletReady && + (!isEVMChain(source.chainId) || !isEVMChain(target.chainId)) + ) { + return isEVMChain(target.chainId) ? ( + + ) : source.chainId === CHAIN_ID_TERRA ? ( + + ) : null; + } + + return ( + + Swap + + ); +}; + export default function Home() { const classes = useStyles(); const [sourceTokenInfo, setSourceTokenInfo] = useState(MATIC_TOKEN_INFO); @@ -167,10 +246,10 @@ export default function Home() { const [isSwapping, setIsSwapping] = useState(false); const [isComputingQuote, setIsComputingQuote] = useState(false); const [hasQuote, setHasQuote] = useState(false); - const { provider, signer } = useEthereumProvider(); + const { provider, signer, signerAddress, disconnect } = useEthereumProvider(); const { enqueueSnackbar } = useSnackbar(); - const [isFirstSwapComplete, setIsFirstSwapComplete] = useState(false); - const [isSecondSwapComplete, setIsSecondSwapComplete] = useState(false); + const [isSourceSwapComplete, setIsSourceSwapComplete] = useState(false); + const [isTargetSwapComplete, setIsTargetSwapComplete] = useState(false); const [sourceTxBlockNumber, setSourceTxBlockNumber] = useState< number | undefined >(undefined); @@ -193,7 +272,7 @@ export default function Home() { await executor.initialize( sourceTokenInfo.address, targetTokenInfo.address, - sourceTokenInfo.isNative + true ); await executor.computeAndVerifySrcPoolAddress().catch((e) => { throw new Error("failed to verify source pool address"); @@ -206,16 +285,8 @@ export default function Home() { executor.setRelayerFee(RELAYER_FEE_UST); const quote = await executor.computeQuoteExactIn(amountIn); setExecutor(executor); - setAmountOut( - parseFloat( - executor.tokens.dstOut.formatAmount(quote.dst.minAmountOut) - ).toFixed(8) - ); - setAmountInUST( - parseFloat( - executor.tokens.dstIn.formatAmount(quote.dst.amountIn) - ).toFixed(2) - ); + setAmountOut(parseFloat(quote.minAmountOut).toFixed(8)); + setAmountInUST(parseFloat(quote.ustAmountIn).toFixed(2)); setHasQuote(true); } } catch (e) { @@ -260,52 +331,65 @@ export default function Home() { setDeadline(deadline); }, []); - const handleSourceChange = useCallback((event) => { - // NOTE: only native-to-native or wrapped-to-wrapped swaps are currently supported - if (event.target.value === WMATIC_TOKEN_INFO.name) { - setSourceTokenInfo(WMATIC_TOKEN_INFO); - setTargetTokenInfo(WETH_TOKEN_INFO); - } else if (event.target.value === WETH_TOKEN_INFO.name) { - setSourceTokenInfo(WETH_TOKEN_INFO); - setTargetTokenInfo(WMATIC_TOKEN_INFO); - } else if (event.target.value === ETH_TOKEN_INFO.name) { - setSourceTokenInfo(ETH_TOKEN_INFO); - setTargetTokenInfo(MATIC_TOKEN_INFO); - } else { - setSourceTokenInfo(MATIC_TOKEN_INFO); - setTargetTokenInfo(ETH_TOKEN_INFO); + const handleSourceChange = useCallback( + (event) => { + const tokenInfo = TOKEN_INFOS.find((x) => x.name === event.target.value); + if (tokenInfo) { + const supportedSwaps = getSupportedSwaps(tokenInfo); + if (supportedSwaps) { + setSourceTokenInfo(tokenInfo); + if (!supportedSwaps.find((x) => x.name === targetTokenInfo.name)) { + setTargetTokenInfo(supportedSwaps[0]); + } + setAmountIn(""); + setAmountOut(""); + } + } + }, + [targetTokenInfo] + ); + + const handleTargetChange = useCallback((event) => { + const tokenInfo = TOKEN_INFOS.find((x) => x.name === event.target.value); + if (tokenInfo) { + setTargetTokenInfo(tokenInfo); + setAmountOut(""); } - setAmountIn(""); - setAmountOut(""); }, []); const reset = useCallback(() => { setIsSwapping(false); setHasQuote(false); - setIsFirstSwapComplete(false); - setIsSecondSwapComplete(false); + setIsSourceSwapComplete(false); + setHasSignedVAA(false); + setIsTargetSwapComplete(false); setAmountIn(""); setAmountOut(""); setSourceTxBlockNumber(undefined); setRelayerTimeoutString(""); - }, []); + disconnect(); + }, [disconnect]); const handleSwapClick = useCallback(async () => { - if (provider && signer && executor) { + if (provider && signer && signerAddress && executor) { try { setIsSwapping(true); - setIsFirstSwapComplete(false); + setIsSourceSwapComplete(false); setHasSignedVAA(false); - setIsSecondSwapComplete(false); + setIsTargetSwapComplete(false); setRelayerTimeoutString(""); - await switchProviderNetwork(provider, sourceTokenInfo.chainId); + await switchEvmProviderNetwork(provider, sourceTokenInfo.chainId); + console.log(signerAddress); - const sourceReceipt = await executor.approveAndSwap(signer); + const sourceReceipt = await executor.evmApproveAndSwap( + signer, + signerAddress + ); console.info( "firstSwapTransactionHash:", sourceReceipt.transactionHash ); - setIsFirstSwapComplete(true); + setIsSourceSwapComplete(true); setSourceTxBlockNumber(sourceReceipt.blockNumber); // Wait for the guardian network to reach consensus and emit the signedVAA @@ -319,7 +403,9 @@ export default function Home() { // Check if the signedVAA has redeemed by the relayer const isCompleted = await getIsTransferCompletedEvmWithRetry( executor.dstExecutionParams.wormhole.tokenBridgeAddress, - executor.quoter.dstProvider, + // TODO: fix typescript error + // @ts-ignore + executor.quoter.getDstEvmProvider(), vaaBytes, // retry for two minutes 3000, @@ -330,14 +416,14 @@ export default function Home() { setRelayerTimeoutString( "Timed out waiting for relayer to complete swap. You'll need to complete it yourself." ); - await switchProviderNetwork(provider, targetTokenInfo.chainId); + await switchEvmProviderNetwork(provider, targetTokenInfo.chainId); const targetReceipt = await executor.fetchVaaAndSwap(signer); console.info( "secondSwapTransactionHash:", targetReceipt.transactionHash ); } - setIsSecondSwapComplete(true); + setIsTargetSwapComplete(true); } catch (e: any) { reset(); console.error(e); @@ -349,6 +435,7 @@ export default function Home() { }, [ provider, signer, + signerAddress, executor, enqueueSnackbar, sourceTokenInfo, @@ -357,19 +444,18 @@ export default function Home() { ]); const readyToSwap = provider && signer && hasQuote; + const disableSelect = isSwapping || isComputingQuote; return (
- - Wormhole NativeSwap Demo - + Wormhole NativeSwap Demo
- + Send {`The max input amount is ${sourceTokenInfo.maxAmount} ${sourceTokenInfo.name}`} ) : null} +
{}} - disabled={true} + onChange={handleTargetChange} + disabled={disableSelect} > Receive (estimated) + +
{`Slippage tolerance: ${slippage}%`} {`Relayer fee: ${RELAYER_FEE_UST} UST`} - {!isSwapping && } - - Swap - + /> - +
@@ -438,14 +537,14 @@ export default function Home() {
- +
Swap completed! - reset()}> + Swap more tokens!
@@ -460,33 +559,48 @@ export default function Home() { {`${amountOut} ${targetTokenInfo.name}`} )} - {isFirstSwapComplete && - !isSecondSwapComplete && + {isSourceSwapComplete && + !isTargetSwapComplete && !relayerTimeoutString && ( - + <> + +
+ )} {relayerTimeoutString && ( {relayerTimeoutString} )} -
- WARNING: this is a Testnet release only + WARNING: this is a testnet release only