├── .gitignore ├── LICENSE ├── README.md ├── contracts ├── .env.sample ├── .gitignore ├── cfg │ ├── truffle-config.avalanche.js │ ├── truffle-config.bsc.js │ ├── truffle-config.ethereum.js │ ├── truffle-config.polygon.js │ └── truffle-config.tokens.js ├── compile_contracts.sh ├── contracts │ ├── CrossChainSwapV2.sol │ ├── CrossChainSwapV3.sol │ ├── Migrations.sol │ ├── shared │ │ ├── IWormhole.sol │ │ ├── Structs.sol │ │ ├── SwapHelper.sol │ │ ├── TokenBridge.sol │ │ └── WETH.sol │ └── tokenImplementations │ │ └── WormUSD.sol ├── deploy_token.sh ├── deploy_v2.sh ├── deploy_v3.sh ├── migrations │ ├── avalanche │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_contracts.js │ ├── bsc │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_contracts.js │ ├── development │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_contracts.js │ ├── ethereum │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_contracts.js │ ├── polygon │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_contracts.js │ └── tokens │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_contracts.js ├── package-lock.json ├── package.json └── truffle-config.js ├── misc ├── .gitignore ├── package.json ├── rm_js_in_src.sh ├── scripts │ ├── src │ │ └── provider.ts │ ├── swap-everything.sh │ └── swap-with-vaa.ts └── tsconfig.json ├── react ├── .env.sample ├── .gitignore ├── craco.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── abi │ │ ├── @uniswap │ │ │ └── v2-periphery │ │ │ │ └── contracts │ │ │ │ └── interfaces │ │ │ │ ├── IUniswapV2Router01.sol │ │ │ │ └── IUniswapV2Router01.json │ │ │ │ └── IUniswapV2Router02.sol │ │ │ │ └── IUniswapV2Router02.json │ │ ├── contracts │ │ │ └── .gitignore │ │ └── erc20.json │ ├── addresses │ │ └── .gitignore │ ├── components │ │ ├── ButtonWithLoader.tsx │ │ ├── CircleLoader.tsx │ │ ├── ErrorBoundary.js │ │ ├── EthereumSignerKey.tsx │ │ ├── Footer.tsx │ │ ├── Settings.tsx │ │ ├── SwapProgress.tsx │ │ ├── TerraWalletKey.tsx │ │ ├── ToggleConnectedButton.tsx │ │ └── TokenSelect.tsx │ ├── contexts │ │ ├── EthereumProviderContext.tsx │ │ └── TerraWalletContext.tsx │ ├── css │ │ └── CircleLoader.css │ ├── hooks │ │ └── useIsWalletReady.ts │ ├── icons │ │ ├── Discord.svg │ │ ├── Docs.svg │ │ ├── Github.svg │ │ ├── Medium.svg │ │ ├── Telegram.svg │ │ ├── Twitter.svg │ │ ├── avax.svg │ │ ├── bsc.svg │ │ ├── eth.svg │ │ ├── polygon.svg │ │ ├── terra.svg │ │ └── wormhole_logo.svg │ ├── index.js │ ├── muiTheme.js │ ├── react-app-env.d.ts │ ├── route │ │ ├── .gitignore │ │ ├── cross-quote.ts │ │ ├── evm.ts │ │ ├── generic.ts │ │ ├── hurricaneswap.ts │ │ ├── pancakeswap.ts │ │ ├── quickswap.ts │ │ ├── terra-ust-transfer.ts │ │ ├── uniswap-core.ts │ │ ├── uniswap-v2.ts │ │ └── uniswap-v3.ts │ ├── swapper │ │ ├── .gitignore │ │ ├── helpers.ts │ │ └── swapper.ts │ ├── utils │ │ ├── .gitignore │ │ ├── consts.ts │ │ ├── getIsTransferCompletedWithRetry.ts │ │ ├── math.ts │ │ └── parseError.ts │ └── views │ │ └── Home.tsx └── tsconfig.json └── swap_relayer ├── .env.sample ├── .gitignore ├── Dockerfile.spy_guardian ├── package-lock.json ├── package.json ├── src ├── evm.ts ├── index.ts └── terra.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | .idea 4 | .arcconfig 5 | *.iml 6 | bin 7 | target 8 | /mutagen.sh 9 | venv 10 | .env 11 | bigtable-admin.json 12 | bigtable-writer.json 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Wormhole Project Contributors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NativeSwap 2 | 3 | https://certusone.github.io/wormhole-nativeswap-example/ 4 | 5 | This is a non-production example program. 6 | 7 | Multi-chain native-to-native token swap using existing DEXes. 8 | 9 | ### Details 10 | 11 | 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). 12 | 13 | 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. 14 | 15 | Here is what happens under the hood of this example: 16 | 17 | - User generates quote from front-end for native-to-native swap. 18 | - User calls the smart contract with its quote on chain A. 19 | - Smart contract on chain A executes swap from native A to UST. If the swap succeeds, the smart contract will execute a Token Bridge transfer of UST with encoded swap parameters for chain B. 20 | - Guardians sign the Token Bridge transfer. 21 | - The relayer reads the signed VAA and calls the smart contract with the VAA as its only argument. 22 | - Smart contract on chain B completes the UST transfer and decodes the swap parameters from the Wormhole message payload. 23 | - Smart contract on chain B executes swap from UST to native B. If the swap succeeds, the smart contract will send native B to user. Otherwise, it will send UST to user. 24 | 25 | The Wormhole message payload for swap parameters are all encoded and decoded on-chain. 26 | 27 | We also wrote a front-end UI using a custom class (UniswapToUniswapExecutor) to perform the quotes for "Exact In" (swapping from an exact amount of native A to an estimated amount of native B) and "Exact Out" (swapping from an estimated amount of native A to an exact amount of native B) swaps and execute these swaps based on this quote. This library uses the ABIs of our example smart contracts to execute the swap transactions. 28 | 29 | ### What's next? 30 | 31 | That is up to you! You are not limited to native-to-native multi-chain swaps. Build in your own smart routing with whichever DEX to perform any swap from chain A to chain B. Wormhole messaging and token transfers with payload are generic enough to adapt this example for any of the chains Wormhole currently supports. 32 | 33 | ### Running 34 | 35 | First compile the example contracts: 36 | 37 | ``` 38 | cd contracts 39 | npm ci 40 | ./compile_contracts.sh 41 | ``` 42 | 43 | Then copy sample.env to .env, edit .env and replace YOUR-PROJECT-ID with your Infura Goerli and Mumbai Project IDs and also add your Ethereum wallet's private key. 44 | These are needed to deploy the example contracts. 45 | 46 | ``` 47 | cp .env.sample .env 48 | # make sure to edit .env file 49 | ``` 50 | 51 | Then deploy the example contracts: 52 | 53 | ``` 54 | ./deploy_v2.sh 55 | ./deploy_v3.sh 56 | ``` 57 | 58 | Then change into the react directory, copy sample.env to .env and replace YOUR-PROJECT-ID with your Infura Goerli and Mumbai Project IDs 59 | 60 | ``` 61 | cd react 62 | cp .env.sample .env 63 | # make sure to edit .env file 64 | ``` 65 | 66 | And finally, start the react app: 67 | 68 | ``` 69 | npm ci 70 | npm run start 71 | ``` 72 | 73 | ### Running the swap relayer 74 | 75 | You need to have a spy_guardian running in TestNet. If there is not already one running, you can build the docker image and start it as follows: 76 | 77 | #### Build the spy_guardian docker container if you don't already have it. 78 | 79 | ``` 80 | $ cd swap_relayer 81 | $ docker build -f Dockerfile.spy_guardian -t spy_guardian . 82 | ``` 83 | 84 | #### Start the spy_guardian docker container in TestNet. 85 | 86 | ``` 87 | $ docker run --platform linux/amd64 --network=host spy_guardian \ 88 | --bootstrap /dns4/wormhole-testnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWBY9ty9CXLBXGQzMuqkziLntsVcyz4pk1zWaJRvJn6Mmt \ 89 | --network /wormhole/testnet/2/1 \ 90 | --spyRPC "[::]:7073" 91 | ``` 92 | 93 | #### Start the swap relayer 94 | 95 | ``` 96 | $ cd swap_relayer 97 | $ cp .env.sample .env 98 | $ # Edit the parameters in .env as appropriate. 99 | $ npm ci 100 | $ npm run build 101 | $ npm run start 102 | ``` 103 | -------------------------------------------------------------------------------- /contracts/.env.sample: -------------------------------------------------------------------------------- 1 | GOERLI_PROVIDER="https://goerli.infura.io/v3/YOUR-PROJECT-ID" 2 | MUMBAI_PROVIDER="https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID" 3 | BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545" 4 | FUJI_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc" 5 | ETH_PRIVATE_KEY= 6 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | build/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /contracts/cfg/truffle-config.avalanche.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 2 | 3 | require('dotenv').config({path:'.env'}); 4 | 5 | /** 6 | * Use this file to configure your truffle project. It's seeded with some 7 | * common settings for different networks and features like migrations, 8 | * compilation and testing. Uncomment the ones you need or modify 9 | * them to suit your project as necessary. 10 | * 11 | * More information about configuration can be found at: 12 | * 13 | * trufflesuite.com/docs/advanced/configuration 14 | * 15 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 16 | * to sign your transactions before they're sent to a remote public node. Infura accounts 17 | * are available for free at: infura.io/register. 18 | * 19 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 20 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 21 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 22 | * 23 | */ 24 | 25 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 26 | // 27 | // const fs = require('fs'); 28 | // const mnemonic = fs.readFileSync('.secret').toString().trim(); 29 | 30 | module.exports = { 31 | contracts_directory: './contracts', 32 | contracts_build_directory: './build/contracts', 33 | migrations_directory: './migrations/avalanche', 34 | /** 35 | * Networks define how you connect to your ethereum client and let you set the 36 | * defaults web3 uses to send transactions. If you don't specify one truffle 37 | * will spin up a development blockchain for you on port 9545 when you 38 | * run `develop` or `test`. You can ask a truffle command to use a specific 39 | * network from the command line, e.g 40 | * 41 | * $ truffle test --network 42 | */ 43 | 44 | networks: { 45 | // Useful for testing. The `development` name is special - truffle uses it by default 46 | // if it's defined here and no other network is specified at the command line. 47 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 48 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 49 | // options below to some value. 50 | // 51 | development: { 52 | host: '127.0.0.1', // Localhost (default: none) 53 | port: 8545, // Standard Ethereum port (default: none) 54 | network_id: '*', // Any network (default: none) 55 | }, 56 | // Another network with more advanced options... 57 | // advanced: { 58 | // port: 8777, // Custom port 59 | // network_id: 1342, // Custom network 60 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 61 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 62 | // from:
, // Account to send txs from (default: accounts[0]) 63 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 64 | // }, 65 | // Useful for deploying to a public network. 66 | // NB: It's important to wrap the provider as a function. 67 | fuji: { 68 | provider: () => new HDWalletProvider( 69 | process.env.ETH_PRIVATE_KEY, 70 | process.env.FUJI_PROVIDER 71 | ), 72 | network_id: 43113, 73 | skipDryRun: true 74 | }, 75 | // Useful for private networks 76 | // private: { 77 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 78 | // network_id: 2111, // This network is yours, in the cloud. 79 | // production: true // Treats this network as if it was a public net. (default: false) 80 | // } 81 | }, 82 | 83 | // Set default mocha options here, use special reporters etc. 84 | mocha: { 85 | // timeout: 100000 86 | }, 87 | 88 | // Configure your compilers 89 | compilers: { 90 | solc: { 91 | version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version) 92 | // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) 93 | // settings: { // See the solidity docs for advice about optimization and evmVersion 94 | optimizer: { 95 | enabled: false, 96 | runs: 200 97 | }, 98 | // evmVersion: 'byzantium' 99 | // } 100 | } 101 | }, 102 | 103 | // Truffle DB is currently disabled by default; to enable it, change enabled: 104 | // false to enabled: true. The default storage location can also be 105 | // overridden by specifying the adapter settings, as shown in the commented code below. 106 | // 107 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 108 | // make a backup of your artifacts to a safe location before enabling this feature. 109 | // 110 | // After you backed up your artifacts you can utilize db by running migrate as follows: 111 | // $ truffle migrate --reset --compile-all 112 | // 113 | // db: { 114 | // enabled: false, 115 | // host: '127.0.0.1', 116 | // adapter: { 117 | // name: 'sqlite', 118 | // settings: { 119 | // directory: '.db' 120 | // } 121 | // } 122 | // } 123 | }; 124 | -------------------------------------------------------------------------------- /contracts/cfg/truffle-config.bsc.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 2 | 3 | require('dotenv').config({path:'.env'}); 4 | 5 | /** 6 | * Use this file to configure your truffle project. It's seeded with some 7 | * common settings for different networks and features like migrations, 8 | * compilation and testing. Uncomment the ones you need or modify 9 | * them to suit your project as necessary. 10 | * 11 | * More information about configuration can be found at: 12 | * 13 | * trufflesuite.com/docs/advanced/configuration 14 | * 15 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 16 | * to sign your transactions before they're sent to a remote public node. Infura accounts 17 | * are available for free at: infura.io/register. 18 | * 19 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 20 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 21 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 22 | * 23 | */ 24 | 25 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 26 | // 27 | // const fs = require('fs'); 28 | // const mnemonic = fs.readFileSync('.secret').toString().trim(); 29 | 30 | module.exports = { 31 | contracts_directory: './contracts', 32 | contracts_build_directory: './build/contracts', 33 | migrations_directory: './migrations/bsc', 34 | /** 35 | * Networks define how you connect to your ethereum client and let you set the 36 | * defaults web3 uses to send transactions. If you don't specify one truffle 37 | * will spin up a development blockchain for you on port 9545 when you 38 | * run `develop` or `test`. You can ask a truffle command to use a specific 39 | * network from the command line, e.g 40 | * 41 | * $ truffle test --network 42 | */ 43 | 44 | networks: { 45 | // Useful for testing. The `development` name is special - truffle uses it by default 46 | // if it's defined here and no other network is specified at the command line. 47 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 48 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 49 | // options below to some value. 50 | // 51 | development: { 52 | host: '127.0.0.1', // Localhost (default: none) 53 | port: 8545, // Standard Ethereum port (default: none) 54 | network_id: '*', // Any network (default: none) 55 | }, 56 | // Another network with more advanced options... 57 | // advanced: { 58 | // port: 8777, // Custom port 59 | // network_id: 1342, // Custom network 60 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 61 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 62 | // from:
, // Account to send txs from (default: accounts[0]) 63 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 64 | // }, 65 | // Useful for deploying to a public network. 66 | // NB: It's important to wrap the provider as a function. 67 | bsc: { 68 | provider: () => new HDWalletProvider( 69 | process.env.ETH_PRIVATE_KEY, 70 | process.env.BSC_PROVIDER 71 | ), 72 | network_id: 97, 73 | skipDryRun: true, 74 | timeoutBlocks: 200 75 | }, 76 | // Useful for private networks 77 | // private: { 78 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 79 | // network_id: 2111, // This network is yours, in the cloud. 80 | // production: true // Treats this network as if it was a public net. (default: false) 81 | // } 82 | }, 83 | 84 | // Set default mocha options here, use special reporters etc. 85 | mocha: { 86 | // timeout: 100000 87 | }, 88 | 89 | // Configure your compilers 90 | compilers: { 91 | solc: { 92 | version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version) 93 | // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) 94 | // settings: { // See the solidity docs for advice about optimization and evmVersion 95 | optimizer: { 96 | enabled: false, 97 | runs: 200 98 | }, 99 | // evmVersion: 'byzantium' 100 | // } 101 | } 102 | }, 103 | 104 | // Truffle DB is currently disabled by default; to enable it, change enabled: 105 | // false to enabled: true. The default storage location can also be 106 | // overridden by specifying the adapter settings, as shown in the commented code below. 107 | // 108 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 109 | // make a backup of your artifacts to a safe location before enabling this feature. 110 | // 111 | // After you backed up your artifacts you can utilize db by running migrate as follows: 112 | // $ truffle migrate --reset --compile-all 113 | // 114 | // db: { 115 | // enabled: false, 116 | // host: '127.0.0.1', 117 | // adapter: { 118 | // name: 'sqlite', 119 | // settings: { 120 | // directory: '.db' 121 | // } 122 | // } 123 | // } 124 | }; 125 | -------------------------------------------------------------------------------- /contracts/cfg/truffle-config.ethereum.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 2 | 3 | require('dotenv').config({path:'.env'}); 4 | 5 | /** 6 | * Use this file to configure your truffle project. It's seeded with some 7 | * common settings for different networks and features like migrations, 8 | * compilation and testing. Uncomment the ones you need or modify 9 | * them to suit your project as necessary. 10 | * 11 | * More information about configuration can be found at: 12 | * 13 | * trufflesuite.com/docs/advanced/configuration 14 | * 15 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 16 | * to sign your transactions before they're sent to a remote public node. Infura accounts 17 | * are available for free at: infura.io/register. 18 | * 19 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 20 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 21 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 22 | * 23 | */ 24 | 25 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 26 | // 27 | // const fs = require('fs'); 28 | // const mnemonic = fs.readFileSync('.secret').toString().trim(); 29 | 30 | module.exports = { 31 | contracts_directory: './contracts', 32 | contracts_build_directory: './build/contracts', 33 | migrations_directory: './migrations/ethereum', 34 | /** 35 | * Networks define how you connect to your ethereum client and let you set the 36 | * defaults web3 uses to send transactions. If you don't specify one truffle 37 | * will spin up a development blockchain for you on port 9545 when you 38 | * run `develop` or `test`. You can ask a truffle command to use a specific 39 | * network from the command line, e.g 40 | * 41 | * $ truffle test --network 42 | */ 43 | 44 | networks: { 45 | // Useful for testing. The `development` name is special - truffle uses it by default 46 | // if it's defined here and no other network is specified at the command line. 47 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 48 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 49 | // options below to some value. 50 | // 51 | development: { 52 | host: '127.0.0.1', // Localhost (default: none) 53 | port: 8545, // Standard Ethereum port (default: none) 54 | network_id: '*', // Any network (default: none) 55 | }, 56 | // Another network with more advanced options... 57 | // advanced: { 58 | // port: 8777, // Custom port 59 | // network_id: 1342, // Custom network 60 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 61 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 62 | // from:
, // Account to send txs from (default: accounts[0]) 63 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 64 | // }, 65 | // Useful for deploying to a public network. 66 | // NB: It's important to wrap the provider as a function. 67 | goerli: { 68 | provider: () => new HDWalletProvider(process.env.ETH_PRIVATE_KEY, process.env.GOERLI_PROVIDER), 69 | network_id: 5, 70 | //gas: 4465030, 71 | //confirmations: 2, // # of confs to wait between deployments. (default: 0) 72 | //timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 73 | //skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 74 | }, 75 | // Useful for private networks 76 | // private: { 77 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 78 | // network_id: 2111, // This network is yours, in the cloud. 79 | // production: true // Treats this network as if it was a public net. (default: false) 80 | // } 81 | }, 82 | 83 | // Set default mocha options here, use special reporters etc. 84 | mocha: { 85 | // timeout: 100000 86 | }, 87 | 88 | // Configure your compilers 89 | compilers: { 90 | solc: { 91 | version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version) 92 | // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) 93 | // settings: { // See the solidity docs for advice about optimization and evmVersion 94 | optimizer: { 95 | enabled: false, 96 | runs: 200 97 | }, 98 | // evmVersion: 'byzantium' 99 | // } 100 | } 101 | }, 102 | 103 | // Truffle DB is currently disabled by default; to enable it, change enabled: 104 | // false to enabled: true. The default storage location can also be 105 | // overridden by specifying the adapter settings, as shown in the commented code below. 106 | // 107 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 108 | // make a backup of your artifacts to a safe location before enabling this feature. 109 | // 110 | // After you backed up your artifacts you can utilize db by running migrate as follows: 111 | // $ truffle migrate --reset --compile-all 112 | // 113 | // db: { 114 | // enabled: false, 115 | // host: '127.0.0.1', 116 | // adapter: { 117 | // name: 'sqlite', 118 | // settings: { 119 | // directory: '.db' 120 | // } 121 | // } 122 | // } 123 | }; 124 | -------------------------------------------------------------------------------- /contracts/cfg/truffle-config.polygon.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 2 | 3 | require("dotenv").config({ path: ".env" }); 4 | 5 | /** 6 | * Use this file to configure your truffle project. It's seeded with some 7 | * common settings for different networks and features like migrations, 8 | * compilation and testing. Uncomment the ones you need or modify 9 | * them to suit your project as necessary. 10 | * 11 | * More information about configuration can be found at: 12 | * 13 | * trufflesuite.com/docs/advanced/configuration 14 | * 15 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 16 | * to sign your transactions before they're sent to a remote public node. Infura accounts 17 | * are available for free at: infura.io/register. 18 | * 19 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 20 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 21 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 22 | * 23 | */ 24 | 25 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 26 | // 27 | // const fs = require('fs'); 28 | // const mnemonic = fs.readFileSync('.secret').toString().trim(); 29 | 30 | module.exports = { 31 | contracts_directory: "./contracts", 32 | contracts_build_directory: "./build/contracts", 33 | migrations_directory: "./migrations/polygon", 34 | /** 35 | * Networks define how you connect to your ethereum client and let you set the 36 | * defaults web3 uses to send transactions. If you don't specify one truffle 37 | * will spin up a development blockchain for you on port 9545 when you 38 | * run `develop` or `test`. You can ask a truffle command to use a specific 39 | * network from the command line, e.g 40 | * 41 | * $ truffle test --network 42 | */ 43 | 44 | networks: { 45 | // Useful for testing. The `development` name is special - truffle uses it by default 46 | // if it's defined here and no other network is specified at the command line. 47 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 48 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 49 | // options below to some value. 50 | // 51 | development: { 52 | host: "127.0.0.1", // Localhost (default: none) 53 | port: 8545, // Standard Ethereum port (default: none) 54 | network_id: "*", // Any network (default: none) 55 | }, 56 | // Another network with more advanced options... 57 | // advanced: { 58 | // port: 8777, // Custom port 59 | // network_id: 1342, // Custom network 60 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 61 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 62 | // from:
, // Account to send txs from (default: accounts[0]) 63 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 64 | // }, 65 | // Useful for deploying to a public network. 66 | // NB: It's important to wrap the provider as a function. 67 | mumbai: { 68 | provider: () => new HDWalletProvider(process.env.ETH_PRIVATE_KEY, process.env.MUMBAI_PROVIDER), 69 | network_id: 80001, 70 | gasPrice: 80000000000, 71 | gas: 7000000, 72 | //confirmations: 2, // # of confs to wait between deployments. (default: 0) 73 | timeoutBlocks: 50, // # of blocks before a deployment times out (minimum/default: 50) 74 | skipDryRun: true, // Skip dry run before migrations? (default: false for public nets ) 75 | }, 76 | // Useful for private networks 77 | // private: { 78 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 79 | // network_id: 2111, // This network is yours, in the cloud. 80 | // production: true // Treats this network as if it was a public net. (default: false) 81 | // } 82 | }, 83 | 84 | // Set default mocha options here, use special reporters etc. 85 | mocha: { 86 | // timeout: 100000 87 | }, 88 | 89 | // Configure your compilers 90 | compilers: { 91 | solc: { 92 | version: "0.7.6", // Fetch exact version from solc-bin (default: truffle's version) 93 | // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) 94 | // settings: { // See the solidity docs for advice about optimization and evmVersion 95 | optimizer: { 96 | enabled: false, 97 | runs: 200, 98 | }, 99 | // evmVersion: 'byzantium' 100 | // } 101 | }, 102 | }, 103 | 104 | // Truffle DB is currently disabled by default; to enable it, change enabled: 105 | // false to enabled: true. The default storage location can also be 106 | // overridden by specifying the adapter settings, as shown in the commented code below. 107 | // 108 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 109 | // make a backup of your artifacts to a safe location before enabling this feature. 110 | // 111 | // After you backed up your artifacts you can utilize db by running migrate as follows: 112 | // $ truffle migrate --reset --compile-all 113 | // 114 | // db: { 115 | // enabled: false, 116 | // host: '127.0.0.1', 117 | // adapter: { 118 | // name: 'sqlite', 119 | // settings: { 120 | // directory: '.db' 121 | // } 122 | // } 123 | // } 124 | }; 125 | -------------------------------------------------------------------------------- /contracts/cfg/truffle-config.tokens.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 2 | 3 | require("dotenv").config({ path: ".env" }); 4 | 5 | /** 6 | * Use this file to configure your truffle project. It's seeded with some 7 | * common settings for different networks and features like migrations, 8 | * compilation and testing. Uncomment the ones you need or modify 9 | * them to suit your project as necessary. 10 | * 11 | * More information about configuration can be found at: 12 | * 13 | * trufflesuite.com/docs/advanced/configuration 14 | * 15 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 16 | * to sign your transactions before they're sent to a remote public node. Infura accounts 17 | * are available for free at: infura.io/register. 18 | * 19 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 20 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 21 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 22 | * 23 | */ 24 | 25 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 26 | // 27 | // const fs = require('fs'); 28 | // const mnemonic = fs.readFileSync('.secret').toString().trim(); 29 | 30 | module.exports = { 31 | contracts_directory: "./contracts", 32 | contracts_build_directory: "./build/contracts", 33 | migrations_directory: "./migrations/tokens", 34 | /** 35 | * Networks define how you connect to your ethereum client and let you set the 36 | * defaults web3 uses to send transactions. If you don't specify one truffle 37 | * will spin up a development blockchain for you on port 9545 when you 38 | * run `develop` or `test`. You can ask a truffle command to use a specific 39 | * network from the command line, e.g 40 | * 41 | * $ truffle test --network 42 | */ 43 | 44 | networks: { 45 | // Useful for testing. The `development` name is special - truffle uses it by default 46 | // if it's defined here and no other network is specified at the command line. 47 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 48 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 49 | // options below to some value. 50 | // 51 | development: { 52 | host: "127.0.0.1", // Localhost (default: none) 53 | port: 8545, // Standard Ethereum port (default: none) 54 | network_id: "*", // Any network (default: none) 55 | }, 56 | // Another network with more advanced options... 57 | // advanced: { 58 | // port: 8777, // Custom port 59 | // network_id: 1342, // Custom network 60 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 61 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 62 | // from:
, // Account to send txs from (default: accounts[0]) 63 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 64 | // }, 65 | // Useful for deploying to a public network. 66 | // NB: It's important to wrap the provider as a function. 67 | goerli: { 68 | provider: () => new HDWalletProvider(process.env.ETH_PRIVATE_KEY, process.env.GOERLI_PROVIDER), 69 | network_id: 5, 70 | //gas: 4465030, 71 | //confirmations: 2, // # of confs to wait between deployments. (default: 0) 72 | //timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 73 | //skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 74 | }, 75 | // Useful for private networks 76 | // private: { 77 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 78 | // network_id: 2111, // This network is yours, in the cloud. 79 | // production: true // Treats this network as if it was a public net. (default: false) 80 | // } 81 | }, 82 | 83 | // Set default mocha options here, use special reporters etc. 84 | mocha: { 85 | // timeout: 100000 86 | }, 87 | 88 | // Configure your compilers 89 | compilers: { 90 | solc: { 91 | version: "0.7.6", // Fetch exact version from solc-bin (default: truffle's version) 92 | // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) 93 | // settings: { // See the solidity docs for advice about optimization and evmVersion 94 | optimizer: { 95 | enabled: false, 96 | runs: 200, 97 | }, 98 | // evmVersion: 'byzantium' 99 | // } 100 | }, 101 | }, 102 | 103 | // Truffle DB is currently disabled by default; to enable it, change enabled: 104 | // false to enabled: true. The default storage location can also be 105 | // overridden by specifying the adapter settings, as shown in the commented code below. 106 | // 107 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 108 | // make a backup of your artifacts to a safe location before enabling this feature. 109 | // 110 | // After you backed up your artifacts you can utilize db by running migrate as follows: 111 | // $ truffle migrate --reset --compile-all 112 | // 113 | // db: { 114 | // enabled: false, 115 | // host: '127.0.0.1', 116 | // adapter: { 117 | // name: 'sqlite', 118 | // settings: { 119 | // directory: '.db' 120 | // } 121 | // } 122 | // } 123 | }; 124 | -------------------------------------------------------------------------------- /contracts/compile_contracts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | npx truffle compile --config cfg/truffle-config.ethereum.js 6 | npx truffle compile --config cfg/truffle-config.polygon.js 7 | 8 | CONTRACTS="../react/src/abi/contracts" 9 | 10 | mkdir -p $CONTRACTS 11 | 12 | cp -r build/contracts/* $CONTRACTS 13 | -------------------------------------------------------------------------------- /contracts/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.6; 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | if (msg.sender == owner) _; 10 | } 11 | 12 | constructor() public { 13 | owner = msg.sender; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/contracts/shared/IWormhole.sol: -------------------------------------------------------------------------------- 1 | // contracts/Messages.sol 2 | // SPDX-License-Identifier: Apache 2 3 | 4 | pragma solidity ^0.7.6; 5 | pragma abicoder v2; 6 | 7 | import "./Structs.sol"; 8 | 9 | interface IWormhole is Structs { 10 | event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel); 11 | 12 | function publishMessage( 13 | uint32 nonce, 14 | bytes memory payload, 15 | uint8 consistencyLevel 16 | ) external payable returns (uint64 sequence); 17 | 18 | function parseAndVerifyVM(bytes calldata encodedVM) external view returns (Structs.VM memory vm, bool valid, string memory reason); 19 | 20 | function verifyVM(Structs.VM memory vm) external view returns (bool valid, string memory reason); 21 | 22 | function verifySignatures(bytes32 hash, Structs.Signature[] memory signatures, Structs.GuardianSet memory guardianSet) external pure returns (bool valid, string memory reason) ; 23 | 24 | function parseVM(bytes memory encodedVM) external pure returns (Structs.VM memory vm); 25 | 26 | function getGuardianSet(uint32 index) external view returns (Structs.GuardianSet memory) ; 27 | 28 | function getCurrentGuardianSetIndex() external view returns (uint32) ; 29 | 30 | function getGuardianSetExpiry() external view returns (uint32) ; 31 | 32 | function governanceActionIsConsumed(bytes32 hash) external view returns (bool) ; 33 | 34 | function isInitialized(address impl) external view returns (bool) ; 35 | 36 | function chainId() external view returns (uint16) ; 37 | 38 | function governanceChainId() external view returns (uint16); 39 | 40 | function governanceContract() external view returns (bytes32); 41 | 42 | function messageFee() external view returns (uint256) ; 43 | } -------------------------------------------------------------------------------- /contracts/contracts/shared/Structs.sol: -------------------------------------------------------------------------------- 1 | // contracts/Structs.sol 2 | // SPDX-License-Identifier: Apache 2 3 | 4 | pragma solidity ^0.7.6; 5 | pragma abicoder v2; 6 | 7 | interface Structs { 8 | struct Provider { 9 | uint16 chainId; 10 | uint16 governanceChainId; 11 | bytes32 governanceContract; 12 | } 13 | 14 | struct GuardianSet { 15 | address[] keys; 16 | uint32 expirationTime; 17 | } 18 | 19 | struct Signature { 20 | bytes32 r; 21 | bytes32 s; 22 | uint8 v; 23 | uint8 guardianIndex; 24 | } 25 | 26 | struct VM { 27 | uint8 version; 28 | uint32 timestamp; 29 | uint32 nonce; 30 | uint16 emitterChainId; 31 | bytes32 emitterAddress; 32 | uint64 sequence; 33 | uint8 consistencyLevel; 34 | bytes payload; 35 | 36 | uint32 guardianSetIndex; 37 | Signature[] signatures; 38 | 39 | bytes32 hash; 40 | } 41 | } -------------------------------------------------------------------------------- /contracts/contracts/shared/SwapHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2 2 | 3 | pragma solidity ^0.7.6; 4 | pragma abicoder v2; 5 | 6 | import './IWormhole.sol'; 7 | import 'solidity-bytes-utils/contracts/BytesLib.sol'; 8 | 9 | /// @title Helper library for cross-chain swaps 10 | /// @notice Contains functions necessary for parsing encoded VAAs 11 | /// and structs containing swap parameters 12 | library SwapHelper { 13 | using BytesLib for bytes; 14 | 15 | /// @dev Parameters needed for exactIn swap type 16 | struct ExactInParameters { 17 | uint256 amountIn; 18 | uint256 amountOutMinimum; 19 | uint256 targetAmountOutMinimum; 20 | bytes32 targetChainRecipient; 21 | uint256 deadline; 22 | uint24 poolFee; 23 | } 24 | 25 | /// @dev Parameters needed for exactOut swap type 26 | struct ExactOutParameters { 27 | uint256 amountOut; 28 | uint256 amountInMaximum; 29 | uint256 targetAmountOut; 30 | bytes32 targetChainRecipient; 31 | uint256 deadline; 32 | uint24 poolFee; 33 | } 34 | 35 | /// @dev Parameters parsed from a VAA for executing swaps 36 | /// on the destination chain 37 | struct DecodedVaaParameters { 38 | // in order of decoding 39 | uint8 version; 40 | uint256 swapAmount; 41 | address contractAddress; 42 | bytes32 fromAddress; 43 | uint256 estimatedAmount; 44 | address recipientAddress; 45 | address[2] path; 46 | uint256 deadline; 47 | uint24 poolFee; 48 | uint8 swapFunctionType; 49 | uint256 relayerFee; 50 | } 51 | 52 | /// @dev Decodes parameters encoded in a VAA 53 | function decodeVaaPayload( 54 | bytes memory vmPayload 55 | ) public view returns (DecodedVaaParameters memory decoded) { 56 | uint index = 0; 57 | 58 | decoded.version = vmPayload.toUint8(index); 59 | index += 1; 60 | 61 | decoded.swapAmount = vmPayload.toUint256(index); 62 | index += 32; 63 | 64 | // skip 65 | index += 46; 66 | 67 | decoded.contractAddress = vmPayload.toAddress(index); 68 | index += 20; 69 | 70 | // skip 71 | index += 2; 72 | 73 | decoded.fromAddress = vmPayload.toBytes32(index); 74 | index += 32; 75 | 76 | decoded.estimatedAmount = vmPayload.toUint256(index); 77 | index += 44; 78 | 79 | decoded.recipientAddress = vmPayload.toAddress(index); 80 | index += 20; 81 | 82 | decoded.path[0] = vmPayload.toAddress(index); 83 | index += 20; 84 | 85 | decoded.path[1] = vmPayload.toAddress(index); 86 | index += 20; 87 | 88 | decoded.deadline = vmPayload.toUint256(index); 89 | index += 32; 90 | 91 | // skip 92 | index += 1; 93 | 94 | decoded.poolFee = vmPayload.toUint16(index); 95 | index += 2; 96 | 97 | decoded.swapFunctionType = vmPayload.toUint8(index); 98 | index += 1; 99 | 100 | decoded.relayerFee = vmPayload.toUint256(index); 101 | index += 32; 102 | 103 | require(vmPayload.length == index, "invalid payload length"); 104 | } 105 | } -------------------------------------------------------------------------------- /contracts/contracts/shared/TokenBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2 2 | 3 | pragma solidity ^0.7.6; 4 | pragma abicoder v2; 5 | 6 | interface TokenBridge { 7 | function transferTokensWithPayload( 8 | address token, 9 | uint256 amount, 10 | uint16 recipientChain, 11 | bytes32 recipient, 12 | uint32 nonce, 13 | bytes memory payload 14 | ) external payable returns (uint64); 15 | function completeTransferWithPayload( 16 | bytes memory encodedVm 17 | ) external returns (bytes memory); 18 | } -------------------------------------------------------------------------------- /contracts/contracts/shared/WETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2 2 | 3 | pragma solidity ^0.7.6; 4 | pragma abicoder v2; 5 | 6 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 7 | 8 | interface IWETH is IERC20 { 9 | function deposit() external payable; 10 | function withdraw(uint amount) external; 11 | } -------------------------------------------------------------------------------- /contracts/contracts/tokenImplementations/WormUSD.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2 2 | 3 | pragma solidity ^0.7.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract WormUSD is ERC20 { 8 | constructor(address mintToAddress, uint8 decimals, uint256 supply) ERC20("wormUSD", "WUSD"){ 9 | _setupDecimals(decimals); 10 | _mint(mintToAddress, supply*10**decimals); 11 | } 12 | } -------------------------------------------------------------------------------- /contracts/deploy_token.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | npx truffle migrate --config cfg/truffle-config.tokens.js --network goerli --reset 5 | -------------------------------------------------------------------------------- /contracts/deploy_v2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | npx truffle migrate --config cfg/truffle-config.avalanche.js --network fuji --reset 5 | npx truffle migrate --config cfg/truffle-config.bsc.js --network bsc --reset 6 | npx truffle migrate --config cfg/truffle-config.polygon.js --network mumbai --reset -------------------------------------------------------------------------------- /contracts/deploy_v3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | npx truffle migrate --config cfg/truffle-config.ethereum.js --network goerli --reset 5 | -------------------------------------------------------------------------------- /contracts/migrations/avalanche/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/migrations/avalanche/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const fsp = require("fs/promises"); 2 | 3 | const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2"); 4 | const SwapHelper = artifacts.require("SwapHelper"); 5 | 6 | const scriptsAddressPath = "../react/src/addresses"; 7 | 8 | module.exports = async function (deployer, network) { 9 | const routerAddress = "0x7e3411b04766089cfaa52db688855356a12f05d1"; // hurricaneswap router 10 | const feeTokenAddress = "0x8F23C5BE43FBfE7a30c04e4eDEE3D18995Fe7E2d"; // wormUSD 11 | const tokenBridgeAddress = "0x61E44E506Ca5659E6c0bba9b678586fA2d729756"; 12 | const wrappedAvaxAddress = "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3"; 13 | 14 | await deployer.deploy(SwapHelper); 15 | await deployer.link(SwapHelper, CrossChainSwapV2); 16 | await deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedAvaxAddress); 17 | 18 | // save the contract address somewhere 19 | await fsp.mkdir(scriptsAddressPath, { recursive: true }); 20 | 21 | await fsp.writeFile( 22 | `${scriptsAddressPath}/${network}.ts`, 23 | `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` 24 | ); 25 | 26 | //deployer.link(ConvertLib, MetaCoin); 27 | //deployer.deploy(MetaCoin); 28 | }; 29 | -------------------------------------------------------------------------------- /contracts/migrations/bsc/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/migrations/bsc/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const fsp = require("fs/promises"); 2 | 3 | const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2"); 4 | const SwapHelper = artifacts.require("SwapHelper"); 5 | 6 | const scriptsAddressPath = "../react/src/addresses"; 7 | 8 | module.exports = async function (deployer, network) { 9 | const routerAddress = "0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3"; // pancakeswap 10 | const feeTokenAddress = "0x2A29D46D7e0B997358E9726DA0210Af212f2dfd7"; // wormUSD 11 | const tokenBridgeAddress = "0x9dcF9D205C9De35334D646BeE44b2D2859712A09"; 12 | const wrappedBnbAddress = "0xae13d989dac2f0debff460ac112a837c89baa7cd"; 13 | 14 | await deployer.deploy(SwapHelper); 15 | await deployer.link(SwapHelper, CrossChainSwapV2); 16 | await deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedBnbAddress); 17 | 18 | // save the contract address somewhere 19 | await fsp.mkdir(scriptsAddressPath, { recursive: true }); 20 | 21 | await fsp.writeFile( 22 | `${scriptsAddressPath}/${network}.ts`, 23 | `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` 24 | ); 25 | 26 | //deployer.link(ConvertLib, MetaCoin); 27 | //deployer.deploy(MetaCoin); 28 | }; 29 | -------------------------------------------------------------------------------- /contracts/migrations/development/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/migrations/development/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const CrossChainSwapV3 = artifacts.require('CrossChainSwapV3'); 2 | const CrossChainSwapV2 = artifacts.require('CrossChainSwapV2'); 3 | const BytesDecodingTest = artifacts.require('BytesDecodingTest'); 4 | 5 | module.exports = function(deployer) { 6 | // CrossChainSwapV3 7 | { 8 | const routerAddress = '0xE592427A0AEce92De3Edee1F18E0157C05861564' 9 | const feeTokenAddress = '0x36Ed51Afc79619b299b238898E72ce482600568a' // wUST 10 | const tokenBridgeAddress = '0xF890982f9310df57d00f659cf4fd87e65adEd8d7'; 11 | 12 | deployer.deploy(CrossChainSwapV3, routerAddress, feeTokenAddress, tokenBridgeAddress); 13 | } 14 | 15 | // CrossChainSwapV2 16 | { 17 | const routerAddress = '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff'; // quickwap 18 | const feeTokenAddress = '0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c'; // wUST 19 | const tokenBridgeAddress = '0x377D55a7928c046E18eEbb61977e714d2a76472a'; 20 | 21 | deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress); 22 | } 23 | 24 | // BytesDecodingTest 25 | deployer.deploy(BytesDecodingTest) 26 | 27 | 28 | //deployer.link(ConvertLib, MetaCoin); 29 | //deployer.deploy(MetaCoin); 30 | }; 31 | -------------------------------------------------------------------------------- /contracts/migrations/ethereum/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/migrations/ethereum/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const fsp = require("fs/promises"); 2 | 3 | const CrossChainSwapV3 = artifacts.require("CrossChainSwapV3"); 4 | const SwapHelper = artifacts.require("SwapHelper"); 5 | 6 | const scriptsAddressPath = "../react/src/addresses"; 7 | 8 | module.exports = async function (deployer, network) { 9 | const routerAddress = "0xE592427A0AEce92De3Edee1F18E0157C05861564"; 10 | const feeTokenAddress = "0x6336c2dA64408Fcc277e0E1104aC6c34c431464c"; // wormUSD 11 | const tokenBridgeAddress = "0xF890982f9310df57d00f659cf4fd87e65adEd8d7"; 12 | const wrappedEthAddress = "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"; 13 | 14 | await deployer.deploy(SwapHelper); 15 | await deployer.link(SwapHelper, CrossChainSwapV3); 16 | await deployer.deploy(CrossChainSwapV3, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedEthAddress); 17 | 18 | // save the contract address somewhere 19 | await fsp.mkdir(scriptsAddressPath, { recursive: true }); 20 | 21 | await fsp.writeFile( 22 | `${scriptsAddressPath}/goerli.ts`, 23 | `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV3.address}';` 24 | ); 25 | 26 | //deployer.link(ConvertLib, MetaCoin); 27 | //deployer.deploy(MetaCoin); 28 | }; 29 | -------------------------------------------------------------------------------- /contracts/migrations/polygon/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/migrations/polygon/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const fsp = require("fs/promises"); 2 | 3 | const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2"); 4 | const SwapHelper = artifacts.require("SwapHelper"); 5 | 6 | const scriptsAddressPath = "../react/src/addresses"; 7 | 8 | module.exports = async function (deployer, network) { 9 | const routerAddress = "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff"; // quickwap 10 | const feeTokenAddress = "0xcf7BEE494B42cB5A902dF000158037Ad334eB4a7"; // wormUSD 11 | const tokenBridgeAddress = "0x377D55a7928c046E18eEbb61977e714d2a76472a"; 12 | const wrappedMaticAddress = "0x9c3c9283d3e44854697cd22d3faa240cfb032889"; 13 | 14 | await deployer.deploy(SwapHelper); 15 | await deployer.link(SwapHelper, CrossChainSwapV2); 16 | await deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedMaticAddress); 17 | 18 | // save the contract address somewhere 19 | await fsp.mkdir(scriptsAddressPath, { recursive: true }); 20 | 21 | await fsp.writeFile( 22 | `${scriptsAddressPath}/${network}.ts`, 23 | `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` 24 | ); 25 | 26 | //deployer.link(ConvertLib, MetaCoin); 27 | //deployer.deploy(MetaCoin); 28 | }; 29 | -------------------------------------------------------------------------------- /contracts/migrations/tokens/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/migrations/tokens/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ path: ".env" }); 2 | 3 | const WormUSD = artifacts.require("WormUSD"); 4 | 5 | module.exports = async function (deployer, network) { 6 | const mintAddress = process.env.mintToAddress; 7 | const tokenDecimals = process.env.decimals; 8 | const tokenSupply = process.env.supply; 9 | 10 | await deployer.deploy(WormUSD, mintAddress, tokenDecimals, tokenSupply); 11 | }; 12 | -------------------------------------------------------------------------------- /contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": {}, 6 | "author": "", 7 | "license": "ISC", 8 | "dependencies": { 9 | "@truffle/hdwallet-provider": "^2.0.0", 10 | "@uniswap/v2-periphery": "^1.1.0-beta.0", 11 | "@uniswap/v3-periphery": "^1.3.0", 12 | "dotenv": "^14.2.0", 13 | "solidity-bytes-utils": "github:GNSPS/solidity-bytes-utils#feat/update-0.7.0", 14 | "truffle": "^5.4.29" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contracts/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * trufflesuite.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 22 | // const infuraKey = 'fj4jll3k.....'; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync('.secret').toString().trim(); 26 | 27 | module.exports = { 28 | contracts_directory: './contracts', 29 | contracts_build_directory: './build/contracts', 30 | migrations_directory: './migrations/development', 31 | 32 | /** 33 | * Networks define how you connect to your ethereum client and let you set the 34 | * defaults web3 uses to send transactions. If you don't specify one truffle 35 | * will spin up a development blockchain for you on port 9545 when you 36 | * run `develop` or `test`. You can ask a truffle command to use a specific 37 | * network from the command line, e.g 38 | * 39 | * $ truffle test --network 40 | */ 41 | 42 | networks: { 43 | development: { 44 | host: '127.0.0.1', // Localhost (default: none) 45 | port: 8545, // Standard Ethereum port (default: none) 46 | network_id: '*', // Any network (default: none) 47 | } 48 | }, 49 | 50 | // Set default mocha options here, use special reporters etc. 51 | mocha: { 52 | // timeout: 100000 53 | }, 54 | 55 | // Configure your compilers 56 | compilers: { 57 | solc: { 58 | version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version) 59 | // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) 60 | // settings: { // See the solidity docs for advice about optimization and evmVersion 61 | optimizer: { 62 | enabled: false, 63 | runs: 200 64 | }, 65 | // evmVersion: 'byzantium' 66 | // } 67 | } 68 | }, 69 | 70 | // Truffle DB is enabled in this project by default. Enabling Truffle DB surfaces access to the @truffle/db package 71 | // for querying data about the contracts, deployments, and networks in this project 72 | //db: { 73 | // enabled: true 74 | //} 75 | }; 76 | -------------------------------------------------------------------------------- /misc/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | scripts/*.js 4 | scripts/src/*.js 5 | src 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /misc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://certusone.github.io/wormhole-nativeswap-example", 3 | "name": "NativeSwap", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "build": "tsc", 8 | "clean": "rm_js_in_src.sh" 9 | }, 10 | "dependencies": { 11 | "@certusone/wormhole-sdk": "^0.1.6", 12 | "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", 13 | "@material-ui/core": "^4.12.2", 14 | "@material-ui/icons": "^4.11.2", 15 | "@material-ui/lab": "^4.0.0-alpha.60", 16 | "@metamask/detect-provider": "^1.2.0", 17 | "@terra-money/terra.js": "^2.0.14", 18 | "@terra-money/wallet-provider": "^2.2.0", 19 | "@types/node": "^16.11.19", 20 | "@types/react": "^17.0.38", 21 | "@types/react-dom": "^17.0.11", 22 | "@uniswap/smart-order-router": "^2.1.1", 23 | "@uniswap/v2-core": "^1.0.1", 24 | "@uniswap/v2-sdk": "^3.0.1", 25 | "@uniswap/v3-periphery": "1.3", 26 | "@uniswap/v3-sdk": "^3.8.1", 27 | "ethers": "^5.5.3", 28 | "jsbi": "^3.2.5", 29 | "notistack": "^1.0.10", 30 | "react": "^17.0.2", 31 | "react-dom": "^17.0.2", 32 | "react-scripts": "4.0.3", 33 | "typescript": "^4.4.2", 34 | "use-debounce": "^7.0.1" 35 | }, 36 | "devDependencies": { 37 | "@craco/craco": "^6.3.0", 38 | "gh-pages": "^3.2.3", 39 | "wasm-loader": "^1.3.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /misc/rm_js_in_src.sh: -------------------------------------------------------------------------------- 1 | rm src/addresses/*.js 2 | rm src/route/*.js 3 | rm src/swapper/*.js 4 | rm src/utils/*.js 5 | -------------------------------------------------------------------------------- /misc/scripts/src/provider.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | import { 4 | ETH_TOKEN_INFO, 5 | MATIC_TOKEN_INFO, 6 | AVAX_TOKEN_INFO, 7 | BNB_TOKEN_INFO, 8 | } from "../../src/utils/consts"; 9 | 10 | export function makeProvider(tokenAddress: string) { 11 | switch (tokenAddress) { 12 | case ETH_TOKEN_INFO.address: { 13 | return new ethers.providers.StaticJsonRpcProvider( 14 | process.env.GOERLI_PROVIDER 15 | ); 16 | } 17 | case MATIC_TOKEN_INFO.address: { 18 | return new ethers.providers.StaticJsonRpcProvider( 19 | process.env.MUMBAI_PROVIDER 20 | ); 21 | } 22 | case AVAX_TOKEN_INFO.address: { 23 | return new ethers.providers.StaticJsonRpcProvider( 24 | process.env.FUJI_PROVIDER 25 | ); 26 | } 27 | case BNB_TOKEN_INFO.address: { 28 | return new ethers.providers.StaticJsonRpcProvider( 29 | process.env.BSC_PROVIDER 30 | ); 31 | } 32 | default: { 33 | throw Error("unrecognized token address"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /misc/scripts/swap-everything.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | root=$(dirname $0) 6 | script="${root}/swap-with-vaa.js" 7 | 8 | echo `which node` 9 | 10 | node $script --in ETH --out MATIC 11 | node $script --in ETH --out BNB 12 | node $script --in ETH --out AVAX 13 | node $script --in MATIC --out BNB 14 | node $script --in MATIC --out AVAX 15 | node $script --in BNB --out MATIC 16 | 17 | echo "done" -------------------------------------------------------------------------------- /misc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "esModuleInterop": true 5 | }, 6 | "files": [ 7 | "scripts/swap-with-vaa.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /react/.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_GOERLI_PROVIDER="https://goerli.infura.io/v3/YOUR-PROJECT-ID" 2 | REACT_APP_MUMBAI_PROVIDER="https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID" 3 | REACT_APP_FUJI_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc" 4 | REACT_APP_BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545" 5 | -------------------------------------------------------------------------------- /react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /react/craco.config.js: -------------------------------------------------------------------------------- 1 | const { addBeforeLoader, loaderByName } = require("@craco/craco"); 2 | 3 | module.exports = { 4 | webpack: { 5 | configure: (webpackConfig) => { 6 | const wasmExtensionRegExp = /\.wasm$/; 7 | webpackConfig.resolve.extensions.push(".wasm"); 8 | 9 | webpackConfig.module.rules.forEach((rule) => { 10 | (rule.oneOf || []).forEach((oneOf) => { 11 | if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) { 12 | oneOf.exclude.push(wasmExtensionRegExp); 13 | } 14 | }); 15 | }); 16 | 17 | const wasmLoader = { 18 | test: /\.wasm$/, 19 | include: /node_modules\/(bridge|token-bridge)/, 20 | loaders: ["wasm-loader"], 21 | }; 22 | 23 | addBeforeLoader(webpackConfig, loaderByName("file-loader"), wasmLoader); 24 | 25 | return webpackConfig; 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://certusone.github.io/wormhole-nativeswap-example", 3 | "name": "NativeSwap", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@certusone/wormhole-sdk": "^0.1.6", 8 | "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", 9 | "@material-ui/core": "^4.12.2", 10 | "@material-ui/icons": "^4.11.2", 11 | "@material-ui/lab": "^4.0.0-alpha.60", 12 | "@metamask/detect-provider": "^1.2.0", 13 | "@terra-money/terra.js": "^2.0.14", 14 | "@terra-money/wallet-provider": "^2.2.0", 15 | "@types/node": "^16.11.19", 16 | "@types/react": "^17.0.38", 17 | "@types/react-dom": "^17.0.11", 18 | "@uniswap/smart-order-router": "^2.1.1", 19 | "@uniswap/v2-core": "^1.0.1", 20 | "@uniswap/v2-sdk": "^3.0.1", 21 | "@uniswap/v3-periphery": "1.3", 22 | "@uniswap/v3-sdk": "^3.8.1", 23 | "ethers": "^5.5.3", 24 | "jsbi": "^3.2.5", 25 | "notistack": "^1.0.10", 26 | "react": "^17.0.2", 27 | "react-dom": "^17.0.2", 28 | "react-scripts": "4.0.3", 29 | "typescript": "^4.4.2", 30 | "use-debounce": "^7.0.1" 31 | }, 32 | "scripts": { 33 | "start": "craco start", 34 | "build": "craco build", 35 | "test": "craco test", 36 | "eject": "react-scripts eject", 37 | "predeploy": "npm run build", 38 | "deploy": "gh-pages -d build" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@craco/craco": "^6.3.0", 60 | "gh-pages": "^3.2.3", 61 | "wasm-loader": "^1.3.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wormhole-foundation/example-wormhole-nativeswap/37649e5eaa61ca31c54feec719251c511c176875/react/public/favicon.ico -------------------------------------------------------------------------------- /react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | NativeSwap 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wormhole-foundation/example-wormhole-nativeswap/37649e5eaa61ca31c54feec719251c511c176875/react/public/logo192.png -------------------------------------------------------------------------------- /react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wormhole-foundation/example-wormhole-nativeswap/37649e5eaa61ca31c54feec719251c511c176875/react/public/logo512.png -------------------------------------------------------------------------------- /react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Cross Chain Swap", 3 | "name": "Cross Chain Swap Example Program", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Home from "./views/Home"; 2 | 3 | export default function App() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /react/src/abi/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | -------------------------------------------------------------------------------- /react/src/abi/erc20.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "constant": true, 5 | "inputs": [], 6 | "name": "name", 7 | "outputs": [ 8 | { 9 | "name": "", 10 | "type": "string" 11 | } 12 | ], 13 | "payable": false, 14 | "stateMutability": "view", 15 | "type": "function" 16 | }, 17 | { 18 | "constant": false, 19 | "inputs": [ 20 | { 21 | "name": "_spender", 22 | "type": "address" 23 | }, 24 | { 25 | "name": "_value", 26 | "type": "uint256" 27 | } 28 | ], 29 | "name": "approve", 30 | "outputs": [ 31 | { 32 | "name": "", 33 | "type": "bool" 34 | } 35 | ], 36 | "payable": false, 37 | "stateMutability": "nonpayable", 38 | "type": "function" 39 | }, 40 | { 41 | "constant": true, 42 | "inputs": [], 43 | "name": "totalSupply", 44 | "outputs": [ 45 | { 46 | "name": "", 47 | "type": "uint256" 48 | } 49 | ], 50 | "payable": false, 51 | "stateMutability": "view", 52 | "type": "function" 53 | }, 54 | { 55 | "constant": false, 56 | "inputs": [ 57 | { 58 | "name": "_from", 59 | "type": "address" 60 | }, 61 | { 62 | "name": "_to", 63 | "type": "address" 64 | }, 65 | { 66 | "name": "_value", 67 | "type": "uint256" 68 | } 69 | ], 70 | "name": "transferFrom", 71 | "outputs": [ 72 | { 73 | "name": "", 74 | "type": "bool" 75 | } 76 | ], 77 | "payable": false, 78 | "stateMutability": "nonpayable", 79 | "type": "function" 80 | }, 81 | { 82 | "constant": true, 83 | "inputs": [], 84 | "name": "decimals", 85 | "outputs": [ 86 | { 87 | "name": "", 88 | "type": "uint8" 89 | } 90 | ], 91 | "payable": false, 92 | "stateMutability": "view", 93 | "type": "function" 94 | }, 95 | { 96 | "constant": true, 97 | "inputs": [ 98 | { 99 | "name": "_owner", 100 | "type": "address" 101 | } 102 | ], 103 | "name": "balanceOf", 104 | "outputs": [ 105 | { 106 | "name": "balance", 107 | "type": "uint256" 108 | } 109 | ], 110 | "payable": false, 111 | "stateMutability": "view", 112 | "type": "function" 113 | }, 114 | { 115 | "constant": true, 116 | "inputs": [], 117 | "name": "symbol", 118 | "outputs": [ 119 | { 120 | "name": "", 121 | "type": "string" 122 | } 123 | ], 124 | "payable": false, 125 | "stateMutability": "view", 126 | "type": "function" 127 | }, 128 | { 129 | "constant": false, 130 | "inputs": [ 131 | { 132 | "name": "_to", 133 | "type": "address" 134 | }, 135 | { 136 | "name": "_value", 137 | "type": "uint256" 138 | } 139 | ], 140 | "name": "transfer", 141 | "outputs": [ 142 | { 143 | "name": "", 144 | "type": "bool" 145 | } 146 | ], 147 | "payable": false, 148 | "stateMutability": "nonpayable", 149 | "type": "function" 150 | }, 151 | { 152 | "constant": true, 153 | "inputs": [ 154 | { 155 | "name": "_owner", 156 | "type": "address" 157 | }, 158 | { 159 | "name": "_spender", 160 | "type": "address" 161 | } 162 | ], 163 | "name": "allowance", 164 | "outputs": [ 165 | { 166 | "name": "", 167 | "type": "uint256" 168 | } 169 | ], 170 | "payable": false, 171 | "stateMutability": "view", 172 | "type": "function" 173 | }, 174 | { 175 | "payable": true, 176 | "stateMutability": "payable", 177 | "type": "fallback" 178 | }, 179 | { 180 | "anonymous": false, 181 | "inputs": [ 182 | { 183 | "indexed": true, 184 | "name": "owner", 185 | "type": "address" 186 | }, 187 | { 188 | "indexed": true, 189 | "name": "spender", 190 | "type": "address" 191 | }, 192 | { 193 | "indexed": false, 194 | "name": "value", 195 | "type": "uint256" 196 | } 197 | ], 198 | "name": "Approval", 199 | "type": "event" 200 | }, 201 | { 202 | "anonymous": false, 203 | "inputs": [ 204 | { 205 | "indexed": true, 206 | "name": "from", 207 | "type": "address" 208 | }, 209 | { 210 | "indexed": true, 211 | "name": "to", 212 | "type": "address" 213 | }, 214 | { 215 | "indexed": false, 216 | "name": "value", 217 | "type": "uint256" 218 | } 219 | ], 220 | "name": "Transfer", 221 | "type": "event" 222 | } 223 | ] 224 | } -------------------------------------------------------------------------------- /react/src/addresses/.gitignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | *.js 3 | -------------------------------------------------------------------------------- /react/src/components/ButtonWithLoader.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | CircularProgress, 4 | makeStyles, 5 | Typography, 6 | } from "@material-ui/core"; 7 | import { ReactChild } from "react"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | root: { 11 | position: "relative", 12 | }, 13 | button: { 14 | marginTop: theme.spacing(2), 15 | textTransform: "none", 16 | width: "100%", 17 | }, 18 | loader: { 19 | position: "absolute", 20 | bottom: 0, 21 | left: "50%", 22 | marginLeft: -12, 23 | marginBottom: 6, 24 | }, 25 | error: { 26 | marginTop: theme.spacing(1), 27 | textAlign: "center", 28 | }, 29 | })); 30 | 31 | export default function ButtonWithLoader({ 32 | disabled, 33 | onClick, 34 | showLoader, 35 | error, 36 | children, 37 | className, 38 | }: { 39 | disabled?: boolean; 40 | onClick: () => void; 41 | showLoader?: boolean; 42 | error?: string; 43 | children: ReactChild; 44 | className?: string; 45 | }) { 46 | const classes = useStyles(); 47 | return ( 48 | <> 49 |
50 | 59 | {showLoader ? ( 60 | 65 | ) : null} 66 |
67 | {error ? ( 68 | 69 | {error} 70 | 71 | ) : null} 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /react/src/components/CircleLoader.tsx: -------------------------------------------------------------------------------- 1 | import "../css/CircleLoader.css"; 2 | 3 | export default function CircleLoader() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /react/src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import { Typography } from "@material-ui/core"; 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static getDerivedStateFromError(error) { 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | console.error(error, errorInfo); 16 | } 17 | 18 | render() { 19 | if (this.state.hasError) { 20 | return ( 21 | 22 | "An unexpected error has occurred. Please refresh the page." 23 | 24 | ); 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /react/src/components/EthereumSignerKey.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@material-ui/core"; 2 | import { useEthereumProvider } from "../contexts/EthereumProviderContext"; 3 | import ToggleConnectedButton from "./ToggleConnectedButton"; 4 | 5 | const EthereumSignerKey = () => { 6 | const { connect, disconnect, signerAddress, providerError } = 7 | useEthereumProvider(); 8 | return ( 9 | <> 10 | 16 | {providerError ? ( 17 | 18 | {providerError} 19 | 20 | ) : null} 21 | 22 | ); 23 | }; 24 | 25 | export default EthereumSignerKey; 26 | -------------------------------------------------------------------------------- /react/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, makeStyles, Typography } from "@material-ui/core"; 2 | import Discord from "../icons/Discord.svg"; 3 | import Docs from "../icons/Docs.svg"; 4 | import Github from "../icons/Github.svg"; 5 | import Medium from "../icons/Medium.svg"; 6 | import Telegram from "../icons/Telegram.svg"; 7 | import Twitter from "../icons/Twitter.svg"; 8 | import Wormhole from "../icons/wormhole_logo.svg"; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | footer: { 12 | margin: theme.spacing(2, 0, 2), 13 | textAlign: "center", 14 | }, 15 | socialIcon: { 16 | "& img": { 17 | height: 24, 18 | width: 24, 19 | }, 20 | }, 21 | builtWithContainer: { 22 | alignItems: "center", 23 | justifyContent: "center", 24 | opacity: 0.5, 25 | marginTop: theme.spacing(1), 26 | }, 27 | wormholeIcon: { 28 | height: 48, 29 | width: 192, 30 | filter: "contrast(0)", 31 | transition: "filter 0.5s", 32 | "&:hover": { 33 | filter: "contrast(1)", 34 | }, 35 | verticalAlign: "middle", 36 | marginRight: theme.spacing(1), 37 | }, 38 | })); 39 | 40 | export default function Footer() { 41 | const classes = useStyles(); 42 | return ( 43 |
44 |
45 | 51 | Docs 52 | 53 | 59 | Discord 60 | 61 | 67 | Github 68 | 69 | 75 | Medium 76 | 77 | 83 | Telegram 84 | 85 | 91 | Twitter 92 | 93 |
94 |
95 | 100 | Wormhole 101 | 102 |
103 |
104 | Open Source 105 | Built with ❤ 106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /react/src/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogContent, 5 | DialogTitle, 6 | InputAdornment, 7 | TextField, 8 | } from "@material-ui/core"; 9 | import SettingsIcon from "@material-ui/icons/Settings"; 10 | import { makeStyles } from "@material-ui/styles"; 11 | import { useState } from "react"; 12 | 13 | const useStyles = makeStyles({ 14 | topScrollPaper: { 15 | alignItems: "flex-start", 16 | }, 17 | topPaperScrollBody: { 18 | verticalAlign: "top", 19 | }, 20 | button: { 21 | float: "right", 22 | "&:hover": { 23 | backgroundColor: "transparent", 24 | }, 25 | }, 26 | }); 27 | 28 | const clamp = (value: number, min: number, max: number) => { 29 | if (isNaN(value)) { 30 | return value; 31 | } 32 | return Math.min(Math.max(min, value), max); 33 | }; 34 | 35 | export default function Settings({ 36 | disabled, 37 | slippage, 38 | deadline, 39 | onSlippageChange, 40 | onDeadlineChange, 41 | }: { 42 | disabled: boolean; 43 | slippage: string; 44 | deadline: string; 45 | onSlippageChange: (slippage: string) => void; 46 | onDeadlineChange: (deadline: string) => void; 47 | }) { 48 | const classes = useStyles(); 49 | const [dialogIsOpen, setDialogIsOpen] = useState(false); 50 | 51 | const dialog = ( 52 | setDialogIsOpen(false)} 56 | maxWidth="xs" 57 | scroll="paper" 58 | > 59 | Transaction Settings 60 | 61 | %, 68 | }} 69 | margin="normal" 70 | type="number" 71 | onChange={(event) => { 72 | onSlippageChange( 73 | event.target.value === "" 74 | ? "" 75 | : clamp(parseFloat(event.target.value), 0, 100).toString() 76 | ); 77 | }} 78 | > 79 | minutes 87 | ), 88 | }} 89 | margin="normal" 90 | type="number" 91 | onChange={(event) => { 92 | onDeadlineChange( 93 | event.target.value === "" 94 | ? "" 95 | : clamp(parseFloat(event.target.value), 1, 100).toString() 96 | ); 97 | }} 98 | > 99 | 100 | 101 | ); 102 | 103 | return ( 104 |
105 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /react/src/components/SwapProgress.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChainId, 3 | CHAIN_ID_AVAX, 4 | CHAIN_ID_POLYGON, 5 | isEVMChain, 6 | } from "@certusone/wormhole-sdk"; 7 | import { LinearProgress, makeStyles, Typography } from "@material-ui/core"; 8 | import { useEffect, useState } from "react"; 9 | import { useEthereumProvider } from "../contexts/EthereumProviderContext"; 10 | import { getChainName } from "../utils/consts"; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | root: { 14 | marginTop: theme.spacing(2), 15 | textAlign: "center", 16 | }, 17 | message: { 18 | marginTop: theme.spacing(1), 19 | }, 20 | })); 21 | 22 | export default function TransactionProgress({ 23 | chainId, 24 | txBlockNumber, 25 | hasSignedVAA, 26 | isTargetSwapComplete, 27 | }: { 28 | chainId: ChainId; 29 | txBlockNumber: number | undefined; 30 | hasSignedVAA: boolean; 31 | isTargetSwapComplete: boolean; 32 | }) { 33 | const classes = useStyles(); 34 | const { provider } = useEthereumProvider(); 35 | const [currentBlock, setCurrentBlock] = useState(0); 36 | useEffect(() => { 37 | if (hasSignedVAA || !txBlockNumber) return; 38 | if (isEVMChain(chainId) && provider) { 39 | let cancelled = false; 40 | (async () => { 41 | while (!cancelled) { 42 | await new Promise((resolve) => setTimeout(resolve, 500)); 43 | try { 44 | const newBlock = await provider.getBlockNumber(); 45 | if (!cancelled) { 46 | setCurrentBlock(newBlock); 47 | } 48 | } catch (e) { 49 | console.error(e); 50 | } 51 | } 52 | })(); 53 | return () => { 54 | cancelled = true; 55 | }; 56 | } 57 | }, [hasSignedVAA, chainId, provider, txBlockNumber]); 58 | let blockDiff = 59 | txBlockNumber !== undefined && txBlockNumber && currentBlock 60 | ? currentBlock - txBlockNumber 61 | : 0; 62 | const expectedBlocks = 63 | chainId === CHAIN_ID_POLYGON ? 512 : CHAIN_ID_AVAX ? 1 : 15; 64 | blockDiff = Math.min(Math.max(blockDiff, 0), expectedBlocks); 65 | let value; 66 | let valueBuffer; 67 | let message; 68 | if (!hasSignedVAA) { 69 | value = (blockDiff / expectedBlocks) * 50; 70 | valueBuffer = 50; 71 | message = `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${getChainName( 72 | chainId 73 | )}...`; 74 | } else if (!isTargetSwapComplete) { 75 | value = 50; 76 | valueBuffer = 100; 77 | message = "Waiting for relayer to complete swap..."; 78 | } else { 79 | value = 100; 80 | valueBuffer = 100; 81 | message = "Success!"; 82 | } 83 | return ( 84 |
85 | 90 | 91 | {message} 92 | 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /react/src/components/TerraWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useTerraWallet } from "../contexts/TerraWalletContext"; 2 | import ToggleConnectedButton from "./ToggleConnectedButton"; 3 | 4 | const TerraWalletKey = () => { 5 | const { connect, disconnect, connected, wallet } = useTerraWallet(); 6 | const pk = 7 | (wallet && 8 | wallet.wallets && 9 | wallet.wallets.length > 0 && 10 | wallet.wallets[0].terraAddress) || 11 | ""; 12 | return ( 13 | 19 | ); 20 | }; 21 | 22 | export default TerraWalletKey; 23 | -------------------------------------------------------------------------------- /react/src/components/ToggleConnectedButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, makeStyles, Tooltip } from "@material-ui/core"; 2 | 3 | const useStyles = makeStyles((theme) => ({ 4 | button: { 5 | display: "block", 6 | margin: `${theme.spacing(1)}px auto`, 7 | width: "100%", 8 | maxWidth: 400, 9 | }, 10 | })); 11 | 12 | const ToggleConnectedButton = ({ 13 | connect, 14 | disconnect, 15 | connected, 16 | pk, 17 | }: { 18 | connect(): any; 19 | disconnect(): any; 20 | connected: boolean; 21 | pk: string; 22 | }) => { 23 | const classes = useStyles(); 24 | const is0x = pk.startsWith("0x"); 25 | return connected ? ( 26 | 27 | 37 | 38 | ) : ( 39 | 48 | ); 49 | }; 50 | 51 | export default ToggleConnectedButton; 52 | -------------------------------------------------------------------------------- /react/src/components/TokenSelect.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ListItemIcon, 3 | ListItemText, 4 | makeStyles, 5 | MenuItem, 6 | TextField, 7 | } from "@material-ui/core"; 8 | import { 9 | AVAX_TOKEN_INFO, 10 | BNB_TOKEN_INFO, 11 | ETH_TOKEN_INFO, 12 | MATIC_TOKEN_INFO, 13 | TokenInfo, 14 | UST_TOKEN_INFO, 15 | } from "../utils/consts"; 16 | 17 | import ethIcon from "../icons/eth.svg"; 18 | import polygonIcon from "../icons/polygon.svg"; 19 | import terraIcon from "../icons/terra.svg"; 20 | import bscIcon from "../icons/bsc.svg"; 21 | import avaxIcon from "../icons/avax.svg"; 22 | 23 | const useStyles = makeStyles((theme) => ({ 24 | select: { 25 | "& .MuiSelect-root": { 26 | display: "flex", 27 | alignItems: "center", 28 | }, 29 | }, 30 | listItemIcon: { 31 | minWidth: 40, 32 | }, 33 | icon: { 34 | height: 24, 35 | maxWidth: 24, 36 | }, 37 | })); 38 | 39 | const getLogo = (name: string) => { 40 | switch (name) { 41 | case ETH_TOKEN_INFO.name: 42 | return ethIcon; 43 | case MATIC_TOKEN_INFO.name: 44 | return polygonIcon; 45 | case UST_TOKEN_INFO.name: 46 | return terraIcon; 47 | case AVAX_TOKEN_INFO.name: 48 | return avaxIcon; 49 | case BNB_TOKEN_INFO.name: 50 | return bscIcon; 51 | default: 52 | return ""; 53 | } 54 | }; 55 | 56 | const createTokenMenuItem = ({ name }: TokenInfo, classes: any) => ( 57 | 58 | 59 | {name} 60 | 61 | {name} 62 | 63 | ); 64 | 65 | interface TokenSelectProps { 66 | tokens: TokenInfo[]; 67 | value: string; 68 | onChange: (event: any) => void; 69 | disabled: boolean; 70 | } 71 | 72 | export default function TokenSelect({ 73 | tokens, 74 | value, 75 | onChange, 76 | disabled, 77 | }: TokenSelectProps) { 78 | const classes = useStyles(); 79 | 80 | return ( 81 | 90 | {tokens.map((token) => createTokenMenuItem(token, classes))} 91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /react/src/contexts/EthereumProviderContext.tsx: -------------------------------------------------------------------------------- 1 | import detectEthereumProvider from "@metamask/detect-provider"; 2 | import { BigNumber, ethers } from "ethers"; 3 | import React, { 4 | ReactChildren, 5 | useCallback, 6 | useContext, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | 11 | export type Provider = ethers.providers.Web3Provider | undefined; 12 | export type Signer = ethers.Signer | undefined; 13 | 14 | interface IEthereumProviderContext { 15 | connect(): void; 16 | disconnect(): void; 17 | provider: Provider; 18 | chainId: number | undefined; 19 | signer: Signer; 20 | signerAddress: string | undefined; 21 | providerError: string | null; 22 | } 23 | 24 | const EthereumProviderContext = React.createContext({ 25 | connect: () => {}, 26 | disconnect: () => {}, 27 | provider: undefined, 28 | chainId: undefined, 29 | signer: undefined, 30 | signerAddress: undefined, 31 | providerError: null, 32 | }); 33 | export const EthereumProviderProvider = ({ 34 | children, 35 | }: { 36 | children: ReactChildren; 37 | }) => { 38 | const [providerError, setProviderError] = useState(null); 39 | const [provider, setProvider] = useState(undefined); 40 | const [chainId, setChainId] = useState(undefined); 41 | const [signer, setSigner] = useState(undefined); 42 | const [signerAddress, setSignerAddress] = useState( 43 | undefined 44 | ); 45 | const connect = useCallback(() => { 46 | setProviderError(null); 47 | detectEthereumProvider() 48 | .then((detectedProvider) => { 49 | if (detectedProvider) { 50 | const provider = new ethers.providers.Web3Provider( 51 | // @ts-ignore 52 | detectedProvider, 53 | "any" 54 | ); 55 | provider 56 | .send("eth_requestAccounts", []) 57 | .then(() => { 58 | setProviderError(null); 59 | setProvider(provider); 60 | provider 61 | .getNetwork() 62 | .then((network) => { 63 | setChainId(network.chainId); 64 | }) 65 | .catch(() => { 66 | setProviderError( 67 | "An error occurred while getting the network" 68 | ); 69 | }); 70 | const signer = provider.getSigner(); 71 | setSigner(signer); 72 | signer 73 | .getAddress() 74 | .then((address) => { 75 | setSignerAddress(address); 76 | }) 77 | .catch(() => { 78 | setProviderError( 79 | "An error occurred while getting the signer address" 80 | ); 81 | }); 82 | // TODO: try using ethers directly 83 | // @ts-ignore 84 | if (detectedProvider && detectedProvider.on) { 85 | // @ts-ignore 86 | detectedProvider.on("chainChanged", (chainId) => { 87 | try { 88 | setChainId(BigNumber.from(chainId).toNumber()); 89 | } catch (e) {} 90 | }); 91 | // @ts-ignore 92 | detectedProvider.on("accountsChanged", (accounts) => { 93 | try { 94 | const signer = provider.getSigner(); 95 | setSigner(signer); 96 | signer 97 | .getAddress() 98 | .then((address) => { 99 | setSignerAddress(address); 100 | }) 101 | .catch(() => { 102 | setProviderError( 103 | "An error occurred while getting the signer address" 104 | ); 105 | }); 106 | } catch (e) {} 107 | }); 108 | } 109 | }) 110 | .catch(() => { 111 | setProviderError( 112 | "An error occurred while requesting eth accounts" 113 | ); 114 | }); 115 | } else { 116 | setProviderError("Please install MetaMask"); 117 | } 118 | }) 119 | .catch(() => { 120 | setProviderError("Please install MetaMask"); 121 | }); 122 | }, []); 123 | const disconnect = useCallback(() => { 124 | setProviderError(null); 125 | setProvider(undefined); 126 | setChainId(undefined); 127 | setSigner(undefined); 128 | setSignerAddress(undefined); 129 | }, []); 130 | const contextValue = useMemo( 131 | () => ({ 132 | connect, 133 | disconnect, 134 | provider, 135 | chainId, 136 | signer, 137 | signerAddress, 138 | providerError, 139 | }), 140 | [ 141 | connect, 142 | disconnect, 143 | provider, 144 | chainId, 145 | signer, 146 | signerAddress, 147 | providerError, 148 | ] 149 | ); 150 | return ( 151 | 152 | {children} 153 | 154 | ); 155 | }; 156 | export const useEthereumProvider = () => { 157 | return useContext(EthereumProviderContext); 158 | }; 159 | -------------------------------------------------------------------------------- /react/src/contexts/TerraWalletContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | NetworkInfo, 3 | Wallet, 4 | WalletProvider, 5 | useWallet, 6 | } from "@terra-money/wallet-provider"; 7 | import React, { 8 | ReactChildren, 9 | useCallback, 10 | useContext, 11 | useMemo, 12 | useState, 13 | } from "react"; 14 | 15 | const testnet: NetworkInfo = { 16 | name: "testnet", 17 | chainID: "bombay-12", 18 | lcd: "https://bombay-lcd.terra.dev", 19 | }; 20 | 21 | const walletConnectChainIds: Record = { 22 | 0: testnet, 23 | }; 24 | 25 | interface ITerraWalletContext { 26 | connect(): void; 27 | disconnect(): void; 28 | connected: boolean; 29 | wallet: any; 30 | } 31 | 32 | const TerraWalletContext = React.createContext({ 33 | connect: () => {}, 34 | disconnect: () => {}, 35 | connected: false, 36 | wallet: null, 37 | }); 38 | 39 | export const TerraWalletWrapper = ({ 40 | children, 41 | }: { 42 | children: ReactChildren; 43 | }) => { 44 | // TODO: Use wallet instead of useConnectedWallet. 45 | const terraWallet = useWallet(); 46 | const [, setWallet] = useState(undefined); 47 | const [connected, setConnected] = useState(false); 48 | 49 | const connect = useCallback(() => { 50 | const CHROME_EXTENSION = 1; 51 | if (terraWallet) { 52 | terraWallet.connect(terraWallet.availableConnectTypes[CHROME_EXTENSION]); 53 | setWallet(terraWallet); 54 | setConnected(true); 55 | } 56 | }, [terraWallet]); 57 | 58 | const disconnect = useCallback(() => { 59 | setConnected(false); 60 | setWallet(undefined); 61 | }, []); 62 | 63 | const contextValue = useMemo( 64 | () => ({ 65 | connect, 66 | disconnect, 67 | connected, 68 | wallet: terraWallet, 69 | }), 70 | [connect, disconnect, connected, terraWallet] 71 | ); 72 | 73 | return ( 74 | 75 | {children} 76 | 77 | ); 78 | }; 79 | 80 | export const TerraWalletProvider = ({ 81 | children, 82 | }: { 83 | children: ReactChildren; 84 | }) => { 85 | return ( 86 | 90 | {children} 91 | 92 | ); 93 | }; 94 | 95 | export const useTerraWallet = () => { 96 | return useContext(TerraWalletContext); 97 | }; 98 | -------------------------------------------------------------------------------- /react/src/css/CircleLoader.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --basis: linear-gradient( 3 | 160deg, 4 | rgba(69, 74, 117, 0.473) 0%, 5 | rgba(98, 104, 143, 0.445) 100% 6 | ), 7 | linear-gradient( 8 | 45deg, 9 | rgba(153, 69, 255, 0.411) 0%, 10 | rgba(0, 209, 139, 0.404) 100% 11 | ); 12 | /* --gradient1: rgb(26, 212, 150); */ 13 | /* --gradient2: rgb(176, 139, 221); */ 14 | --gradient1: rgb(117, 228, 187); 15 | --gradient2: rgb(193, 164, 230); 16 | } 17 | 18 | #loaderContainer { 19 | display: flex; 20 | width: max-content; 21 | height: max-content; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | @keyframes rotation { 26 | 0% { 27 | transform: rotate(0deg); 28 | } 29 | 100% { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | @keyframes switch { 34 | 0% { 35 | top: 50%; 36 | transform: translateX(-50%) translateY(-50%); 37 | width: 200px; 38 | height: 200px; 39 | box-shadow: 0 -130px 0 -75px var(--gradient1); 40 | } 41 | 25% { 42 | top: 50%; 43 | transform: translateX(-50%) translateY(-50%); 44 | width: 200px; 45 | height: 200px; 46 | box-shadow: 0 -130px 0 -75px var(--gradient1); 47 | } 48 | 50% { 49 | top: calc(100% - 55px); 50 | width: 50px; 51 | height: 50px; 52 | box-shadow: 0 -130px 0 75px var(--gradient1); 53 | transform: translateX(-50%) translateY(0); 54 | } 55 | 75% { 56 | top: calc(100% - 55px); 57 | width: 50px; 58 | height: 50px; 59 | box-shadow: 0 -130px 0 75px var(--gradient1); 60 | transform: translateX(-50%) translateY(0); 61 | } 62 | 100% { 63 | top: 50%; 64 | transform: translateX(-50%) translateY(-50%); 65 | width: 200px; 66 | height: 200px; 67 | box-shadow: 0 -130px 0 -75px var(--gradient1); 68 | } 69 | } 70 | #circle { 71 | width: 325px; 72 | height: 325px; 73 | display: block; 74 | background: var(--basis); 75 | border-radius: 500%; 76 | position: relative; 77 | animation: rotation 2s linear infinite; 78 | } 79 | #inner { 80 | width: 200px; 81 | height: 200px; 82 | background: var(--gradient2); 83 | position: absolute; 84 | left: 50%; 85 | top: 50%; 86 | transform: translateX(-50%) translateY(-50%); 87 | border-radius: 100%; 88 | box-shadow: 0 -130px 0 -75px #222; 89 | animation: switch 8s ease-in-out infinite; 90 | } 91 | -------------------------------------------------------------------------------- /react/src/hooks/useIsWalletReady.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, CHAIN_ID_TERRA, isEVMChain } from "@certusone/wormhole-sdk"; 2 | import { hexlify, hexStripZeros } from "@ethersproject/bytes"; 3 | import { useConnectedWallet } from "@terra-money/wallet-provider"; 4 | import { useCallback, useMemo } from "react"; 5 | import { useEthereumProvider } from "../contexts/EthereumProviderContext"; 6 | import { getEvmChainId } from "../utils/consts"; 7 | 8 | const createWalletStatus = ( 9 | isReady: boolean, 10 | statusMessage: string = "", 11 | forceNetworkSwitch: () => void, 12 | walletAddress?: string 13 | ) => ({ 14 | isReady, 15 | statusMessage, 16 | forceNetworkSwitch, 17 | walletAddress, 18 | }); 19 | 20 | function useIsWalletReady( 21 | chainId: ChainId, 22 | enableNetworkAutoswitch: boolean = true 23 | ): { 24 | isReady: boolean; 25 | statusMessage: string; 26 | walletAddress?: string; 27 | forceNetworkSwitch: () => void; 28 | } { 29 | const autoSwitch = enableNetworkAutoswitch; 30 | const terraWallet = useConnectedWallet(); 31 | const hasTerraWallet = !!terraWallet; 32 | const { 33 | provider, 34 | signerAddress, 35 | chainId: evmChainId, 36 | } = useEthereumProvider(); 37 | const hasEthInfo = !!provider && !!signerAddress; 38 | const correctEvmNetwork = getEvmChainId(chainId); 39 | const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork; 40 | 41 | const forceNetworkSwitch = useCallback(() => { 42 | if (provider && correctEvmNetwork) { 43 | if (!isEVMChain(chainId)) { 44 | return; 45 | } 46 | try { 47 | provider.send("wallet_switchEthereumChain", [ 48 | { chainId: hexStripZeros(hexlify(correctEvmNetwork)) }, 49 | ]); 50 | } catch (e) {} 51 | } 52 | }, [provider, correctEvmNetwork, chainId]); 53 | 54 | return useMemo(() => { 55 | if ( 56 | chainId === CHAIN_ID_TERRA && 57 | hasTerraWallet && 58 | terraWallet?.walletAddress 59 | ) { 60 | // TODO: terraWallet does not update on wallet changes 61 | return createWalletStatus( 62 | true, 63 | undefined, 64 | forceNetworkSwitch, 65 | terraWallet.walletAddress 66 | ); 67 | } 68 | if (isEVMChain(chainId) && hasEthInfo && signerAddress) { 69 | if (hasCorrectEvmNetwork) { 70 | return createWalletStatus( 71 | true, 72 | undefined, 73 | forceNetworkSwitch, 74 | signerAddress 75 | ); 76 | } else { 77 | if (provider && correctEvmNetwork && autoSwitch) { 78 | forceNetworkSwitch(); 79 | } 80 | return createWalletStatus( 81 | false, 82 | `Wallet is not connected to testnet. Expected Chain ID: ${correctEvmNetwork}`, 83 | forceNetworkSwitch, 84 | undefined 85 | ); 86 | } 87 | } 88 | 89 | return createWalletStatus( 90 | false, 91 | "Wallet not connected", 92 | forceNetworkSwitch, 93 | undefined 94 | ); 95 | }, [ 96 | chainId, 97 | autoSwitch, 98 | forceNetworkSwitch, 99 | hasTerraWallet, 100 | hasEthInfo, 101 | correctEvmNetwork, 102 | hasCorrectEvmNetwork, 103 | provider, 104 | signerAddress, 105 | terraWallet, 106 | ]); 107 | } 108 | 109 | export default useIsWalletReady; 110 | -------------------------------------------------------------------------------- /react/src/icons/Discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react/src/icons/Docs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /react/src/icons/Github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react/src/icons/Medium.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /react/src/icons/Telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react/src/icons/Twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react/src/icons/avax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /react/src/icons/bsc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /react/src/icons/eth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /react/src/icons/polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /react/src/icons/terra.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 13 | 23 | 24 | -------------------------------------------------------------------------------- /react/src/icons/wormhole_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /react/src/index.js: -------------------------------------------------------------------------------- 1 | import { CssBaseline } from "@material-ui/core"; 2 | import { ThemeProvider } from "@material-ui/core/styles"; 3 | import { SnackbarProvider } from "notistack"; 4 | import ReactDOM from "react-dom"; 5 | import App from "./App"; 6 | import ErrorBoundary from "./components/ErrorBoundary"; 7 | import { EthereumProviderProvider } from "./contexts/EthereumProviderContext"; 8 | import { TerraWalletProvider } from "./contexts/TerraWalletContext"; 9 | import { theme } from "./muiTheme"; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | , 25 | document.getElementById("root") 26 | ); 27 | -------------------------------------------------------------------------------- /react/src/muiTheme.js: -------------------------------------------------------------------------------- 1 | import { createTheme, responsiveFontSizes } from "@material-ui/core"; 2 | 3 | export const COLORS = { 4 | blue: "#1975e6", 5 | blueWithTransparency: "rgba(25, 117, 230, 0.8)", 6 | gray: "#4e4e54", 7 | green: "#0ac2af", 8 | greenWithTransparency: "rgba(10, 194, 175, 0.8)", 9 | lightGreen: "rgba(51, 242, 223, 1)", 10 | lightBlue: "#83b9fc", 11 | nearBlack: "#000008", 12 | nearBlackWithMinorTransparency: "rgba(0,0,0,.25)", 13 | red: "#aa0818", 14 | darkRed: "#810612", 15 | }; 16 | 17 | export const theme = responsiveFontSizes( 18 | createTheme({ 19 | palette: { 20 | type: "dark", 21 | background: { 22 | default: COLORS.nearBlack, 23 | paper: COLORS.nearBlack, 24 | }, 25 | divider: COLORS.gray, 26 | text: { 27 | primary: "rgba(255,255,255,0.98)", 28 | }, 29 | primary: { 30 | main: COLORS.blueWithTransparency, 31 | light: COLORS.lightBlue, 32 | }, 33 | secondary: { 34 | main: COLORS.greenWithTransparency, 35 | light: COLORS.lightGreen, 36 | }, 37 | error: { 38 | main: COLORS.red, 39 | }, 40 | }, 41 | typography: { 42 | fontFamily: "'Sora', sans-serif", 43 | h1: { 44 | fontWeight: "200", 45 | }, 46 | h2: { 47 | fontWeight: "200", 48 | }, 49 | h4: { 50 | fontWeight: "500", 51 | }, 52 | }, 53 | overrides: { 54 | MuiCssBaseline: { 55 | "@global": { 56 | "*": { 57 | scrollbarWidth: "thin", 58 | scrollbarColor: `${COLORS.gray} ${COLORS.nearBlackWithMinorTransparency}`, 59 | }, 60 | "*::-webkit-scrollbar": { 61 | width: "8px", 62 | height: "8px", 63 | backgroundColor: COLORS.nearBlackWithMinorTransparency, 64 | }, 65 | "*::-webkit-scrollbar-thumb": { 66 | backgroundColor: COLORS.gray, 67 | borderRadius: "4px", 68 | }, 69 | "*::-webkit-scrollbar-corner": { 70 | // this hides an annoying white box which appears when both scrollbars are present 71 | backgroundColor: "transparent", 72 | }, 73 | }, 74 | }, 75 | MuiAccordion: { 76 | root: { 77 | backgroundColor: COLORS.nearBlackWithMinorTransparency, 78 | "&:before": { 79 | display: "none", 80 | }, 81 | }, 82 | rounded: { 83 | "&:first-child": { 84 | borderTopLeftRadius: "16px", 85 | borderTopRightRadius: "16px", 86 | }, 87 | "&:last-child": { 88 | borderBottomLeftRadius: "16px", 89 | borderBottomRightRadius: "16px", 90 | }, 91 | }, 92 | }, 93 | MuiAlert: { 94 | root: { 95 | borderRadius: "8px", 96 | border: "1px solid", 97 | }, 98 | }, 99 | MuiButton: { 100 | root: { 101 | borderRadius: "5px", 102 | textTransform: "none", 103 | }, 104 | }, 105 | MuiLink: { 106 | root: { 107 | color: COLORS.lightBlue, 108 | }, 109 | }, 110 | MuiPaper: { 111 | rounded: { 112 | borderRadius: "16px", 113 | }, 114 | }, 115 | MuiStepper: { 116 | root: { 117 | backgroundColor: "transparent", 118 | padding: 0, 119 | }, 120 | }, 121 | MuiStep: { 122 | root: { 123 | backgroundColor: COLORS.nearBlackWithMinorTransparency, 124 | borderRadius: "16px", 125 | padding: 16, 126 | }, 127 | }, 128 | MuiStepConnector: { 129 | lineVertical: { 130 | borderLeftWidth: 0, 131 | }, 132 | }, 133 | MuiStepContent: { 134 | root: { 135 | borderLeftWidth: 0, 136 | }, 137 | }, 138 | MuiStepLabel: { 139 | label: { 140 | fontSize: 16, 141 | fontWeight: "300", 142 | "&.MuiStepLabel-active": { 143 | fontWeight: "300", 144 | }, 145 | "&.MuiStepLabel-completed": { 146 | fontWeight: "300", 147 | }, 148 | }, 149 | }, 150 | MuiTab: { 151 | root: { 152 | fontSize: 18, 153 | fontWeight: "300", 154 | padding: 12, 155 | textTransform: "none", 156 | }, 157 | }, 158 | }, 159 | }) 160 | ); 161 | -------------------------------------------------------------------------------- /react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /react/src/route/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /react/src/route/cross-quote.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | import { QuickswapRouter as MaticRouter } from "./quickswap"; 4 | import { UniswapV3Router as EthRouter } from "./uniswap-v3"; 5 | import { TerraUstTransfer as UstRouter } from "./terra-ust-transfer"; 6 | import { HurricaneswapRouter as AvaxRouter } from "./hurricaneswap"; 7 | import { PancakeswapRouter as BnbRouter } from "./pancakeswap"; 8 | import { 9 | ETH_TOKEN_INFO, 10 | MATIC_TOKEN_INFO, 11 | AVAX_TOKEN_INFO, 12 | BNB_TOKEN_INFO, 13 | UST_TOKEN_INFO, 14 | } from "../utils/consts"; 15 | import { addFixedAmounts, subtractFixedAmounts } from "../utils/math"; 16 | import { UstLocation } from "./generic"; 17 | import { 18 | ExactInParameters, 19 | ExactOutParameters, 20 | makeExactInParameters, 21 | makeExactOutParameters, 22 | } from "./uniswap-core"; 23 | import { 24 | ChainId, 25 | CHAIN_ID_ETH, 26 | CHAIN_ID_POLYGON, 27 | CHAIN_ID_AVAX, 28 | CHAIN_ID_BSC, 29 | CHAIN_ID_TERRA, 30 | } from "@certusone/wormhole-sdk"; 31 | 32 | export { PROTOCOL as PROTOCOL_UNISWAP_V2 } from "./uniswap-v2"; 33 | export { PROTOCOL as PROTOCOL_UNISWAP_V3 } from "./uniswap-v3"; 34 | export { PROTOCOL as PROTOCOL_TERRA_UST_TRANSFER } from "./terra-ust-transfer"; 35 | 36 | export const TERRA_UST = UST_TOKEN_INFO.address; 37 | 38 | export enum QuoteType { 39 | ExactIn = 1, 40 | ExactOut, 41 | } 42 | 43 | export function makeEvmProviderFromAddress(tokenAddress: string) { 44 | switch (tokenAddress) { 45 | case ETH_TOKEN_INFO.address: { 46 | const url = process.env.REACT_APP_GOERLI_PROVIDER; 47 | if (!url) { 48 | throw new Error("Could not find REACT_APP_GOERLI_PROVIDER"); 49 | } 50 | return new ethers.providers.StaticJsonRpcProvider(url); 51 | } 52 | case MATIC_TOKEN_INFO.address: { 53 | const url = process.env.REACT_APP_MUMBAI_PROVIDER; 54 | if (!url) { 55 | throw new Error("Could not find REACT_APP_MUMBAI_PROVIDER"); 56 | } 57 | return new ethers.providers.StaticJsonRpcProvider(url); 58 | } 59 | case AVAX_TOKEN_INFO.address: { 60 | const url = process.env.REACT_APP_FUJI_PROVIDER; 61 | if (!url) { 62 | throw new Error("Could not find REACT_APP_FUJI_PROVIDER"); 63 | } 64 | return new ethers.providers.StaticJsonRpcProvider(url); 65 | } 66 | case BNB_TOKEN_INFO.address: { 67 | const url = process.env.REACT_APP_BSC_PROVIDER; 68 | if (!url) { 69 | throw new Error("Could not find REACT_APP_BSC_PROVIDER"); 70 | } 71 | return new ethers.providers.StaticJsonRpcProvider(url); 72 | } 73 | default: { 74 | throw Error("unrecognized evm token address"); 75 | } 76 | } 77 | } 78 | 79 | export function getChainIdFromAddress(tokenAddress: string) { 80 | switch (tokenAddress) { 81 | case ETH_TOKEN_INFO.address: { 82 | return CHAIN_ID_ETH; 83 | } 84 | case MATIC_TOKEN_INFO.address: { 85 | return CHAIN_ID_POLYGON; 86 | } 87 | case AVAX_TOKEN_INFO.address: { 88 | return CHAIN_ID_AVAX; 89 | } 90 | case BNB_TOKEN_INFO.address: { 91 | return CHAIN_ID_BSC; 92 | } 93 | case UST_TOKEN_INFO.address: { 94 | return CHAIN_ID_TERRA; 95 | } 96 | default: { 97 | throw Error("unrecognized evm token address"); 98 | } 99 | } 100 | } 101 | 102 | async function makeRouter(tokenAddress: string, loc: UstLocation) { 103 | switch (tokenAddress) { 104 | case ETH_TOKEN_INFO.address: { 105 | const provider = makeEvmProviderFromAddress(tokenAddress); 106 | const router = new EthRouter(provider); 107 | await router.initialize(loc); 108 | return router; 109 | } 110 | case MATIC_TOKEN_INFO.address: { 111 | const provider = makeEvmProviderFromAddress(tokenAddress); 112 | const router = new MaticRouter(provider); 113 | await router.initialize(loc); 114 | return router; 115 | } 116 | case AVAX_TOKEN_INFO.address: { 117 | const provider = makeEvmProviderFromAddress(tokenAddress); 118 | const router = new AvaxRouter(provider); 119 | await router.initialize(loc); 120 | return router; 121 | } 122 | case BNB_TOKEN_INFO.address: { 123 | const provider = makeEvmProviderFromAddress(tokenAddress); 124 | const router = new BnbRouter(provider); 125 | await router.initialize(loc); 126 | return router; 127 | } 128 | case UST_TOKEN_INFO.address: { 129 | return new UstRouter(); 130 | } 131 | default: { 132 | throw Error("unrecognized chain id"); 133 | } 134 | } 135 | } 136 | 137 | function splitSlippageInHalf(totalSlippage: string): string { 138 | const divisor = ethers.FixedNumber.from("2"); 139 | return ethers.FixedNumber.from(totalSlippage) 140 | .divUnsafe(divisor) 141 | .round(4) 142 | .toString(); 143 | } 144 | 145 | export interface RelayerFee { 146 | amount: string; 147 | tokenAddress: string; 148 | } 149 | 150 | export interface ExactInCrossParameters { 151 | amountIn: string; 152 | ustAmountIn: string; 153 | minAmountOut: string; 154 | src: ExactInParameters | undefined; 155 | dst: ExactInParameters | undefined; 156 | relayerFee: RelayerFee; 157 | } 158 | 159 | export interface ExactOutCrossParameters { 160 | amountOut: string; 161 | ustAmountIn: string; 162 | maxAmountIn: string; 163 | src: ExactOutParameters | undefined; 164 | dst: ExactOutParameters | undefined; 165 | relayerFee: RelayerFee; 166 | } 167 | 168 | export class UniswapToUniswapQuoter { 169 | // tokens 170 | tokenInAddress: string; 171 | tokenOutAddress: string; 172 | 173 | // routers 174 | srcRouter: UstRouter | EthRouter | MaticRouter | AvaxRouter | BnbRouter; 175 | dstRouter: UstRouter | EthRouter | MaticRouter | AvaxRouter | BnbRouter; 176 | 177 | async initialize(tokenInAddress: string, tokenOutAddress: string) { 178 | if (tokenInAddress !== this.tokenInAddress) { 179 | this.tokenInAddress = tokenInAddress; 180 | this.srcRouter = await makeRouter(tokenInAddress, UstLocation.Out); 181 | } 182 | 183 | if (tokenOutAddress !== this.tokenOutAddress) { 184 | this.tokenOutAddress = tokenOutAddress; 185 | this.dstRouter = await makeRouter(tokenOutAddress, UstLocation.In); 186 | } 187 | } 188 | 189 | async computeAndVerifySrcPoolAddress(): Promise { 190 | return this.srcRouter.computeAndVerifyPoolAddress(); 191 | } 192 | 193 | async computeAndVerifyDstPoolAddress(): Promise { 194 | return this.dstRouter.computeAndVerifyPoolAddress(); 195 | } 196 | 197 | computeSwapSlippage(slippage: string): string { 198 | if (this.isSrcUst() || this.isDstUst()) { 199 | return slippage; 200 | } 201 | 202 | return splitSlippageInHalf(slippage); 203 | } 204 | 205 | getRelayerFee(amount: string): RelayerFee { 206 | if (this.isSrcUst()) { 207 | return { 208 | amount: this.srcRouter.computeUnitAmountOut(amount), 209 | tokenAddress: TERRA_UST, // TODO: make sure this is the right address for bridge transfer? 210 | }; 211 | } 212 | 213 | const relayerFee: RelayerFee = { 214 | amount: this.srcRouter.computeUnitAmountOut(amount), 215 | tokenAddress: this.srcRouter.getTokenOutAddress(), 216 | }; 217 | return relayerFee; 218 | } 219 | 220 | makeSrcExactInParameters( 221 | amountIn: string, 222 | minAmountOut: string 223 | ): ExactInParameters | undefined { 224 | if (this.isSrcUst()) { 225 | return undefined; 226 | } 227 | // @ts-ignore 228 | return makeExactInParameters(this.srcRouter, amountIn, minAmountOut); 229 | } 230 | 231 | makeDstExactInParameters( 232 | amountIn: string, 233 | minAmountOut: string 234 | ): ExactInParameters | undefined { 235 | if (this.isDstUst()) { 236 | return undefined; 237 | } 238 | // @ts-ignore 239 | return makeExactInParameters(this.dstRouter, amountIn, minAmountOut); 240 | } 241 | 242 | async computeExactInParameters( 243 | amountIn: string, 244 | slippage: string, 245 | relayerFeeUst: string 246 | ): Promise { 247 | const singleSlippage = this.computeSwapSlippage(slippage); 248 | 249 | // src quote 250 | const srcRouter = this.srcRouter; 251 | const srcMinAmountOut = await srcRouter.fetchExactInQuote( 252 | amountIn, 253 | singleSlippage 254 | ); 255 | 256 | // dst quote 257 | const dstRouter = this.dstRouter; 258 | const dstAmountIn = srcMinAmountOut; //srcRouter.formatAmountOut(srcMinAmountOut); 259 | if (Number(dstAmountIn) < Number(relayerFeeUst)) { 260 | throw Error( 261 | `srcAmountOut <= relayerFeeUst. ${dstAmountIn} vs ${relayerFeeUst}` 262 | ); 263 | } 264 | 265 | const dstAmountInAfterFee = subtractFixedAmounts( 266 | dstAmountIn, 267 | relayerFeeUst, 268 | dstRouter.getTokenInDecimals() 269 | ); 270 | 271 | const dstMinAmountOut = await dstRouter.fetchExactInQuote( 272 | dstAmountInAfterFee, 273 | singleSlippage 274 | ); 275 | 276 | // organize parameters 277 | const params: ExactInCrossParameters = { 278 | amountIn: amountIn, 279 | ustAmountIn: dstAmountInAfterFee, 280 | minAmountOut: dstMinAmountOut, 281 | src: this.makeSrcExactInParameters(amountIn, srcMinAmountOut), 282 | dst: this.makeDstExactInParameters(dstAmountInAfterFee, dstMinAmountOut), 283 | relayerFee: this.getRelayerFee(relayerFeeUst), 284 | }; 285 | return params; 286 | } 287 | 288 | makeSrcExactOutParameters( 289 | amountOut: string, 290 | maxAmountIn: string 291 | ): ExactOutParameters | undefined { 292 | if (this.isSrcUst()) { 293 | return undefined; 294 | } 295 | // @ts-ignore 296 | return makeExactOutParameters(this.srcRouter, amountOut, maxAmountIn); 297 | } 298 | 299 | makeDstExactOutParameters( 300 | amountOut: string, 301 | maxAmountIn: string 302 | ): ExactOutParameters | undefined { 303 | if (this.isDstUst()) { 304 | return undefined; 305 | } 306 | // @ts-ignore 307 | return makeExactOutParameters(this.dstRouter, amountOut, maxAmountIn); 308 | } 309 | 310 | async computeExactOutParameters( 311 | amountOut: string, 312 | slippage: string, 313 | relayerFeeUst: string 314 | ): Promise { 315 | const singleSlippage = splitSlippageInHalf(slippage); 316 | 317 | // dst quote first 318 | const dstRouter = this.dstRouter; 319 | const dstMaxAmountIn = await dstRouter.fetchExactOutQuote( 320 | amountOut, 321 | singleSlippage 322 | ); 323 | 324 | // src quote 325 | const srcRouter = this.srcRouter; 326 | const srcAmountOut = dstMaxAmountIn; 327 | if (Number(srcAmountOut) < Number(relayerFeeUst)) { 328 | throw Error( 329 | `dstAmountIn <= relayerFeeUst. ${srcAmountOut} vs ${relayerFeeUst}` 330 | ); 331 | } 332 | 333 | const srcAmountOutBeforeFee = addFixedAmounts( 334 | srcAmountOut, 335 | relayerFeeUst, 336 | srcRouter.getTokenOutDecimals() 337 | ); 338 | 339 | const srcMaxAmountIn = await srcRouter.fetchExactOutQuote( 340 | srcAmountOutBeforeFee, 341 | singleSlippage 342 | ); 343 | 344 | // organize parameters 345 | const params: ExactOutCrossParameters = { 346 | amountOut: amountOut, 347 | ustAmountIn: dstMaxAmountIn, 348 | maxAmountIn: srcMaxAmountIn, 349 | src: this.makeSrcExactOutParameters( 350 | srcAmountOutBeforeFee, 351 | srcMaxAmountIn 352 | ), 353 | dst: this.makeDstExactOutParameters(amountOut, dstMaxAmountIn), 354 | relayerFee: this.getRelayerFee(relayerFeeUst), 355 | }; 356 | return params; 357 | } 358 | 359 | setDeadlines(deadline: string): void { 360 | if (!this.isSrcUst()) { 361 | // @ts-ignore 362 | this.srcRouter.setDeadline(deadline); 363 | } 364 | if (!this.isDstUst()) { 365 | // @ts-ignore 366 | this.dstRouter.setDeadline(deadline); 367 | } 368 | } 369 | 370 | isSrcUst(): boolean { 371 | return this.tokenInAddress === TERRA_UST; 372 | } 373 | 374 | isDstUst(): boolean { 375 | return this.tokenOutAddress === TERRA_UST; 376 | } 377 | 378 | getSrcEvmProvider(): ethers.providers.Provider | undefined { 379 | if (this.isSrcUst()) { 380 | return undefined; 381 | } 382 | // @ts-ignore 383 | return this.srcRouter.getProvider(); 384 | } 385 | 386 | getDstEvmProvider(): ethers.providers.Provider | undefined { 387 | if (this.isDstUst()) { 388 | return undefined; 389 | } 390 | // @ts-ignore 391 | return this.dstRouter.getProvider(); 392 | } 393 | 394 | getSrcChainId(): ChainId { 395 | return getChainIdFromAddress(this.tokenInAddress); 396 | } 397 | 398 | getDstChainId(): ChainId { 399 | return getChainIdFromAddress(this.tokenOutAddress); 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /react/src/route/evm.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | import { GenericToken } from "./generic"; 4 | 5 | // erc20 spec 6 | import { abi as Erc20Abi } from "../abi/erc20.json"; 7 | import { 8 | TransactionReceipt, 9 | TransactionRequest, 10 | TransactionResponse, 11 | } from "@ethersproject/abstract-provider"; 12 | import { APPROVAL_GAS_LIMIT } from "../utils/consts"; 13 | 14 | export class EvmToken extends GenericToken { 15 | token: ethers.Contract; 16 | decimals: number; 17 | 18 | async initialize(provider: ethers.providers.Provider, tokenAddress: string) { 19 | this.token = await makeErc20Contract(provider, tokenAddress); 20 | this.decimals = await this.token.decimals(); 21 | } 22 | 23 | static async create( 24 | provider: ethers.providers.Provider, 25 | tokenAddress: string 26 | ): Promise { 27 | const o = new EvmToken(); 28 | await o.initialize(provider, tokenAddress); 29 | return o; 30 | } 31 | 32 | getAddress(): string { 33 | return this.token.address; 34 | } 35 | 36 | getDecimals(): number { 37 | return this.decimals; 38 | } 39 | 40 | getContract(): ethers.Contract { 41 | return this.token; 42 | } 43 | 44 | async getBalanceOf(signer: ethers.Wallet) { 45 | const decimals = this.getDecimals(); 46 | const balanceBeforeDecimals = await this.token.balanceOf(signer.address); 47 | return ethers.utils.formatUnits(balanceBeforeDecimals.toString(), decimals); 48 | } 49 | 50 | computeUnitAmount(amount: string): ethers.BigNumber { 51 | return ethers.utils.parseUnits(amount, this.getDecimals()); 52 | } 53 | 54 | formatAmount(unitAmount: ethers.BigNumber): string { 55 | return ethers.utils.formatUnits(unitAmount, this.getDecimals()); 56 | } 57 | 58 | addAmounts(left: string, right: string): string { 59 | const sum = ethers.FixedNumber.from(left).addUnsafe( 60 | ethers.FixedNumber.from(right) 61 | ); 62 | return sum.round(this.getDecimals()).toString(); 63 | } 64 | 65 | subtractAmounts(left: string, right: string): string { 66 | const sum = ethers.FixedNumber.from(left).subUnsafe( 67 | ethers.FixedNumber.from(right) 68 | ); 69 | return sum.round(this.getDecimals()).toString(); 70 | } 71 | } 72 | 73 | export async function makeErc20Contract( 74 | provider: ethers.providers.Provider, 75 | tokenAddress: string 76 | ): Promise { 77 | return new ethers.Contract(tokenAddress, Erc20Abi, provider); 78 | } 79 | 80 | export async function approveContractTokenSpend( 81 | provider: ethers.providers.Provider, 82 | signer: ethers.Wallet, 83 | tokenContract: ethers.Contract, 84 | smartContractAddress: string, 85 | swapAmount: ethers.BigNumber 86 | ): Promise { 87 | // build transaction for token spending 88 | const unsignedTx: TransactionRequest = 89 | await tokenContract.populateTransaction.approve( 90 | smartContractAddress, 91 | swapAmount 92 | ); 93 | const nonce = await provider.getTransactionCount(signer.address, "latest"); 94 | 95 | const gasPrice = await signer.getGasPrice(); 96 | const parsedGasPrice = ethers.utils.hexlify(parseInt(gasPrice.toString())); 97 | 98 | unsignedTx.nonce = nonce; 99 | unsignedTx.gasLimit = ethers.BigNumber.from(APPROVAL_GAS_LIMIT); 100 | unsignedTx.gasPrice = ethers.BigNumber.from(parsedGasPrice); 101 | 102 | // sign and send transaction 103 | const tx: TransactionResponse = await signer.sendTransaction(unsignedTx); 104 | return tx.wait(); 105 | } 106 | -------------------------------------------------------------------------------- /react/src/route/generic.ts: -------------------------------------------------------------------------------- 1 | export enum UstLocation { 2 | In = 1, 3 | Out, 4 | } 5 | 6 | export abstract class RouterCore { 7 | abstract computeAndVerifyPoolAddress(): Promise; 8 | 9 | abstract computePoolAddress(): string; 10 | 11 | //abstract computeUnitAmountIn(amount: string): string; 12 | 13 | abstract computeUnitAmountOut(amount: string): string; 14 | 15 | abstract fetchExactInQuote( 16 | amountOut: string, 17 | slippage: string 18 | ): Promise; 19 | 20 | abstract fetchExactOutQuote( 21 | amountOut: string, 22 | slippage: string 23 | ): Promise; 24 | 25 | abstract formatAmountIn(amount: string): string; 26 | 27 | abstract formatAmountOut(amount: string): string; 28 | 29 | abstract getProtocol(): string; 30 | 31 | abstract getTokenInDecimals(): number; 32 | 33 | abstract getTokenOutDecimals(): number; 34 | 35 | abstract getTokenOutAddress(): string; 36 | } 37 | 38 | export abstract class GenericToken { 39 | abstract getAddress(): string; 40 | 41 | abstract getDecimals(): number; 42 | } 43 | -------------------------------------------------------------------------------- /react/src/route/hurricaneswap.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | import { AVAX_TOKEN_INFO } from "../utils/consts"; 4 | import { UstLocation } from "./generic"; 5 | import { UniswapV2Router } from "./uniswap-v2"; 6 | 7 | export { PROTOCOL } from "./uniswap-v2"; 8 | 9 | const HURRICANESWAP_FACTORY_ADDRESS = ""; 10 | 11 | export class HurricaneswapRouter extends UniswapV2Router { 12 | constructor(provider: ethers.providers.Provider) { 13 | super(provider); 14 | super.setFactoryAddress(HURRICANESWAP_FACTORY_ADDRESS); 15 | } 16 | 17 | async initialize(ustLocation: UstLocation): Promise { 18 | await super.initializeTokens(AVAX_TOKEN_INFO, ustLocation); 19 | return; 20 | } 21 | 22 | computePoolAddress(): string { 23 | // cannot find factory address on testnet 24 | return "0xD8087870E8869e45154189d434DF61C19e77ae30"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /react/src/route/pancakeswap.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | import { BNB_TOKEN_INFO } from "../utils/consts"; 4 | import { UstLocation } from "./generic"; 5 | import { UniswapV2Router } from "./uniswap-v2"; 6 | 7 | export { PROTOCOL } from "./uniswap-v2"; 8 | 9 | const PANCAKESWAP_FACTORY_ADDRESS = ""; 10 | 11 | export class PancakeswapRouter extends UniswapV2Router { 12 | constructor(provider: ethers.providers.Provider) { 13 | super(provider); 14 | super.setFactoryAddress(PANCAKESWAP_FACTORY_ADDRESS); 15 | } 16 | 17 | async initialize(ustLocation: UstLocation): Promise { 18 | await super.initializeTokens(BNB_TOKEN_INFO, ustLocation); 19 | return; 20 | } 21 | 22 | computePoolAddress(): string { 23 | // cannot find factory address on testnet 24 | return "0x8682096d4A2a2f3cd63147D05e4BAB47634e2AD1"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /react/src/route/quickswap.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | import { MATIC_TOKEN_INFO } from "../utils/consts"; 4 | import { UstLocation } from "./generic"; 5 | import { UniswapV2Router } from "./uniswap-v2"; 6 | 7 | export { PROTOCOL } from "./uniswap-v2"; 8 | 9 | const QUICKSWAP_FACTORY_ADDRESS = "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32"; 10 | 11 | export class QuickswapRouter extends UniswapV2Router { 12 | constructor(provider: ethers.providers.Provider) { 13 | super(provider); 14 | super.setFactoryAddress(QUICKSWAP_FACTORY_ADDRESS); 15 | } 16 | 17 | async initialize(ustLocation: UstLocation): Promise { 18 | await super.initializeTokens(MATIC_TOKEN_INFO, ustLocation); 19 | return; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /react/src/route/terra-ust-transfer.ts: -------------------------------------------------------------------------------- 1 | import { Dec, Int } from "@terra-money/terra.js"; 2 | 3 | import { UST_TOKEN_INFO } from "../utils/consts"; 4 | import { RouterCore } from "./generic"; 5 | 6 | export const PROTOCOL = "TerraUstTransfer"; 7 | 8 | const UST_DECIMALS = 6; 9 | 10 | const UST_AMOUNT_MULTIPLIER = "1000000"; 11 | 12 | export class TerraUstTransfer extends RouterCore { 13 | computePoolAddress(): string { 14 | return UST_TOKEN_INFO.address; 15 | } 16 | 17 | computeAndVerifyPoolAddress(): Promise { 18 | return new Promise((resolve) => { 19 | return resolve(this.computePoolAddress()); 20 | }); 21 | } 22 | 23 | formatAmountIn(amount: string): string { 24 | const formatted = new Dec(amount).div(UST_AMOUNT_MULTIPLIER); 25 | return formatted.toString(); 26 | } 27 | 28 | formatAmountOut(amount: string): string { 29 | return this.formatAmountIn(amount); 30 | } 31 | 32 | computeUnitAmountIn(amount: string): string { 33 | const unitified = new Dec(amount).mul(UST_AMOUNT_MULTIPLIER); 34 | return new Int(unitified.toString()).toString(); 35 | } 36 | 37 | computeUnitAmountOut(amount: string): string { 38 | return this.computeUnitAmountIn(amount); 39 | } 40 | 41 | getProtocol(): string { 42 | return PROTOCOL; 43 | } 44 | 45 | async fetchExactInQuote(amountIn: string, slippage: string): Promise { 46 | return amountIn; 47 | } 48 | 49 | async fetchExactOutQuote( 50 | amountOut: string, 51 | slippage: string 52 | ): Promise { 53 | return amountOut; 54 | } 55 | 56 | getTokenInDecimals(): number { 57 | return UST_DECIMALS; 58 | } 59 | 60 | getTokenOutDecimals(): number { 61 | return UST_DECIMALS; 62 | } 63 | 64 | getTokenOutAddress(): string { 65 | return this.computePoolAddress(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /react/src/route/uniswap-core.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { ethers } from "ethers"; 3 | import { CurrencyAmount, Token } from "@uniswap/sdk-core"; 4 | 5 | import { EvmToken } from "./evm"; 6 | import { RouterCore, UstLocation } from "./generic"; 7 | import { TokenInfo } from "../utils/consts"; 8 | 9 | export function computeTradeDeadline(deadline: string): ethers.BigNumber { 10 | return ethers.BigNumber.from(Math.floor(Date.now() / 1000)).add(deadline); 11 | } 12 | 13 | export class UniEvmToken { 14 | erc20: EvmToken; 15 | uniToken: Token; 16 | 17 | constructor(chainId: number, erc20: EvmToken) { 18 | this.erc20 = erc20; 19 | 20 | const address = this.getAddress(); 21 | const decimals = this.getDecimals(); 22 | 23 | this.uniToken = new Token(chainId, address, decimals); 24 | } 25 | 26 | getUniToken(): Token { 27 | return this.uniToken; 28 | } 29 | 30 | getEvmToken(): EvmToken { 31 | return this.erc20; 32 | } 33 | 34 | getDecimals(): number { 35 | return this.erc20.getDecimals(); 36 | } 37 | 38 | getContract(): ethers.Contract { 39 | return this.erc20.getContract(); 40 | } 41 | 42 | getAddress(): string { 43 | return this.erc20.getAddress(); 44 | } 45 | 46 | async getBalanceOf(signer: ethers.Wallet) { 47 | return this.erc20.getBalanceOf(signer); 48 | } 49 | 50 | computeUnitAmount(amount: string): ethers.BigNumber { 51 | return this.erc20.computeUnitAmount(amount); 52 | } 53 | 54 | formatAmount(unitAmount: ethers.BigNumber): string { 55 | return this.erc20.formatAmount(unitAmount); 56 | } 57 | 58 | computeCurrencyAmount(amount: string): CurrencyAmount { 59 | const unitAmount = this.computeUnitAmount(amount); 60 | return CurrencyAmount.fromRawAmount( 61 | this.getUniToken(), 62 | unitAmount.toString() 63 | ); 64 | } 65 | 66 | addAmounts(left: string, right: string): string { 67 | return this.erc20.addAmounts(left, right); 68 | } 69 | 70 | subtractAmounts(left: string, right: string): string { 71 | return this.erc20.subtractAmounts(left, right); 72 | } 73 | } 74 | 75 | export async function makeUniEvmToken( 76 | provider: ethers.providers.Provider, 77 | chainId: number, 78 | tokenAddress: string 79 | ): Promise { 80 | const erc20 = await EvmToken.create(provider, tokenAddress); 81 | return new UniEvmToken(chainId, erc20); 82 | } 83 | 84 | function stringToBigNumber(value: string): ethers.BigNumber { 85 | return ethers.BigNumber.from(value); 86 | } 87 | 88 | export interface ExactInParameters { 89 | protocol: string; 90 | amountIn: ethers.BigNumber; 91 | minAmountOut: ethers.BigNumber; 92 | deadline: ethers.BigNumber; 93 | poolFee: string; 94 | path: [string, string]; 95 | } 96 | 97 | export interface ExactOutParameters { 98 | protocol: string; 99 | amountOut: ethers.BigNumber; 100 | maxAmountIn: ethers.BigNumber; 101 | deadline: ethers.BigNumber; 102 | poolFee: string; 103 | path: [string, string]; 104 | } 105 | 106 | export function makeExactInParameters( 107 | router: UniswapRouterCore, 108 | amountIn: string, 109 | minAmountOut: string 110 | ): ExactInParameters { 111 | const params: ExactInParameters = { 112 | protocol: router.getProtocol(), 113 | amountIn: router.tokenIn.computeUnitAmount(amountIn), 114 | minAmountOut: router.tokenOut.computeUnitAmount(minAmountOut), 115 | poolFee: router.getPoolFee(), 116 | deadline: router.getTradeDeadline(), 117 | path: [router.tokenIn.getAddress(), router.tokenOut.getAddress()], 118 | }; 119 | return params; 120 | } 121 | 122 | export function makeExactOutParameters( 123 | router: UniswapRouterCore, 124 | amountOut: string, 125 | maxAmountIn: string 126 | ): ExactOutParameters { 127 | const params: ExactOutParameters = { 128 | protocol: router.getProtocol(), 129 | amountOut: router.tokenOut.computeUnitAmount(amountOut), 130 | maxAmountIn: router.tokenIn.computeUnitAmount(maxAmountIn), 131 | poolFee: router.getPoolFee(), 132 | deadline: router.getTradeDeadline(), 133 | path: [router.tokenIn.getAddress(), router.tokenOut.getAddress()], 134 | }; 135 | return params; 136 | } 137 | 138 | export abstract class UniswapRouterCore extends RouterCore { 139 | provider: ethers.providers.Provider; 140 | network: ethers.providers.Network; 141 | 142 | // wormhole 143 | chainId: number; 144 | 145 | // tokens 146 | tokenIn: UniEvmToken; 147 | tokenOut: UniEvmToken; 148 | 149 | // params 150 | deadline: string = ""; 151 | 152 | constructor(provider: ethers.providers.Provider) { 153 | super(); 154 | this.provider = provider; 155 | } 156 | 157 | public getProvider(): ethers.providers.Provider { 158 | return this.provider; 159 | } 160 | 161 | public async initializeTokens( 162 | tokenInfo: TokenInfo, 163 | ustLocation: UstLocation 164 | ): Promise { 165 | this.network = await this.provider.getNetwork(); 166 | 167 | const network = this.network; 168 | 169 | if (ustLocation === UstLocation.Out) { 170 | [this.tokenIn, this.tokenOut] = await Promise.all([ 171 | makeUniEvmToken(this.provider, network.chainId, tokenInfo.address), 172 | makeUniEvmToken( 173 | this.provider, 174 | network.chainId, 175 | tokenInfo.ustPairedAddress 176 | ), 177 | ]); 178 | } else { 179 | [this.tokenIn, this.tokenOut] = await Promise.all([ 180 | makeUniEvmToken( 181 | this.provider, 182 | network.chainId, 183 | tokenInfo.ustPairedAddress 184 | ), 185 | makeUniEvmToken(this.provider, network.chainId, tokenInfo.address), 186 | ]); 187 | } 188 | return; 189 | } 190 | 191 | public getPoolFee(): string { 192 | return ""; 193 | } 194 | 195 | public setDeadline(deadline: string): void { 196 | this.deadline = deadline; 197 | } 198 | 199 | public getTradeDeadline(): ethers.BigNumber { 200 | return computeTradeDeadline(this.deadline); 201 | } 202 | 203 | /* 204 | public computeUnitAmountIn(amount: string): string { 205 | return this.tokenIn.computeUnitAmount(amount).toString(); 206 | } 207 | */ 208 | 209 | public computeUnitAmountOut(amount: string): string { 210 | return this.tokenOut.computeUnitAmount(amount).toString(); 211 | } 212 | 213 | public formatAmountIn(amount: string): string { 214 | return this.tokenIn.formatAmount(stringToBigNumber(amount)); 215 | } 216 | 217 | public formatAmountOut(amount: string): string { 218 | return this.tokenOut.formatAmount(stringToBigNumber(amount)); 219 | } 220 | 221 | public getTokenInDecimals(): number { 222 | return this.tokenIn.getDecimals(); 223 | } 224 | 225 | public getTokenOutDecimals(): number { 226 | return this.tokenOut.getDecimals(); 227 | } 228 | 229 | public getTokenOutAddress(): string { 230 | return this.tokenOut.getAddress(); 231 | } 232 | 233 | abstract getProtocol(): string; 234 | } 235 | -------------------------------------------------------------------------------- /react/src/route/uniswap-v2.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { CurrencyAmount, TradeType } from "@uniswap/sdk-core"; 3 | import { abi as IUniswapV2PairABI } from "@uniswap/v2-core/build/UniswapV2Pair.json"; 4 | import { computePairAddress, Pair, Route, Trade } from "@uniswap/v2-sdk"; 5 | 6 | import { UniswapRouterCore } from "./uniswap-core"; 7 | 8 | export const PROTOCOL = "UniswapV2"; 9 | 10 | // uniswap v3 (ethereum) 11 | //export const UNISWAP_V3_FACTORY_ADDRESS = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; 12 | //export const UNISWAP_V3_ROUTER_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564'; 13 | 14 | // quickswap (polygon) 15 | export const QUICKSWAP_V2_ROUTER_ADDRESS = 16 | "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff"; 17 | 18 | export class UniswapV2Router extends UniswapRouterCore { 19 | factoryAddress: string; 20 | pairContract: ethers.Contract; 21 | pair: Pair; 22 | 23 | setFactoryAddress(factoryAddress: string) { 24 | this.factoryAddress = factoryAddress; 25 | return; 26 | } 27 | 28 | computePoolAddress(): string { 29 | if (this.factoryAddress === undefined) { 30 | throw Error("factoryAddress is undefined. use setFactoryAddress"); 31 | } 32 | 33 | return computePairAddress({ 34 | factoryAddress: this.factoryAddress, 35 | tokenA: this.tokenIn.getUniToken(), 36 | tokenB: this.tokenOut.getUniToken(), 37 | }); 38 | } 39 | 40 | async computeAndVerifyPoolAddress(): Promise { 41 | const pairAddress = this.computePoolAddress(); 42 | 43 | // verify by attempting to call factory() 44 | const poolContract = new ethers.Contract( 45 | pairAddress, 46 | IUniswapV2PairABI, 47 | this.provider 48 | ); 49 | await poolContract.factory(); 50 | 51 | return pairAddress; 52 | } 53 | 54 | async createPool(): Promise { 55 | const pairAddress = this.computePoolAddress(); 56 | 57 | const pairContract = new ethers.Contract( 58 | pairAddress, 59 | IUniswapV2PairABI, 60 | this.provider 61 | ); 62 | 63 | const [token0, reserves] = await Promise.all([ 64 | pairContract.token0(), 65 | pairContract.getReserves(), 66 | ]); 67 | 68 | const reserve0 = reserves._reserve0.toString(); 69 | const reserve1 = reserves._reserve1.toString(); 70 | 71 | const tokenIn = this.tokenIn; 72 | const tokenOut = this.tokenOut; 73 | 74 | if (token0.toLowerCase() === tokenIn.getAddress().toLowerCase()) { 75 | return new Pair( 76 | CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), reserve0), 77 | CurrencyAmount.fromRawAmount(tokenOut.getUniToken(), reserve1) 78 | ); 79 | } 80 | 81 | return new Pair( 82 | CurrencyAmount.fromRawAmount(tokenOut.getUniToken(), reserve0), 83 | CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), reserve1) 84 | ); 85 | } 86 | 87 | async fetchExactInQuote(amountIn: string, slippage: string): Promise { 88 | // create pool 89 | const pair = await this.createPool(); 90 | 91 | // let's get that quote 92 | const tokenIn = this.tokenIn; 93 | const tokenOut = this.tokenOut; 94 | 95 | const route = new Route( 96 | [pair], 97 | tokenIn.getUniToken(), 98 | tokenOut.getUniToken() 99 | ); 100 | const currencyAmountIn = tokenIn.computeCurrencyAmount(amountIn); 101 | 102 | const quote = new Trade(route, currencyAmountIn, TradeType.EXACT_INPUT); 103 | 104 | const decimals = tokenOut.getDecimals(); 105 | const minAmountOut = ethers.FixedNumber.from( 106 | quote.outputAmount.toSignificant(decimals) 107 | ); 108 | 109 | // calculate output amount with slippage 110 | const slippageMultiplier = ethers.FixedNumber.from("1").subUnsafe( 111 | ethers.FixedNumber.from(slippage) 112 | ); 113 | const minAmountOutWithSlippage = minAmountOut 114 | .mulUnsafe(slippageMultiplier) 115 | .round(decimals); 116 | 117 | /* 118 | return tokenOut 119 | .computeUnitAmount(minAmountOutWithSlippage.toString()) 120 | .toString(); 121 | */ 122 | return minAmountOutWithSlippage.toString(); 123 | } 124 | 125 | async fetchExactOutQuote( 126 | amountOut: string, 127 | slippage: string 128 | ): Promise { 129 | // create pool 130 | const pair = await this.createPool(); 131 | 132 | // let's get that quote 133 | const tokenIn = this.tokenIn; 134 | const tokenOut = this.tokenOut; 135 | 136 | const route = new Route( 137 | [pair], 138 | tokenIn.getUniToken(), 139 | tokenOut.getUniToken() 140 | ); 141 | const currencyAmountOut = tokenOut.computeCurrencyAmount(amountOut); 142 | 143 | const quote = new Trade(route, currencyAmountOut, TradeType.EXACT_OUTPUT); 144 | 145 | const decimals = tokenIn.getDecimals(); 146 | const maxAmountIn = ethers.FixedNumber.from( 147 | quote.inputAmount.toSignificant(decimals) 148 | ); 149 | 150 | const slippageDivisor = ethers.FixedNumber.from("1").subUnsafe( 151 | ethers.FixedNumber.from(slippage) 152 | ); 153 | const maxAmountInWithSlippage = maxAmountIn 154 | .divUnsafe(slippageDivisor) 155 | .round(decimals); 156 | 157 | /* 158 | return tokenIn 159 | .computeUnitAmount(maxAmountInWithSlippage.toString()) 160 | .toString(); 161 | */ 162 | return maxAmountInWithSlippage.toString(); 163 | } 164 | 165 | getProtocol(): string { 166 | return PROTOCOL; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /react/src/route/uniswap-v3.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import JSBI from "jsbi"; 3 | import { CurrencyAmount, Token, TradeType } from "@uniswap/sdk-core"; 4 | import { abi as IUniswapV3PoolABI } from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json"; 5 | import { 6 | computePoolAddress, 7 | FeeAmount, 8 | nearestUsableTick, 9 | Pool, 10 | Route, 11 | TickMath, 12 | TICK_SPACINGS, 13 | Trade, 14 | } from "@uniswap/v3-sdk"; 15 | 16 | import { UniswapRouterCore } from "./uniswap-core"; 17 | import { ETH_TOKEN_INFO } from "../utils/consts"; 18 | import { UstLocation } from "./generic"; 19 | 20 | export const PROTOCOL = "UniswapV3"; 21 | 22 | const UNISWAP_V3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; 23 | 24 | export class UniswapV3Router extends UniswapRouterCore { 25 | poolContract: ethers.Contract; 26 | pool: Pool; 27 | poolFee: FeeAmount; 28 | 29 | constructor(provider: ethers.providers.Provider) { 30 | super(provider); 31 | 32 | // set fee amount for our example 33 | this.poolFee = FeeAmount.MEDIUM; 34 | } 35 | 36 | async initialize(ustLocation: UstLocation): Promise { 37 | await this.initializeTokens(ETH_TOKEN_INFO, ustLocation); 38 | return; 39 | } 40 | 41 | getPoolFee(): string { 42 | return this.poolFee.toString(); 43 | } 44 | 45 | computePoolAddress(): string { 46 | return computePoolAddress({ 47 | factoryAddress: UNISWAP_V3_FACTORY_ADDRESS, 48 | fee: this.poolFee, 49 | tokenA: this.tokenIn.getUniToken(), 50 | tokenB: this.tokenOut.getUniToken(), 51 | }); 52 | } 53 | 54 | async computeAndVerifyPoolAddress(): Promise { 55 | const pairAddress = this.computePoolAddress(); 56 | 57 | // verify by attempting to call factory() 58 | const poolContract = new ethers.Contract( 59 | pairAddress, 60 | IUniswapV3PoolABI, 61 | this.provider 62 | ); 63 | await poolContract.factory(); 64 | 65 | return pairAddress; 66 | } 67 | 68 | async createPool(): Promise { 69 | const poolAddress = this.computePoolAddress(); 70 | 71 | const poolContract = new ethers.Contract( 72 | poolAddress, 73 | IUniswapV3PoolABI, 74 | this.provider 75 | ); 76 | this.poolContract = poolContract; 77 | 78 | const [liquidity, slot] = await Promise.all([ 79 | poolContract.liquidity(), 80 | poolContract.slot0(), 81 | ]); 82 | 83 | // grab necessary data from slot 84 | const sqrtPriceX96 = slot[0]; 85 | const tick = slot[1]; 86 | 87 | // create JSBI version of liquidity numbers 88 | const bigLiq = JSBI.BigInt(liquidity); 89 | const negBigLiq = JSBI.multiply(bigLiq, JSBI.BigInt(-1)); 90 | 91 | const tickConstructorArgs = [ 92 | { 93 | index: nearestUsableTick( 94 | TickMath.MIN_TICK, 95 | TICK_SPACINGS[this.poolFee] 96 | ), 97 | liquidityNet: bigLiq, 98 | liquidityGross: bigLiq, 99 | }, 100 | { 101 | index: nearestUsableTick( 102 | TickMath.MAX_TICK, 103 | TICK_SPACINGS[this.poolFee] 104 | ), 105 | liquidityNet: negBigLiq, 106 | liquidityGross: bigLiq, 107 | }, 108 | ]; 109 | 110 | return new Pool( 111 | this.tokenIn.getUniToken(), 112 | this.tokenOut.getUniToken(), 113 | this.poolFee, 114 | sqrtPriceX96.toString(), //note the description discrepancy - sqrtPriceX96 and sqrtRatioX96 are interchangable values 115 | liquidity, 116 | tick, 117 | tickConstructorArgs 118 | ); 119 | } 120 | 121 | async computeTradeExactIn( 122 | amount: string 123 | ): Promise> { 124 | // create pool 125 | const pool = await this.createPool(); 126 | 127 | // let's get that quote 128 | const tokenIn = this.tokenIn; 129 | const tokenOut = this.tokenOut; 130 | 131 | const amountIn = tokenIn.computeUnitAmount(amount); 132 | 133 | const route = new Route( 134 | [pool], 135 | tokenIn.getUniToken(), 136 | tokenOut.getUniToken() 137 | ); 138 | return Trade.fromRoute( 139 | route, 140 | CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), amountIn.toString()), 141 | TradeType.EXACT_INPUT 142 | ); 143 | } 144 | 145 | async computeTradeExactOut( 146 | amount: string 147 | ): Promise> { 148 | // create pool 149 | const pool = await this.createPool(); 150 | 151 | // let's get that quote 152 | const tokenIn = this.tokenIn; 153 | const tokenOut = this.tokenOut; 154 | 155 | const amountOut = tokenOut.computeUnitAmount(amount); 156 | 157 | const route = new Route( 158 | [pool], 159 | tokenIn.getUniToken(), 160 | tokenOut.getUniToken() 161 | ); 162 | return Trade.fromRoute( 163 | route, 164 | CurrencyAmount.fromRawAmount( 165 | tokenOut.getUniToken(), 166 | amountOut.toString() 167 | ), 168 | TradeType.EXACT_OUTPUT 169 | ); 170 | } 171 | 172 | async fetchExactInQuote(amountIn: string, slippage: string): Promise { 173 | // get the quote 174 | const trade = await this.computeTradeExactIn(amountIn); 175 | 176 | const tokenOut = this.tokenOut; 177 | const decimals = tokenOut.getDecimals(); 178 | 179 | // calculate output amount with slippage 180 | const minAmountOut = ethers.FixedNumber.from( 181 | trade.outputAmount.toSignificant(decimals) 182 | ); 183 | 184 | const slippageMultiplier = ethers.FixedNumber.from("1").subUnsafe( 185 | ethers.FixedNumber.from(slippage) 186 | ); 187 | const minAmountOutWithSlippage = minAmountOut 188 | .mulUnsafe(slippageMultiplier) 189 | .round(decimals); 190 | 191 | /* 192 | return tokenOut 193 | .computeUnitAmount(minAmountOutWithSlippage.toString()) 194 | .toString(); 195 | */ 196 | return minAmountOutWithSlippage.toString(); 197 | } 198 | 199 | async fetchExactOutQuote( 200 | amountOut: string, 201 | slippage: string 202 | ): Promise { 203 | // get the quote 204 | const trade = await this.computeTradeExactOut(amountOut); 205 | 206 | const tokenIn = this.tokenIn; 207 | const decimals = tokenIn.getDecimals(); 208 | 209 | // calculate output amount with slippage 210 | const maxAmountIn = ethers.FixedNumber.from( 211 | trade.inputAmount.toSignificant(decimals) 212 | ); 213 | 214 | const slippageDivisor = ethers.FixedNumber.from("1").subUnsafe( 215 | ethers.FixedNumber.from(slippage) 216 | ); 217 | const maxAmountInWithSlippage = maxAmountIn 218 | .divUnsafe(slippageDivisor) 219 | .round(decimals); 220 | 221 | /* 222 | return tokenIn 223 | .computeUnitAmount(maxAmountInWithSlippage.toString()) 224 | .toString(); 225 | */ 226 | return maxAmountInWithSlippage.toString(); 227 | } 228 | 229 | getProtocol(): string { 230 | return PROTOCOL; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /react/src/swapper/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /react/src/swapper/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { TransactionReceipt } from "@ethersproject/abstract-provider"; 3 | 4 | import { 5 | EVM_ETH_NETWORK_CHAIN_ID, 6 | EVM_POLYGON_NETWORK_CHAIN_ID, 7 | EVM_AVAX_NETWORK_CHAIN_ID, 8 | //EVM_BSC_NETWORK_CHAIN_ID, 9 | } from "../utils/consts"; 10 | 11 | export const CROSSCHAINSWAP_GAS_PARAMETERS_EIP1559 = { 12 | gasLimit: "694200", 13 | //maxFeePerGas: "250000000000", 14 | maxFeePerGas: "100420690000", 15 | maxPriorityFeePerGas: "1690000000", 16 | }; 17 | 18 | export const CROSSCHAINSWAP_GAS_PARAMETERS_EVM = { 19 | gasLimit: "694200", 20 | //gasPrice: "250000000000", 21 | gasPrice: "20420690000", 22 | }; 23 | 24 | export const EVM_EIP1559_CHAIN_IDS = [ 25 | EVM_ETH_NETWORK_CHAIN_ID, 26 | EVM_POLYGON_NETWORK_CHAIN_ID, 27 | EVM_AVAX_NETWORK_CHAIN_ID, 28 | ]; 29 | 30 | export async function getEvmGasParametersForContract( 31 | contract: ethers.Contract 32 | ): Promise { 33 | const chainId = await getChainIdFromContract(contract); 34 | 35 | if (EVM_EIP1559_CHAIN_IDS.indexOf(chainId) >= 0) { 36 | return CROSSCHAINSWAP_GAS_PARAMETERS_EIP1559; 37 | } 38 | 39 | return CROSSCHAINSWAP_GAS_PARAMETERS_EVM; 40 | } 41 | 42 | async function getChainIdFromContract( 43 | contract: ethers.Contract 44 | ): Promise { 45 | const network = await contract.provider.getNetwork(); 46 | return network.chainId; 47 | } 48 | 49 | // exact in 50 | // 51 | export async function evmSwapExactInFromVaaNative( 52 | swapContractWithSigner: ethers.Contract, 53 | signedVaa: Uint8Array 54 | ): Promise { 55 | const gasParams = await getEvmGasParametersForContract( 56 | swapContractWithSigner 57 | ); 58 | 59 | const tx = await swapContractWithSigner.recvAndSwapExactNativeIn( 60 | signedVaa, 61 | gasParams 62 | ); 63 | return tx.wait(); 64 | } 65 | 66 | export async function evmSwapExactInFromVaaToken( 67 | swapContractWithSigner: ethers.Contract, 68 | signedVaa: Uint8Array 69 | ): Promise { 70 | const gasParams = await getEvmGasParametersForContract( 71 | swapContractWithSigner 72 | ); 73 | 74 | const tx = await swapContractWithSigner.recvAndSwapExactIn( 75 | signedVaa, 76 | gasParams 77 | ); 78 | return tx.wait(); 79 | } 80 | 81 | // exact out 82 | // 83 | export async function evmSwapExactOutFromVaaNative( 84 | swapContractWithSigner: ethers.Contract, 85 | signedVaa: Uint8Array 86 | ): Promise { 87 | const gasParams = await getEvmGasParametersForContract( 88 | swapContractWithSigner 89 | ); 90 | 91 | const tx = await swapContractWithSigner.recvAndSwapExactNativeOut( 92 | signedVaa, 93 | gasParams 94 | ); 95 | return tx.wait(); 96 | } 97 | 98 | export async function evmSwapExactOutFromVaaToken( 99 | swapContractWithSigner: ethers.Contract, 100 | signedVaa: Uint8Array 101 | ): Promise { 102 | const gasParams = await getEvmGasParametersForContract( 103 | swapContractWithSigner 104 | ); 105 | 106 | const tx = await swapContractWithSigner.recvAndSwapExactOut( 107 | signedVaa, 108 | gasParams 109 | ); 110 | return tx.wait(); 111 | } 112 | -------------------------------------------------------------------------------- /react/src/utils/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /react/src/utils/consts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChainId, 3 | CHAIN_ID_ETH, 4 | CHAIN_ID_POLYGON, 5 | CHAIN_ID_TERRA, 6 | CHAIN_ID_AVAX, 7 | CHAIN_ID_BSC, 8 | } from "@certusone/wormhole-sdk"; 9 | 10 | export const EVM_POLYGON_NETWORK_CHAIN_ID = 80001; 11 | export const EVM_ETH_NETWORK_CHAIN_ID = 5; 12 | export const EVM_AVAX_NETWORK_CHAIN_ID = 43113; 13 | export const EVM_BSC_NETWORK_CHAIN_ID = 97; 14 | 15 | export interface TokenInfo { 16 | name: string; 17 | address: string; 18 | chainId: ChainId; 19 | evmChainId: number | undefined; 20 | maxAmount: number; 21 | ustPairedAddress: string | undefined; 22 | } 23 | 24 | export const MATIC_TOKEN_INFO: TokenInfo = { 25 | name: "MATIC", 26 | address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889", 27 | chainId: CHAIN_ID_POLYGON, 28 | evmChainId: EVM_POLYGON_NETWORK_CHAIN_ID, 29 | //logo: polygonIcon, 30 | maxAmount: 0.1, 31 | ustPairedAddress: "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c", 32 | }; 33 | 34 | export const ETH_TOKEN_INFO: TokenInfo = { 35 | name: "ETH", 36 | address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", 37 | chainId: CHAIN_ID_ETH, 38 | evmChainId: EVM_ETH_NETWORK_CHAIN_ID, 39 | //logo: ethIcon, 40 | maxAmount: 0.01, 41 | ustPairedAddress: "0x36Ed51Afc79619b299b238898E72ce482600568a", 42 | }; 43 | 44 | export const AVAX_TOKEN_INFO: TokenInfo = { 45 | name: "AVAX", 46 | address: "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3", 47 | chainId: CHAIN_ID_AVAX, 48 | evmChainId: EVM_AVAX_NETWORK_CHAIN_ID, 49 | //logo: avaxIcon, 50 | maxAmount: 0.01, 51 | ustPairedAddress: "0xe09ed38e5cd1014444846f62376ac88c5232cde9", 52 | }; 53 | 54 | export const BNB_TOKEN_INFO: TokenInfo = { 55 | name: "BNB", 56 | address: "0xae13d989dac2f0debff460ac112a837c89baa7cd", 57 | chainId: CHAIN_ID_BSC, 58 | evmChainId: EVM_BSC_NETWORK_CHAIN_ID, 59 | //logo: bscIcon, 60 | maxAmount: 0.01, 61 | ustPairedAddress: "0x7b8eae1e85c8b189ee653d3f78733f4f788bb2c1", 62 | }; 63 | 64 | export const UST_TOKEN_INFO: TokenInfo = { 65 | name: "UST", 66 | address: "uusd", 67 | chainId: CHAIN_ID_TERRA, 68 | evmChainId: undefined, 69 | //logo: terraIcon, 70 | maxAmount: 10.0, 71 | ustPairedAddress: undefined, 72 | }; 73 | 74 | export const TOKEN_INFOS = [ 75 | MATIC_TOKEN_INFO, 76 | ETH_TOKEN_INFO, 77 | AVAX_TOKEN_INFO, 78 | BNB_TOKEN_INFO, 79 | // TODO: support swaps from/to terra 80 | // UST_TOKEN_INFO, 81 | ]; 82 | 83 | export const getSupportedSwaps = (tokenInfo: TokenInfo) => { 84 | return TOKEN_INFOS.filter((x) => x !== tokenInfo); 85 | }; 86 | 87 | export const getEvmChainId = (chainId: ChainId): number | undefined => { 88 | switch (chainId) { 89 | case CHAIN_ID_ETH: 90 | return EVM_ETH_NETWORK_CHAIN_ID; 91 | case CHAIN_ID_POLYGON: 92 | return EVM_POLYGON_NETWORK_CHAIN_ID; 93 | case CHAIN_ID_AVAX: 94 | return EVM_AVAX_NETWORK_CHAIN_ID; 95 | case CHAIN_ID_BSC: 96 | return EVM_BSC_NETWORK_CHAIN_ID; 97 | default: 98 | return undefined; 99 | } 100 | }; 101 | 102 | export const getChainName = (chainId: ChainId) => { 103 | switch (chainId) { 104 | case CHAIN_ID_ETH: 105 | return "Ethereum"; 106 | case CHAIN_ID_POLYGON: 107 | return "Polygon"; 108 | case CHAIN_ID_AVAX: 109 | return "Avalanche"; 110 | case CHAIN_ID_BSC: 111 | return "BSC"; 112 | default: 113 | return ""; 114 | } 115 | }; 116 | 117 | export const RELAYER_FEE_UST = "0.25"; 118 | 119 | export const WORMHOLE_RPC_HOSTS = [ 120 | "https://wormhole-v2-testnet-api.certus.one", 121 | ]; 122 | 123 | // core bridge 124 | export const CORE_BRIDGE_ADDRESS_ETHEREUM = 125 | "0x706abc4E45D419950511e474C7B9Ed348A4a716c"; 126 | 127 | export const CORE_BRIDGE_ADDRESS_POLYGON = 128 | "0x0CBE91CF822c73C2315FB05100C2F714765d5c20"; 129 | 130 | export const CORE_BRIDGE_ADDRESS_AVALANCHE = 131 | "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"; 132 | 133 | export const CORE_BRIDGE_ADDRESS_BSC = 134 | "0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D"; 135 | 136 | export const CORE_BRIDGE_ADDRESS_TERRA = 137 | "terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v"; 138 | 139 | // token bridge 140 | export const TOKEN_BRIDGE_ADDRESS_ETHEREUM = 141 | "0xF890982f9310df57d00f659cf4fd87e65adEd8d7"; 142 | 143 | export const TOKEN_BRIDGE_ADDRESS_POLYGON = 144 | "0x377D55a7928c046E18eEbb61977e714d2a76472a"; 145 | 146 | export const TOKEN_BRIDGE_ADDRESS_BSC = 147 | "0x9dcF9D205C9De35334D646BeE44b2D2859712A09"; 148 | 149 | export const TOKEN_BRIDGE_ADDRESS_AVALANCHE = 150 | "0x61E44E506Ca5659E6c0bba9b678586fA2d729756"; 151 | 152 | export const TOKEN_BRIDGE_ADDRESS_TERRA = 153 | "terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a"; 154 | 155 | // gas 156 | export const APPROVAL_GAS_LIMIT = "100000"; 157 | -------------------------------------------------------------------------------- /react/src/utils/getIsTransferCompletedWithRetry.ts: -------------------------------------------------------------------------------- 1 | import { getIsTransferCompletedEth } from "@certusone/wormhole-sdk"; 2 | import { ethers } from "ethers"; 3 | 4 | export default async function getIsTransferCompletedEvmWithRetry( 5 | tokenBridgeAddress: string, 6 | provider: ethers.providers.Provider, 7 | signedVAA: Uint8Array, 8 | retryTimeoutMs: number, 9 | retryAttempts: number 10 | ) { 11 | let result = false; 12 | let attempts = 0; 13 | while (attempts < retryAttempts) { 14 | try { 15 | result = await getIsTransferCompletedEth( 16 | tokenBridgeAddress, 17 | provider, 18 | signedVAA 19 | ); 20 | } catch (e) { 21 | console.error(e); 22 | } 23 | if (result) { 24 | break; 25 | } 26 | await new Promise((resolve) => setTimeout(resolve, retryTimeoutMs)); 27 | attempts++; 28 | } 29 | return result; 30 | } 31 | -------------------------------------------------------------------------------- /react/src/utils/math.ts: -------------------------------------------------------------------------------- 1 | import { FixedNumber } from "ethers"; 2 | 3 | export function addFixedAmounts( 4 | left: string, 5 | right: string, 6 | decimals: number 7 | ): string { 8 | const sum = FixedNumber.from(left).addUnsafe(FixedNumber.from(right)); 9 | return sum.round(decimals).toString(); 10 | } 11 | 12 | export function subtractFixedAmounts( 13 | left: string, 14 | right: string, 15 | decimals: number 16 | ): string { 17 | const diff = FixedNumber.from(left).subUnsafe(FixedNumber.from(right)); 18 | return diff.round(decimals).toString(); 19 | } 20 | -------------------------------------------------------------------------------- /react/src/utils/parseError.ts: -------------------------------------------------------------------------------- 1 | const MM_ERR_WITH_INFO_START = 2 | "VM Exception while processing transaction: revert "; 3 | const parseError = (e: any) => 4 | e?.data?.message?.startsWith(MM_ERR_WITH_INFO_START) 5 | ? e.data.message.replace(MM_ERR_WITH_INFO_START, "") 6 | : e?.response?.data?.error // terra error 7 | ? e.response.data.error 8 | : e?.message 9 | ? e.message 10 | : "An unknown error occurred"; 11 | export default parseError; 12 | -------------------------------------------------------------------------------- /react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "strictPropertyInitialization": false, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /swap_relayer/.env.sample: -------------------------------------------------------------------------------- 1 | # TestNet 2 | SPY_SERVICE_HOST=localhost:7073 3 | # You can omit the following to get signed VAAs from every emitter. 4 | #SPY_SERVICE_FILTERS=[{"chain_id":2,"emitter_address":"0x000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d7"},{"chain_id":3,"emitter_address":"terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a"},{"chain_id":4,"emitter_address":"0x0000000000000000000000009dcF9D205C9De35334D646BeE44b2D2859712A09"},{"chain_id":5,"emitter_address":"0x000000000000000000000000377d55a7928c046e18eebb61977e714d2a76472a"},{"chain_id":6,"emitter_address":"0x0000000000000000000000007bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"}] 5 | 6 | EVM_CHAINS=ETH,BSC,POLYGON,AVAX 7 | 8 | ETH_PROVIDER=https://goerli.infura.io/v3/YOUR_PROJECT_ID 9 | ETH_TOKEN_BRIDGE_ADDRESS=0xF890982f9310df57d00f659cf4fd87e65adEd8d7 10 | ETH_CHAIN_ID=2 11 | ETH_ABI=V3 12 | 13 | BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545" 14 | BSC_TOKEN_BRIDGE_ADDRESS=0x9dcF9D205C9De35334D646BeE44b2D2859712A09 15 | BSC_CHAIN_ID=4 16 | BSC_ABI=V2 17 | 18 | POLYGON_PROVIDER=https://polygon-mumbai.infura.io/v3/YOUR_PROJECT_ID 19 | POLYGON_TOKEN_BRIDGE_ADDRESS=0x377D55a7928c046E18eEbb61977e714d2a76472a 20 | POLYGON_CHAIN_ID=5 21 | POLYGON_ABI=V2 22 | 23 | AVAX_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc" 24 | AVAX_TOKEN_BRIDGE_ADDRESS=0x61E44E506Ca5659E6c0bba9b678586fA2d729756 25 | AVAX_CHAIN_ID=6 26 | AVAX_ABI=V2 27 | 28 | WALLET_PRIVATE_KEY=your_key_here 29 | 30 | TERRA_PROVIDER=https://bombay-lcd.terra.dev 31 | TERRA_CHAIN_ID=bombay-12 32 | TERRA_NAME=testnet 33 | TERRA_WALLET_PRIVATE_KEY=your_key_here 34 | TERRA_GAS_PRICE_URL=https://bombay-fcd.terra.dev/v1/txs/gas_prices 35 | TERRA_TOKEN_BRIDGE_ADDRESS=terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a 36 | 37 | #LOG_DIR=/var/logs 38 | LOG_LEVEL=debug 39 | 40 | ETH_CONTRACT_ADDRESS=0x9e7Cae3a46ED297b0a05FCEeb41160fC5218E14f 41 | BSC_CONTRACT_ADDRESS=0x0DC183c2eFAA5e1749B85f13621F5cC6aCcDa786 42 | POLYGON_CONTRACT_ADDRESS=0x72F2F646dC979a9fA8aA685B8a47b7afe2fE0516 43 | TERRA_CONTRACT_ADDRESS=terra163shc8unyqrndgcldaj2q9kgnqs82v0kgkhynf 44 | AVAX_CONTRACT_ADDRESS=0x52D8A50AF35b0760335F29a4D6aaF0604B7D7484 45 | -------------------------------------------------------------------------------- /swap_relayer/.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | .env -------------------------------------------------------------------------------- /swap_relayer/Dockerfile.spy_guardian: -------------------------------------------------------------------------------- 1 | FROM docker.io/golang:1.17.0-alpine as builder 2 | 3 | RUN apk add --no-cache git gcc linux-headers alpine-sdk bash 4 | 5 | WORKDIR /app 6 | RUN git clone https://github.com/certusone/wormhole.git 7 | 8 | WORKDIR /app/wormhole/tools 9 | RUN CGO_ENABLED=0 ./build.sh 10 | 11 | WORKDIR /app/wormhole 12 | RUN tools/bin/buf lint && tools/bin/buf generate 13 | 14 | WORKDIR /app/wormhole/node/tools 15 | RUN go build -mod=readonly -o /dlv github.com/go-delve/delve/cmd/dlv 16 | 17 | WORKDIR /app/wormhole/node 18 | RUN go build -race -gcflags="all=-N -l" -mod=readonly -o /guardiand github.com/certusone/wormhole/node 19 | 20 | FROM docker.io/golang:1.17.0-alpine 21 | 22 | WORKDIR /app 23 | COPY --from=builder /guardiand /app/guardiand 24 | 25 | ENV PATH="/app:${PATH}" 26 | RUN addgroup -S pyth -g 10001 && adduser -S pyth -G pyth -u 10001 27 | RUN chown -R pyth:pyth . 28 | USER pyth 29 | 30 | ENTRYPOINT [ "guardiand", "spy", "--nodeKey", "/tmp/node.key" ] 31 | -------------------------------------------------------------------------------- /swap_relayer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swap_relay", 3 | "version": "1.0.0", 4 | "description": "Swap listener and relayer demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node lib/swap_relayer/src/index.js" 9 | }, 10 | "author": "", 11 | "license": "Apache-2.0", 12 | "devDependencies": { 13 | "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", 14 | "@types/jest": "^27.0.2", 15 | "@types/long": "^4.0.1", 16 | "@types/node": "^16.6.1", 17 | "axios": "^0.24.0", 18 | "esm": "^3.2.25", 19 | "jest": "^27.3.1", 20 | "prettier": "^2.3.2", 21 | "ts-jest": "^27.0.7", 22 | "tslint": "^6.1.3", 23 | "tslint-config-prettier": "^1.18.0", 24 | "typescript": "^4.3.5" 25 | }, 26 | "dependencies": { 27 | "@certusone/wormhole-sdk": "^0.1.4", 28 | "@certusone/wormhole-spydk": "^0.0.1", 29 | "async-mutex": "^0.3.2", 30 | "condition-variable": "^1.0.0", 31 | "body-parser": "^1.19.0", 32 | "cors": "^2.8.5", 33 | "dotenv": "^10.0.0", 34 | "ethers": "^5.5.3", 35 | "@terra-money/terra.js": "^3.0.4", 36 | "winston": "^3.3.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swap_relayer/src/evm.ts: -------------------------------------------------------------------------------- 1 | import { getIsTransferCompletedEth, hexToUint8Array } from "@certusone/wormhole-sdk"; 2 | 3 | import { ethers } from "ethers"; 4 | 5 | import { abi as SWAP_CONTRACT_V2_ABI } from "../../react/src/abi/contracts/CrossChainSwapV2.json"; 6 | import { abi as SWAP_CONTRACT_V3_ABI } from "../../react/src/abi/contracts/CrossChainSwapV3.json"; 7 | 8 | import * as swap from "../../react/src/swapper/helpers"; 9 | 10 | import { logger, OurEnvironment, Type3Payload } from "./index"; 11 | 12 | export type EvmEnvironment = { 13 | name: string; 14 | chain_id: number; 15 | provider_url: string; 16 | contract_address: string; 17 | token_bridge_address: string; 18 | wallet_private_key: string; 19 | abi_version: string; 20 | }; 21 | 22 | type EvmContractData = { 23 | chain_id: number; 24 | name: string; 25 | contractAddress: string; 26 | tokenBridgeAddress: string; 27 | contract: ethers.Contract; 28 | provider: ethers.providers.StaticJsonRpcProvider; 29 | wallet: ethers.Wallet; 30 | contractWithSigner: ethers.Contract; 31 | }; 32 | 33 | let evmContractData = new Map(); 34 | 35 | export function loadEvmConfig(): EvmEnvironment[] { 36 | let evm_configs: EvmEnvironment[] = []; 37 | let evms = process.env.EVM_CHAINS.split(","); 38 | for (const evm of evms) { 39 | let key_chain_id: string = evm + "_CHAIN_ID"; 40 | let val_chain_id: string = eval("process.env." + key_chain_id); 41 | if (!val_chain_id) { 42 | logger.error("Missing environment variable " + key_chain_id); 43 | return undefined; 44 | } 45 | 46 | let key_provider: string = evm + "_PROVIDER"; 47 | let val_provider: string = eval("process.env." + key_provider); 48 | if (!val_provider) { 49 | logger.error("Missing environment variable " + key_provider); 50 | return undefined; 51 | } 52 | 53 | let key_contract_address: string = evm + "_CONTRACT_ADDRESS"; 54 | let val_contract_address: string = eval("process.env." + key_contract_address); 55 | if (!val_contract_address) { 56 | logger.error("Missing environment variable " + key_contract_address); 57 | return undefined; 58 | } 59 | 60 | let key_token_bridge_address: string = evm + "_TOKEN_BRIDGE_ADDRESS"; 61 | let val_token_bridge_address: string = eval("process.env." + key_token_bridge_address); 62 | if (!val_token_bridge_address) { 63 | logger.error("Missing environment variable " + key_token_bridge_address); 64 | return undefined; 65 | } 66 | 67 | let key_wallet_private_key: string = evm + "_WALLET_PRIVATE_KEY"; 68 | let val_wallet_private_key: string = eval("process.env." + key_wallet_private_key); 69 | if (!val_wallet_private_key) val_wallet_private_key = process.env.WALLET_PRIVATE_KEY; 70 | if (!val_wallet_private_key) { 71 | logger.error("Missing environment variable " + key_wallet_private_key + " or WALLET_PRIVATE_KEY"); 72 | return undefined; 73 | } 74 | 75 | let key_abi_version: string = evm + "_ABI"; 76 | let val_abi_version: string = eval("process.env." + key_abi_version); 77 | if (!val_abi_version) { 78 | logger.error("Missing environment variable " + key_abi_version); 79 | return undefined; 80 | } 81 | 82 | if (val_abi_version !== "V2" && val_abi_version !== "V3") { 83 | logger.error( 84 | "Invalid value of environment variable " + 85 | key_abi_version + 86 | ", is [" + 87 | val_abi_version + 88 | "], must be either V2 or V3" 89 | ); 90 | return undefined; 91 | } 92 | 93 | evm_configs.push({ 94 | name: evm, 95 | chain_id: parseInt(val_chain_id), 96 | provider_url: val_provider, 97 | contract_address: val_contract_address, 98 | token_bridge_address: val_token_bridge_address, 99 | wallet_private_key: val_wallet_private_key, 100 | abi_version: val_abi_version, 101 | }); 102 | } 103 | 104 | return evm_configs; 105 | } 106 | 107 | export function makeEvmContractData(envs: EvmEnvironment[]) { 108 | if (!envs) return; 109 | for (const evm of envs) { 110 | evmContractData.set(evm.chain_id, makeContractDataForEvm(evm)); 111 | } 112 | } 113 | 114 | function makeContractDataForEvm(env: EvmEnvironment): EvmContractData { 115 | let contractAddress: string = env.contract_address.toLowerCase(); 116 | if (contractAddress.search("0x") === 0) { 117 | contractAddress = contractAddress.substring(2); 118 | } 119 | 120 | logger.info( 121 | "Connecting to " + 122 | env.name + 123 | ": chain_id: " + 124 | env.chain_id + 125 | ", contract address: [" + 126 | contractAddress + 127 | "], node: [" + 128 | env.provider_url + 129 | "], token bridge address: [" + 130 | env.token_bridge_address + 131 | "], abi version: [" + 132 | env.abi_version + 133 | "]" 134 | ); 135 | 136 | const provider = new ethers.providers.StaticJsonRpcProvider(env.provider_url); 137 | 138 | const contract = new ethers.Contract( 139 | contractAddress, 140 | env.abi_version == "V2" ? SWAP_CONTRACT_V2_ABI : SWAP_CONTRACT_V3_ABI, 141 | provider 142 | ); 143 | 144 | const wallet = new ethers.Wallet(env.wallet_private_key, provider); 145 | const contractWithSigner = contract.connect(wallet); 146 | 147 | return { 148 | chain_id: env.chain_id, 149 | name: env.name, 150 | contractAddress: contractAddress, 151 | tokenBridgeAddress: env.token_bridge_address, 152 | contract: contract, 153 | provider: provider, 154 | wallet: wallet, 155 | contractWithSigner: contractWithSigner, 156 | }; 157 | } 158 | 159 | export function isEvmContract(contractAddress: string, chain_id: number): boolean { 160 | let ecd = evmContractData.get(chain_id); 161 | return ecd && ecd.contractAddress === contractAddress; 162 | } 163 | 164 | export async function relayVaaToEvm(vaaBytes: string, t3Payload: Type3Payload) { 165 | let ecd = evmContractData.get(t3Payload.targetChainId); 166 | if (!ecd) { 167 | logger.error("relayVaaToEvm: chain id " + t3Payload.targetChainId + " does not exist!"); 168 | } 169 | 170 | let exactIn: boolean = false; 171 | let error: boolean = false; 172 | if (t3Payload.swapFunctionType === 1) { 173 | exactIn = true; 174 | } else if (t3Payload.swapFunctionType !== 2) { 175 | error = true; 176 | logger.error("relayVaaTo" + ecd.name + ": unsupported swapFunctionType: [" + t3Payload.swapFunctionType + "]"); 177 | } 178 | if (error) return; 179 | 180 | logger.debug( 181 | "relayVaaTo" + ecd.name + ": chain_id: " + ecd.chain_id + ", contractAddress: [" + t3Payload.contractAddress + "]" 182 | ); 183 | 184 | const signedVaaArray = hexToUint8Array(vaaBytes); 185 | await relayVaaToEvmChain(t3Payload, ecd, signedVaaArray, exactIn); 186 | } 187 | 188 | async function relayVaaToEvmChain( 189 | t3Payload: Type3Payload, 190 | tcd: EvmContractData, 191 | signedVaaArray: Uint8Array, 192 | exactIn: boolean 193 | ) { 194 | logger.debug( 195 | "relayVaaTo" + 196 | tcd.name + 197 | ": checking if already redeemed on " + 198 | tcd.name + 199 | " using tokenBridgeAddress [" + 200 | tcd.tokenBridgeAddress + 201 | "]" 202 | ); 203 | 204 | if (await isRedeemedOnEvm(tcd, signedVaaArray)) { 205 | logger.info( 206 | "relayVaaTo" + 207 | tcd.name + 208 | ": srcChain: " + 209 | t3Payload.sourceChainId + 210 | ", targetChain: " + 211 | t3Payload.targetChainId + 212 | ", contract: [" + 213 | t3Payload.contractAddress + 214 | "], exactIn: " + 215 | exactIn + 216 | ": completed: already transferred" 217 | ); 218 | 219 | return; 220 | } 221 | 222 | logger.info( 223 | "relayVaaTo" + 224 | tcd.name + 225 | ": srcChain: " + 226 | t3Payload.sourceChainId + 227 | ", targetChain: " + 228 | t3Payload.targetChainId + 229 | ", contract: [" + 230 | t3Payload.contractAddress + 231 | "], exactIn: " + 232 | exactIn + 233 | ": submitting redeem request" 234 | ); 235 | 236 | try { 237 | let receipt: any = null; 238 | if (exactIn) { 239 | logger.debug("relayVaaTo: calling evmSwapExactInFromVaaNative()"); 240 | receipt = await swap.evmSwapExactInFromVaaNative(tcd.contractWithSigner, signedVaaArray); 241 | } else { 242 | logger.debug("relayVaaTo: calling evmSwapExactOutFromVaaNative()"); 243 | receipt = await swap.evmSwapExactOutFromVaaNative(tcd.contractWithSigner, signedVaaArray); 244 | } 245 | 246 | logger.info( 247 | "relayVaaTo" + 248 | tcd.name + 249 | ": srcChain: " + 250 | t3Payload.sourceChainId + 251 | ", targetChain: " + 252 | t3Payload.targetChainId + 253 | ", contract: [" + 254 | t3Payload.contractAddress + 255 | "], exactIn: " + 256 | exactIn + 257 | ": completed: success, txHash: " + 258 | receipt.transactionHash 259 | ); 260 | } catch (e: any) { 261 | if (await isRedeemedOnEvm(tcd, signedVaaArray)) { 262 | logger.info( 263 | "relayVaaTo" + 264 | tcd.name + 265 | ": srcChain: " + 266 | t3Payload.sourceChainId + 267 | ", targetChain: " + 268 | t3Payload.targetChainId + 269 | ", contract: [" + 270 | t3Payload.contractAddress + 271 | "], exactIn: " + 272 | exactIn + 273 | ": completed: relay failed because the vaa has already been redeemed" 274 | ); 275 | 276 | return; 277 | } 278 | 279 | logger.error( 280 | "relayVaaTo" + 281 | tcd.name + 282 | ": contract: [" + 283 | t3Payload.contractAddress + 284 | "], exactIn: " + 285 | exactIn + 286 | ": transaction failed: %o", 287 | e 288 | ); 289 | } 290 | 291 | if (await isRedeemedOnEvm(tcd, signedVaaArray)) { 292 | logger.info( 293 | "relayVaaTo" + 294 | tcd.name + 295 | ": srcChain: " + 296 | t3Payload.sourceChainId + 297 | ", targetChain: " + 298 | t3Payload.targetChainId + 299 | ", contract: [" + 300 | t3Payload.contractAddress + 301 | "], exactIn: " + 302 | exactIn + 303 | ": redeem confirmed" 304 | ); 305 | } else { 306 | logger.error( 307 | "relayVaaTo" + 308 | tcd.name + 309 | ": srcChain: " + 310 | t3Payload.sourceChainId + 311 | ", targetChain: " + 312 | t3Payload.targetChainId + 313 | ", contract: [" + 314 | t3Payload.contractAddress + 315 | "], exactIn: " + 316 | exactIn + 317 | ": completed: failed to confirm redeem!" 318 | ); 319 | } 320 | } 321 | 322 | async function isRedeemedOnEvm(tcd: EvmContractData, signedVaaArray: Uint8Array): Promise { 323 | let redeemed: boolean = false; 324 | try { 325 | redeemed = await getIsTransferCompletedEth(tcd.tokenBridgeAddress, tcd.provider, signedVaaArray); 326 | } catch (e) { 327 | logger.error( 328 | "relayVaaTo" + tcd.name + ": failed to check if transfer is already complete, will attempt the transfer, e: %o", 329 | e 330 | ); 331 | } 332 | 333 | return redeemed; 334 | } 335 | -------------------------------------------------------------------------------- /swap_relayer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Mutex } from "async-mutex"; 2 | let CondVar = require("condition-variable"); 3 | 4 | import { 5 | ChainId, 6 | CHAIN_ID_SOLANA, 7 | CHAIN_ID_TERRA, 8 | hexToUint8Array, 9 | uint8ArrayToHex, 10 | getEmitterAddressEth, 11 | getEmitterAddressSolana, 12 | getEmitterAddressTerra, 13 | } from "@certusone/wormhole-sdk"; 14 | 15 | import { importCoreWasm, setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm"; 16 | import { createSpyRPCServiceClient, subscribeSignedVAA } from "@certusone/wormhole-spydk"; 17 | import { ethers } from "ethers"; 18 | import { EvmEnvironment, isEvmContract, loadEvmConfig, makeEvmContractData, relayVaaToEvm } from "./evm"; 19 | import { isTerraContract, loadTerraConfig, makeTerraContractData, relayVaaToTerra, TerraEnvironment } from "./terra"; 20 | 21 | export let logger: any; 22 | 23 | let configFile: string = ".env"; 24 | if (process.env.SWAP_RELAY_CONFIG) { 25 | configFile = process.env.SWAP_RELAY_CONFIG; 26 | } 27 | 28 | console.log("Loading config file [%s]", configFile); 29 | require("dotenv").config({ path: configFile }); 30 | 31 | initLogger(); 32 | 33 | export type OurEnvironment = { 34 | spy_host: string; 35 | spy_filters: string; 36 | 37 | evm_configs: EvmEnvironment[]; 38 | terra_config: TerraEnvironment; 39 | }; 40 | 41 | export type Type3Payload = { 42 | sourceChainId: number; 43 | targetChainId: number; 44 | contractAddress: string; 45 | relayerFee: ethers.BigNumber; 46 | swapFunctionType: number; 47 | }; 48 | 49 | type PendingEvent = { 50 | vaaBytes: string; 51 | t3Payload: Type3Payload; 52 | receiveTime: Date; 53 | }; 54 | 55 | setDefaultWasm("node"); 56 | 57 | let success: boolean; 58 | let env: OurEnvironment; 59 | [success, env] = loadConfig(); 60 | 61 | let seqMap = new Map(); 62 | 63 | const mutex = new Mutex(); 64 | let condition = new CondVar(); 65 | let pendingQueue = new Array(); 66 | 67 | if (success) { 68 | logger.info("swap_relayer starting up, will listen for signed VAAs from [" + env.spy_host + "]"); 69 | 70 | try { 71 | makeEvmContractData(env.evm_configs); 72 | makeTerraContractData(env.terra_config); 73 | } catch (e: any) { 74 | logger.error("failed to connect to target contracts: %o", e); 75 | success = false; 76 | } 77 | 78 | if (success) { 79 | run_worker(); 80 | spy_listen(); 81 | } 82 | } 83 | 84 | function loadConfig(): [boolean, OurEnvironment] { 85 | if (!process.env.SPY_SERVICE_HOST) { 86 | logger.error("Missing environment variable SPY_SERVICE_HOST"); 87 | return [false, undefined]; 88 | } 89 | 90 | let evm_configs: EvmEnvironment[] = null; 91 | if (process.env.EVM_CHAINS) { 92 | evm_configs = loadEvmConfig(); 93 | if (!evm_configs) return [false, undefined]; 94 | } 95 | 96 | let terra_config = loadTerraConfig(); 97 | if (!terra_config) return [false, undefined]; 98 | 99 | return [ 100 | true, 101 | { 102 | spy_host: process.env.SPY_SERVICE_HOST, 103 | spy_filters: process.env.SPY_SERVICE_FILTERS, 104 | evm_configs: evm_configs, 105 | terra_config: terra_config, 106 | }, 107 | ]; 108 | } 109 | 110 | async function spy_listen() { 111 | (async () => { 112 | var filter = {}; 113 | if (env.spy_filters) { 114 | const parsedJsonFilters = eval(env.spy_filters); 115 | 116 | var myFilters = []; 117 | for (var i = 0; i < parsedJsonFilters.length; i++) { 118 | var myChainId = parseInt(parsedJsonFilters[i].chain_id) as ChainId; 119 | var myEmitterAddress = await encodeEmitterAddress(myChainId, parsedJsonFilters[i].emitter_address); 120 | var myEmitterFilter = { 121 | emitterFilter: { 122 | chainId: myChainId, 123 | emitterAddress: myEmitterAddress, 124 | }, 125 | }; 126 | logger.info( 127 | "adding filter: chainId: [" + 128 | myEmitterFilter.emitterFilter.chainId + 129 | "], emitterAddress: [" + 130 | myEmitterFilter.emitterFilter.emitterAddress + 131 | "]" 132 | ); 133 | myFilters.push(myEmitterFilter); 134 | } 135 | 136 | logger.info("setting " + myFilters.length + " filters"); 137 | filter = { 138 | filters: myFilters, 139 | }; 140 | } else { 141 | logger.info("processing all signed VAAs"); 142 | } 143 | 144 | const client = createSpyRPCServiceClient(env.spy_host); 145 | const stream = await subscribeSignedVAA(client, filter); 146 | 147 | stream.on("data", ({ vaaBytes }) => { 148 | processVaa(vaaBytes); 149 | }); 150 | 151 | logger.info("swap_relayer waiting for transfer signed VAAs"); 152 | })(); 153 | } 154 | 155 | async function encodeEmitterAddress(myChainId, emitterAddressStr): Promise { 156 | if (myChainId === CHAIN_ID_SOLANA) { 157 | return await getEmitterAddressSolana(emitterAddressStr); 158 | } 159 | 160 | if (myChainId === CHAIN_ID_TERRA) { 161 | return await getEmitterAddressTerra(emitterAddressStr); 162 | } 163 | 164 | return getEmitterAddressEth(emitterAddressStr); 165 | } 166 | 167 | async function processVaa(vaaBytes: string) { 168 | let receiveTime = new Date(); 169 | // logger.debug("processVaa"); 170 | const { parse_vaa } = await importCoreWasm(); 171 | const parsedVAA = parse_vaa(hexToUint8Array(vaaBytes)); 172 | // logger.debug("processVaa: parsedVAA: %o", parsedVAA); 173 | 174 | let emitter_address: string = uint8ArrayToHex(parsedVAA.emitter_address); 175 | 176 | let seqNumKey: string = parsedVAA.emitter_chain.toString() + ":" + emitter_address; 177 | let lastSeqNum = seqMap.get(seqNumKey); 178 | if (lastSeqNum) { 179 | if (lastSeqNum >= parsedVAA.sequence) { 180 | logger.debug("ignoring duplicate: emitter: [" + seqNumKey + "], seqNum: " + parsedVAA.sequence); 181 | return; 182 | } 183 | } 184 | 185 | seqMap.set(seqNumKey, parsedVAA.sequence); 186 | 187 | let t3Payload: Type3Payload = null; 188 | try { 189 | t3Payload = decodeSignedVAAPayloadType3(parsedVAA, parsedVAA.emitter_chain); 190 | } catch (e) { 191 | logger.error("failed to parse type 3 vaa: %o", e); 192 | return; 193 | } 194 | 195 | if (t3Payload) { 196 | if (isOurContract(t3Payload.contractAddress, t3Payload.targetChainId)) { 197 | logger.info( 198 | "enqueuing type 3 vaa: emitter: [" + 199 | parsedVAA.emitter_chain + 200 | ":" + 201 | emitter_address + 202 | "], seqNum: " + 203 | parsedVAA.sequence + 204 | ", target: [" + 205 | t3Payload.targetChainId + 206 | ":" + 207 | t3Payload.contractAddress + 208 | "], relayerFee: [" + 209 | t3Payload.relayerFee + 210 | "], swapFunctionType: [" + 211 | t3Payload.swapFunctionType + 212 | "]" 213 | ); 214 | 215 | await postVaa(vaaBytes, t3Payload, receiveTime); 216 | } else { 217 | logger.debug( 218 | "dropping type 3 vaa for unsupported contract: emitter: [" + 219 | parsedVAA.emitter_chain + 220 | ":" + 221 | emitter_address + 222 | "], seqNum: " + 223 | parsedVAA.sequence + 224 | ", target: [" + 225 | t3Payload.targetChainId + 226 | ":" + 227 | t3Payload.contractAddress + 228 | "], relayerFee: [" + 229 | t3Payload.relayerFee + 230 | "], swapFunctionType: [" + 231 | t3Payload.swapFunctionType + 232 | "]" 233 | ); 234 | } 235 | // } else { 236 | // logger.debug( 237 | // "dropping vaa: emitter: [" + 238 | // parsedVAA.emitter_chain + 239 | // ":" + 240 | // emitter_address + 241 | // "], seqNum: " + 242 | // parsedVAA.sequence + 243 | // " payloadType: " + 244 | // parsedVAA.payload[0] 245 | // ); 246 | } 247 | } 248 | 249 | function decodeSignedVAAPayloadType3(parsedVAA: any, sourceChainId: number): Type3Payload { 250 | const payload = Buffer.from(new Uint8Array(parsedVAA.payload)); 251 | if (payload[0] !== 3) return undefined; 252 | 253 | logger.info("decodeSignedVAAPayloadType3: length: " + payload.length); 254 | if (payload.length < 101) { 255 | logger.error( 256 | "decodeSignedVAAPayloadType3: dropping type 3 vaa because the payload is too short to determine the target chain id, length: " + 257 | payload.length 258 | ); 259 | return undefined; 260 | } 261 | 262 | const targetChainId = payload.readUInt16BE(99); 263 | logger.info("decodeSignedVAAPayloadType3: target ChainId: " + targetChainId); 264 | 265 | let contractAddress: string = ""; 266 | let swapFunctionType: number = 0; 267 | 268 | if (targetChainId === 3) { 269 | logger.info("decodeSignedVAAPayloadType3: terraContractAddr: [" + payload.slice(67, 67 + 32).toString("hex") + "]"); 270 | 271 | contractAddress = payload.slice(67, 67 + 32).toString("hex"); 272 | } else { 273 | if (payload.length < 272) { 274 | logger.error( 275 | "decodeSignedVAAPayloadType3: dropping type 3 vaa because the payload is too short to extract the contract fields, length: " + 276 | payload.length + 277 | ", target chain id: " + 278 | targetChainId 279 | ); 280 | return undefined; 281 | } 282 | contractAddress = payload.slice(79, 79 + 20).toString("hex"); 283 | swapFunctionType = payload.readUInt8(272); 284 | } 285 | 286 | return { 287 | sourceChainId: sourceChainId, 288 | targetChainId: targetChainId, 289 | contractAddress: contractAddress, 290 | relayerFee: ethers.BigNumber.from(payload.slice(273, 273 + 32)), 291 | swapFunctionType: swapFunctionType, 292 | }; 293 | } 294 | 295 | function isOurContract(contractAddress: string, chainId: number): boolean { 296 | return isEvmContract(contractAddress, chainId) || isTerraContract(contractAddress, chainId); 297 | } 298 | 299 | async function postVaa(vaaBytes: any, t3Payload: Type3Payload, receiveTime: Date) { 300 | let event: PendingEvent = { 301 | vaaBytes: vaaBytes, 302 | t3Payload: t3Payload, 303 | receiveTime: receiveTime, 304 | }; 305 | 306 | await mutex.runExclusive(() => { 307 | pendingQueue.push(event); 308 | logger.debug("posting event, there are now " + pendingQueue.length + " enqueued events"); 309 | if (condition) { 310 | logger.debug("hitting condition variable."); 311 | condition.complete(true); 312 | } 313 | }); 314 | } 315 | 316 | const COND_VAR_TIMEOUT = 10000; 317 | 318 | async function run_worker() { 319 | await mutex.runExclusive(async () => { 320 | await condition.wait(COND_VAR_TIMEOUT, callBack); 321 | }); 322 | } 323 | 324 | async function callBack(err: any, result: any) { 325 | // logger.debug( 326 | // "entering callback, pendingEvents: " + 327 | // pendingQueue.length + 328 | // ", err: %o, result: %o", 329 | // err, 330 | // result 331 | // ); 332 | 333 | let done = false; 334 | do { 335 | let currEvent: PendingEvent = null; 336 | 337 | await mutex.runExclusive(async () => { 338 | condition = null; 339 | if (pendingQueue.length !== 0) { 340 | currEvent = pendingQueue[0]; 341 | pendingQueue.pop(); 342 | } else { 343 | done = true; 344 | condition = new CondVar(); 345 | await condition.wait(COND_VAR_TIMEOUT, callBack); 346 | } 347 | }); 348 | 349 | if (currEvent) { 350 | logger.debug("in callback, relaying event."); 351 | try { 352 | await relayVaa(currEvent.vaaBytes, currEvent.t3Payload); 353 | } catch (e) { 354 | logger.error("failed to relay type 3 vaa: %o", e); 355 | } 356 | 357 | await mutex.runExclusive(async () => { 358 | if (pendingQueue.length === 0) { 359 | logger.debug("in callback, no more pending events, rearming the condition."); 360 | done = true; 361 | condition = new CondVar(); 362 | await condition.wait(COND_VAR_TIMEOUT, callBack); 363 | } else { 364 | logger.debug("in callback, there are " + pendingQueue.length + " pending events."); 365 | } 366 | }); 367 | } 368 | } while (!done); 369 | 370 | // logger.debug("leaving callback."); 371 | } 372 | 373 | async function relayVaa(vaaBytes: string, t3Payload: Type3Payload) { 374 | if (t3Payload.targetChainId === 3) { 375 | await relayVaaToTerra(t3Payload, vaaBytes); 376 | return; 377 | } 378 | 379 | await relayVaaToEvm(vaaBytes, t3Payload); 380 | } 381 | 382 | ///////////////////////////////// Start of logger stuff /////////////////////////////////////////// 383 | 384 | function initLogger() { 385 | const winston = require("winston"); 386 | 387 | let useConsole: boolean = true; 388 | let logFileName: string = ""; 389 | if (process.env.LOG_DIR) { 390 | useConsole = false; 391 | logFileName = process.env.LOG_DIR + "/swap_relay." + new Date().toISOString() + ".log"; 392 | } 393 | 394 | let logLevel = "info"; 395 | if (process.env.LOG_LEVEL) { 396 | logLevel = process.env.LOG_LEVEL; 397 | } 398 | 399 | let transport: any; 400 | if (useConsole) { 401 | console.log("swap_relay is logging to the console at level [%s]", logLevel); 402 | 403 | transport = new winston.transports.Console({ 404 | level: logLevel, 405 | }); 406 | } else { 407 | console.log("swap_relay is logging to [%s] at level [%s]", logFileName, logLevel); 408 | 409 | transport = new winston.transports.File({ 410 | filename: logFileName, 411 | level: logLevel, 412 | }); 413 | } 414 | 415 | const logConfiguration = { 416 | transports: [transport], 417 | format: winston.format.combine( 418 | winston.format.splat(), 419 | winston.format.simple(), 420 | winston.format.timestamp({ 421 | format: "YYYY-MM-DD HH:mm:ss.SSS", 422 | }), 423 | winston.format.printf((info: any) => `${[info.timestamp]}|${info.level}|${info.message}`) 424 | ), 425 | }; 426 | 427 | logger = winston.createLogger(logConfiguration); 428 | } 429 | -------------------------------------------------------------------------------- /swap_relayer/src/terra.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHAIN_ID_TERRA, 3 | getIsTransferCompletedTerra, 4 | redeemOnTerra, 5 | uint8ArrayToHex, 6 | } from "@certusone/wormhole-sdk"; 7 | 8 | import axios from "axios"; 9 | import { bech32 } from "bech32"; 10 | import { zeroPad } from "ethers/lib/utils"; 11 | import { fromUint8Array } from "js-base64"; 12 | 13 | import * as Terra from "@terra-money/terra.js"; 14 | 15 | import { logger, OurEnvironment, Type3Payload } from "./index"; 16 | 17 | export type TerraEnvironment = { 18 | terra_enabled: boolean; 19 | terra_provider_url: string; 20 | terra_chain_id: string; 21 | terra_name: string; 22 | terra_contract_address: string; 23 | terra_token_bridge_address: string; 24 | terra_wallet_private_key: string; 25 | terra_gas_price_url: string; 26 | }; 27 | 28 | type TerraContractData = { 29 | name: string; 30 | contractAddress: string; 31 | encodedContractAddress: string; 32 | tokenBridgeAddress: string; 33 | lcdConfig: Terra.LCDClientConfig; 34 | lcdClient: Terra.LCDClient; 35 | wallet: Terra.Wallet; 36 | gasPriceUrl: string; 37 | }; 38 | 39 | let terraContractData: TerraContractData = null; 40 | 41 | export function loadTerraConfig(): TerraEnvironment { 42 | let terra_enabled: boolean = false; 43 | if (process.env.TERRA_PROVIDER) { 44 | terra_enabled = true; 45 | 46 | if (!process.env.TERRA_CHAIN_ID) { 47 | logger.error("Missing environment variable WALLET_PRIVATE_KEY"); 48 | return undefined; 49 | } 50 | 51 | if (!process.env.TERRA_NAME) { 52 | logger.error("Missing environment variable WALLET_PRIVATE_KEY"); 53 | return undefined; 54 | } 55 | 56 | if (!process.env.TERRA_WALLET_PRIVATE_KEY) { 57 | logger.error("Missing environment variable TERRA_WALLET_PRIVATE_KEY"); 58 | return undefined; 59 | } 60 | 61 | if (!process.env.TERRA_GAS_PRICE_URL) { 62 | logger.error("Missing environment variable TERRA_GAS_PRICE_URL"); 63 | return undefined; 64 | } 65 | 66 | if (!process.env.TERRA_CONTRACT_ADDRESS) { 67 | logger.error("Missing environment variable TERRA_CONTRACT_ADDRESS"); 68 | return undefined; 69 | } 70 | 71 | if (!process.env.TERRA_TOKEN_BRIDGE_ADDRESS) { 72 | logger.error("Missing environment variable TERRA_TOKEN_BRIDGE_ADDRESS"); 73 | return undefined; 74 | } 75 | } 76 | 77 | return { 78 | terra_enabled: terra_enabled, 79 | terra_provider_url: process.env.TERRA_PROVIDER, 80 | terra_chain_id: process.env.TERRA_CHAIN_ID, 81 | terra_name: process.env.TERRA_NAME, 82 | terra_contract_address: process.env.TERRA_CONTRACT_ADDRESS, 83 | terra_token_bridge_address: process.env.TERRA_TOKEN_BRIDGE_ADDRESS, 84 | terra_wallet_private_key: process.env.TERRA_WALLET_PRIVATE_KEY, 85 | terra_gas_price_url: process.env.TERRA_GAS_PRICE_URL, 86 | }; 87 | } 88 | 89 | export function makeTerraContractData(env: TerraEnvironment) { 90 | if (!env.terra_enabled) return; 91 | let contractAddress: string = env.terra_contract_address.toLowerCase(); 92 | if (contractAddress.search("0x") == 0) { 93 | contractAddress = contractAddress.substring(2); 94 | } 95 | 96 | let encodedContractAddress: string = Buffer.from( 97 | zeroPad(bech32.fromWords(bech32.decode(contractAddress).words), 32) 98 | ).toString("hex"); 99 | 100 | logger.info( 101 | "Connecting to Terra: contract address: [" + 102 | contractAddress + 103 | "], encoded contract address: [" + 104 | encodedContractAddress + 105 | "], node: [" + 106 | env.terra_provider_url + 107 | "], token bridge address: [" + 108 | env.terra_token_bridge_address + 109 | "], terra chain id: [" + 110 | env.terra_chain_id + 111 | "], terra name: [" + 112 | env.terra_name + 113 | "]" 114 | ); 115 | 116 | const lcdConfig = { 117 | URL: env.terra_provider_url, 118 | chainID: env.terra_chain_id, 119 | name: env.terra_name, 120 | }; 121 | 122 | const lcdClient = new Terra.LCDClient(lcdConfig); 123 | 124 | const mk = new Terra.MnemonicKey({ 125 | mnemonic: env.terra_wallet_private_key, 126 | }); 127 | 128 | const wallet = lcdClient.wallet(mk); 129 | 130 | terraContractData = { 131 | name: "Terra", 132 | contractAddress: contractAddress, 133 | encodedContractAddress: encodedContractAddress, 134 | tokenBridgeAddress: env.terra_token_bridge_address, 135 | lcdConfig: lcdConfig, 136 | lcdClient: lcdClient, 137 | wallet: wallet, 138 | gasPriceUrl: env.terra_gas_price_url, 139 | }; 140 | } 141 | 142 | export function isTerraContract( 143 | contractAddress: string, 144 | chain_id: number 145 | ): boolean { 146 | if (chain_id !== CHAIN_ID_TERRA) return false; 147 | if (!terraContractData) return false; 148 | 149 | let retVal: boolean = 150 | terraContractData && 151 | contractAddress === terraContractData.encodedContractAddress; 152 | logger.debug( 153 | "isTerraContract: comparing [" + 154 | contractAddress + 155 | "] to [" + 156 | terraContractData.encodedContractAddress + 157 | "]: " + 158 | retVal 159 | ); 160 | 161 | return retVal; 162 | } 163 | 164 | export async function relayVaaToTerra( 165 | t3Payload: Type3Payload, 166 | vaaBytes: string 167 | ) { 168 | if (!terraContractData) return; 169 | 170 | // logger.debug( 171 | // "relayVaaToTerra: checking if already redeemed using tokenBridgeAddress [" + 172 | // terraContractData.tokenBridgeAddress + 173 | // "]" 174 | // ); 175 | 176 | // if (await isRedeemedOnTerra(terraContractData, vaaBytes)) { 177 | // logger.info( 178 | // "relayVaaToTerra: srcChain: " + 179 | // t3Payload.sourceChainId + 180 | // ", targetChain: " + 181 | // t3Payload.targetChainId + 182 | // ", contract: [" + 183 | // t3Payload.contractAddress + 184 | // "]: completed: already redeemed" 185 | // ); 186 | 187 | // return; 188 | // } 189 | 190 | try { 191 | logger.debug( 192 | "relayVaaToTerra: creating message using contract address [" + 193 | terraContractData.contractAddress + 194 | "]" 195 | ); 196 | 197 | logger.debug( 198 | "relayVaaToTerra: vaa as hex: [" + 199 | Buffer.from(vaaBytes, "hex").toString("hex") + 200 | "]" 201 | ); 202 | 203 | logger.debug( 204 | "relayVaaToTerra: vaa as base64: [" + 205 | Buffer.from(vaaBytes, "hex").toString("base64") + 206 | "]" 207 | ); 208 | const msg = new Terra.MsgExecuteContract( 209 | terraContractData.wallet.key.accAddress, 210 | terraContractData.contractAddress, 211 | { 212 | submit_vaa: { 213 | data: Buffer.from(vaaBytes, "hex").toString("base64"), 214 | }, 215 | } 216 | ); 217 | 218 | // logger.debug("relayVaaToTerra: getting gas prices"); 219 | // const gasPrices = terraContractData.lcdClient.config.gasPrices; 220 | 221 | // logger.debug("relayVaaToTerra: estimating fees"); 222 | // const feeEstimate = await terraContractData.lcdClient.tx.estimateFee(terraContractData.wallet.key.accAddress, [msg], { 223 | // feeDenoms: ["uluna"], 224 | // gasPrices, 225 | // }); 226 | 227 | logger.debug("relayVaaToTerra: creating transaction"); 228 | const tx = await terraContractData.wallet.createAndSignTx({ 229 | msgs: [msg], 230 | memo: "swap relayer", 231 | feeDenoms: ["uluna"], 232 | // gasPrices, 233 | // fee: feeEstimate, 234 | }); 235 | 236 | logger.info( 237 | "relayVaaToTerra: contract: [" + 238 | t3Payload.contractAddress + 239 | "]: submitting redeem request" 240 | ); 241 | 242 | const receipt = await terraContractData.lcdClient.tx.broadcast(tx); 243 | 244 | logger.info( 245 | "relayVaaToTerra: srcChain: " + 246 | t3Payload.sourceChainId + 247 | ", targetChain: " + 248 | t3Payload.targetChainId + 249 | ", contract: [" + 250 | t3Payload.contractAddress + 251 | "]: completed: success: %o", 252 | receipt 253 | ); 254 | 255 | // logger.info( 256 | // "relayVaaToTerra: contract: [" + 257 | // t3Payload.contractAddress + 258 | // "]: success, txHash: " + 259 | // receipt.transactionHash 260 | // ); 261 | } catch (e: any) { 262 | // if (await isRedeemedOnTerra(terraContractData, vaaBytes)) { 263 | // logger.info( 264 | // "relayVaaToTerra: srcChain: " + 265 | // t3Payload.sourceChainId + 266 | // ", targetChain: " + 267 | // t3Payload.targetChainId + 268 | // ", contract: [" + 269 | // t3Payload.contractAddress + 270 | // "]: completed: relay failed because the vaa has already been redeemed" 271 | // ); 272 | 273 | // return; 274 | // } 275 | 276 | logger.error( 277 | "relayVaaToTerra: srcChain: " + 278 | t3Payload.sourceChainId + 279 | ", targetChain: " + 280 | t3Payload.targetChainId + 281 | ", contract: [" + 282 | t3Payload.contractAddress + 283 | "]: completed: transaction failed: %o", 284 | e 285 | ); 286 | } 287 | 288 | // if (await isRedeemedOnTerra(terraContractData, vaaBytes)) { 289 | // logger.info( 290 | // "relayVaaToTerra: srcChain: " + 291 | // t3Payload.sourceChainId + 292 | // ", targetChain: " + 293 | // t3Payload.targetChainId + 294 | // ", contract: [" + 295 | // t3Payload.contractAddress + 296 | // "]: redeem confirmed" 297 | // ); 298 | // } else { 299 | // logger.error( 300 | // "relayVaaToTerra: srcChain: " + 301 | // t3Payload.sourceChainId + 302 | // ", targetChain: " + 303 | // t3Payload.targetChainId + 304 | // ", contract: [" + 305 | // t3Payload.contractAddress + 306 | // "]: completed: failed to confirm redeem!" 307 | // ); 308 | // } 309 | } 310 | 311 | async function isRedeemedOnTerra( 312 | terraContractData: TerraContractData, 313 | vaaBytes: string 314 | ): Promise { 315 | let msg: Terra.MsgExecuteContract = null; 316 | let sequenceNumber: number = 0; 317 | try { 318 | msg = new Terra.MsgExecuteContract( 319 | terraContractData.wallet.key.accAddress, 320 | terraContractData.tokenBridgeAddress, 321 | { 322 | submit_vaa: { 323 | data: Buffer.from(vaaBytes, "hex").toString("base64"), 324 | }, 325 | } 326 | ); 327 | 328 | sequenceNumber = await terraContractData.wallet.sequence(); 329 | } catch (e) { 330 | logger.error( 331 | "isRedeemedOnTerra: failed to create message or look up sequence number, e: %o", 332 | e 333 | ); 334 | 335 | return false; 336 | } 337 | 338 | try { 339 | await terraContractData.lcdClient.tx.estimateFee( 340 | [{ sequenceNumber: sequenceNumber }], 341 | { 342 | msgs: [msg], 343 | } 344 | ); 345 | } catch (e) { 346 | if (e.response.data.error.includes("VaaAlreadyExecuted")) { 347 | return true; 348 | } 349 | 350 | logger.error( 351 | "isRedeemedOnTerra: failed to check if transfer is already complete, e: %o", 352 | e 353 | ); 354 | } 355 | 356 | return false; 357 | } 358 | -------------------------------------------------------------------------------- /swap_relayer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "target": "esnext", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "lib": ["es2019"], 8 | "skipLibCheck": true, 9 | "allowJs": true, 10 | "resolveJsonModule": true 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "**/__tests__/*"] 14 | } 15 | --------------------------------------------------------------------------------