Resolve merge conflict

This commit is contained in:
Karl Kempe 2022-01-31 22:51:14 +00:00
commit 20cff589b0
63 changed files with 3797 additions and 1288 deletions

View File

@ -6,7 +6,7 @@ Multi-chain native-to-native token swap using existing DEXes.
### Details
Using liquidity of native vs UST (i.e. the UST highway), one can swap from native A on chain A to native B on chain B. For this specific example, we demonstrate a swap between Polygon (Mumbai testnet) and Ethereum (Goerli testnet) between MATIC and ETH. We wrote example smart contracts to interact with Uniswap V3 and Uniswap V2 forks (QuickSwap in this specific example for Polygon). Any DEX can be used to replace our example as long as the swap for a particular DEX has all of its parameters to perform the swap(s).
Using liquidity of native vs UST (i.e. the UST highway), one can swap from native A on chain A to native B on chain B. For this specific example, we demonstrate a swap between any combination of ETH (Goerli testnet), AVAX (Fuji testnet), MATIC (Mumbai testnet) and BNB (BSC testnet). We wrote example smart contracts to interact with Uniswap V3 and Uniswap V2 forks. Any DEX can be used to replace our example as long as the swap for a particular DEX has all of its parameters to perform the swap(s).
A protocol that hosts NativeSwap is expected to run its own relayer to enhance its user experience by only requiring a one-click transaction to perform the complete swap. Otherwise the user will have to perform an extra transaction to manually allow the final swap.
@ -49,8 +49,8 @@ cp .env.sample .env
Then deploy the example contracts:
```
./deploy_to_goerli.sh
./deploy_to_mumbai.sh
./deploy_v2.sh
./deploy_v3.sh
```
Then change into the react directory, copy sample.env to .env and replace YOUR-PROJECT-ID with your Infura Goerli and Mumbai Project IDs

View File

@ -1,5 +1,5 @@
GOERLI_PROVIDER=https://goerli.infura.io/v3/YOUR-PROJECT-ID
MUMBAI_PROVIDER=https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID
BSC_PROVIDER=https://data-seed-prebsc-1-s1.binance.org:8545
FUJI_PROVIDER=https://api.avax-test.network/ext/bc/C/rpc
GOERLI_PROVIDER="https://goerli.infura.io/v3/YOUR-PROJECT-ID"
MUMBAI_PROVIDER="https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID"
BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545"
FUJI_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc"
ETH_PRIVATE_KEY=

View File

@ -0,0 +1,123 @@
const HDWalletProvider = require('@truffle/hdwallet-provider');
require('dotenv').config({path:'.env'});
/**
* Use this file to configure your truffle project. It's seeded with some
* common settings for different networks and features like migrations,
* compilation and testing. Uncomment the ones you need or modify
* them to suit your project as necessary.
*
* More information about configuration can be found at:
*
* trufflesuite.com/docs/advanced/configuration
*
* To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
* to sign your transactions before they're sent to a remote public node. Infura accounts
* are available for free at: infura.io/register.
*
* You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
* public/private key pairs. If you're publishing your code to GitHub make sure you load this
* phrase from a file you've .gitignored so it doesn't accidentally become public.
*
*/
// const HDWalletProvider = require('@truffle/hdwallet-provider');
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync('.secret').toString().trim();
module.exports = {
contracts_directory: './contracts',
contracts_build_directory: './build/contracts',
migrations_directory: './migrations/avalanche',
/**
* Networks define how you connect to your ethereum client and let you set the
* defaults web3 uses to send transactions. If you don't specify one truffle
* will spin up a development blockchain for you on port 9545 when you
* run `develop` or `test`. You can ask a truffle command to use a specific
* network from the command line, e.g
*
* $ truffle test --network <network-name>
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
development: {
host: '127.0.0.1', // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: '*', // Any network (default: none)
},
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websocket: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
fuji: {
provider: () => new HDWalletProvider(
process.env.ETH_PRIVATE_KEY,
process.env.FUJI_PROVIDER
),
network_id: 43113,
skipDryRun: true
},
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use '0.5.1' you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200
},
// evmVersion: 'byzantium'
// }
}
},
// Truffle DB is currently disabled by default; to enable it, change enabled:
// false to enabled: true. The default storage location can also be
// overridden by specifying the adapter settings, as shown in the commented code below.
//
// NOTE: It is not possible to migrate your contracts to truffle DB and you should
// make a backup of your artifacts to a safe location before enabling this feature.
//
// After you backed up your artifacts you can utilize db by running migrate as follows:
// $ truffle migrate --reset --compile-all
//
// db: {
// enabled: false,
// host: '127.0.0.1',
// adapter: {
// name: 'sqlite',
// settings: {
// directory: '.db'
// }
// }
// }
};

View File

@ -0,0 +1,124 @@
const HDWalletProvider = require('@truffle/hdwallet-provider');
require('dotenv').config({path:'.env'});
/**
* Use this file to configure your truffle project. It's seeded with some
* common settings for different networks and features like migrations,
* compilation and testing. Uncomment the ones you need or modify
* them to suit your project as necessary.
*
* More information about configuration can be found at:
*
* trufflesuite.com/docs/advanced/configuration
*
* To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
* to sign your transactions before they're sent to a remote public node. Infura accounts
* are available for free at: infura.io/register.
*
* You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
* public/private key pairs. If you're publishing your code to GitHub make sure you load this
* phrase from a file you've .gitignored so it doesn't accidentally become public.
*
*/
// const HDWalletProvider = require('@truffle/hdwallet-provider');
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync('.secret').toString().trim();
module.exports = {
contracts_directory: './contracts',
contracts_build_directory: './build/contracts',
migrations_directory: './migrations/bsc',
/**
* Networks define how you connect to your ethereum client and let you set the
* defaults web3 uses to send transactions. If you don't specify one truffle
* will spin up a development blockchain for you on port 9545 when you
* run `develop` or `test`. You can ask a truffle command to use a specific
* network from the command line, e.g
*
* $ truffle test --network <network-name>
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
development: {
host: '127.0.0.1', // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: '*', // Any network (default: none)
},
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websocket: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
bsc: {
provider: () => new HDWalletProvider(
process.env.ETH_PRIVATE_KEY,
process.env.BSC_PROVIDER
),
network_id: 97,
skipDryRun: true,
timeoutBlocks: 200
},
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use '0.5.1' you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200
},
// evmVersion: 'byzantium'
// }
}
},
// Truffle DB is currently disabled by default; to enable it, change enabled:
// false to enabled: true. The default storage location can also be
// overridden by specifying the adapter settings, as shown in the commented code below.
//
// NOTE: It is not possible to migrate your contracts to truffle DB and you should
// make a backup of your artifacts to a safe location before enabling this feature.
//
// After you backed up your artifacts you can utilize db by running migrate as follows:
// $ truffle migrate --reset --compile-all
//
// db: {
// enabled: false,
// host: '127.0.0.1',
// adapter: {
// name: 'sqlite',
// settings: {
// directory: '.db'
// }
// }
// }
};

View File

@ -2,8 +2,8 @@
set -euo pipefail
npx truffle compile --config truffle-config.ethereum.js
npx truffle compile --config truffle-config.polygon.js
npx truffle compile --config cfg/truffle-config.ethereum.js
npx truffle compile --config cfg/truffle-config.polygon.js
CONTRACTS="../react/src/abi/contracts"

View File

@ -44,7 +44,8 @@ contract CrossChainSwapV2 {
uint8 public immutable typeExactOut = 2;
uint8 public immutable typeNativeSwap = 1;
uint8 public immutable typeTokenSwap = 2;
uint16 public immutable expectedVaaLength = 262;
uint16 public immutable expectedVaaLength = 274;
uint8 public immutable terraChainId = 3;
IUniswapV2Router02 public immutable swapRouter;
address public immutable feeTokenAddress;
address public immutable tokenBridgeAddress;
@ -385,17 +386,26 @@ contract CrossChainSwapV2 {
swapParams.deadline
);
// encode payload for second swap
bytes memory payload = abi.encodePacked(
swapParams.targetAmountOutMinimum,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactIn,
typeNativeSwap
);
// create payload variable
bytes memory payload;
// UST is native to Terra - no need for swap instructions
if (targetChainId == terraChainId) {
payload = abi.encodePacked(
swapParams.targetChainRecipient
);
} else {
payload = abi.encodePacked(
swapParams.targetAmountOutMinimum,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactIn,
typeNativeSwap
);
}
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(
@ -761,17 +771,26 @@ contract CrossChainSwapV2 {
typeNativeSwap
);
// encode payload for second swap
bytes memory payload = abi.encodePacked(
swapParams.targetAmountOut,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactOut,
typeNativeSwap
);
// create payload variable
bytes memory payload;
// UST is native to Terra - no need for swap instructions
if (targetChainId == terraChainId) {
payload = abi.encodePacked(
swapParams.targetChainRecipient
);
} else {
payload = abi.encodePacked(
swapParams.targetAmountOut,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactOut,
typeNativeSwap
);
}
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(

View File

@ -48,7 +48,8 @@ contract CrossChainSwapV3 {
uint8 public immutable typeExactOut = 2;
uint8 public immutable typeNativeSwap = 1;
uint8 public immutable typeTokenSwap = 2;
uint16 public immutable expectedVaaLength = 262;
uint16 public immutable expectedVaaLength = 274;
uint8 public immutable terraChainId = 3;
IUniswapRouter public immutable swapRouter;
address public immutable feeTokenAddress;
address public immutable tokenBridgeAddress;
@ -403,17 +404,26 @@ contract CrossChainSwapV3 {
typeNativeSwap
);
// encode payload for second swap
bytes memory payload = abi.encodePacked(
swapParams.targetAmountOutMinimum,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactIn,
typeNativeSwap
);
// create payload variable
bytes memory payload;
// UST is native to Terra - no need for swap instructions
if (targetChainId == terraChainId) {
payload = abi.encodePacked(
swapParams.targetChainRecipient
);
} else {
payload = abi.encodePacked(
swapParams.targetAmountOutMinimum,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactIn,
typeNativeSwap
);
}
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(
@ -788,17 +798,26 @@ contract CrossChainSwapV3 {
typeNativeSwap
);
// encode payload for second swap
bytes memory payload = abi.encodePacked(
swapParams.targetAmountOut,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactOut,
typeNativeSwap
);
// create payload variable
bytes memory payload;
// UST is native to Terra - no need for swap instructions
if (targetChainId == terraChainId) {
payload = abi.encodePacked(
swapParams.targetChainRecipient
);
} else {
payload = abi.encodePacked(
swapParams.targetAmountOut,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactOut,
typeNativeSwap
);
}
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(

View File

@ -18,7 +18,7 @@ library SwapHelper {
uint256 amountIn;
uint256 amountOutMinimum;
uint256 targetAmountOutMinimum;
address targetChainRecipient;
bytes32 targetChainRecipient;
uint256 deadline;
uint24 poolFee;
}
@ -28,7 +28,7 @@ library SwapHelper {
uint256 amountOut;
uint256 amountInMaximum;
uint256 targetAmountOut;
address targetChainRecipient;
bytes32 targetChainRecipient;
uint256 deadline;
uint24 poolFee;
}
@ -75,7 +75,7 @@ library SwapHelper {
index += 32;
decoded.estimatedAmount = encodedVm.payload.toUint256(index);
index += 32;
index += 44;
decoded.recipientAddress = encodedVm.payload.toAddress(index);
index += 20;

View File

@ -1,2 +0,0 @@
#!/bin/bash
npx truffle migrate --config truffle-config.polygon.js --network mumbai --reset

6
contracts/deploy_v2.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
npx truffle migrate --config cfg/truffle-config.avalanche.js --network fuji --reset
npx truffle migrate --config cfg/truffle-config.bsc.js --network bsc --reset
npx truffle migrate --config cfg/truffle-config.polygon.js --network mumbai --reset

View File

@ -1,2 +1,4 @@
#!/bin/bash
npx truffle migrate --config truffle-config.ethereum.js --network goerli --reset
set -euo pipefail
npx truffle migrate --config truffle-config.ethereum.js --network goerli --reset

View File

@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};

View File

@ -0,0 +1,34 @@
const fsp = require("fs/promises");
const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2");
const SwapHelper = artifacts.require("SwapHelper");
const scriptsAddressPath = "../react/src/addresses";
module.exports = async function(deployer, network) {
const routerAddress = "0x7e3411b04766089cfaa52db688855356a12f05d1"; // hurricaneswap
const feeTokenAddress = "0xe09ed38e5cd1014444846f62376ac88c5232cde9"; // wUST
const tokenBridgeAddress = "0x61E44E506Ca5659E6c0bba9b678586fA2d729756";
const wrappedAvaxAddress = "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3";
await deployer.deploy(SwapHelper);
await deployer.link(SwapHelper, CrossChainSwapV2);
await deployer.deploy(
CrossChainSwapV2,
routerAddress,
feeTokenAddress,
tokenBridgeAddress,
wrappedAvaxAddress
);
// save the contract address somewhere
await fsp.mkdir(scriptsAddressPath, { recursive: true });
await fsp.writeFile(
`${scriptsAddressPath}/${network}.ts`,
`export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';`
);
//deployer.link(ConvertLib, MetaCoin);
//deployer.deploy(MetaCoin);
};

View File

@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};

View File

@ -0,0 +1,34 @@
const fsp = require("fs/promises");
const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2");
const SwapHelper = artifacts.require("SwapHelper");
const scriptsAddressPath = "../react/src/addresses";
module.exports = async function(deployer, network) {
const routerAddress = "0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3"; // pancakeswap
const feeTokenAddress = "0x7b8eae1e85c8b189ee653d3f78733f4f788bb2c1"; // wUST
const tokenBridgeAddress = "0x9dcF9D205C9De35334D646BeE44b2D2859712A09";
const wrappedBnbAddress = "0xae13d989dac2f0debff460ac112a837c89baa7cd";
await deployer.deploy(SwapHelper);
await deployer.link(SwapHelper, CrossChainSwapV2);
await deployer.deploy(
CrossChainSwapV2,
routerAddress,
feeTokenAddress,
tokenBridgeAddress,
wrappedBnbAddress
);
// save the contract address somewhere
await fsp.mkdir(scriptsAddressPath, { recursive: true });
await fsp.writeFile(
`${scriptsAddressPath}/${network}.ts`,
`export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';`
);
//deployer.link(ConvertLib, MetaCoin);
//deployer.deploy(MetaCoin);
};

6
misc/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.env
node_modules/
scripts/*.js
scripts/src/*.js
src
package-lock.json

41
misc/package.json Normal file
View File

@ -0,0 +1,41 @@
{
"homepage": "https://certusone.github.io/wormhole-nativeswap-example",
"name": "NativeSwap",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc",
"clean": "rm_js_in_src.sh"
},
"dependencies": {
"@certusone/wormhole-sdk": "^0.1.6",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@metamask/detect-provider": "^1.2.0",
"@terra-money/terra.js": "^2.0.14",
"@terra-money/wallet-provider": "^2.2.0",
"@types/node": "^16.11.19",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"@uniswap/smart-order-router": "^2.1.1",
"@uniswap/v2-core": "^1.0.1",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-periphery": "1.3",
"@uniswap/v3-sdk": "^3.8.1",
"ethers": "^5.5.3",
"jsbi": "^3.2.5",
"notistack": "^1.0.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"typescript": "^4.4.2",
"use-debounce": "^7.0.1"
},
"devDependencies": {
"@craco/craco": "^6.3.0",
"gh-pages": "^3.2.3",
"wasm-loader": "^1.3.0"
}
}

4
misc/rm_js_in_src.sh Executable file
View File

@ -0,0 +1,4 @@
rm src/addresses/*.js
rm src/route/*.js
rm src/swapper/*.js
rm src/utils/*.js

View File

@ -0,0 +1,36 @@
import { ethers } from "ethers";
import {
ETH_TOKEN_INFO,
MATIC_TOKEN_INFO,
AVAX_TOKEN_INFO,
BNB_TOKEN_INFO,
} from "../../src/utils/consts";
export function makeProvider(tokenAddress: string) {
switch (tokenAddress) {
case ETH_TOKEN_INFO.address: {
return new ethers.providers.StaticJsonRpcProvider(
process.env.GOERLI_PROVIDER
);
}
case MATIC_TOKEN_INFO.address: {
return new ethers.providers.StaticJsonRpcProvider(
process.env.MUMBAI_PROVIDER
);
}
case AVAX_TOKEN_INFO.address: {
return new ethers.providers.StaticJsonRpcProvider(
process.env.FUJI_PROVIDER
);
}
case BNB_TOKEN_INFO.address: {
return new ethers.providers.StaticJsonRpcProvider(
process.env.BSC_PROVIDER
);
}
default: {
throw Error("unrecognized token address");
}
}
}

View File

@ -0,0 +1,17 @@
#!/bin/bash
set -euo pipefail
root=$(dirname $0)
script="${root}/swap-with-vaa.js"
echo `which node`
node $script --in ETH --out MATIC
node $script --in ETH --out BNB
node $script --in ETH --out AVAX
node $script --in MATIC --out BNB
node $script --in MATIC --out AVAX
node $script --in BNB --out MATIC
echo "done"

View File

@ -0,0 +1,470 @@
import yargs from "yargs";
import { ethers } from "ethers";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import {
ExactInCrossParameters,
ExactOutCrossParameters,
UniswapToUniswapQuoter,
} from "../src/route/cross-quote";
import { UniswapToUniswapExecutor } from "../src/swapper/swapper";
import {
ETH_TOKEN_INFO,
MATIC_TOKEN_INFO,
AVAX_TOKEN_INFO,
BNB_TOKEN_INFO,
UST_TOKEN_INFO,
} from "../src/utils/consts";
import { makeProvider } from "./src/provider";
require("dotenv").config({ path: ".env" });
// swap related parameters (configurable in UI)
const SWAP_AMOUNT_IN_MATIC = "0.0069";
const SWAP_AMOUNT_IN_ETH = "0.000907";
const SWAP_AMOUNT_IN_AVAX = "0.0075";
const SWAP_AMOUNT_IN_BNB = "0.0015";
const SWAP_AMOUNT_IN_UST = "3.40";
const SWAP_DEADLINE = "1800";
const SWAP_SLIPPAGE = "0.01";
// token bridge things
const BRIDGE_RELAYER_FEE_UST = "0.25";
interface Arguments {
in: string;
out: string;
}
function parseArgs(): Arguments {
const parsed = yargs(process.argv.slice(2))
.option("in", {
string: true,
description: "Name of inbound token",
required: true,
})
.option("out", {
string: true,
description: "Name of outbound token",
required: true,
})
.help("h")
.alias("h", "help").argv;
const args: Arguments = {
in: parsed.in,
out: parsed.out,
};
return args;
}
export function makeEvmWallet(
provider: ethers.providers.Provider
): ethers.Wallet {
return new ethers.Wallet(process.env.ETH_PRIVATE_KEY, provider);
}
/*
async function fetchTokenBalance(signer, contract) {
const decimals = await contract.decimals();
const balanceBeforeDecimals = (await contract.balanceOf(signer.address)).toString();
const balance = ethers.utils.formatUnits(balanceBeforeDecimals, decimals);
return balance;
}
*/
// only exist as placeholder for actual wallet connection
function determineWalletFromToken(tokenAddress: string): ethers.Wallet {
return makeEvmWallet(makeProvider(tokenAddress));
}
function determineAmountFromToken(tokenAddress: string): string {
switch (tokenAddress) {
case ETH_TOKEN_INFO.address: {
return SWAP_AMOUNT_IN_ETH;
}
case MATIC_TOKEN_INFO.address: {
return SWAP_AMOUNT_IN_MATIC;
}
case AVAX_TOKEN_INFO.address: {
return SWAP_AMOUNT_IN_AVAX;
}
case BNB_TOKEN_INFO.address: {
return SWAP_AMOUNT_IN_BNB;
}
case UST_TOKEN_INFO.address: {
return SWAP_AMOUNT_IN_UST;
}
default: {
throw Error("you suck");
}
}
}
function logExactInParameters(
quoter: UniswapToUniswapQuoter,
params: ExactInCrossParameters
): void {
console.info(`amountIn: ${params.amountIn}`);
console.info(`minAmountOut: ${params.minAmountOut}`);
const src = params.src;
if (src === undefined) {
console.warn(` src is undefined (ust?)`);
} else {
console.info(`src`);
console.info(` protocol: ${src.protocol}`);
//console.info(` amountIn: ${quoter.srcTokenIn.formatAmount(src.amountIn)}`);
console.info(
` amountIn: ${quoter.srcRouter.formatAmountIn(
src.amountIn.toString()
)}`
);
console.info(
// ` minAmountOut: ${quoter.srcTokenOut.formatAmount(src.minAmountOut)}`
` minAmountOut: ${quoter.srcRouter.formatAmountOut(
src.minAmountOut.toString()
)}`
);
console.info(` poolFee: ${src.poolFee}`);
console.info(` deadline: ${src.deadline.toString()}`);
console.info(` path: ${src.path}`);
}
const dst = params.dst;
console.info(`dst`);
if (dst === undefined) {
console.warn(` dst is undefined (ust?)`);
} else {
console.info(` protocol: ${dst.protocol}`);
//console.info(` amountIn: ${quoter.dstTokenIn.formatAmount(dst.amountIn)}`);
console.info(
` amountIn: ${quoter.dstRouter.formatAmountIn(
dst.amountIn.toString()
)}`
);
console.info(
// ` minAmountOut: ${quoter.dstTokenOut.formatAmount(dst.minAmountOut)}`
` minAmountOut: ${quoter.dstRouter.formatAmountOut(
dst.minAmountOut.toString()
)}`
);
console.info(` poolFee: ${dst.poolFee}`);
console.info(` deadline: ${dst.deadline.toString()}`);
console.info(` path: ${dst.path}`);
const relayerFee = params.relayerFee;
console.info(`relayerFee`);
console.info(` tokenAddress: ${relayerFee.tokenAddress}`);
console.info(
` amount: ${quoter.dstRouter.formatAmountIn(relayerFee.amount)}`
);
}
return;
}
async function swapEverythingExactIn(
swapper: UniswapToUniswapExecutor,
tokenInAddress: string,
tokenOutAddress: string,
isNative: boolean,
amountIn: string,
recipientAddress: string
): Promise<void> {
const isTerraSrc = tokenInAddress === UST_TOKEN_INFO.address;
if (isTerraSrc) {
throw Error("cannot use terra source yet");
}
// connect src wallet
const srcWallet = determineWalletFromToken(tokenInAddress);
console.info(`sender: ${await srcWallet.getAddress()}`);
console.info(`recipient: ${recipientAddress}`);
// tokens selected, let's initialize
await swapper.initialize(tokenInAddress, tokenOutAddress, isNative);
console.info(`quoter initialized`);
// verify pool address on src and dst
await swapper
.computeAndVerifySrcPoolAddress()
.then((address) => {
console.info(`srcPool: ${address}`);
return address;
})
.catch((response) => {
console.error(
`failed to find a pool address for src. how to handle in the front-end?`
);
process.exit(1);
});
await swapper
.computeAndVerifyDstPoolAddress()
.then((address) => {
console.info(`dstPool: ${address}`);
return address;
})
.catch((response) => {
console.error(
`failed to find a pool address for dst. how to handle in the front-end?`
);
process.exit(1);
});
// set deadline
swapper.setDeadlines(SWAP_DEADLINE);
swapper.setSlippage(SWAP_SLIPPAGE);
swapper.setRelayerFee(BRIDGE_RELAYER_FEE_UST);
const exactInParameters: ExactInCrossParameters =
await swapper.computeQuoteExactIn(amountIn);
console.info("exactInParameters");
logExactInParameters(swapper.quoter, exactInParameters);
// do the src swap
if (isTerraSrc) {
// do terra method
throw Error("terra src not implemented yet");
} else {
console.info("approveAndSwap");
const srcSwapReceipt = await swapper.evmApproveAndSwap(
srcWallet,
recipientAddress
);
console.info(`src transaction: ${srcSwapReceipt.transactionHash}`);
}
// do the dst swap after fetching vaa
// connect dst wallet
const dstWallet = determineWalletFromToken(tokenOutAddress);
console.info("fetchVaaAndSwap");
//const dstSwapReceipt = await swapper.fetchVaaAndSwap(dstWallet);
//console.info(`dst transaction: ${dstSwapReceipt.transactionHash}`);
console.warn("jk");
return;
}
function logExactOutParameters(
quoter: UniswapToUniswapQuoter,
params: ExactOutCrossParameters
): void {
const src = params.src;
console.info(`src`);
console.info(` protocol: ${src.protocol}`);
console.info(
` amountOut: ${quoter.srcRouter.formatAmountOut(
src.amountOut.toString()
)}`
);
console.info(
` maxAmountIn: ${quoter.srcRouter.formatAmountIn(
src.maxAmountIn.toString()
)}`
);
console.info(` poolFee: ${src.poolFee}`);
console.info(` deadline: ${src.deadline.toString()}`);
console.info(` path: ${src.path}`);
const dst = params.dst;
console.info(`dst`);
console.info(` protocol: ${dst.protocol}`);
console.info(
` amountOut: ${quoter.dstRouter.formatAmountOut(
dst.amountOut.toString()
)}`
);
console.info(
` maxAmountIn: ${quoter.dstRouter.formatAmountIn(
dst.maxAmountIn.toString()
)}`
);
console.info(` poolFee: ${dst.poolFee}`);
console.info(` deadline: ${dst.deadline.toString()}`);
console.info(` path: ${dst.path}`);
const relayerFee = params.relayerFee;
console.info(`relayerFee`);
console.info(` tokenAddress: ${relayerFee.tokenAddress}`);
console.info(
` amount: ${quoter.dstRouter.formatAmountIn(
relayerFee.amount.toString()
)}`
);
return;
}
async function swapEverythingExactOut(
swapper: UniswapToUniswapExecutor,
tokenInAddress: string,
tokenOutAddress: string,
isNative: boolean,
amountOut: string,
recipientAddress: string
): Promise<void> {
// connect src wallet
const srcWallet = determineWalletFromToken(tokenInAddress);
console.info(`wallet pubkey: ${await srcWallet.getAddress()}`);
// tokens selected, let's initialize
await swapper.initialize(tokenInAddress, tokenOutAddress, isNative);
console.info(`quoter initialized`);
// verify pool address on src and dst
await swapper
.computeAndVerifySrcPoolAddress()
.then((address) => {
console.info(`srcPool: ${address}`);
return address;
})
.catch((response) => {
console.error(
`failed to find a pool address for src. how to handle in the front-end?`
);
process.exit(1);
});
await swapper
.computeAndVerifyDstPoolAddress()
.then((address) => {
console.info(`dstPool: ${address}`);
return address;
})
.catch((response) => {
console.error(
`failed to find a pool address for dst. how to handle in the front-end?`
);
process.exit(1);
});
// set deadline
swapper.setDeadlines(SWAP_DEADLINE);
swapper.setSlippage(SWAP_SLIPPAGE);
swapper.setRelayerFee(BRIDGE_RELAYER_FEE_UST);
const exactOutParameters: ExactOutCrossParameters =
await swapper.computeQuoteExactOut(amountOut);
console.info("exactOutParameters");
logExactOutParameters(swapper.quoter, exactOutParameters);
// do the src swap
console.info("approveAndSwap");
const srcSwapReceipt = await swapper.evmApproveAndSwap(
srcWallet,
recipientAddress
);
console.info(`src transaction: ${srcSwapReceipt.transactionHash}`);
// do the dst swap after fetching vaa
// connect dst wallet
const dstWallet = determineWalletFromToken(tokenOutAddress);
console.info("fetchVaaAndSwap");
//const dstSwapReceipt = await swapper.fetchVaaAndSwap(dstWallet);
//console.info(`dst transaction: ${dstSwapReceipt.transactionHash}`);
console.warn("jk");
return;
}
function getTokenInfo(name: string) {
switch (name) {
case "ETH": {
return ETH_TOKEN_INFO;
}
case "MATIC": {
return MATIC_TOKEN_INFO;
}
case "UST": {
return UST_TOKEN_INFO;
}
case "AVAX": {
return AVAX_TOKEN_INFO;
}
case "BNB": {
return BNB_TOKEN_INFO;
}
default: {
throw Error("invalid token name");
}
}
}
async function main() {
const args = parseArgs();
const testExactIn = true;
const isNative = true;
const swapper = new UniswapToUniswapExecutor();
swapper.setTransport(NodeHttpTransport());
const tokenIn = getTokenInfo(args.in);
const tokenOut = getTokenInfo(args.out);
//const tokenOut = UST_TOKEN_INFO;
const recipientAddress = "0x4e2dfAD7D7d0076b5A0A41223E4Bee390C33251C";
//const recipientAddress = "terra1vewnsxcy5fqjslyyy409cw8js550esen38n8ey";
if (testExactIn) {
console.info(`testing exact in. native=${isNative}`);
console.info(`${tokenIn.name} -> ${tokenOut.name}`);
await swapEverythingExactIn(
swapper,
tokenIn.address,
tokenOut.address,
isNative,
determineAmountFromToken(tokenIn.address),
recipientAddress
);
if (tokenOut.address === UST_TOKEN_INFO.address) {
console.warn("not pinging back");
} else {
console.info(`${tokenOut.name} -> ${tokenIn.name}`);
await swapEverythingExactIn(
swapper,
tokenOut.address,
tokenIn.address,
isNative,
determineAmountFromToken(tokenOut.address),
recipientAddress
);
}
} else {
console.info(`testing exact out. native=${isNative}`);
console.info(`${tokenIn.name} -> ${tokenOut.name}`);
await swapEverythingExactOut(
swapper,
tokenIn.address,
tokenOut.address,
isNative,
determineAmountFromToken(tokenOut.address),
recipientAddress
);
console.info(`${tokenOut.name} -> ${tokenIn.name}`);
await swapEverythingExactOut(
swapper,
tokenOut.address,
tokenIn.address,
isNative,
determineAmountFromToken(tokenIn.address),
recipientAddress
);
}
return;
}
main();

9
misc/tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true
},
"files": [
"scripts/swap-with-vaa.ts"
]
}

View File

@ -1,2 +1,4 @@
REACT_APP_GOERLI_PROVIDER=https://goerli.infura.io/v3/YOUR-PROJECT-ID
REACT_APP_MUMBAI_PROVIDER=https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID
REACT_APP_GOERLI_PROVIDER="https://goerli.infura.io/v3/YOUR-PROJECT-ID"
REACT_APP_MUMBAI_PROVIDER="https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID"
REACT_APP_FUJI_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc"
REACT_APP_BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545"

174
react/package-lock.json generated
View File

@ -14,6 +14,8 @@
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@metamask/detect-provider": "^1.2.0",
"@terra-money/terra.js": "^2.0.14",
"@terra-money/wallet-provider": "^2.2.0",
"@types/node": "^16.11.19",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
@ -1893,6 +1895,36 @@
"rxjs": "^7.3.0"
}
},
"node_modules/@certusone/wormhole-sdk/node_modules/@terra-money/wallet-provider": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz",
"integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==",
"dependencies": {
"@terra-dev/browser-check": "^2.5.3",
"@terra-dev/chrome-extension": "^2.5.3",
"@terra-dev/readonly-wallet": "^2.5.3",
"@terra-dev/readonly-wallet-modal": "^2.5.3",
"@terra-dev/use-wallet": "^2.5.3",
"@terra-dev/wallet-types": "^2.5.3",
"@terra-dev/walletconnect": "^2.5.3",
"@terra-dev/web-connector-controller": "^0.8.1",
"@terra-dev/web-connector-interface": "^0.8.1",
"fast-deep-equal": "^3.1.3",
"rxjs": "^7.4.0"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"@terra-money/terra.js": "^2.0.0",
"react": "^17.0.0"
},
"peerDependenciesMeta": {
"react-router-dom": {
"optional": true
}
}
},
"node_modules/@cnakazawa/watch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
@ -4494,12 +4526,24 @@
"@terra-money/terra.js": "^2.0.0"
}
},
"node_modules/@terra-money/terra.js": {
"version": "2.1.23",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.1.23.tgz",
"integrity": "sha512-nSAR35zqjKUn1Jzqevf30s47XRlW/VXU01YgK3n9ndmX15lkdlgFvqaV7UezK0xAmCpm+7xWIrtBTMmZpVBkMQ==",
"node_modules/@terra-dev/web-extension": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@terra-dev/web-extension/-/web-extension-0.6.0.tgz",
"integrity": "sha512-IyIWHLfweZCb5nHuMyzavnMYposnZMvpsA/89zZPIgIooxhxE//uZD+Ty+ptt4nvkbOgEFKdKIKe5rIHqgVLpA==",
"dependencies": {
"@terra-money/terra.js": "^1.8.0 || ^2.0.0",
"bowser": "^2.11.0",
"rxjs": "^7.3.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@terra-money/terra.js": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.0.14.tgz",
"integrity": "sha512-GeMadRIPaOedODa5a0pJ2+76l7MeFSIfSJZ2vvWPRco6MRIQLw/k0cZpPKMLm2Zo54li/oY1mrR+r3uxLM7q3Q==",
"dependencies": {
"@terra-money/terra.proto": "^0.1.7",
"axios": "^0.21.1",
"bech32": "^2.0.0",
"bip32": "^2.0.6",
@ -4514,7 +4558,7 @@
"ws": "^7.4.2"
},
"engines": {
"node": ">=14"
"node": ">=12"
}
},
"node_modules/@terra-money/terra.js/node_modules/axios": {
@ -4525,38 +4569,27 @@
"follow-redirects": "^1.14.0"
}
},
"node_modules/@terra-money/terra.proto": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz",
"integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==",
"dependencies": {
"google-protobuf": "^3.17.3",
"long": "^4.0.0",
"protobufjs": "~6.11.2"
}
},
"node_modules/@terra-money/wallet-provider": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz",
"integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.2.0.tgz",
"integrity": "sha512-K8NLpJ/yak8Pq6jQpjVr7yWDIbxjTp42OXaAS+xlTufqQwWbCR7coAGbm2FpYX43j4uymuSAICZvsOS1qrFeYA==",
"dependencies": {
"@terra-dev/browser-check": "^2.5.3",
"@terra-dev/chrome-extension": "^2.5.3",
"@terra-dev/readonly-wallet": "^2.5.3",
"@terra-dev/readonly-wallet-modal": "^2.5.3",
"@terra-dev/use-wallet": "^2.5.3",
"@terra-dev/wallet-types": "^2.5.3",
"@terra-dev/walletconnect": "^2.5.3",
"@terra-dev/web-connector-controller": "^0.8.1",
"@terra-dev/web-connector-interface": "^0.8.1",
"@terra-dev/browser-check": "^2.2.0",
"@terra-dev/chrome-extension": "^2.2.0",
"@terra-dev/readonly-wallet": "^2.2.0",
"@terra-dev/readonly-wallet-modal": "^2.2.0",
"@terra-dev/use-wallet": "^2.2.0",
"@terra-dev/wallet-types": "^2.2.0",
"@terra-dev/walletconnect": "^2.2.0",
"@terra-dev/web-extension": "^0.6.0",
"@terra-money/terra.js": "^2.0.0",
"fast-deep-equal": "^3.1.3",
"rxjs": "^7.4.0"
"rxjs": "^7.3.0"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"@terra-money/terra.js": "^2.0.0",
"react": "^17.0.0"
},
"peerDependenciesMeta": {
@ -12324,7 +12357,8 @@
"node_modules/google-protobuf": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.19.3.tgz",
"integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A=="
"integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A==",
"peer": true
},
"node_modules/graceful-fs": {
"version": "4.2.9",
@ -27356,6 +27390,26 @@
"js-base64": "^3.6.1",
"protobufjs": "^6.11.2",
"rxjs": "^7.3.0"
},
"dependencies": {
"@terra-money/wallet-provider": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz",
"integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==",
"requires": {
"@terra-dev/browser-check": "^2.5.3",
"@terra-dev/chrome-extension": "^2.5.3",
"@terra-dev/readonly-wallet": "^2.5.3",
"@terra-dev/readonly-wallet-modal": "^2.5.3",
"@terra-dev/use-wallet": "^2.5.3",
"@terra-dev/wallet-types": "^2.5.3",
"@terra-dev/walletconnect": "^2.5.3",
"@terra-dev/web-connector-controller": "^0.8.1",
"@terra-dev/web-connector-interface": "^0.8.1",
"fast-deep-equal": "^3.1.3",
"rxjs": "^7.4.0"
}
}
}
},
"@cnakazawa/watch": {
@ -29148,12 +29202,21 @@
"rxjs": "^7.4.0"
}
},
"@terra-money/terra.js": {
"version": "2.1.23",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.1.23.tgz",
"integrity": "sha512-nSAR35zqjKUn1Jzqevf30s47XRlW/VXU01YgK3n9ndmX15lkdlgFvqaV7UezK0xAmCpm+7xWIrtBTMmZpVBkMQ==",
"@terra-dev/web-extension": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@terra-dev/web-extension/-/web-extension-0.6.0.tgz",
"integrity": "sha512-IyIWHLfweZCb5nHuMyzavnMYposnZMvpsA/89zZPIgIooxhxE//uZD+Ty+ptt4nvkbOgEFKdKIKe5rIHqgVLpA==",
"requires": {
"@terra-money/terra.js": "^1.8.0 || ^2.0.0",
"bowser": "^2.11.0",
"rxjs": "^7.3.0"
}
},
"@terra-money/terra.js": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-2.0.14.tgz",
"integrity": "sha512-GeMadRIPaOedODa5a0pJ2+76l7MeFSIfSJZ2vvWPRco6MRIQLw/k0cZpPKMLm2Zo54li/oY1mrR+r3uxLM7q3Q==",
"requires": {
"@terra-money/terra.proto": "^0.1.7",
"axios": "^0.21.1",
"bech32": "^2.0.0",
"bip32": "^2.0.6",
@ -29178,32 +29241,22 @@
}
}
},
"@terra-money/terra.proto": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz",
"integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==",
"requires": {
"google-protobuf": "^3.17.3",
"long": "^4.0.0",
"protobufjs": "~6.11.2"
}
},
"@terra-money/wallet-provider": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.5.3.tgz",
"integrity": "sha512-v/5Z35gCo4nZyZCu3nYDFvhwuvlyDeNSSYmN9KUc9ewoIO9K/2fi3vxcOLcvqq5PYowwwod21vgaQ9QHFV+8eA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.2.0.tgz",
"integrity": "sha512-K8NLpJ/yak8Pq6jQpjVr7yWDIbxjTp42OXaAS+xlTufqQwWbCR7coAGbm2FpYX43j4uymuSAICZvsOS1qrFeYA==",
"requires": {
"@terra-dev/browser-check": "^2.5.3",
"@terra-dev/chrome-extension": "^2.5.3",
"@terra-dev/readonly-wallet": "^2.5.3",
"@terra-dev/readonly-wallet-modal": "^2.5.3",
"@terra-dev/use-wallet": "^2.5.3",
"@terra-dev/wallet-types": "^2.5.3",
"@terra-dev/walletconnect": "^2.5.3",
"@terra-dev/web-connector-controller": "^0.8.1",
"@terra-dev/web-connector-interface": "^0.8.1",
"@terra-dev/browser-check": "^2.2.0",
"@terra-dev/chrome-extension": "^2.2.0",
"@terra-dev/readonly-wallet": "^2.2.0",
"@terra-dev/readonly-wallet-modal": "^2.2.0",
"@terra-dev/use-wallet": "^2.2.0",
"@terra-dev/wallet-types": "^2.2.0",
"@terra-dev/walletconnect": "^2.2.0",
"@terra-dev/web-extension": "^0.6.0",
"@terra-money/terra.js": "^2.0.0",
"fast-deep-equal": "^3.1.3",
"rxjs": "^7.4.0"
"rxjs": "^7.3.0"
}
},
"@tootallnate/once": {
@ -35422,7 +35475,8 @@
"google-protobuf": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.19.3.tgz",
"integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A=="
"integrity": "sha512-3GRDj8o9XjcALYjgxNKeD7Wm6w/V8r1Jo4sLYMic9+VaIMLBx8TQeHP9yaoRoDymNONhnkmmveDPyjw/Fpw8+A==",
"peer": true
},
"graceful-fs": {
"version": "4.2.9",

View File

@ -10,6 +10,8 @@
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@metamask/detect-provider": "^1.2.0",
"@terra-money/terra.js": "^2.0.14",
"@terra-money/wallet-provider": "^2.2.0",
"@types/node": "^16.11.19",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",

View File

@ -1,2 +1,2 @@
goerli.ts
mumbai.ts
*.ts
*.js

View File

@ -5,7 +5,7 @@ import Github from "../icons/Github.svg";
import Medium from "../icons/Medium.svg";
import Telegram from "../icons/Telegram.svg";
import Twitter from "../icons/Twitter.svg";
import Wormhole from "../icons/wormhole-network.svg";
import Wormhole from "../icons/wormhole_logo.svg";
const useStyles = makeStyles((theme) => ({
footer: {
@ -19,7 +19,6 @@ const useStyles = makeStyles((theme) => ({
},
},
builtWithContainer: {
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: 0.5,
@ -27,7 +26,7 @@ const useStyles = makeStyles((theme) => ({
},
wormholeIcon: {
height: 48,
width: 48,
width: 192,
filter: "contrast(0)",
transition: "filter 0.5s",
"&:hover": {
@ -92,24 +91,18 @@ export default function Footer() {
<img src={Twitter} alt="Twitter" />
</IconButton>
</div>
<div>
<a
href="https://wormholenetwork.com/"
target="_blank"
rel="noopener noreferrer"
>
<img src={Wormhole} alt="Wormhole" className={classes.wormholeIcon} />
</a>
</div>
<div className={classes.builtWithContainer}>
<div>
<a
href="https://wormholenetwork.com/"
target="_blank"
rel="noopener noreferrer"
>
<img
src={Wormhole}
alt="Wormhole"
className={classes.wormholeIcon}
/>
</a>
</div>
<div>
<Typography variant="body2">Open Source</Typography>
<Typography variant="body2">Built with &#10084;</Typography>
</div>
<Typography variant="body2">Open Source</Typography>
<Typography variant="body2">Built with &#10084;</Typography>
</div>
</footer>
);

View File

@ -1,7 +1,8 @@
import { ChainId, CHAIN_ID_POLYGON, isEVMChain } from "@certusone/wormhole-sdk";
import { ChainId, isEVMChain } from "@certusone/wormhole-sdk";
import { LinearProgress, makeStyles, Typography } from "@material-ui/core";
import { useEffect, useState } from "react";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { getChainName } from "../utils/consts";
const useStyles = makeStyles((theme) => ({
root: {
@ -16,17 +17,19 @@ const useStyles = makeStyles((theme) => ({
export default function TransactionProgress({
chainId,
txBlockNumber,
step,
hasSignedVAA,
isTargetSwapComplete,
}: {
chainId: ChainId;
txBlockNumber: number | undefined;
step: number;
hasSignedVAA: boolean;
isTargetSwapComplete: boolean;
}) {
const classes = useStyles();
const { provider } = useEthereumProvider();
const [currentBlock, setCurrentBlock] = useState(0);
useEffect(() => {
if (step !== 1 || !txBlockNumber) return;
if (hasSignedVAA || !txBlockNumber) return;
if (isEVMChain(chainId) && provider) {
let cancelled = false;
(async () => {
@ -46,33 +49,30 @@ export default function TransactionProgress({
cancelled = true;
};
}
}, [step, chainId, provider, txBlockNumber]);
const blockDiff =
}, [hasSignedVAA, chainId, provider, txBlockNumber]);
let blockDiff =
txBlockNumber !== undefined && txBlockNumber && currentBlock
? currentBlock - txBlockNumber
: 0;
const expectedBlocks = 15;
blockDiff = Math.min(Math.max(blockDiff, 0), expectedBlocks);
let value;
let valueBuffer;
let message;
switch (step) {
case 1:
value = (blockDiff / expectedBlocks) * 50;
valueBuffer = 50;
message = `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${
chainId === CHAIN_ID_POLYGON ? "Polygon" : "Ethereum"
}...`;
break;
case 2:
value = 50;
valueBuffer = 100;
message = "Waiting for relayer to complete swap...";
break;
case 3:
value = 100;
valueBuffer = 100;
message = "";
break;
if (!hasSignedVAA) {
value = (blockDiff / expectedBlocks) * 50;
valueBuffer = 50;
message = `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${getChainName(
chainId
)}...`;
} else if (!isTargetSwapComplete) {
value = 50;
valueBuffer = 100;
message = "Waiting for relayer to complete swap...";
} else {
value = 100;
valueBuffer = 100;
message = "Success!";
}
return (
<div className={classes.root}>

View File

@ -0,0 +1,22 @@
import { useTerraWallet } from "../contexts/TerraWalletContext";
import ToggleConnectedButton from "./ToggleConnectedButton";
const TerraWalletKey = () => {
const { connect, disconnect, connected, wallet } = useTerraWallet();
const pk =
(wallet &&
wallet.wallets &&
wallet.wallets.length > 0 &&
wallet.wallets[0].terraAddress) ||
"";
return (
<ToggleConnectedButton
connect={connect}
disconnect={disconnect}
connected={connected}
pk={pk}
/>
);
};
export default TerraWalletKey;

View File

@ -5,7 +5,20 @@ import {
MenuItem,
TextField,
} from "@material-ui/core";
import { TokenInfo } from "../utils/consts";
import {
AVAX_TOKEN_INFO,
BNB_TOKEN_INFO,
ETH_TOKEN_INFO,
MATIC_TOKEN_INFO,
TokenInfo,
UST_TOKEN_INFO,
} from "../utils/consts";
import ethIcon from "../icons/eth.svg";
import polygonIcon from "../icons/polygon.svg";
import terraIcon from "../icons/terra.svg";
import bscIcon from "../icons/bsc.svg";
import avaxIcon from "../icons/avax.svg";
const useStyles = makeStyles((theme) => ({
select: {
@ -23,10 +36,27 @@ const useStyles = makeStyles((theme) => ({
},
}));
const createTokenMenuItem = ({ name, logo }: TokenInfo, classes: any) => (
const getLogo = (name: string) => {
switch (name) {
case ETH_TOKEN_INFO.name:
return ethIcon;
case MATIC_TOKEN_INFO.name:
return polygonIcon;
case UST_TOKEN_INFO.name:
return terraIcon;
case AVAX_TOKEN_INFO.name:
return avaxIcon;
case BNB_TOKEN_INFO.name:
return bscIcon;
default:
return "";
}
};
const createTokenMenuItem = ({ name }: TokenInfo, classes: any) => (
<MenuItem key={name} value={name}>
<ListItemIcon className={classes.listItemIcon}>
<img src={logo} alt={name} className={classes.icon} />
<img src={getLogo(name)} alt={name} className={classes.icon} />
</ListItemIcon>
<ListItemText>{name}</ListItemText>
</MenuItem>

View File

@ -0,0 +1,97 @@
import {
NetworkInfo,
Wallet,
WalletProvider,
useWallet,
} from "@terra-money/wallet-provider";
import React, {
ReactChildren,
useCallback,
useContext,
useMemo,
useState,
} from "react";
const testnet: NetworkInfo = {
name: "testnet",
chainID: "bombay-12",
lcd: "https://bombay-lcd.terra.dev",
};
const walletConnectChainIds: Record<number, NetworkInfo> = {
0: testnet,
};
interface ITerraWalletContext {
connect(): void;
disconnect(): void;
connected: boolean;
wallet: any;
}
const TerraWalletContext = React.createContext<ITerraWalletContext>({
connect: () => {},
disconnect: () => {},
connected: false,
wallet: null,
});
export const TerraWalletWrapper = ({
children,
}: {
children: ReactChildren;
}) => {
// TODO: Use wallet instead of useConnectedWallet.
const terraWallet = useWallet();
const [, setWallet] = useState<Wallet | undefined>(undefined);
const [connected, setConnected] = useState(false);
const connect = useCallback(() => {
const CHROME_EXTENSION = 1;
if (terraWallet) {
terraWallet.connect(terraWallet.availableConnectTypes[CHROME_EXTENSION]);
setWallet(terraWallet);
setConnected(true);
}
}, [terraWallet]);
const disconnect = useCallback(() => {
setConnected(false);
setWallet(undefined);
}, []);
const contextValue = useMemo(
() => ({
connect,
disconnect,
connected,
wallet: terraWallet,
}),
[connect, disconnect, connected, terraWallet]
);
return (
<TerraWalletContext.Provider value={contextValue}>
{children}
</TerraWalletContext.Provider>
);
};
export const TerraWalletProvider = ({
children,
}: {
children: ReactChildren;
}) => {
return (
<WalletProvider
defaultNetwork={testnet}
walletConnectChainIds={walletConnectChainIds}
>
<TerraWalletWrapper>{children}</TerraWalletWrapper>
</WalletProvider>
);
};
export const useTerraWallet = () => {
return useContext(TerraWalletContext);
};

View File

@ -1,12 +1,10 @@
import { ChainId, CHAIN_ID_SOLANA, isEVMChain } from "@certusone/wormhole-sdk";
import { ChainId, CHAIN_ID_TERRA, isEVMChain } from "@certusone/wormhole-sdk";
import { hexlify, hexStripZeros } from "@ethersproject/bytes";
import { useConnectedWallet } from "@terra-money/wallet-provider";
import { useCallback, useMemo } from "react";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
// import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { getEvmChainId } from "../utils/consts";
const CLUSTER = "testnet"; // TODO: change this
const createWalletStatus = (
isReady: boolean,
statusMessage: string = "",
@ -29,8 +27,8 @@ function useIsWalletReady(
forceNetworkSwitch: () => void;
} {
const autoSwitch = enableNetworkAutoswitch;
// const solanaWallet = useSolanaWallet();
// const solPK = solanaWallet?.publicKey;
const terraWallet = useConnectedWallet();
const hasTerraWallet = !!terraWallet;
const {
provider,
signerAddress,
@ -54,14 +52,19 @@ function useIsWalletReady(
}, [provider, correctEvmNetwork, chainId]);
return useMemo(() => {
//if (chainId === CHAIN_ID_SOLANA && solPK) {
// return createWalletStatus(
// true,
// undefined,
// forceNetworkSwitch,
// solPK.toString()
// );
//}
if (
chainId === CHAIN_ID_TERRA &&
hasTerraWallet &&
terraWallet?.walletAddress
) {
// TODO: terraWallet does not update on wallet changes
return createWalletStatus(
true,
undefined,
forceNetworkSwitch,
terraWallet.walletAddress
);
}
if (isEVMChain(chainId) && hasEthInfo && signerAddress) {
if (hasCorrectEvmNetwork) {
return createWalletStatus(
@ -76,7 +79,7 @@ function useIsWalletReady(
}
return createWalletStatus(
false,
`Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${correctEvmNetwork}`,
`Wallet is not connected to testnet. Expected Chain ID: ${correctEvmNetwork}`,
forceNetworkSwitch,
undefined
);
@ -93,12 +96,13 @@ function useIsWalletReady(
chainId,
autoSwitch,
forceNetworkSwitch,
// solPK,
hasTerraWallet,
hasEthInfo,
correctEvmNetwork,
hasCorrectEvmNetwork,
provider,
signerAddress,
terraWallet,
]);
}

15
react/src/icons/avax.svg Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 254 254" style="enable-background:new 0 0 254 254;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#E84142;}
.st1{fill:#FFFFFF;}
</style>
<g>
<circle class="st0" cx="127" cy="127" r="127"/>
<path class="st1" d="M171.8,130.3c4.4-7.6,11.5-7.6,15.9,0l27.4,48.1c4.4,7.6,0.8,13.8-8,13.8h-55.2c-8.7,0-12.3-6.2-8-13.8
L171.8,130.3z M118.8,37.7c4.4-7.6,11.4-7.6,15.8,0l6.1,11L155.1,74c3.5,7.2,3.5,15.7,0,22.9l-48.3,83.7
c-4.4,6.8-11.7,11.1-19.8,11.6H46.9c-8.8,0-12.4-6.1-8-13.8L118.8,37.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 825 B

12
react/src/icons/bsc.svg Normal file
View File

@ -0,0 +1,12 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.3025 0L9.67897 6.12683L13.5847 8.39024L20.3025 4.52683L27.0204 8.39024L30.9261 6.12683L20.3025 0Z" fill="#F0B90B"/>
<path d="M27.0204 11.5902L30.9261 13.8537V18.3805L24.2083 22.2439V29.9707L20.3025 32.2341L16.3968 29.9707V22.2439L9.67897 18.3805V13.8537L13.5847 11.5902L20.3025 15.4537L27.0204 11.5902Z" fill="#F0B90B"/>
<path d="M30.9261 21.5805V26.1073L27.0204 28.3707V23.8439L30.9261 21.5805Z" fill="#F0B90B"/>
<path d="M26.9814 31.5707L33.6992 27.7073V19.9805L37.605 17.7171V29.9707L26.9814 36.0976V31.5707Z" fill="#F0B90B"/>
<path d="M33.6992 12.2537L29.7935 9.99025L33.6992 7.72683L37.605 9.99025V14.5171L33.6992 16.7805V12.2537Z" fill="#F0B90B"/>
<path d="M16.3968 37.7366V33.2098L20.3025 35.4732L24.2083 33.2098V37.7366L20.3025 40L16.3968 37.7366Z" fill="#F0B90B"/>
<path d="M13.5847 28.3707L9.67897 26.1073V21.5805L13.5847 23.8439V28.3707Z" fill="#F0B90B"/>
<path d="M20.3025 12.2537L16.3968 9.99025L20.3025 7.72683L24.2083 9.99025L20.3025 12.2537Z" fill="#F0B90B"/>
<path d="M10.8116 9.99025L6.90586 12.2537V16.7805L3.00012 14.5171V9.99025L6.90586 7.72683L10.8116 9.99025Z" fill="#F0B90B"/>
<path d="M3.00012 17.7171L6.90586 19.9805V27.7073L13.6237 31.5707V36.0976L3.00012 29.9707V17.7171Z" fill="#F0B90B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

23
react/src/icons/terra.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 288.9 274" style="enable-background:new 0 0 288.9 274;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#2849A9;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#5795ED;}
</style>
<path class="st0" d="M151.1,0.3c33.7,0,64.9,12.1,88.7,32.9c31.8,24.5,22.6,113.9-9.6,90.3c-70.8-0.3-202.4-38.2-163.2-90.3
c4-5.3,9-9.6,14.5-13.7h-0.3c0.9-0.5,1.9-1,2.8-1.6c0.9-0.5,1.9-1.1,2.8-1.6h0c2.8-1.6,5.6-3.1,8.7-4.3
C112.5,4.6,131.3,0.3,151.1,0.3z M174.9,272.8c-14.2,0.9-42.6-21.4-50.7-50.9c-15.1-55.9,107.2-84.4,118.7-85.4
c31.2,0.9,38.9,38.2,16.1,76.7C229.3,262.6,175.5,272.8,174.9,272.8z"/>
<path class="st1" d="M14.8,77.9c9.9,2.8,70.5-16.5,88.4-43.8c0.3-0.3,14.2-21.7-12.7-22c-3.1,0-11.7,0.3-20.1,5.3
c-4,2.5-7.7,5-11.4,7.8c-5.8,4.3-11.3,9.5-16.5,14.4h0l-0.2,0.2c-5.3,5-10.2,10.9-14.5,16.8c-4.3,5.9-8.3,12.4-11.7,18.9
c-0.2,0.5-0.4,0.9-0.6,1.2C15.2,77,15,77.4,14.8,77.9z M86.5,272.8c1.9-2.8,3.1-36.6,1.9-45.3c-1.2-8.7-4-26.4-20.7-55.6
c-2.8-4.7-16.1-26.4-26-39.7c-5.6-7.8-11.7-15-17.8-22.3h0c-5.1-6-10.2-12.1-15-18.4c-0.3,0.8-0.5,1.5-0.8,2.2s-0.5,1.4-0.8,2.2
c-2.5,7.1-4.3,14.6-5.6,22.4S0,133.8,0,141.8c0,8.1,0.6,15.8,1.9,23.6s3.4,15.2,5.6,22.4c2.2,7.1,5.3,14.3,8.7,20.8
s7.4,13,11.7,18.9c4.3,5.9,9.3,11.5,14.5,16.8c4.9,5.3,10.8,10.2,16.7,14.6h0h0c4.6,3.1,9.3,6.2,13.9,9c8.5,5,11.7,5,13.4,5
C86.4,272.8,86.4,272.8,86.5,272.8z M288.9,141.8c0,18.9-3.7,36.9-10.2,53.4c-15.7,17-115.3-20.7-130.8-26.6c-1.2-0.5-2-0.7-2-0.8
c-15.8-6.8-63.3-27.9-67.7-60.8c-6.2-47.5,89.6-80.7,131.9-82c4.9,0,20.4,0.3,29.4,7.5C269.8,59.2,288.9,98.4,288.9,141.8z
M188.8,260.1c-3.7,12.1,10.2,16.5,22.6,10.6c24.7-13,45.1-33.2,59-57.1c0.9-1.2,0-2.5-1.5-2.2C255.6,212.6,195.6,236.5,188.8,260.1
z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,8 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 73.18 86.09">
<defs>
<style>.cls-1{fill:red;}.cls-2{fill:#0073ff;}.cls-3{fill:#00f3d7;}</style>
</defs>
<path class="cls-1" d="M30.29,43.05A47.76,47.76,0,0,0,16.72,9.63a1.7,1.7,0,0,0-1.2-.5H4.34A1.67,1.67,0,0,0,2.67,10.8V22.29A1.69,1.69,0,0,0,4.24,24a19.15,19.15,0,0,1,0,38.18A1.68,1.68,0,0,0,2.67,63.8V75.29A1.68,1.68,0,0,0,4.34,77H15.52a1.66,1.66,0,0,0,1.2-.51A47.75,47.75,0,0,0,30.29,43.05Z"/>
<path class="cls-2" d="M70.51,63.8a1.68,1.68,0,0,0-1.57-1.66,19.15,19.15,0,0,1,0-38.18,1.69,1.69,0,0,0,1.57-1.67V10.8a1.67,1.67,0,0,0-1.67-1.67H57.66a1.7,1.7,0,0,0-1.2.5,47.93,47.93,0,0,0,0,66.83,1.66,1.66,0,0,0,1.2.51H68.84a1.68,1.68,0,0,0,1.67-1.68Z"/>
<path class="cls-3" d="M28.06,3.14a1.89,1.89,0,0,0-1.75,2.58,102.89,102.89,0,0,1,7.05,37.33,102.87,102.87,0,0,1-7,37.32A1.89,1.89,0,0,0,28.06,83h17a1.88,1.88,0,0,0,1.74-2.58,102.33,102.33,0,0,1,0-74.65,1.88,1.88,0,0,0-1.74-2.58Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 993 B

View File

@ -0,0 +1,23 @@
<svg id="Group_2616" data-name="Group 2616" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="176.577" height="30.781" viewBox="0 0 176.577 30.781">
<defs>
<clipPath id="clip-path">
<rect id="Rectangle_1331" data-name="Rectangle 1331" width="176.577" height="30.781" transform="translate(0 0)" fill="#fff"/>
</clipPath>
</defs>
<g id="Group_2614" data-name="Group 2614" transform="translate(0 0)" clip-path="url(#clip-path)">
<path id="Path_1571" data-name="Path 1571" d="M139.86,29.253l-4.039,11.421a.261.261,0,0,1-.268.177h-.306a.282.282,0,0,1-.273-.177l-3.028-8.01-3.023,8.01a.272.272,0,0,1-.268.177h-.311a.269.269,0,0,1-.268-.177l-4.039-11.417c-.062-.177.005-.287.177-.287h1.25a.26.26,0,0,1,.273.177l2.879,8.647,2.927-7.919a.277.277,0,0,1,.273-.177h.263a.271.271,0,0,1,.268.177l3.014,7.919,2.812-8.647a.26.26,0,0,1,.273-.177h1.231c.177,0,.244.11.182.283" transform="translate(-82.566 -19.288)" fill="#fff"/>
<path id="Path_1572" data-name="Path 1572" d="M200.285,34.471a5.96,5.96,0,1,1-5.96-6.017,5.879,5.879,0,0,1,5.96,6.017m-1.653,0c0-2.769-1.806-4.676-4.307-4.676s-4.307,1.907-4.307,4.676,1.806,4.676,4.307,4.676,4.307-1.907,4.307-4.676" transform="translate(-125.412 -18.944)" fill="#fff"/>
<path id="Path_1573" data-name="Path 1573" d="M253.32,40.186a.288.288,0,0,1-.254.474H251.94a.222.222,0,0,1-.182-.105l-2.826-4.264h-3.037v4.13a.243.243,0,0,1-.24.24h-1.1a.243.243,0,0,1-.239-.24V29.21a.244.244,0,0,1,.239-.24h4.326c3.028,0,4.523,1.591,4.523,3.66a3.445,3.445,0,0,1-2.841,3.469Zm-4.748-5.236c2.381,0,3.181-.987,3.181-2.319s-.8-2.319-3.181-2.319h-2.678v4.638Z" transform="translate(-162.664 -19.288)" fill="#fff"/>
<path id="Path_1574" data-name="Path 1574" d="M303.514,28.4h.359a.226.226,0,0,1,.24.24v11.4a.226.226,0,0,1-.24.24h-1.1a.226.226,0,0,1-.24-.24V31.947l-4.432,6.415a.182.182,0,0,1-.331,0l-4.431-6.415v8.092a.226.226,0,0,1-.24.24h-1.1a.226.226,0,0,1-.24-.24v-11.4a.226.226,0,0,1,.24-.24h.355a.36.36,0,0,1,.3.163l5.28,7.373,5.284-7.373a.346.346,0,0,1,.292-.163" transform="translate(-194.255 -18.906)" fill="#fff"/>
<path id="Path_1575" data-name="Path 1575" d="M361.126,29.21V40.42a.244.244,0,0,1-.24.24h-1.1a.244.244,0,0,1-.24-.24V35.313h-7.277V40.42a.244.244,0,0,1-.24.24h-1.1a.244.244,0,0,1-.24-.24V29.21a.244.244,0,0,1,.24-.24h1.1a.244.244,0,0,1,.24.24v4.762h7.277V29.21a.244.244,0,0,1,.24-.24h1.1a.244.244,0,0,1,.24.24" transform="translate(-233.489 -19.288)" fill="#fff"/>
<path id="Path_1576" data-name="Path 1576" d="M414.109,34.471a5.96,5.96,0,1,1-5.96-6.017,5.879,5.879,0,0,1,5.96,6.017m-1.653,0c0-2.769-1.806-4.676-4.307-4.676s-4.307,1.907-4.307,4.676,1.806,4.676,4.307,4.676,4.307-1.907,4.307-4.676" transform="translate(-267.773 -18.944)" fill="#fff"/>
<path id="Path_1577" data-name="Path 1577" d="M466.416,39.558v.862a.226.226,0,0,1-.24.24h-7.794a.244.244,0,0,1-.24-.24V29.21a.244.244,0,0,1,.24-.24h1.1a.244.244,0,0,1,.239.24V39.318h6.458a.226.226,0,0,1,.24.24" transform="translate(-305.025 -19.288)" fill="#fff"/>
<path id="Path_1578" data-name="Path 1578" d="M510.615,39.558v.862a.226.226,0,0,1-.24.24h-8.417a.244.244,0,0,1-.24-.24V29.21a.244.244,0,0,1,.24-.24h8.226a.226.226,0,0,1,.24.24v.862a.226.226,0,0,1-.24.24h-6.889v3.66h6.123a.225.225,0,0,1,.239.235v.867a.226.226,0,0,1-.239.239h-6.123v4.005h7.081a.226.226,0,0,1,.24.24" transform="translate(-334.038 -19.288)" fill="#fff"/>
<path id="Path_1579" data-name="Path 1579" d="M15.39,30.781a15.39,15.39,0,1,1,15.39-15.39,15.408,15.408,0,0,1-15.39,15.39m0-30.1A14.708,14.708,0,1,0,30.1,15.39,14.724,14.724,0,0,0,15.39.683" transform="translate(0 0)" fill="#fff"/>
<path id="Path_1580" data-name="Path 1580" d="M22.272,32.5A13.829,13.829,0,1,1,36.1,18.672,13.844,13.844,0,0,1,22.272,32.5m0-27.065A13.237,13.237,0,1,0,35.509,18.672,13.251,13.251,0,0,0,22.272,5.436" transform="translate(-5.622 -3.225)" fill="#fff"/>
<path id="Path_1581" data-name="Path 1581" d="M29.154,34.22A12.266,12.266,0,1,1,41.42,21.954,12.28,12.28,0,0,1,29.154,34.22m0-24.032A11.766,11.766,0,1,0,40.92,21.954,11.779,11.779,0,0,0,29.154,10.188" transform="translate(-11.244 -6.449)" fill="#fff"/>
<path id="Path_1582" data-name="Path 1582" d="M36.035,35.94a10.7,10.7,0,1,1,10.7-10.7,10.717,10.717,0,0,1-10.7,10.7m0-21A10.295,10.295,0,1,0,46.33,25.235,10.307,10.307,0,0,0,36.035,14.941" transform="translate(-16.865 -9.675)" fill="#fff"/>
<path id="Path_1583" data-name="Path 1583" d="M42.916,37.66a9.142,9.142,0,1,1,9.142-9.142,9.153,9.153,0,0,1-9.142,9.142m0-17.966a8.824,8.824,0,1,0,8.824,8.824,8.833,8.833,0,0,0-8.824-8.824" transform="translate(-22.486 -12.9)" fill="#fff"/>
<path id="Path_1584" data-name="Path 1584" d="M49.8,39.379a7.58,7.58,0,1,1,7.58-7.58,7.589,7.589,0,0,1-7.58,7.58m0-14.933A7.353,7.353,0,1,0,57.151,31.8,7.361,7.361,0,0,0,49.8,24.446" transform="translate(-28.108 -16.124)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -5,6 +5,7 @@ import ReactDOM from "react-dom";
import App from "./App";
import ErrorBoundary from "./components/ErrorBoundary";
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
import { TerraWalletProvider } from "./contexts/TerraWalletContext";
import { theme } from "./muiTheme";
ReactDOM.render(
@ -12,9 +13,11 @@ ReactDOM.render(
<ThemeProvider theme={theme}>
<CssBaseline>
<EthereumProviderProvider>
<SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
<TerraWalletProvider>
<SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
</TerraWalletProvider>
</EthereumProviderProvider>
</CssBaseline>
</ThemeProvider>

1
react/src/route/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.js

View File

@ -1,41 +1,132 @@
import { ethers } from "ethers";
import { UniEvmToken } from "./uniswap-core";
import { QuickswapRouter } from "./quickswap";
import { SingleAmmSwapRouter as UniswapV3Router } from "./uniswap-v3";
import { QuickswapRouter as MaticRouter } from "./quickswap";
import { UniswapV3Router as EthRouter } from "./uniswap-v3";
import { TerraUstTransfer as UstRouter } from "./terra-ust-transfer";
import { HurricaneswapRouter as AvaxRouter } from "./hurricaneswap";
import { PancakeswapRouter as BnbRouter } from "./pancakeswap";
import {
ETH_NETWORK_CHAIN_ID,
POLYGON_NETWORK_CHAIN_ID,
ETH_TOKEN_INFO,
MATIC_TOKEN_INFO,
AVAX_TOKEN_INFO,
BNB_TOKEN_INFO,
UST_TOKEN_INFO,
} from "../utils/consts";
import { addFixedAmounts, subtractFixedAmounts } from "../utils/math";
import { UstLocation } from "./generic";
import {
ExactInParameters,
ExactOutParameters,
makeExactInParameters,
makeExactOutParameters,
} from "./uniswap-core";
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_POLYGON,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_TERRA,
} from "@certusone/wormhole-sdk";
export { PROTOCOL as PROTOCOL_UNISWAP_V2 } from "./uniswap-v2";
export { PROTOCOL as PROTOCOL_UNISWAP_V3 } from "./uniswap-v3";
export { PROTOCOL as PROTOCOL_TERRA_UST_TRANSFER } from "./terra-ust-transfer";
export const TERRA_UST = UST_TOKEN_INFO.address;
export enum QuoteType {
ExactIn = 1,
ExactOut,
}
function makeRouter(provider: ethers.providers.Provider, id: number) {
switch (id) {
case ETH_NETWORK_CHAIN_ID: {
return new UniswapV3Router(provider);
export function makeEvmProviderFromAddress(tokenAddress: string) {
switch (tokenAddress) {
case ETH_TOKEN_INFO.address: {
const url = process.env.REACT_APP_GOERLI_PROVIDER;
if (!url) {
throw new Error("Could not find REACT_APP_GOERLI_PROVIDER");
}
return new ethers.providers.StaticJsonRpcProvider(url);
}
case POLYGON_NETWORK_CHAIN_ID: {
return new QuickswapRouter(provider);
case MATIC_TOKEN_INFO.address: {
const url = process.env.REACT_APP_MUMBAI_PROVIDER;
if (!url) {
throw new Error("Could not find REACT_APP_MUMBAI_PROVIDER");
}
return new ethers.providers.StaticJsonRpcProvider(url);
}
case AVAX_TOKEN_INFO.address: {
const url = process.env.REACT_APP_FUJI_PROVIDER;
if (!url) {
throw new Error("Could not find REACT_APP_FUJI_PROVIDER");
}
return new ethers.providers.StaticJsonRpcProvider(url);
}
case BNB_TOKEN_INFO.address: {
const url = process.env.REACT_APP_BSC_PROVIDER;
if (!url) {
throw new Error("Could not find REACT_APP_BSC_PROVIDER");
}
return new ethers.providers.StaticJsonRpcProvider(url);
}
default: {
throw Error("unrecognized chain id");
throw Error("unrecognized evm token address");
}
}
}
export function getUstAddress(id: number): string {
switch (id) {
case ETH_NETWORK_CHAIN_ID: {
return "0x36Ed51Afc79619b299b238898E72ce482600568a";
export function getChainIdFromAddress(tokenAddress: string) {
switch (tokenAddress) {
case ETH_TOKEN_INFO.address: {
return CHAIN_ID_ETH;
}
case POLYGON_NETWORK_CHAIN_ID: {
return "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c";
case MATIC_TOKEN_INFO.address: {
return CHAIN_ID_POLYGON;
}
case AVAX_TOKEN_INFO.address: {
return CHAIN_ID_AVAX;
}
case BNB_TOKEN_INFO.address: {
return CHAIN_ID_BSC;
}
case UST_TOKEN_INFO.address: {
return CHAIN_ID_TERRA;
}
default: {
throw Error("unrecognized evm token address");
}
}
}
async function makeRouter(tokenAddress: string, loc: UstLocation) {
switch (tokenAddress) {
case ETH_TOKEN_INFO.address: {
const provider = makeEvmProviderFromAddress(tokenAddress);
const router = new EthRouter(provider);
await router.initialize(loc);
return router;
}
case MATIC_TOKEN_INFO.address: {
const provider = makeEvmProviderFromAddress(tokenAddress);
const router = new MaticRouter(provider);
await router.initialize(loc);
return router;
}
case AVAX_TOKEN_INFO.address: {
const provider = makeEvmProviderFromAddress(tokenAddress);
const router = new AvaxRouter(provider);
await router.initialize(loc);
return router;
}
case BNB_TOKEN_INFO.address: {
const provider = makeEvmProviderFromAddress(tokenAddress);
const router = new BnbRouter(provider);
await router.initialize(loc);
return router;
}
case UST_TOKEN_INFO.address: {
return new UstRouter();
}
default: {
throw Error("unrecognized chain id");
@ -51,123 +142,101 @@ function splitSlippageInHalf(totalSlippage: string): string {
.toString();
}
interface RelayerFee {
amount: ethers.BigNumber;
export interface RelayerFee {
amount: string;
tokenAddress: string;
}
export interface ExactInParameters {
protocol: string;
amountIn: ethers.BigNumber;
minAmountOut: ethers.BigNumber;
deadline: ethers.BigNumber;
poolFee: string;
path: [string, string];
}
export interface ExactInCrossParameters {
src: ExactInParameters;
dst: ExactInParameters;
amountIn: string;
ustAmountIn: string;
minAmountOut: string;
src: ExactInParameters | undefined;
dst: ExactInParameters | undefined;
relayerFee: RelayerFee;
}
export interface ExactOutParameters {
protocol: string;
amountOut: ethers.BigNumber;
maxAmountIn: ethers.BigNumber;
deadline: ethers.BigNumber;
poolFee: string;
path: [string, string];
}
export interface ExactOutCrossParameters {
src: ExactOutParameters;
dst: ExactOutParameters;
amountOut: string;
ustAmountIn: string;
maxAmountIn: string;
src: ExactOutParameters | undefined;
dst: ExactOutParameters | undefined;
relayerFee: RelayerFee;
}
export class UniswapToUniswapQuoter {
// providers
srcProvider: ethers.providers.Provider;
dstProvider: ethers.providers.Provider;
// networks
srcNetwork: ethers.providers.Network;
dstNetwork: ethers.providers.Network;
// tokens
tokenInAddress: string;
tokenOutAddress: string;
// routers
srcRouter: UniswapV3Router | QuickswapRouter;
dstRouter: UniswapV3Router | QuickswapRouter;
srcRouter: UstRouter | EthRouter | MaticRouter | AvaxRouter | BnbRouter;
dstRouter: UstRouter | EthRouter | MaticRouter | AvaxRouter | BnbRouter;
// tokens
srcTokenIn: UniEvmToken;
srcTokenOut: UniEvmToken;
dstTokenIn: UniEvmToken;
dstTokenOut: UniEvmToken;
async initialize(tokenInAddress: string, tokenOutAddress: string) {
if (tokenInAddress !== this.tokenInAddress) {
this.tokenInAddress = tokenInAddress;
this.srcRouter = await makeRouter(tokenInAddress, UstLocation.Out);
}
constructor(
srcProvider: ethers.providers.Provider,
dstProvider: ethers.providers.Provider
) {
this.srcProvider = srcProvider;
this.dstProvider = dstProvider;
}
async initialize(): Promise<void> {
[this.srcNetwork, this.dstNetwork] = await Promise.all([
this.srcProvider.getNetwork(),
this.dstProvider.getNetwork(),
]);
this.srcRouter = makeRouter(this.srcProvider, this.srcNetwork.chainId);
this.dstRouter = makeRouter(this.dstProvider, this.dstNetwork.chainId);
return;
}
sameChain(): boolean {
return this.srcNetwork.chainId === this.dstNetwork.chainId;
}
async makeSrcTokens(
tokenInAddress: string
): Promise<[UniEvmToken, UniEvmToken]> {
const ustOutAddress = getUstAddress(this.srcNetwork.chainId);
const router = this.srcRouter;
[this.srcTokenIn, this.srcTokenOut] = await Promise.all([
router.makeToken(tokenInAddress),
router.makeToken(ustOutAddress),
]);
return [this.srcTokenIn, this.srcTokenOut];
}
async makeDstTokens(
tokenOutAddress: string
): Promise<[UniEvmToken, UniEvmToken]> {
const ustInAddress = getUstAddress(this.dstNetwork.chainId);
const router = this.dstRouter;
[this.dstTokenIn, this.dstTokenOut] = await Promise.all([
router.makeToken(ustInAddress),
router.makeToken(tokenOutAddress),
]);
return [this.dstTokenIn, this.dstTokenOut];
if (tokenOutAddress !== this.tokenOutAddress) {
this.tokenOutAddress = tokenOutAddress;
this.dstRouter = await makeRouter(tokenOutAddress, UstLocation.In);
}
}
async computeAndVerifySrcPoolAddress(): Promise<string> {
return this.srcRouter.computeAndVerifyPoolAddress(
this.srcTokenIn,
this.srcTokenOut
);
return this.srcRouter.computeAndVerifyPoolAddress();
}
async computeAndVerifyDstPoolAddress(): Promise<string> {
return this.dstRouter.computeAndVerifyPoolAddress(
this.dstTokenIn,
this.dstTokenOut
);
return this.dstRouter.computeAndVerifyPoolAddress();
}
computeSwapSlippage(slippage: string): string {
if (this.isSrcUst() || this.isDstUst()) {
return slippage;
}
return splitSlippageInHalf(slippage);
}
getRelayerFee(amount: string): RelayerFee {
if (this.isSrcUst()) {
return {
amount: this.srcRouter.computeUnitAmountOut(amount),
tokenAddress: TERRA_UST, // TODO: make sure this is the right address for bridge transfer?
};
}
const relayerFee: RelayerFee = {
amount: this.srcRouter.computeUnitAmountOut(amount),
tokenAddress: this.srcRouter.getTokenOutAddress(),
};
return relayerFee;
}
makeSrcExactInParameters(
amountIn: string,
minAmountOut: string
): ExactInParameters | undefined {
if (this.isSrcUst()) {
return undefined;
}
// @ts-ignore
return makeExactInParameters(this.srcRouter, amountIn, minAmountOut);
}
makeDstExactInParameters(
amountIn: string,
minAmountOut: string
): ExactInParameters | undefined {
if (this.isDstUst()) {
return undefined;
}
// @ts-ignore
return makeExactInParameters(this.dstRouter, amountIn, minAmountOut);
}
async computeExactInParameters(
@ -175,71 +244,69 @@ export class UniswapToUniswapQuoter {
slippage: string,
relayerFeeUst: string
): Promise<ExactInCrossParameters> {
const singleSlippage = splitSlippageInHalf(slippage);
const singleSlippage = this.computeSwapSlippage(slippage);
// src quote
const srcRouter = this.srcRouter;
const srcTokenIn = this.srcTokenIn;
const srcTokenOut = this.srcTokenOut;
const srcMinAmountOut = await srcRouter.fetchQuoteAmountOut(
srcTokenIn,
srcTokenOut,
const srcMinAmountOut = await srcRouter.fetchExactInQuote(
amountIn,
singleSlippage
);
// dst quote
const dstRouter = this.dstRouter;
const dstAmountIn = this.srcTokenOut.formatAmount(srcMinAmountOut);
const dstAmountIn = srcMinAmountOut; //srcRouter.formatAmountOut(srcMinAmountOut);
if (Number(dstAmountIn) < Number(relayerFeeUst)) {
throw Error(
`srcAmountOut <= relayerFeeUst. ${dstAmountIn} vs ${relayerFeeUst}`
);
}
const dstTokenIn = this.dstTokenIn;
const dstTokenOut = this.dstTokenOut;
const dstAmountInAfterFee = dstTokenIn.subtractAmounts(
const dstAmountInAfterFee = subtractFixedAmounts(
dstAmountIn,
relayerFeeUst
relayerFeeUst,
dstRouter.getTokenInDecimals()
);
const dstMinAmountOut = await dstRouter.fetchQuoteAmountOut(
dstTokenIn,
dstTokenOut,
const dstMinAmountOut = await dstRouter.fetchExactInQuote(
dstAmountInAfterFee,
singleSlippage
);
const srcParameters: ExactInParameters = {
protocol: srcRouter.getProtocol(),
amountIn: srcTokenIn.computeUnitAmount(amountIn),
minAmountOut: srcMinAmountOut,
poolFee: srcRouter.getPoolFee(),
deadline: srcRouter.getTradeDeadline(),
path: [srcTokenIn.getAddress(), srcTokenOut.getAddress()],
};
const dstParameters: ExactInParameters = {
protocol: dstRouter.getProtocol(),
amountIn: dstTokenIn.computeUnitAmount(dstAmountInAfterFee),
minAmountOut: dstMinAmountOut,
poolFee: dstRouter.getPoolFee(),
deadline: dstRouter.getTradeDeadline(),
path: [dstTokenIn.getAddress(), dstTokenOut.getAddress()],
};
// organize parameters
const params: ExactInCrossParameters = {
src: srcParameters,
dst: dstParameters,
relayerFee: {
amount: dstTokenIn.computeUnitAmount(relayerFeeUst),
tokenAddress: this.dstTokenIn.getAddress(),
},
amountIn: amountIn,
ustAmountIn: dstAmountInAfterFee,
minAmountOut: dstMinAmountOut,
src: this.makeSrcExactInParameters(amountIn, srcMinAmountOut),
dst: this.makeDstExactInParameters(dstAmountInAfterFee, dstMinAmountOut),
relayerFee: this.getRelayerFee(relayerFeeUst),
};
return params;
}
makeSrcExactOutParameters(
amountOut: string,
maxAmountIn: string
): ExactOutParameters | undefined {
if (this.isSrcUst()) {
return undefined;
}
// @ts-ignore
return makeExactOutParameters(this.srcRouter, amountOut, maxAmountIn);
}
makeDstExactOutParameters(
amountOut: string,
maxAmountIn: string
): ExactOutParameters | undefined {
if (this.isDstUst()) {
return undefined;
}
// @ts-ignore
return makeExactOutParameters(this.dstRouter, amountOut, maxAmountIn);
}
async computeExactOutParameters(
amountOut: string,
slippage: string,
@ -249,69 +316,86 @@ export class UniswapToUniswapQuoter {
// dst quote first
const dstRouter = this.dstRouter;
const dstTokenIn = this.dstTokenIn;
const dstTokenOut = this.dstTokenOut;
const dstMaxAmountIn = await dstRouter.fetchQuoteAmountIn(
dstTokenIn,
dstTokenOut,
const dstMaxAmountIn = await dstRouter.fetchExactOutQuote(
amountOut,
singleSlippage
);
// src quote
const srcRouter = this.srcRouter;
const srcAmountOut = this.dstTokenIn.formatAmount(dstMaxAmountIn);
const srcAmountOut = dstMaxAmountIn;
if (Number(srcAmountOut) < Number(relayerFeeUst)) {
throw Error(
`dstAmountIn <= relayerFeeUst. ${srcAmountOut} vs ${relayerFeeUst}`
);
}
const srcTokenIn = this.srcTokenIn;
const srcTokenOut = this.srcTokenOut;
const srcAmountOutBeforeFee = srcTokenOut.addAmounts(
const srcAmountOutBeforeFee = addFixedAmounts(
srcAmountOut,
relayerFeeUst
relayerFeeUst,
srcRouter.getTokenOutDecimals()
);
const srcMaxAmountIn = await srcRouter.fetchQuoteAmountIn(
srcTokenIn,
srcTokenOut,
const srcMaxAmountIn = await srcRouter.fetchExactOutQuote(
srcAmountOutBeforeFee,
singleSlippage
);
const srcParameters: ExactOutParameters = {
protocol: srcRouter.getProtocol(),
amountOut: srcTokenOut.computeUnitAmount(srcAmountOutBeforeFee),
maxAmountIn: srcMaxAmountIn,
poolFee: srcRouter.getPoolFee(),
deadline: srcRouter.getTradeDeadline(),
path: [srcTokenIn.getAddress(), srcTokenOut.getAddress()],
};
const dstParameters: ExactOutParameters = {
protocol: dstRouter.getProtocol(),
amountOut: dstTokenOut.computeUnitAmount(amountOut),
maxAmountIn: dstMaxAmountIn,
poolFee: dstRouter.getPoolFee(),
deadline: dstRouter.getTradeDeadline(),
path: [dstTokenIn.getAddress(), dstTokenOut.getAddress()],
};
// organize parameters
const params: ExactOutCrossParameters = {
src: srcParameters,
dst: dstParameters,
relayerFee: {
amount: dstTokenIn.computeUnitAmount(relayerFeeUst),
tokenAddress: this.dstTokenIn.getAddress(),
},
amountOut: amountOut,
ustAmountIn: dstMaxAmountIn,
maxAmountIn: srcMaxAmountIn,
src: this.makeSrcExactOutParameters(
srcAmountOutBeforeFee,
srcMaxAmountIn
),
dst: this.makeDstExactOutParameters(amountOut, dstMaxAmountIn),
relayerFee: this.getRelayerFee(relayerFeeUst),
};
return params;
}
setDeadlines(deadline: string): void {
this.srcRouter.setDeadline(deadline);
this.dstRouter.setDeadline(deadline);
if (!this.isSrcUst()) {
// @ts-ignore
this.srcRouter.setDeadline(deadline);
}
if (!this.isDstUst()) {
// @ts-ignore
this.dstRouter.setDeadline(deadline);
}
}
isSrcUst(): boolean {
return this.tokenInAddress === TERRA_UST;
}
isDstUst(): boolean {
return this.tokenOutAddress === TERRA_UST;
}
getSrcEvmProvider(): ethers.providers.Provider | undefined {
if (this.isSrcUst()) {
return undefined;
}
// @ts-ignore
return this.srcRouter.getProvider();
}
getDstEvmProvider(): ethers.providers.Provider | undefined {
if (this.isDstUst()) {
return undefined;
}
// @ts-ignore
return this.dstRouter.getProvider();
}
getSrcChainId(): ChainId {
return getChainIdFromAddress(this.tokenInAddress);
}
getDstChainId(): ChainId {
return getChainIdFromAddress(this.tokenOutAddress);
}
}

View File

@ -1,7 +1,38 @@
export abstract class DexRouter {
abstract makeToken(tokenAddress: string): any;
abstract quoteLot(tokenA: any, tokenB: any, amount: string): Promise<any>;
abstract setSlippage(slippage: string): void;
export enum UstLocation {
In = 1,
Out,
}
export abstract class RouterCore {
abstract computeAndVerifyPoolAddress(): Promise<string>;
abstract computePoolAddress(): string;
//abstract computeUnitAmountIn(amount: string): string;
abstract computeUnitAmountOut(amount: string): string;
abstract fetchExactInQuote(
amountOut: string,
slippage: string
): Promise<string>;
abstract fetchExactOutQuote(
amountOut: string,
slippage: string
): Promise<string>;
abstract formatAmountIn(amount: string): string;
abstract formatAmountOut(amount: string): string;
abstract getProtocol(): string;
abstract getTokenInDecimals(): number;
abstract getTokenOutDecimals(): number;
abstract getTokenOutAddress(): string;
}
export abstract class GenericToken {
@ -9,16 +40,3 @@ export abstract class GenericToken {
abstract getDecimals(): number;
}
// TODO: wrap SwapRoute and other routes
export class GenericRoute {
route: any;
constructor(route: any) {
this.route = route;
}
getRoute(): any {
return this.route;
}
}

View File

@ -0,0 +1,26 @@
import { ethers } from "ethers";
import { AVAX_TOKEN_INFO } from "../utils/consts";
import { UstLocation } from "./generic";
import { UniswapV2Router } from "./uniswap-v2";
export { PROTOCOL } from "./uniswap-v2";
const HURRICANESWAP_FACTORY_ADDRESS = "";
export class HurricaneswapRouter extends UniswapV2Router {
constructor(provider: ethers.providers.Provider) {
super(provider);
super.setFactoryAddress(HURRICANESWAP_FACTORY_ADDRESS);
}
async initialize(ustLocation: UstLocation): Promise<void> {
await super.initializeTokens(AVAX_TOKEN_INFO, ustLocation);
return;
}
computePoolAddress(): string {
// cannot find factory address on testnet
return "0xD8087870E8869e45154189d434DF61C19e77ae30";
}
}

View File

@ -0,0 +1,26 @@
import { ethers } from "ethers";
import { BNB_TOKEN_INFO } from "../utils/consts";
import { UstLocation } from "./generic";
import { UniswapV2Router } from "./uniswap-v2";
export { PROTOCOL } from "./uniswap-v2";
const PANCAKESWAP_FACTORY_ADDRESS = "";
export class PancakeswapRouter extends UniswapV2Router {
constructor(provider: ethers.providers.Provider) {
super(provider);
super.setFactoryAddress(PANCAKESWAP_FACTORY_ADDRESS);
}
async initialize(ustLocation: UstLocation): Promise<void> {
await super.initializeTokens(BNB_TOKEN_INFO, ustLocation);
return;
}
computePoolAddress(): string {
// cannot find factory address on testnet
return "0x8682096d4A2a2f3cd63147D05e4BAB47634e2AD1";
}
}

View File

@ -1,12 +1,21 @@
import { ethers } from "ethers";
import { QUICKSWAP_FACTORY_ADDRESS } from "../utils/consts";
import { SingleAmmSwapRouter } from "./uniswap-v2";
import { MATIC_TOKEN_INFO } from "../utils/consts";
import { UstLocation } from "./generic";
import { UniswapV2Router } from "./uniswap-v2";
export { PROTOCOL } from "./uniswap-v2";
export class QuickswapRouter extends SingleAmmSwapRouter {
const QUICKSWAP_FACTORY_ADDRESS = "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32";
export class QuickswapRouter extends UniswapV2Router {
constructor(provider: ethers.providers.Provider) {
super(provider);
super.setFactoryAddress(QUICKSWAP_FACTORY_ADDRESS);
}
async initialize(ustLocation: UstLocation): Promise<void> {
await super.initializeTokens(MATIC_TOKEN_INFO, ustLocation);
return;
}
}

View File

@ -0,0 +1,67 @@
import { Dec, Int } from "@terra-money/terra.js";
import { UST_TOKEN_INFO } from "../utils/consts";
import { RouterCore } from "./generic";
export const PROTOCOL = "TerraUstTransfer";
const UST_DECIMALS = 6;
const UST_AMOUNT_MULTIPLIER = "1000000";
export class TerraUstTransfer extends RouterCore {
computePoolAddress(): string {
return UST_TOKEN_INFO.address;
}
computeAndVerifyPoolAddress(): Promise<string> {
return new Promise<string>((resolve) => {
return resolve(this.computePoolAddress());
});
}
formatAmountIn(amount: string): string {
const formatted = new Dec(amount).div(UST_AMOUNT_MULTIPLIER);
return formatted.toString();
}
formatAmountOut(amount: string): string {
return this.formatAmountIn(amount);
}
computeUnitAmountIn(amount: string): string {
const unitified = new Dec(amount).mul(UST_AMOUNT_MULTIPLIER);
return new Int(unitified.toString()).toString();
}
computeUnitAmountOut(amount: string): string {
return this.computeUnitAmountIn(amount);
}
getProtocol(): string {
return PROTOCOL;
}
async fetchExactInQuote(amountIn: string, slippage: string): Promise<string> {
return amountIn;
}
async fetchExactOutQuote(
amountOut: string,
slippage: string
): Promise<string> {
return amountOut;
}
getTokenInDecimals(): number {
return UST_DECIMALS;
}
getTokenOutDecimals(): number {
return UST_DECIMALS;
}
getTokenOutAddress(): string {
return this.computePoolAddress();
}
}

View File

@ -1,7 +1,10 @@
//@ts-nocheck
import { ethers } from "ethers";
import { CurrencyAmount, Token } from "@uniswap/sdk-core";
import { EvmToken } from "./evm";
import { RouterCore, UstLocation } from "./generic";
import { TokenInfo } from "../utils/consts";
export function computeTradeDeadline(deadline: string): ethers.BigNumber {
return ethers.BigNumber.from(Math.floor(Date.now() / 1000)).add(deadline);
@ -78,46 +81,112 @@ export async function makeUniEvmToken(
return new UniEvmToken(chainId, erc20);
}
export abstract class UniswapRouterCore {
function stringToBigNumber(value: string): ethers.BigNumber {
return ethers.BigNumber.from(value);
}
export interface ExactInParameters {
protocol: string;
amountIn: ethers.BigNumber;
minAmountOut: ethers.BigNumber;
deadline: ethers.BigNumber;
poolFee: string;
path: [string, string];
}
export interface ExactOutParameters {
protocol: string;
amountOut: ethers.BigNumber;
maxAmountIn: ethers.BigNumber;
deadline: ethers.BigNumber;
poolFee: string;
path: [string, string];
}
export function makeExactInParameters(
router: UniswapRouterCore,
amountIn: string,
minAmountOut: string
): ExactInParameters {
const params: ExactInParameters = {
protocol: router.getProtocol(),
amountIn: router.tokenIn.computeUnitAmount(amountIn),
minAmountOut: router.tokenOut.computeUnitAmount(minAmountOut),
poolFee: router.getPoolFee(),
deadline: router.getTradeDeadline(),
path: [router.tokenIn.getAddress(), router.tokenOut.getAddress()],
};
return params;
}
export function makeExactOutParameters(
router: UniswapRouterCore,
amountOut: string,
maxAmountIn: string
): ExactOutParameters {
const params: ExactOutParameters = {
protocol: router.getProtocol(),
amountOut: router.tokenOut.computeUnitAmount(amountOut),
maxAmountIn: router.tokenIn.computeUnitAmount(maxAmountIn),
poolFee: router.getPoolFee(),
deadline: router.getTradeDeadline(),
path: [router.tokenIn.getAddress(), router.tokenOut.getAddress()],
};
return params;
}
export abstract class UniswapRouterCore extends RouterCore {
provider: ethers.providers.Provider;
network: ethers.providers.Network;
// wormhole
chainId: number;
// tokens
tokenIn: UniEvmToken;
tokenOut: UniEvmToken;
// params
deadline: string = "";
constructor(provider: ethers.providers.Provider) {
super();
this.provider = provider;
}
public async makeToken(tokenAddress: string): Promise<UniEvmToken> {
const network = await this.provider.getNetwork();
return makeUniEvmToken(this.provider, network.chainId, tokenAddress);
public getProvider(): ethers.providers.Provider {
return this.provider;
}
abstract computePoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): string;
public async initializeTokens(
tokenInfo: TokenInfo,
ustLocation: UstLocation
): Promise<void> {
this.network = await this.provider.getNetwork();
abstract computeAndVerifyPoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): Promise<string>;
const network = this.network;
abstract fetchQuoteAmountOut(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountOut: string,
slippage: string
): Promise<ethers.BigNumber>;
abstract fetchQuoteAmountIn(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountOut: string,
slippage: string
): Promise<ethers.BigNumber>;
abstract getProtocol(): string;
if (ustLocation === UstLocation.Out) {
[this.tokenIn, this.tokenOut] = await Promise.all([
makeUniEvmToken(this.provider, network.chainId, tokenInfo.address),
makeUniEvmToken(
this.provider,
network.chainId,
tokenInfo.ustPairedAddress
),
]);
} else {
[this.tokenIn, this.tokenOut] = await Promise.all([
makeUniEvmToken(
this.provider,
network.chainId,
tokenInfo.ustPairedAddress
),
makeUniEvmToken(this.provider, network.chainId, tokenInfo.address),
]);
}
return;
}
public getPoolFee(): string {
return "";
@ -130,4 +199,36 @@ export abstract class UniswapRouterCore {
public getTradeDeadline(): ethers.BigNumber {
return computeTradeDeadline(this.deadline);
}
/*
public computeUnitAmountIn(amount: string): string {
return this.tokenIn.computeUnitAmount(amount).toString();
}
*/
public computeUnitAmountOut(amount: string): string {
return this.tokenOut.computeUnitAmount(amount).toString();
}
public formatAmountIn(amount: string): string {
return this.tokenIn.formatAmount(stringToBigNumber(amount));
}
public formatAmountOut(amount: string): string {
return this.tokenOut.formatAmount(stringToBigNumber(amount));
}
public getTokenInDecimals(): number {
return this.tokenIn.getDecimals();
}
public getTokenOutDecimals(): number {
return this.tokenOut.getDecimals();
}
public getTokenOutAddress(): string {
return this.tokenOut.getAddress();
}
abstract getProtocol(): string;
}

View File

@ -3,11 +3,19 @@ import { CurrencyAmount, TradeType } from "@uniswap/sdk-core";
import { abi as IUniswapV2PairABI } from "@uniswap/v2-core/build/UniswapV2Pair.json";
import { computePairAddress, Pair, Route, Trade } from "@uniswap/v2-sdk";
import { UniEvmToken, UniswapRouterCore } from "./uniswap-core";
import { UniswapRouterCore } from "./uniswap-core";
export const PROTOCOL = "UniswapV2";
export class SingleAmmSwapRouter extends UniswapRouterCore {
// uniswap v3 (ethereum)
//export const UNISWAP_V3_FACTORY_ADDRESS = '0x1F98431c8aD98523631AE4a59f267346ea31F984';
//export const UNISWAP_V3_ROUTER_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564';
// quickswap (polygon)
export const QUICKSWAP_V2_ROUTER_ADDRESS =
"0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff";
export class UniswapV2Router extends UniswapRouterCore {
factoryAddress: string;
pairContract: ethers.Contract;
pair: Pair;
@ -17,23 +25,20 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
return;
}
computePoolAddress(tokenIn: UniEvmToken, tokenOut: UniEvmToken): string {
computePoolAddress(): string {
if (this.factoryAddress === undefined) {
throw Error("factoryAddress is undefined. use setFactoryAddress");
}
return computePairAddress({
factoryAddress: this.factoryAddress,
tokenA: tokenIn.getUniToken(),
tokenB: tokenOut.getUniToken(),
tokenA: this.tokenIn.getUniToken(),
tokenB: this.tokenOut.getUniToken(),
});
}
async computeAndVerifyPoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): Promise<string> {
const pairAddress = this.computePoolAddress(tokenIn, tokenOut);
async computeAndVerifyPoolAddress(): Promise<string> {
const pairAddress = this.computePoolAddress();
// verify by attempting to call factory()
const poolContract = new ethers.Contract(
@ -46,8 +51,8 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
return pairAddress;
}
async createPool(tokenIn: UniEvmToken, tokenOut: UniEvmToken): Promise<Pair> {
const pairAddress = this.computePoolAddress(tokenIn, tokenOut);
async createPool(): Promise<Pair> {
const pairAddress = this.computePoolAddress();
const pairContract = new ethers.Contract(
pairAddress,
@ -63,6 +68,9 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
const reserve0 = reserves._reserve0.toString();
const reserve1 = reserves._reserve1.toString();
const tokenIn = this.tokenIn;
const tokenOut = this.tokenOut;
if (token0.toLowerCase() === tokenIn.getAddress().toLowerCase()) {
return new Pair(
CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), reserve0),
@ -76,15 +84,13 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
);
}
async fetchQuoteAmountOut(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountIn: string,
slippage: string
): Promise<ethers.BigNumber> {
async fetchExactInQuote(amountIn: string, slippage: string): Promise<string> {
// create pool
const pair = await this.createPool(tokenIn, tokenOut);
const pair = await this.createPool();
// let's get that quote
const tokenIn = this.tokenIn;
const tokenOut = this.tokenOut;
const route = new Route(
[pair],
@ -108,18 +114,24 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
.mulUnsafe(slippageMultiplier)
.round(decimals);
return tokenOut.computeUnitAmount(minAmountOutWithSlippage.toString());
/*
return tokenOut
.computeUnitAmount(minAmountOutWithSlippage.toString())
.toString();
*/
return minAmountOutWithSlippage.toString();
}
async fetchQuoteAmountIn(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
async fetchExactOutQuote(
amountOut: string,
slippage: string
): Promise<ethers.BigNumber> {
): Promise<string> {
// create pool
const pair = await this.createPool(tokenIn, tokenOut);
const pair = await this.createPool();
// let's get that quote
const tokenIn = this.tokenIn;
const tokenOut = this.tokenOut;
const route = new Route(
[pair],
@ -142,7 +154,12 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
.divUnsafe(slippageDivisor)
.round(decimals);
return tokenIn.computeUnitAmount(maxAmountInWithSlippage.toString());
/*
return tokenIn
.computeUnitAmount(maxAmountInWithSlippage.toString())
.toString();
*/
return maxAmountInWithSlippage.toString();
}
getProtocol(): string {

View File

@ -13,12 +13,15 @@ import {
Trade,
} from "@uniswap/v3-sdk";
import { UniEvmToken, UniswapRouterCore } from "./uniswap-core";
import { UNISWAP_V3_FACTORY_ADDRESS } from "../utils/consts";
import { UniswapRouterCore } from "./uniswap-core";
import { ETH_TOKEN_INFO } from "../utils/consts";
import { UstLocation } from "./generic";
export const PROTOCOL = "UniswapV3";
export class SingleAmmSwapRouter extends UniswapRouterCore {
const UNISWAP_V3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
export class UniswapV3Router extends UniswapRouterCore {
poolContract: ethers.Contract;
pool: Pool;
poolFee: FeeAmount;
@ -30,24 +33,26 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
this.poolFee = FeeAmount.MEDIUM;
}
async initialize(ustLocation: UstLocation): Promise<void> {
await this.initializeTokens(ETH_TOKEN_INFO, ustLocation);
return;
}
getPoolFee(): string {
return this.poolFee.toString();
}
computePoolAddress(tokenIn: UniEvmToken, tokenOut: UniEvmToken): string {
computePoolAddress(): string {
return computePoolAddress({
factoryAddress: UNISWAP_V3_FACTORY_ADDRESS,
fee: this.poolFee,
tokenA: tokenIn.getUniToken(),
tokenB: tokenOut.getUniToken(),
tokenA: this.tokenIn.getUniToken(),
tokenB: this.tokenOut.getUniToken(),
});
}
async computeAndVerifyPoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): Promise<string> {
const pairAddress = this.computePoolAddress(tokenIn, tokenOut);
async computeAndVerifyPoolAddress(): Promise<string> {
const pairAddress = this.computePoolAddress();
// verify by attempting to call factory()
const poolContract = new ethers.Contract(
@ -60,8 +65,8 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
return pairAddress;
}
async createPool(tokenIn: UniEvmToken, tokenOut: UniEvmToken): Promise<Pool> {
const poolAddress = this.computePoolAddress(tokenIn, tokenOut);
async createPool(): Promise<Pool> {
const poolAddress = this.computePoolAddress();
const poolContract = new ethers.Contract(
poolAddress,
@ -103,8 +108,8 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
];
return new Pool(
tokenIn.getUniToken(),
tokenOut.getUniToken(),
this.tokenIn.getUniToken(),
this.tokenOut.getUniToken(),
this.poolFee,
sqrtPriceX96.toString(), //note the description discrepancy - sqrtPriceX96 and sqrtRatioX96 are interchangable values
liquidity,
@ -114,13 +119,15 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
}
async computeTradeExactIn(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amount: string
): Promise<Trade<Token, Token, TradeType.EXACT_INPUT>> {
// create pool
const pool = await this.createPool(tokenIn, tokenOut);
const pool = await this.createPool();
// let's get that quote
const tokenIn = this.tokenIn;
const tokenOut = this.tokenOut;
const amountIn = tokenIn.computeUnitAmount(amount);
const route = new Route(
@ -136,13 +143,15 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
}
async computeTradeExactOut(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amount: string
): Promise<Trade<Token, Token, TradeType.EXACT_OUTPUT>> {
// create pool
const pool = await this.createPool(tokenIn, tokenOut);
const pool = await this.createPool();
// let's get that quote
const tokenIn = this.tokenIn;
const tokenOut = this.tokenOut;
const amountOut = tokenOut.computeUnitAmount(amount);
const route = new Route(
@ -160,15 +169,11 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
);
}
async fetchQuoteAmountOut(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountIn: string,
slippage: string
): Promise<ethers.BigNumber> {
async fetchExactInQuote(amountIn: string, slippage: string): Promise<string> {
// get the quote
const trade = await this.computeTradeExactIn(tokenIn, tokenOut, amountIn);
const trade = await this.computeTradeExactIn(amountIn);
const tokenOut = this.tokenOut;
const decimals = tokenOut.getDecimals();
// calculate output amount with slippage
@ -183,18 +188,22 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
.mulUnsafe(slippageMultiplier)
.round(decimals);
return tokenOut.computeUnitAmount(minAmountOutWithSlippage.toString());
/*
return tokenOut
.computeUnitAmount(minAmountOutWithSlippage.toString())
.toString();
*/
return minAmountOutWithSlippage.toString();
}
async fetchQuoteAmountIn(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
async fetchExactOutQuote(
amountOut: string,
slippage: string
): Promise<ethers.BigNumber> {
): Promise<string> {
// get the quote
const trade = await this.computeTradeExactOut(tokenIn, tokenOut, amountOut);
const trade = await this.computeTradeExactOut(amountOut);
const tokenIn = this.tokenIn;
const decimals = tokenIn.getDecimals();
// calculate output amount with slippage
@ -209,7 +218,12 @@ export class SingleAmmSwapRouter extends UniswapRouterCore {
.divUnsafe(slippageDivisor)
.round(decimals);
return tokenIn.computeUnitAmount(maxAmountInWithSlippage.toString());
/*
return tokenIn
.computeUnitAmount(maxAmountInWithSlippage.toString())
.toString();
*/
return maxAmountInWithSlippage.toString();
}
getProtocol(): string {

1
react/src/swapper/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.js

View File

@ -0,0 +1,111 @@
import { ethers } from "ethers";
import { TransactionReceipt } from "@ethersproject/abstract-provider";
import {
EVM_ETH_NETWORK_CHAIN_ID,
EVM_POLYGON_NETWORK_CHAIN_ID,
EVM_AVAX_NETWORK_CHAIN_ID,
//EVM_BSC_NETWORK_CHAIN_ID,
} from "../utils/consts";
export const CROSSCHAINSWAP_GAS_PARAMETERS_EIP1559 = {
gasLimit: "694200",
//maxFeePerGas: "250000000000",
maxFeePerGas: "100420690000",
maxPriorityFeePerGas: "1690000000",
};
export const CROSSCHAINSWAP_GAS_PARAMETERS_EVM = {
gasLimit: "694200",
//gasPrice: "250000000000",
gasPrice: "20420690000",
};
export const EVM_EIP1559_CHAIN_IDS = [
EVM_ETH_NETWORK_CHAIN_ID,
EVM_POLYGON_NETWORK_CHAIN_ID,
EVM_AVAX_NETWORK_CHAIN_ID,
];
export async function getEvmGasParametersForContract(
contract: ethers.Contract
): Promise<any> {
const chainId = await getChainIdFromContract(contract);
if (EVM_EIP1559_CHAIN_IDS.indexOf(chainId) >= 0) {
return CROSSCHAINSWAP_GAS_PARAMETERS_EIP1559;
}
return CROSSCHAINSWAP_GAS_PARAMETERS_EVM;
}
async function getChainIdFromContract(
contract: ethers.Contract
): Promise<number> {
const network = await contract.provider.getNetwork();
return network.chainId;
}
// exact in
//
export async function evmSwapExactInFromVaaNative(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const gasParams = await getEvmGasParametersForContract(
swapContractWithSigner
);
const tx = await swapContractWithSigner.recvAndSwapExactNativeIn(
signedVaa,
gasParams
);
return tx.wait();
}
export async function evmSwapExactInFromVaaToken(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const gasParams = await getEvmGasParametersForContract(
swapContractWithSigner
);
const tx = await swapContractWithSigner.recvAndSwapExactIn(
signedVaa,
gasParams
);
return tx.wait();
}
// exact out
//
export async function evmSwapExactOutFromVaaNative(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const gasParams = await getEvmGasParametersForContract(
swapContractWithSigner
);
const tx = await swapContractWithSigner.recvAndSwapExactNativeOut(
signedVaa,
gasParams
);
return tx.wait();
}
export async function evmSwapExactOutFromVaaToken(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const gasParams = await getEvmGasParametersForContract(
swapContractWithSigner
);
const tx = await swapContractWithSigner.recvAndSwapExactOut(
signedVaa,
gasParams
);
return tx.wait();
}

View File

@ -1,9 +1,13 @@
//@ts-nocheck
import { ethers } from "ethers";
import { TransactionReceipt } from "@ethersproject/abstract-provider";
import {
CHAIN_ID_POLYGON as WORMHOLE_CHAIN_ID_POLYGON,
CHAIN_ID_ETH as WORMHOLE_CHAIN_ID_ETHEREUM,
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_POLYGON,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_TERRA,
getEmitterAddressEth,
hexToUint8Array,
nativeToHexString,
@ -11,7 +15,6 @@ import {
getSignedVAAWithRetry,
} from "@certusone/wormhole-sdk";
import { grpc } from "@improbable-eng/grpc-web";
import { UniEvmToken } from "../route/uniswap-core";
import {
PROTOCOL_UNISWAP_V2,
// PROTOCOL_UNISWAP_V3,
@ -21,27 +24,48 @@ import {
UniswapToUniswapQuoter,
} from "../route/cross-quote";
import {
TOKEN_BRIDGE_ADDRESS_ETHEREUM,
TOKEN_BRIDGE_ADDRESS_POLYGON,
TOKEN_BRIDGE_ADDRESS_TERRA,
TOKEN_BRIDGE_ADDRESS_AVALANCHE,
TOKEN_BRIDGE_ADDRESS_BSC,
CORE_BRIDGE_ADDRESS_ETHEREUM,
CORE_BRIDGE_ADDRESS_POLYGON,
TOKEN_BRIDGE_ADDRESS_ETHEREUM,
CORE_BRIDGE_ADDRESS_TERRA,
CORE_BRIDGE_ADDRESS_AVALANCHE,
CORE_BRIDGE_ADDRESS_BSC,
WORMHOLE_RPC_HOSTS,
POLYGON_NETWORK_CHAIN_ID,
ETH_NETWORK_CHAIN_ID,
WETH_TOKEN_INFO,
WMATIC_TOKEN_INFO,
//ETH_NETWORK_CHAIN_ID,
//POLYGON_NETWORK_CHAIN_ID,
//TERRA_NETWORK_CHAIN_ID,
UST_TOKEN_INFO,
} from "../utils/consts";
import {
CROSSCHAINSWAP_GAS_PARAMETERS,
swapExactInFromVaaNative,
swapExactInFromVaaToken,
swapExactOutFromVaaNative,
swapExactOutFromVaaToken,
} from "./util";
evmSwapExactInFromVaaNative,
evmSwapExactInFromVaaToken,
evmSwapExactOutFromVaaNative,
evmSwapExactOutFromVaaToken,
getEvmGasParametersForContract,
} from "./helpers";
import { abi as SWAP_CONTRACT_V2_ABI } from "../abi/contracts/CrossChainSwapV2.json";
import { abi as SWAP_CONTRACT_V3_ABI } from "../abi/contracts/CrossChainSwapV3.json";
import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM } from "../addresses/goerli";
import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON } from "../addresses/mumbai";
import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_AVALANCHE } from "../addresses/fuji";
import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_BSC } from "../addresses/bsc";
import { makeErc20Contract } from "../route/evm";
// placeholders
const CROSSCHAINSWAP_CONTRACT_ADDRESS_TERRA =
"terra163shc8unyqrndgcldaj2q9kgnqs82v0kgkhynf";
function makeNullSwapPath(): any[] {
const zeroBuffer = Buffer.alloc(20);
const nullAddress = "0x" + zeroBuffer.toString("hex");
return [nullAddress, nullAddress];
}
const NULL_SWAP_PATH = makeNullSwapPath();
interface SwapContractParameters {
address: string;
@ -63,7 +87,7 @@ const EXECUTION_PARAMETERS_ETHEREUM: ExecutionParameters = {
address: CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM,
},
wormhole: {
chainId: WORMHOLE_CHAIN_ID_ETHEREUM,
chainId: CHAIN_ID_ETH,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_ETHEREUM,
tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_ETHEREUM,
},
@ -74,34 +98,77 @@ const EXECUTION_PARAMETERS_POLYGON: ExecutionParameters = {
address: CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON,
},
wormhole: {
chainId: WORMHOLE_CHAIN_ID_POLYGON,
chainId: CHAIN_ID_POLYGON,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_POLYGON,
tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_POLYGON,
},
};
function makeExecutionParameters(id: number): ExecutionParameters {
switch (id) {
case ETH_NETWORK_CHAIN_ID: {
const EXECUTION_PARAMETERS_AVALANCHE: ExecutionParameters = {
crossChainSwap: {
address: CROSSCHAINSWAP_CONTRACT_ADDRESS_AVALANCHE,
},
wormhole: {
chainId: CHAIN_ID_AVAX,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_AVALANCHE,
tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_AVALANCHE,
},
};
const EXECUTION_PARAMETERS_BSC: ExecutionParameters = {
crossChainSwap: {
address: CROSSCHAINSWAP_CONTRACT_ADDRESS_BSC,
},
wormhole: {
chainId: CHAIN_ID_BSC,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_BSC,
tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_BSC,
},
};
const EXECUTION_PARAMETERS_TERRA: ExecutionParameters = {
crossChainSwap: {
address: CROSSCHAINSWAP_CONTRACT_ADDRESS_TERRA,
},
wormhole: {
chainId: CHAIN_ID_TERRA,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_TERRA,
tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_TERRA,
},
};
function makeExecutionParameters(chainId: ChainId): ExecutionParameters {
switch (chainId) {
case CHAIN_ID_ETH: {
return EXECUTION_PARAMETERS_ETHEREUM;
}
case POLYGON_NETWORK_CHAIN_ID: {
case CHAIN_ID_POLYGON: {
return EXECUTION_PARAMETERS_POLYGON;
}
case CHAIN_ID_AVAX: {
return EXECUTION_PARAMETERS_AVALANCHE;
}
case CHAIN_ID_BSC: {
return EXECUTION_PARAMETERS_BSC;
}
case CHAIN_ID_TERRA: {
return EXECUTION_PARAMETERS_TERRA;
}
default: {
throw Error("unrecognized chain id");
}
}
}
async function approveContractTokenSpend(
async function evmApproveContractTokenSpend(
provider: ethers.providers.Provider,
signer: ethers.Signer,
tokenContract: ethers.Contract,
tokenAddress: string, //ethers.Contract,
swapContractAddress: string,
amount: ethers.BigNumber
): Promise<TransactionReceipt> {
// build transaction for token spending
const tokenContract = await makeErc20Contract(provider, tokenAddress);
const unsignedTx = await tokenContract.populateTransaction.approve(
swapContractAddress,
amount
@ -140,7 +207,7 @@ function makeCrossChainSwapV2Contract(
return new ethers.Contract(contractAddress, SWAP_CONTRACT_V2_ABI, provider);
}
function makeCrossChainSwapContract(
function makeCrossChainSwapEvmContract(
provider: ethers.providers.Provider,
protocol: string,
contractAddress: string
@ -163,19 +230,62 @@ function addressToBytes32(
return hexToUint8Array(hexString);
}
async function approveAndSwapExactIn(
function evmMakeExactInSwapParameters(
amountIn: ethers.BigNumber,
recipientAddress: string,
dstWormholeChainId: ChainId,
quoteParams: ExactInCrossParameters
): any[] {
const src = quoteParams.src;
const dst = quoteParams.dst;
if (dst === undefined) {
return [
amountIn,
src.minAmountOut,
0,
addressToBytes32(recipientAddress, dstWormholeChainId),
src.deadline,
src.poolFee || 0,
];
}
return [
amountIn,
src.minAmountOut,
dst.minAmountOut,
addressToBytes32(recipientAddress, dstWormholeChainId),
src.deadline,
dst.poolFee || src.poolFee || 0,
];
}
function makePathArray(
quoteParams: ExactInCrossParameters | ExactOutCrossParameters
): any[] {
if (quoteParams.src === undefined) {
return NULL_SWAP_PATH.concat(quoteParams.dst.path);
} else if (quoteParams.dst === undefined) {
return quoteParams.src.path.concat(NULL_SWAP_PATH);
} else {
return quoteParams.src.path.concat(quoteParams.dst.path);
}
}
async function evmApproveAndSwapExactIn(
srcProvider: ethers.providers.Provider,
srcWallet: ethers.Signer,
srcTokenIn: UniEvmToken,
tokenInAddress: string,
quoteParams: ExactInCrossParameters,
srcExecutionParams: ExecutionParameters,
dstExecutionParams: ExecutionParameters,
isNative: boolean
isNative: boolean,
recipientAddress: string
): Promise<TransactionReceipt> {
const swapContractParams = srcExecutionParams.crossChainSwap;
const protocol = quoteParams.src.protocol;
const swapContract = makeCrossChainSwapContract(
const swapContract = makeCrossChainSwapEvmContract(
srcProvider,
protocol,
swapContractParams.address
@ -184,35 +294,27 @@ async function approveAndSwapExactIn(
// approve and swap this amount
const amountIn = quoteParams.src.amountIn;
const address = await srcWallet.getAddress();
const swapParams = [
amountIn,
quoteParams.src.minAmountOut,
quoteParams.dst.minAmountOut,
address,
quoteParams.src.deadline,
quoteParams.dst.poolFee || quoteParams.src.poolFee,
];
const pathArray = quoteParams.src.path.concat(quoteParams.dst.path);
const dstWormholeChainId = dstExecutionParams.wormhole.chainId;
const swapParams = evmMakeExactInSwapParameters(
amountIn,
recipientAddress,
dstWormholeChainId,
quoteParams
);
const pathArray = makePathArray(quoteParams);
const dstContractAddress = addressToBytes32(
dstExecutionParams.crossChainSwap.address,
dstWormholeChainId
);
const bridgeNonce = 69;
const gasParams = getEvmGasParametersForContract(swapContract);
// do the swap
if (isNative) {
const gasPlusValue = {
value: amountIn,
gasLimit: CROSSCHAINSWAP_GAS_PARAMETERS.gasLimit,
maxFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxFeePerGas,
maxPriorityFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxPriorityFeePerGas,
};
const transactionParams = { value: amountIn, ...gasParams };
console.info("swapExactNativeInAndTransfer");
const tx = await contractWithSigner.swapExactNativeInAndTransfer(
@ -222,15 +324,15 @@ async function approveAndSwapExactIn(
dstWormholeChainId,
dstContractAddress,
bridgeNonce,
gasPlusValue
transactionParams
);
return tx.wait();
} else {
console.info("approving contract to spend token in");
await approveContractTokenSpend(
await evmApproveContractTokenSpend(
srcProvider,
srcWallet,
srcTokenIn.getContract(),
tokenInAddress,
swapContract.address,
amountIn
);
@ -243,25 +345,27 @@ async function approveAndSwapExactIn(
dstWormholeChainId,
dstContractAddress,
bridgeNonce,
CROSSCHAINSWAP_GAS_PARAMETERS
gasParams
);
return tx.wait();
}
}
async function approveAndSwapExactOut(
// TODO: fix to resemble ExactIn
async function evmApproveAndSwapExactOut(
srcProvider: ethers.providers.Provider,
srcWallet: ethers.Signer,
srcTokenIn: UniEvmToken,
tokenInAddress: string,
quoteParams: ExactOutCrossParameters,
srcExecutionParams: ExecutionParameters,
dstExecutionParams: ExecutionParameters,
isNative: boolean
isNative: boolean,
recipientAddress: string
): Promise<TransactionReceipt> {
const swapContractParams = srcExecutionParams.crossChainSwap;
const protocol = quoteParams.src.protocol;
const swapContract = makeCrossChainSwapContract(
const protocol = quoteParams.src?.protocol;
const swapContract = makeCrossChainSwapEvmContract(
srcProvider,
protocol,
swapContractParams.address
@ -269,36 +373,30 @@ async function approveAndSwapExactOut(
const contractWithSigner = swapContract.connect(srcWallet);
// approve and swap this amount
const amountOut = quoteParams.src.amountOut;
const maxAmountIn = quoteParams.src.maxAmountIn;
const address = await srcWallet.getAddress();
const amountOut = quoteParams.src?.amountOut;
const maxAmountIn = quoteParams.src?.maxAmountIn;
const dstWormholeChainId = dstExecutionParams.wormhole.chainId;
const swapParams = [
amountOut,
maxAmountIn,
quoteParams.dst.amountOut,
address,
addressToBytes32(recipientAddress, dstWormholeChainId),
quoteParams.src.deadline,
quoteParams.dst.poolFee || quoteParams.src.poolFee,
quoteParams.dst.poolFee || quoteParams.src.poolFee || 0,
];
const pathArray = quoteParams.src.path.concat(quoteParams.dst.path);
const pathArray = makePathArray(quoteParams);
const dstWormholeChainId = dstExecutionParams.wormhole.chainId;
const dstContractAddress = addressToBytes32(
dstExecutionParams.crossChainSwap.address,
dstWormholeChainId
);
const bridgeNonce = 69;
const gasParams = getEvmGasParametersForContract(swapContract);
// do the swap
if (isNative) {
const gasPlusValue = {
value: maxAmountIn,
gasLimit: CROSSCHAINSWAP_GAS_PARAMETERS.gasLimit,
maxFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxFeePerGas,
maxPriorityFeePerGas: CROSSCHAINSWAP_GAS_PARAMETERS.maxPriorityFeePerGas,
};
const gasPlusValue = { value: maxAmountIn, ...gasParams };
console.info("swapExactNativeOutAndTransfer");
const tx = await contractWithSigner.swapExactNativeOutAndTransfer(
@ -313,10 +411,10 @@ async function approveAndSwapExactOut(
return tx.wait();
} else {
console.info("approving contract to spend token in");
await approveContractTokenSpend(
await evmApproveContractTokenSpend(
srcProvider,
srcWallet,
srcTokenIn.getContract(),
tokenInAddress,
swapContract.address,
maxAmountIn
);
@ -329,7 +427,7 @@ async function approveAndSwapExactOut(
dstWormholeChainId,
dstContractAddress,
bridgeNonce,
CROSSCHAINSWAP_GAS_PARAMETERS
gasParams
);
return tx.wait();
}
@ -345,7 +443,7 @@ async function swapExactInFromVaa(
): Promise<TransactionReceipt> {
const swapContractParams = dstExecutionParams.crossChainSwap;
const swapContract = makeCrossChainSwapContract(
const swapContract = makeCrossChainSwapEvmContract(
dstProvider,
dstProtocol,
swapContractParams.address
@ -353,11 +451,11 @@ async function swapExactInFromVaa(
const contractWithSigner = swapContract.connect(dstWallet);
if (isNative) {
console.info("swapExactInFromVaaNative");
return swapExactInFromVaaNative(contractWithSigner, signedVaa);
console.info("evmSwapExactInFromVaaNative");
return evmSwapExactInFromVaaNative(contractWithSigner, signedVaa);
} else {
console.info("swapExactInFromVaaToken");
return swapExactInFromVaaToken(contractWithSigner, signedVaa);
console.info("evmSwapExactInFromVaaToken");
return evmSwapExactInFromVaaToken(contractWithSigner, signedVaa);
}
}
@ -371,7 +469,7 @@ async function swapExactOutFromVaa(
): Promise<TransactionReceipt> {
const swapContractParams = dstExecutionParams.crossChainSwap;
const swapContract = makeCrossChainSwapContract(
const swapContract = makeCrossChainSwapEvmContract(
dstProvider,
dstProtocol,
swapContractParams.address
@ -379,46 +477,42 @@ async function swapExactOutFromVaa(
const contractWithSigner = swapContract.connect(dstWallet);
if (isNative) {
console.info("swapExactOutFromVaaNative");
return swapExactOutFromVaaNative(contractWithSigner, signedVaa);
console.info("evmSwapExactOutFromVaaNative");
return evmSwapExactOutFromVaaNative(contractWithSigner, signedVaa);
} else {
console.info("swapExactOutFromVaaToken");
return swapExactOutFromVaaToken(contractWithSigner, signedVaa);
console.info("evmSwapExactOutFromVaaToken");
return evmSwapExactOutFromVaaToken(contractWithSigner, signedVaa);
}
}
interface CrossChainSwapTokens {
srcIn: UniEvmToken;
srcOut: UniEvmToken;
dstIn: UniEvmToken;
dstOut: UniEvmToken;
}
interface VaaSearchParams {
sequence: string;
emitterAddress: string;
}
export function makeProvider(tokenAddress: string) {
export function makeEvmProvider(tokenAddress: string) {
let url;
switch (tokenAddress) {
case WETH_TOKEN_INFO.address: {
const url = process.env.REACT_APP_GOERLI_PROVIDER;
if (!url) {
throw new Error("Could not find REACT_APP_GOERLI_PROVIDER");
}
return new ethers.providers.StaticJsonRpcProvider(url);
}
case WMATIC_TOKEN_INFO.address: {
const url = process.env.REACT_APP_MUMBAI_PROVIDER;
if (!url) {
throw new Error("Could not find REACT_APP_MUMBAI_PROVIDER");
}
return new ethers.providers.StaticJsonRpcProvider(url);
}
default: {
case ETH_TOKEN_INFO.address:
url = process.env.REACT_APP_GOERLI_PROVIDER;
if (!url) throw new Error("REACT_APP_GOERLI_PROVIDER not set");
break;
case MATIC_TOKEN_INFO.address:
url = process.env.REACT_APP_MUMBAI_PROVIDER;
if (!url) throw new Error("REACT_APP_MUMBAI_PROVIDER not set");
break;
case AVAX_TOKEN_INFO.address:
url = process.env.REACT_APP_FUJI_PROVIDER;
if (!url) throw new Error("REACT_APP_FUJI_PROVIDER not set");
break;
case BSC_TOKEN_INFO.address:
url = process.env.REACT_APP_BSC_PROVIDER;
if (!url) throw new Error("REACT_APP_BSC_PROVIDER not set");
break;
default:
throw Error("unrecognized token address");
}
}
return new ethers.providers.StaticJsonRpcProvider(url);
}
export class UniswapToUniswapExecutor {
@ -427,7 +521,6 @@ export class UniswapToUniswapExecutor {
cachedExactInParams: ExactInCrossParameters;
cachedExactOutParams: ExactOutCrossParameters;
quoteType: QuoteType;
tokens: CrossChainSwapTokens;
// swapping
isNative: boolean;
@ -440,8 +533,16 @@ export class UniswapToUniswapExecutor {
transportFactory: grpc.TransportFactory;
vaaSearchParams: VaaSearchParams;
vaaBytes: Uint8Array;
srcReceipt: TransactionReceipt;
dstReceipt: TransactionReceipt;
// receipts
srcEvmReceipt: TransactionReceipt;
dstEvmReceipt: TransactionReceipt;
srcTerraReceipt: any;
dstTerraReceipt: any;
constructor() {
this.quoter = new UniswapToUniswapQuoter();
}
async initialize(
tokenInAddress: string,
@ -450,20 +551,14 @@ export class UniswapToUniswapExecutor {
): Promise<void> {
this.isNative = isNative;
const srcProvider = makeProvider(tokenInAddress);
const dstProvider = makeProvider(tokenOutAddress);
this.quoter = new UniswapToUniswapQuoter(srcProvider, dstProvider);
await this.quoter.initialize();
await this.makeTokens(tokenInAddress, tokenOutAddress);
await this.quoter.initialize(tokenInAddress, tokenOutAddress);
// now that we have a chain id for each network, get contract info for each chain
this.srcExecutionParams = makeExecutionParameters(
this.quoter.srcNetwork.chainId
this.quoter.getSrcChainId()
);
this.dstExecutionParams = makeExecutionParameters(
this.quoter.dstNetwork.chainId
this.quoter.getDstChainId()
);
}
@ -483,6 +578,7 @@ export class UniswapToUniswapExecutor {
this.quoter.setDeadlines(deadline);
}
/*
async makeTokens(
tokenInAddress: string,
tokenOutAddress: string
@ -507,7 +603,7 @@ export class UniswapToUniswapExecutor {
getTokens(): CrossChainSwapTokens {
return this.tokens;
}
*/
async computeAndVerifySrcPoolAddress(): Promise<string> {
return this.quoter.computeAndVerifySrcPoolAddress();
}
@ -546,59 +642,97 @@ export class UniswapToUniswapExecutor {
return this.cachedExactOutParams;
}
getSrcProvider(): ethers.providers.Provider {
return this.quoter.srcProvider;
getSrcEvmProvider(): ethers.providers.Provider {
return this.quoter.getSrcEvmProvider();
}
getDstProvider(): ethers.providers.Provider {
return this.quoter.dstProvider;
getDstEvmProvider(): ethers.providers.Provider {
return this.quoter.getDstEvmProvider();
}
async approveAndSwapExactIn(
wallet: ethers.Signer
getTokenInAddress(): string {
return this.quoter.tokenInAddress;
}
getTokenOutAddress(): string {
return this.quoter.tokenOutAddress;
}
async evmApproveAndSwapExactIn(
srcWallet: ethers.Signer,
recipientAddress: string
): Promise<TransactionReceipt> {
return approveAndSwapExactIn(
this.getSrcProvider(),
wallet,
this.tokens.srcIn,
return evmApproveAndSwapExactIn(
this.getSrcEvmProvider(),
srcWallet,
this.getTokenInAddress(),
this.cachedExactInParams,
this.srcExecutionParams,
this.dstExecutionParams,
this.isNative
this.isNative,
recipientAddress
);
}
async approveAndSwapExactOut(
wallet: ethers.Signer
async evmApproveAndSwapExactOut(
srcWallet: ethers.Signer,
recipientAddress: string
): Promise<TransactionReceipt> {
return approveAndSwapExactOut(
this.getSrcProvider(),
wallet,
this.tokens.srcIn,
return evmApproveAndSwapExactOut(
this.getSrcEvmProvider(),
srcWallet,
this.getTokenInAddress(),
this.cachedExactOutParams,
this.srcExecutionParams,
this.dstExecutionParams,
this.isNative
this.isNative,
recipientAddress
);
}
async approveAndSwap(wallet: ethers.Signer): Promise<TransactionReceipt> {
srcIsUst(): boolean {
return (
this.quoter.tokenInAddress === UST_TOKEN_INFO.address &&
this.cachedExactInParams.src === undefined
);
}
async evmApproveAndSwap(
wallet: ethers.Signer,
recipientAddress: string
): Promise<TransactionReceipt> {
const quoteType = this.quoteType;
if (quoteType === QuoteType.ExactIn) {
this.srcReceipt = await this.approveAndSwapExactIn(wallet);
this.srcEvmReceipt = await this.evmApproveAndSwapExactIn(
wallet,
recipientAddress
);
} else if (quoteType === QuoteType.ExactOut) {
this.srcReceipt = await this.approveAndSwapExactOut(wallet);
this.srcEvmReceipt = await this.evmApproveAndSwapExactOut(
wallet,
recipientAddress
);
} else {
throw Error("no quote found");
}
this.fetchAndSetEmitterAndSequence();
return this.srcReceipt;
this.fetchAndSetEvmEmitterAndSequence();
return this.srcEvmReceipt;
}
fetchAndSetEmitterAndSequence(): void {
const receipt = this.srcReceipt;
// TODO
return;
}
fetchAndSetTerraEmitterAndSequence(): void {
// TODO
return;
}
fetchAndSetEvmEmitterAndSequence(): void {
const receipt = this.srcEvmReceipt;
if (receipt === undefined) {
throw Error("no swap receipt found");
}
@ -623,11 +757,15 @@ export class UniswapToUniswapExecutor {
const emitterAddress = vaaSearchParams.emitterAddress;
console.info(`sequence: ${sequence}, emitterAddress: ${emitterAddress}`);
// wait for VAA to be signed
const vaaResponse = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
this.srcExecutionParams.wormhole.chainId,
vaaSearchParams.emitterAddress,
vaaSearchParams.sequence
vaaSearchParams.sequence,
{
transport: this.transportFactory,
}
);
// grab vaaBytes
this.vaaBytes = vaaResponse.vaaBytes;
@ -636,22 +774,27 @@ export class UniswapToUniswapExecutor {
async fetchVaaAndSwap(wallet: ethers.Signer): Promise<TransactionReceipt> {
await this.fetchSignedVaaFromSwap();
// check if Terra transaction
// TODO: change return as something else (not evm TransactionReceipt)
const quoteType = this.quoteType;
if (quoteType === QuoteType.ExactIn) {
this.dstReceipt = await this.swapExactInFromVaa(wallet);
this.dstEvmReceipt = await this.evmSwapExactInFromVaa(wallet);
} else if (quoteType === QuoteType.ExactOut) {
this.dstReceipt = await this.swapExactOutFromVaa(wallet);
this.dstEvmReceipt = await this.evmSwapExactOutFromVaa(wallet);
} else {
throw Error("no quote found");
}
return this.dstReceipt;
return this.dstEvmReceipt;
}
async swapExactInFromVaa(wallet: ethers.Signer): Promise<TransactionReceipt> {
async evmSwapExactInFromVaa(
wallet: ethers.Signer
): Promise<TransactionReceipt> {
return swapExactInFromVaa(
this.getDstProvider(),
this.getDstEvmProvider(),
wallet,
this.dstExecutionParams,
this.cachedExactInParams.dst.protocol,
@ -660,11 +803,11 @@ export class UniswapToUniswapExecutor {
);
}
async swapExactOutFromVaa(
async evmSwapExactOutFromVaa(
wallet: ethers.Signer
): Promise<TransactionReceipt> {
return swapExactOutFromVaa(
this.getDstProvider(),
this.getDstEvmProvider(),
wallet,
this.dstExecutionParams,
this.cachedExactOutParams.dst.protocol,
@ -673,6 +816,10 @@ export class UniswapToUniswapExecutor {
);
}
setTransport(transportFactory: grpc.TransportFactory) {
this.transportFactory = transportFactory;
}
//getSwapResult(
// walletAddress: string,
// onSwapResult: (result: boolean) => void
@ -680,7 +827,7 @@ export class UniswapToUniswapExecutor {
// console.log(this.cachedExactInParams.dst.protocol);
// console.log(this.dstExecutionParams.crossChainSwap.address);
// const contract = makeCrossChainSwapContract(
// this.getDstProvider(),
// this.getDstEvmProvider(),
// this.quoteType === QuoteType.ExactIn
// ? this.cachedExactInParams.dst.protocol
// : this.cachedExactOutParams.dst.protocol,

View File

@ -1,56 +0,0 @@
import { ethers } from "ethers";
import { TransactionReceipt } from "@ethersproject/abstract-provider";
export const CROSSCHAINSWAP_GAS_PARAMETERS = {
gasLimit: "550000",
maxFeePerGas: "250000000000",
maxPriorityFeePerGas: "1690000000",
};
// exact in
//
export async function swapExactInFromVaaNative(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const tx = await swapContractWithSigner.recvAndSwapExactNativeIn(
signedVaa,
CROSSCHAINSWAP_GAS_PARAMETERS
);
return tx.wait();
}
export async function swapExactInFromVaaToken(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const tx = await swapContractWithSigner.recvAndSwapExactIn(
signedVaa,
CROSSCHAINSWAP_GAS_PARAMETERS
);
return tx.wait();
}
// exact out (TODO: add to util)
//
export async function swapExactOutFromVaaNative(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const tx = await swapContractWithSigner.recvAndSwapExactNativeOut(
signedVaa,
CROSSCHAINSWAP_GAS_PARAMETERS
);
return tx.wait();
}
export async function swapExactOutFromVaaToken(
swapContractWithSigner: ethers.Contract,
signedVaa: Uint8Array
): Promise<TransactionReceipt> {
const tx = await swapContractWithSigner.recvAndSwapExactOut(
signedVaa,
CROSSCHAINSWAP_GAS_PARAMETERS
);
return tx.wait();
}

1
react/src/utils/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.js

View File

@ -2,72 +2,117 @@ import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_POLYGON,
CHAIN_ID_TERRA,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
} from "@certusone/wormhole-sdk";
import ethIcon from "../icons/eth.svg";
import polygonIcon from "../icons/polygon.svg";
export const EVM_POLYGON_NETWORK_CHAIN_ID = 80001;
export const EVM_ETH_NETWORK_CHAIN_ID = 5;
export const EVM_AVAX_NETWORK_CHAIN_ID = 43113;
export const EVM_BSC_NETWORK_CHAIN_ID = 97;
export interface TokenInfo {
name: string;
address: string;
chainId: ChainId;
logo: string;
isNative: boolean;
evmChainId: number | undefined;
maxAmount: number;
ustPairedAddress: string | undefined;
}
export const MATIC_TOKEN_INFO: TokenInfo = {
name: "MATIC",
address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889", // used to compute quote
chainId: CHAIN_ID_POLYGON,
logo: polygonIcon,
isNative: true,
maxAmount: 0.1,
};
export const WMATIC_TOKEN_INFO: TokenInfo = {
name: "WMATIC",
address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889",
chainId: CHAIN_ID_POLYGON,
logo: polygonIcon,
isNative: false,
evmChainId: EVM_POLYGON_NETWORK_CHAIN_ID,
//logo: polygonIcon,
maxAmount: 0.1,
ustPairedAddress: "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c",
};
export const ETH_TOKEN_INFO: TokenInfo = {
name: "ETH",
address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", // used to compute quote
chainId: CHAIN_ID_ETH,
logo: ethIcon,
isNative: true,
maxAmount: 0.01,
};
export const WETH_TOKEN_INFO: TokenInfo = {
name: "WETH",
address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
chainId: CHAIN_ID_ETH,
logo: ethIcon,
isNative: false,
evmChainId: EVM_ETH_NETWORK_CHAIN_ID,
//logo: ethIcon,
maxAmount: 0.01,
ustPairedAddress: "0x36Ed51Afc79619b299b238898E72ce482600568a",
};
export const AVAX_TOKEN_INFO: TokenInfo = {
name: "AVAX",
address: "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3",
chainId: CHAIN_ID_AVAX,
evmChainId: EVM_AVAX_NETWORK_CHAIN_ID,
//logo: avaxIcon,
maxAmount: 0.01,
ustPairedAddress: "0xe09ed38e5cd1014444846f62376ac88c5232cde9",
};
export const BNB_TOKEN_INFO: TokenInfo = {
name: "BNB",
address: "0xae13d989dac2f0debff460ac112a837c89baa7cd",
chainId: CHAIN_ID_BSC,
evmChainId: EVM_BSC_NETWORK_CHAIN_ID,
//logo: bscIcon,
maxAmount: 0.01,
ustPairedAddress: "0x7b8eae1e85c8b189ee653d3f78733f4f788bb2c1",
};
export const UST_TOKEN_INFO: TokenInfo = {
name: "UST",
address: "uusd",
chainId: CHAIN_ID_TERRA,
evmChainId: undefined,
//logo: terraIcon,
maxAmount: 10.0,
ustPairedAddress: undefined,
};
export const TOKEN_INFOS = [
MATIC_TOKEN_INFO,
WMATIC_TOKEN_INFO,
ETH_TOKEN_INFO,
WETH_TOKEN_INFO,
AVAX_TOKEN_INFO,
BNB_TOKEN_INFO,
// TODO: support swaps from/to terra
// UST_TOKEN_INFO,
];
export const ETH_NETWORK_CHAIN_ID = 5;
export const getSupportedSwaps = (tokenInfo: TokenInfo) => {
return TOKEN_INFOS.filter((x) => x !== tokenInfo);
};
export const POLYGON_NETWORK_CHAIN_ID = 80001;
export const getEvmChainId = (chainId: ChainId): number | undefined => {
switch (chainId) {
case CHAIN_ID_ETH:
return EVM_ETH_NETWORK_CHAIN_ID;
case CHAIN_ID_POLYGON:
return EVM_POLYGON_NETWORK_CHAIN_ID;
case CHAIN_ID_AVAX:
return EVM_AVAX_NETWORK_CHAIN_ID;
case CHAIN_ID_BSC:
return EVM_BSC_NETWORK_CHAIN_ID;
default:
return undefined;
}
};
export const getEvmChainId = (chainId: ChainId) =>
chainId === CHAIN_ID_ETH
? ETH_NETWORK_CHAIN_ID
: chainId === CHAIN_ID_POLYGON
? POLYGON_NETWORK_CHAIN_ID
: undefined;
export const getChainName = (chainId: ChainId) => {
switch (chainId) {
case CHAIN_ID_ETH:
return "Ethereum";
case CHAIN_ID_POLYGON:
return "Polygon";
case CHAIN_ID_AVAX:
return "Avalanche";
case CHAIN_ID_BSC:
return "BSC";
default:
return "";
}
};
export const RELAYER_FEE_UST = "0.25";
@ -75,22 +120,37 @@ export const WORMHOLE_RPC_HOSTS = [
"https://wormhole-v2-testnet-api.certus.one",
];
// core bridge
export const CORE_BRIDGE_ADDRESS_ETHEREUM =
"0x706abc4E45D419950511e474C7B9Ed348A4a716c";
export const CORE_BRIDGE_ADDRESS_POLYGON =
"0x0CBE91CF822c73C2315FB05100C2F714765d5c20";
export const CORE_BRIDGE_ADDRESS_AVALANCHE =
"0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C";
export const CORE_BRIDGE_ADDRESS_BSC =
"0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D";
export const CORE_BRIDGE_ADDRESS_TERRA =
"terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v";
// token bridge
export const TOKEN_BRIDGE_ADDRESS_ETHEREUM =
"0xF890982f9310df57d00f659cf4fd87e65adEd8d7";
export const TOKEN_BRIDGE_ADDRESS_POLYGON =
"0x377D55a7928c046E18eEbb61977e714d2a76472a";
export const QUICKSWAP_FACTORY_ADDRESS =
"0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32";
export const TOKEN_BRIDGE_ADDRESS_BSC =
"0x9dcF9D205C9De35334D646BeE44b2D2859712A09";
export const UNISWAP_V3_FACTORY_ADDRESS =
"0x1F98431c8aD98523631AE4a59f267346ea31F984";
export const TOKEN_BRIDGE_ADDRESS_AVALANCHE =
"0x61E44E506Ca5659E6c0bba9b678586fA2d729756";
export const TOKEN_BRIDGE_ADDRESS_TERRA =
"terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a";
// gas
export const APPROVAL_GAS_LIMIT = "100000";

19
react/src/utils/math.ts Normal file
View File

@ -0,0 +1,19 @@
import { FixedNumber } from "ethers";
export function addFixedAmounts(
left: string,
right: string,
decimals: number
): string {
const sum = FixedNumber.from(left).addUnsafe(FixedNumber.from(right));
return sum.round(decimals).toString();
}
export function subtractFixedAmounts(
left: string,
right: string,
decimals: number
): string {
const diff = FixedNumber.from(left).subUnsafe(FixedNumber.from(right));
return diff.round(decimals).toString();
}

View File

@ -7,7 +7,12 @@ import {
TextField,
Typography,
} from "@material-ui/core";
import { ChainId, getSignedVAAWithRetry } from "@certusone/wormhole-sdk";
import {
ChainId,
CHAIN_ID_TERRA,
getSignedVAAWithRetry,
isEVMChain,
} from "@certusone/wormhole-sdk";
import { useCallback, useEffect, useState } from "react";
import ButtonWithLoader from "../components/ButtonWithLoader";
import EthereumSignerKey from "../components/EthereumSignerKey";
@ -16,11 +21,11 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import {
ETH_TOKEN_INFO,
getEvmChainId,
getSupportedSwaps,
MATIC_TOKEN_INFO,
RELAYER_FEE_UST,
TokenInfo,
TOKEN_INFOS,
WETH_TOKEN_INFO,
WMATIC_TOKEN_INFO,
WORMHOLE_RPC_HOSTS,
} from "../utils/consts";
import { COLORS } from "../muiTheme";
@ -37,6 +42,8 @@ import CircleLoader from "../components/CircleLoader";
import { ArrowForward, CheckCircleOutlineRounded } from "@material-ui/icons";
import SwapProgress from "../components/SwapProgress";
import Footer from "../components/Footer";
import TerraWalletKey from "../components/TerraWalletKey";
import useIsWalletReady from "../hooks/useIsWalletReady";
const useStyles = makeStyles((theme) => ({
bg: {
@ -135,7 +142,7 @@ const useStyles = makeStyles((theme) => ({
},
}));
const switchProviderNetwork = async (
const switchEvmProviderNetwork = async (
provider: Web3Provider,
chainId: ChainId
) => {
@ -152,6 +159,78 @@ const switchProviderNetwork = async (
}
};
const ConnectedWalletAddress = ({
chainId,
prefix,
}: {
chainId: ChainId;
prefix: string;
}) => {
const { walletAddress } = useIsWalletReady(chainId, false);
if (walletAddress) {
const is0x = walletAddress.startsWith("0x");
return (
<Typography variant="subtitle2">
{prefix} {walletAddress?.substring(0, is0x ? 6 : 3)}...
{walletAddress?.substring(walletAddress.length - (is0x ? 4 : 3))}
</Typography>
);
}
return null;
};
const SwapButton = ({
source,
target,
disabled,
showLoader,
onClick,
}: {
source: TokenInfo;
target: TokenInfo;
disabled: boolean;
showLoader: boolean;
onClick: () => void;
}) => {
const { isReady: isSourceWalletReady } = useIsWalletReady(
source.chainId,
!disabled
);
const { isReady: isTargetWalletReady } = useIsWalletReady(
target.chainId,
!isEVMChain(source.chainId)
);
if (!isSourceWalletReady) {
return isEVMChain(source.chainId) ? (
<EthereumSignerKey />
) : source.chainId === CHAIN_ID_TERRA ? (
<TerraWalletKey />
) : null;
}
if (
!isTargetWalletReady &&
(!isEVMChain(source.chainId) || !isEVMChain(target.chainId))
) {
return isEVMChain(target.chainId) ? (
<EthereumSignerKey />
) : source.chainId === CHAIN_ID_TERRA ? (
<TerraWalletKey />
) : null;
}
return (
<ButtonWithLoader
disabled={disabled}
showLoader={showLoader}
onClick={onClick}
>
Swap
</ButtonWithLoader>
);
};
export default function Home() {
const classes = useStyles();
const [sourceTokenInfo, setSourceTokenInfo] = useState(MATIC_TOKEN_INFO);
@ -167,10 +246,10 @@ export default function Home() {
const [isSwapping, setIsSwapping] = useState(false);
const [isComputingQuote, setIsComputingQuote] = useState(false);
const [hasQuote, setHasQuote] = useState(false);
const { provider, signer } = useEthereumProvider();
const { provider, signer, signerAddress, disconnect } = useEthereumProvider();
const { enqueueSnackbar } = useSnackbar();
const [isFirstSwapComplete, setIsFirstSwapComplete] = useState(false);
const [isSecondSwapComplete, setIsSecondSwapComplete] = useState(false);
const [isSourceSwapComplete, setIsSourceSwapComplete] = useState(false);
const [isTargetSwapComplete, setIsTargetSwapComplete] = useState(false);
const [sourceTxBlockNumber, setSourceTxBlockNumber] = useState<
number | undefined
>(undefined);
@ -193,7 +272,7 @@ export default function Home() {
await executor.initialize(
sourceTokenInfo.address,
targetTokenInfo.address,
sourceTokenInfo.isNative
true
);
await executor.computeAndVerifySrcPoolAddress().catch((e) => {
throw new Error("failed to verify source pool address");
@ -206,16 +285,8 @@ export default function Home() {
executor.setRelayerFee(RELAYER_FEE_UST);
const quote = await executor.computeQuoteExactIn(amountIn);
setExecutor(executor);
setAmountOut(
parseFloat(
executor.tokens.dstOut.formatAmount(quote.dst.minAmountOut)
).toFixed(8)
);
setAmountInUST(
parseFloat(
executor.tokens.dstIn.formatAmount(quote.dst.amountIn)
).toFixed(2)
);
setAmountOut(parseFloat(quote.minAmountOut).toFixed(8));
setAmountInUST(parseFloat(quote.ustAmountIn).toFixed(2));
setHasQuote(true);
}
} catch (e) {
@ -260,52 +331,65 @@ export default function Home() {
setDeadline(deadline);
}, []);
const handleSourceChange = useCallback((event) => {
// NOTE: only native-to-native or wrapped-to-wrapped swaps are currently supported
if (event.target.value === WMATIC_TOKEN_INFO.name) {
setSourceTokenInfo(WMATIC_TOKEN_INFO);
setTargetTokenInfo(WETH_TOKEN_INFO);
} else if (event.target.value === WETH_TOKEN_INFO.name) {
setSourceTokenInfo(WETH_TOKEN_INFO);
setTargetTokenInfo(WMATIC_TOKEN_INFO);
} else if (event.target.value === ETH_TOKEN_INFO.name) {
setSourceTokenInfo(ETH_TOKEN_INFO);
setTargetTokenInfo(MATIC_TOKEN_INFO);
} else {
setSourceTokenInfo(MATIC_TOKEN_INFO);
setTargetTokenInfo(ETH_TOKEN_INFO);
const handleSourceChange = useCallback(
(event) => {
const tokenInfo = TOKEN_INFOS.find((x) => x.name === event.target.value);
if (tokenInfo) {
const supportedSwaps = getSupportedSwaps(tokenInfo);
if (supportedSwaps) {
setSourceTokenInfo(tokenInfo);
if (!supportedSwaps.find((x) => x.name === targetTokenInfo.name)) {
setTargetTokenInfo(supportedSwaps[0]);
}
setAmountIn("");
setAmountOut("");
}
}
},
[targetTokenInfo]
);
const handleTargetChange = useCallback((event) => {
const tokenInfo = TOKEN_INFOS.find((x) => x.name === event.target.value);
if (tokenInfo) {
setTargetTokenInfo(tokenInfo);
setAmountOut("");
}
setAmountIn("");
setAmountOut("");
}, []);
const reset = useCallback(() => {
setIsSwapping(false);
setHasQuote(false);
setIsFirstSwapComplete(false);
setIsSecondSwapComplete(false);
setIsSourceSwapComplete(false);
setHasSignedVAA(false);
setIsTargetSwapComplete(false);
setAmountIn("");
setAmountOut("");
setSourceTxBlockNumber(undefined);
setRelayerTimeoutString("");
}, []);
disconnect();
}, [disconnect]);
const handleSwapClick = useCallback(async () => {
if (provider && signer && executor) {
if (provider && signer && signerAddress && executor) {
try {
setIsSwapping(true);
setIsFirstSwapComplete(false);
setIsSourceSwapComplete(false);
setHasSignedVAA(false);
setIsSecondSwapComplete(false);
setIsTargetSwapComplete(false);
setRelayerTimeoutString("");
await switchProviderNetwork(provider, sourceTokenInfo.chainId);
await switchEvmProviderNetwork(provider, sourceTokenInfo.chainId);
console.log(signerAddress);
const sourceReceipt = await executor.approveAndSwap(signer);
const sourceReceipt = await executor.evmApproveAndSwap(
signer,
signerAddress
);
console.info(
"firstSwapTransactionHash:",
sourceReceipt.transactionHash
);
setIsFirstSwapComplete(true);
setIsSourceSwapComplete(true);
setSourceTxBlockNumber(sourceReceipt.blockNumber);
// Wait for the guardian network to reach consensus and emit the signedVAA
@ -319,7 +403,9 @@ export default function Home() {
// Check if the signedVAA has redeemed by the relayer
const isCompleted = await getIsTransferCompletedEvmWithRetry(
executor.dstExecutionParams.wormhole.tokenBridgeAddress,
executor.quoter.dstProvider,
// TODO: fix typescript error
// @ts-ignore
executor.quoter.getDstEvmProvider(),
vaaBytes,
// retry for two minutes
3000,
@ -330,14 +416,14 @@ export default function Home() {
setRelayerTimeoutString(
"Timed out waiting for relayer to complete swap. You'll need to complete it yourself."
);
await switchProviderNetwork(provider, targetTokenInfo.chainId);
await switchEvmProviderNetwork(provider, targetTokenInfo.chainId);
const targetReceipt = await executor.fetchVaaAndSwap(signer);
console.info(
"secondSwapTransactionHash:",
targetReceipt.transactionHash
);
}
setIsSecondSwapComplete(true);
setIsTargetSwapComplete(true);
} catch (e: any) {
reset();
console.error(e);
@ -349,6 +435,7 @@ export default function Home() {
}, [
provider,
signer,
signerAddress,
executor,
enqueueSnackbar,
sourceTokenInfo,
@ -357,19 +444,18 @@ export default function Home() {
]);
const readyToSwap = provider && signer && hasQuote;
const disableSelect = isSwapping || isComputingQuote;
return (
<div className={classes.bg}>
<Container className={classes.centeredContainer} maxWidth="sm">
<div className={classes.titleBar}></div>
<Typography variant="h4" color="textSecondary">
Wormhole NativeSwap Demo
</Typography>
<Typography variant="h4">Wormhole NativeSwap Demo</Typography>
<div className={classes.spacer} />
<Paper className={classes.mainPaper}>
<Collapse in={!isFirstSwapComplete}>
<Collapse in={!isSourceSwapComplete}>
<Settings
disabled={isSwapping || isComputingQuote}
disabled={disableSelect}
slippage={slippage}
deadline={deadline}
onSlippageChange={handleSlippageChange}
@ -379,13 +465,13 @@ export default function Home() {
tokens={TOKEN_INFOS}
value={sourceTokenInfo.name}
onChange={handleSourceChange}
disabled={isSwapping || isComputingQuote}
disabled={disableSelect}
></TokenSelect>
<Typography variant="subtitle1">Send</Typography>
<TextField
type="number"
value={amountIn}
disabled={isSwapping || isComputingQuote}
disabled={disableSelect}
InputProps={{ disableUnderline: true }}
className={classes.numberField}
onChange={handleAmountChange}
@ -397,12 +483,16 @@ export default function Home() {
color="error"
>{`The max input amount is ${sourceTokenInfo.maxAmount} ${sourceTokenInfo.name}`}</Typography>
) : null}
<ConnectedWalletAddress
chainId={sourceTokenInfo.chainId}
prefix="From:"
/>
<div className={classes.spacer} />
<TokenSelect
tokens={TOKEN_INFOS}
tokens={getSupportedSwaps(sourceTokenInfo)}
value={targetTokenInfo.name}
onChange={() => {}}
disabled={true}
onChange={handleTargetChange}
disabled={disableSelect}
></TokenSelect>
<Typography variant="subtitle1">Receive (estimated)</Typography>
<TextField
@ -414,10 +504,21 @@ export default function Home() {
inputProps={{ readOnly: true }}
placeholder="0.0"
></TextField>
<ConnectedWalletAddress
chainId={
isEVMChain(sourceTokenInfo.chainId) &&
isEVMChain(targetTokenInfo.chainId)
? sourceTokenInfo.chainId
: targetTokenInfo.chainId
}
prefix="To:"
/>
<div className={classes.spacer} />
<Typography variant="subtitle2">{`Slippage tolerance: ${slippage}%`}</Typography>
<Typography variant="subtitle2">{`Relayer fee: ${RELAYER_FEE_UST} UST`}</Typography>
{!isSwapping && <EthereumSignerKey />}
<ButtonWithLoader
<SwapButton
source={sourceTokenInfo}
target={targetTokenInfo}
disabled={
!readyToSwap ||
isSwapping ||
@ -425,11 +526,9 @@ export default function Home() {
}
showLoader={isSwapping}
onClick={handleSwapClick}
>
Swap
</ButtonWithLoader>
/>
</Collapse>
<Collapse in={isFirstSwapComplete && !isSecondSwapComplete}>
<Collapse in={isSourceSwapComplete && !isTargetSwapComplete}>
<div className={classes.loaderHolder}>
<CircleLoader />
<div className={classes.spacer} />
@ -438,14 +537,14 @@ export default function Home() {
</Typography>
</div>
</Collapse>
<Collapse in={isSecondSwapComplete}>
<Collapse in={isTargetSwapComplete}>
<div className={classes.loaderHolder}>
<CheckCircleOutlineRounded
className={classes.successIcon}
fontSize={"inherit"}
/>
<Typography>Swap completed!</Typography>
<ButtonWithLoader onClick={() => reset()}>
<ButtonWithLoader onClick={reset}>
Swap more tokens!
</ButtonWithLoader>
</div>
@ -460,33 +559,48 @@ export default function Home() {
{`${amountOut} ${targetTokenInfo.name}`}
</Typography>
)}
{isFirstSwapComplete &&
!isSecondSwapComplete &&
{isSourceSwapComplete &&
!isTargetSwapComplete &&
!relayerTimeoutString && (
<SwapProgress
chainId={sourceTokenInfo.chainId}
txBlockNumber={sourceTxBlockNumber}
step={!hasSignedVAA ? 1 : !isSecondSwapComplete ? 2 : 3}
/>
<>
<SwapProgress
chainId={sourceTokenInfo.chainId}
txBlockNumber={sourceTxBlockNumber}
hasSignedVAA={hasSignedVAA}
isTargetSwapComplete={isTargetSwapComplete}
/>
<div className={classes.spacer} />
</>
)}
{relayerTimeoutString && (
<Typography variant="subtitle1">{relayerTimeoutString}</Typography>
)}
<div className={classes.spacer} />
<Typography variant="subtitle2" color="error">
WARNING: this is a Testnet release only
WARNING: this is a testnet release only
</Typography>
</Paper>
<div className={classes.spacer} />
<Footer />
<Link href="https://goerli-faucet.slock.it/" style={{ margin: "5px" }}>
Goerli faucet
Goerli Faucet
</Link>
<Link
href="https://faucet.polygon.technology/"
style={{ margin: "5px" }}
>
Mumbai faucet
Mumbai Faucet
</Link>
<Link
href="https://faucet.avax-test.network/"
style={{ margin: "5px" }}
>
Fuji Faucet
</Link>
<Link
href="https://testnet.binance.org/faucet-smart/"
style={{ margin: "5px" }}
>
BSC Faucet
</Link>
<Link
href="https://github.com/certusone/wormhole-nativeswap-example/"

View File

@ -1,18 +1,44 @@
# TestNet
SPY_SERVICE_HOST=localhost:7073
SPY_SERVICE_FILTERS=[{"chain_id":2,"emitter_address":"0x000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d7"},{"chain_id":5,"emitter_address":"0x000000000000000000000000377d55a7928c046e18eebb61977e714d2a76472a"}]
# You can omit the following to get signed VAAs from every emitter.
#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"}]
ETH_PROVIDER=https://goerli.infura.io/v3/your_project_id
POLYGON_PROVIDER=https://polygon-mumbai.infura.io/v3/your_project_id
EVM_CHAINS=ETH,BSC,POLYGON,AVAX
ETH_PROVIDER=https://goerli.infura.io/v3/YOUR_PROJECT_ID
ETH_TOKEN_BRIDGE_ADDRESS=0xF890982f9310df57d00f659cf4fd87e65adEd8d7
POLYGON_TOKEN_BRIDGE_ADDRESS=0x377D55a7928c046E18eEbb61977e714d2a76472a
ETH_CHAIN_ID=2
ETH_ABI=V3
# If these are defined, use them instead of the ones defined in the code.
ETH_CONTRACT_ADDRESS=0x61D26732B190bdc5771e2a2b3ADB295e1b5A88BF
POLYGON_CONTRACT_ADDRESS=0xc5Ba16A974a0c0E7935285d99F496Ee65eDFB8BA
BSC_PROVIDER="https://data-seed-prebsc-1-s1.binance.org:8545"
BSC_TOKEN_BRIDGE_ADDRESS=0x9dcF9D205C9De35334D646BeE44b2D2859712A09
BSC_CHAIN_ID=4
BSC_ABI=V2
POLYGON_PROVIDER=https://polygon-mumbai.infura.io/v3/YOUR_PROJECT_ID
POLYGON_TOKEN_BRIDGE_ADDRESS=0x377D55a7928c046E18eEbb61977e714d2a76472a
POLYGON_CHAIN_ID=5
POLYGON_ABI=V2
AVAX_PROVIDER="https://api.avax-test.network/ext/bc/C/rpc"
AVAX_TOKEN_BRIDGE_ADDRESS=0x61E44E506Ca5659E6c0bba9b678586fA2d729756
AVAX_CHAIN_ID=6
AVAX_ABI=V2
WALLET_PRIVATE_KEY=your_key_here
TERRA_PROVIDER=https://bombay-lcd.terra.dev
TERRA_CHAIN_ID=bombay-12
TERRA_NAME=testnet
TERRA_WALLET_PRIVATE_KEY=your_key_here
TERRA_GAS_PRICE_URL=https://bombay-fcd.terra.dev/v1/txs/gas_prices
TERRA_TOKEN_BRIDGE_ADDRESS=terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a
#LOG_DIR=/var/logs
LOG_LEVEL=debug
ETH_CONTRACT_ADDRESS=0x9e7Cae3a46ED297b0a05FCEeb41160fC5218E14f
BSC_CONTRACT_ADDRESS=0x0DC183c2eFAA5e1749B85f13621F5cC6aCcDa786
POLYGON_CONTRACT_ADDRESS=0x72F2F646dC979a9fA8aA685B8a47b7afe2fE0516
TERRA_CONTRACT_ADDRESS=terra163shc8unyqrndgcldaj2q9kgnqs82v0kgkhynf
AVAX_CONTRACT_ADDRESS=0x52D8A50AF35b0760335F29a4D6aaF0604B7D7484

View File

@ -14,24 +14,25 @@
"@types/jest": "^27.0.2",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"async-mutex": "^0.3.2",
"axios": "^0.24.0",
"condition-variable": "^1.0.0",
"esm": "^3.2.25",
"ethers": "^5.5.3",
"jest": "^27.3.1",
"prettier": "^2.3.2",
"ts-jest": "^27.0.7",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5",
"winston": "^3.3.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@certusone/wormhole-sdk": "^0.1.4",
"@certusone/wormhole-spydk": "^0.0.1",
"async-mutex": "^0.3.2",
"condition-variable": "^1.0.0",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^10.0.0"
"dotenv": "^10.0.0",
"ethers": "^5.5.3",
"@terra-money/terra.js": "^3.0.4",
"winston": "^3.3.3"
}
}

478
swap_relayer/src/evm.ts Normal file
View File

@ -0,0 +1,478 @@
import {
getIsTransferCompletedEth,
hexToUint8Array,
} from "@certusone/wormhole-sdk";
import { ethers } from "ethers";
import { abi as SWAP_CONTRACT_V2_ABI } from "../../react/src/abi/contracts/CrossChainSwapV2.json";
import { abi as SWAP_CONTRACT_V3_ABI } from "../../react/src/abi/contracts/CrossChainSwapV3.json";
import * as swap from "../../react/src/swapper/helpers";
import { logger, OurEnvironment, Type3Payload } from "./index";
export type EvmEnvironment = {
name: string;
chain_id: number;
provider_url: string;
contract_address: string;
token_bridge_address: string;
wallet_private_key: string;
abi_version: string;
};
type EvmContractData = {
chain_id: number;
name: string;
contractAddress: string;
tokenBridgeAddress: string;
contract: ethers.Contract;
provider: ethers.providers.StaticJsonRpcProvider;
wallet: ethers.Wallet;
contractWithSigner: ethers.Contract;
};
let evmContractData = new Map<number, EvmContractData>();
export function loadEvmConfig(): EvmEnvironment[] {
let evm_configs: EvmEnvironment[] = [];
let evms = process.env.EVM_CHAINS.split(",");
for (const evm of evms) {
let key_chain_id: string = evm + "_CHAIN_ID";
let val_chain_id: string = eval("process.env." + key_chain_id);
if (!val_chain_id) {
logger.error("Missing environment variable " + key_chain_id);
return undefined;
}
let key_provider: string = evm + "_PROVIDER";
let val_provider: string = eval("process.env." + key_provider);
if (!val_provider) {
logger.error("Missing environment variable " + key_provider);
return undefined;
}
let key_contract_address: string = evm + "_CONTRACT_ADDRESS";
let val_contract_address: string = eval(
"process.env." + key_contract_address
);
if (!val_contract_address) {
logger.error("Missing environment variable " + key_contract_address);
return undefined;
}
let key_token_bridge_address: string = evm + "_TOKEN_BRIDGE_ADDRESS";
let val_token_bridge_address: string = eval(
"process.env." + key_token_bridge_address
);
if (!val_token_bridge_address) {
logger.error("Missing environment variable " + key_token_bridge_address);
return undefined;
}
let key_wallet_private_key: string = evm + "_WALLET_PRIVATE_KEY";
let val_wallet_private_key: string = eval(
"process.env." + key_wallet_private_key
);
if (!val_wallet_private_key)
val_wallet_private_key = process.env.WALLET_PRIVATE_KEY;
if (!val_wallet_private_key) {
logger.error(
"Missing environment variable " +
key_wallet_private_key +
" or WALLET_PRIVATE_KEY"
);
return undefined;
}
let key_abi_version: string = evm + "_ABI";
let val_abi_version: string = eval("process.env." + key_abi_version);
if (!val_abi_version) {
logger.error("Missing environment variable " + key_abi_version);
return undefined;
}
if (val_abi_version !== "V2" && val_abi_version !== "V3") {
logger.error(
"Invalid value of environment variable " +
key_abi_version +
", is [" +
val_abi_version +
"], must be either V2 or V3"
);
return undefined;
}
evm_configs.push({
name: evm,
chain_id: parseInt(val_chain_id),
provider_url: val_provider,
contract_address: val_contract_address,
token_bridge_address: val_token_bridge_address,
wallet_private_key: val_wallet_private_key,
abi_version: val_abi_version,
});
}
return evm_configs;
}
export function makeEvmContractData(envs: EvmEnvironment[]) {
if (!envs) return;
for (const evm of envs) {
evmContractData.set(evm.chain_id, makeContractDataForEvm(evm));
}
}
function makeContractDataForEvm(env: EvmEnvironment): EvmContractData {
let contractAddress: string = env.contract_address.toLowerCase();
if (contractAddress.search("0x") === 0) {
contractAddress = contractAddress.substring(2);
}
logger.info(
"Connecting to " +
env.name +
": chain_id: " +
env.chain_id +
", contract address: [" +
contractAddress +
"], node: [" +
env.provider_url +
"], token bridge address: [" +
env.token_bridge_address +
"], abi version: [" +
env.abi_version +
"]"
);
const provider = new ethers.providers.StaticJsonRpcProvider(env.provider_url);
const contract = new ethers.Contract(
contractAddress,
env.abi_version == "V2" ? SWAP_CONTRACT_V2_ABI : SWAP_CONTRACT_V3_ABI,
provider
);
const wallet = new ethers.Wallet(env.wallet_private_key, provider);
const contractWithSigner = contract.connect(wallet);
return {
chain_id: env.chain_id,
name: env.name,
contractAddress: contractAddress,
tokenBridgeAddress: env.token_bridge_address,
contract: contract,
provider: provider,
wallet: wallet,
contractWithSigner: contractWithSigner,
};
}
export function isEvmContract(
contractAddress: string,
chain_id: number
): boolean {
let ecd = evmContractData.get(chain_id);
return ecd && ecd.contractAddress === contractAddress;
}
/*
// GOERLI_PROVIDER = Ethereum
// MUMBAI_PROVIDER = Polygon
if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM) {
// Use one of the V3 swap methods.
} else if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON) {
// Use one of the V2 swap methods.
} else {
// Error
}
if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 1) {
// swapExactInFromVaaNative
} else if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 2) {
// swapExactInFromVaaToken
} else if (
t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 1) {
// swapExactOutFromVaaNative
} else if (t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 2) {
// swapExactOutFromVaaToken
} else {
// error
}
*/
/*
// GOERLI_PROVIDER = Ethereum
// MUMBAI_PROVIDER = Polygon
if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM) {
// Use one of the V3 swap methods.
} else if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON) {
// Use one of the V2 swap methods.
} else {
// Error
}
if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 1) {
// swapExactInFromVaaNative
} else if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 2) {
// swapExactInFromVaaToken
} else if (
t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 1) {
// swapExactOutFromVaaNative
} else if (t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 2) {
// swapExactOutFromVaaToken
} else {
// error
}
*/
export async function relayVaaToEvm(vaaBytes: string, t3Payload: Type3Payload) {
let ecd = evmContractData.get(t3Payload.targetChainId);
if (!ecd) {
logger.error(
"relayVaaToEvm: chain id " + t3Payload.targetChainId + " does not exist!"
);
}
let exactIn: boolean = false;
let error: boolean = false;
if (t3Payload.swapFunctionType === 1) {
exactIn = true;
} else if (t3Payload.swapFunctionType !== 2) {
error = true;
logger.error(
"relayVaaTo" +
ecd.name +
": unsupported swapFunctionType: [" +
t3Payload.swapFunctionType +
"]"
);
}
let native: boolean = false;
if (t3Payload.swapCurrencyType === 1) {
native = true;
} else if (t3Payload.swapCurrencyType !== 2) {
error = true;
logger.error(
"relayVaaTo" +
ecd.name +
": unsupported swapCurrencyType: [" +
t3Payload.swapCurrencyType +
"]"
);
}
if (error) return;
logger.debug(
"relayVaaTo" +
ecd.name +
": chain_id: " +
ecd.chain_id +
", contractAddress: [" +
t3Payload.contractAddress +
"]"
);
const signedVaaArray = hexToUint8Array(vaaBytes);
await relayVaaToEvmChain(t3Payload, ecd, signedVaaArray, exactIn, native);
}
async function relayVaaToEvmChain(
t3Payload: Type3Payload,
tcd: EvmContractData,
signedVaaArray: Uint8Array,
exactIn: boolean,
native: boolean
) {
logger.debug(
"relayVaaTo" +
tcd.name +
": checking if already redeemed on " +
tcd.name +
" using tokenBridgeAddress [" +
tcd.tokenBridgeAddress +
"]"
);
if (await isRedeemedOnEvm(tcd, signedVaaArray)) {
logger.info(
"relayVaaTo" +
tcd.name +
": srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": completed: already transferred"
);
return;
}
logger.info(
"relayVaaTo" +
tcd.name +
": srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": submitting redeem request"
);
try {
let receipt: any = null;
if (exactIn) {
if (native) {
logger.debug("relayVaaTo: calling evmSwapExactInFromVaaNative()");
receipt = await swap.evmSwapExactInFromVaaNative(
tcd.contractWithSigner,
signedVaaArray
);
} else {
logger.debug("relayVaaTo: calling evmSwapExactInFromVaaToken()");
receipt = await swap.evmSwapExactInFromVaaToken(
tcd.contractWithSigner,
signedVaaArray
);
}
} else {
if (native) {
logger.debug("relayVaaTo: calling evmSwapExactOutFromVaaNative()");
receipt = await swap.evmSwapExactOutFromVaaNative(
tcd.contractWithSigner,
signedVaaArray
);
} else {
logger.debug("relayVaaTo: calling evmSwapExactOutFromVaaToken()");
receipt = await swap.evmSwapExactOutFromVaaToken(
tcd.contractWithSigner,
signedVaaArray
);
}
}
logger.info(
"relayVaaTo" +
tcd.name +
": srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": completed: success, txHash: " +
receipt.transactionHash
);
} catch (e: any) {
if (await isRedeemedOnEvm(tcd, signedVaaArray)) {
logger.info(
"relayVaaTo" +
tcd.name +
": srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": completed: relay failed because the vaa has already been redeemed"
);
return;
}
logger.error(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": transaction failed: %o",
e
);
}
if (await isRedeemedOnEvm(tcd, signedVaaArray)) {
logger.info(
"relayVaaTo" +
tcd.name +
": srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": redeem confirmed"
);
} else {
logger.error(
"relayVaaTo" +
tcd.name +
": srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": completed: failed to confirm redeem!"
);
}
}
async function isRedeemedOnEvm(
tcd: EvmContractData,
signedVaaArray: Uint8Array
): Promise<boolean> {
let redeemed: boolean = false;
try {
redeemed = await getIsTransferCompletedEth(
tcd.tokenBridgeAddress,
tcd.provider,
signedVaaArray
);
} catch (e) {
logger.error(
"relayVaaTo" +
tcd.name +
": failed to check if transfer is already complete, will attempt the transfer, e: %o",
e
);
}
return redeemed;
}

View File

@ -10,7 +10,6 @@ import {
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
getIsTransferCompletedEth,
} from "@certusone/wormhole-sdk";
import {
@ -25,14 +24,23 @@ import {
import { ethers } from "ethers";
import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM } from "../../react/src/addresses/goerli";
import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON } from "../../react/src/addresses/mumbai";
import { abi as SWAP_CONTRACT_V2_ABI } from "../../react/src/abi/contracts/CrossChainSwapV2.json";
import { abi as SWAP_CONTRACT_V3_ABI } from "../../react/src/abi/contracts/CrossChainSwapV3.json";
import {
EvmEnvironment,
isEvmContract,
loadEvmConfig,
makeEvmContractData,
relayVaaToEvm,
} from "./evm";
import * as swap from "../../react/src/swapper/util";
import {
isTerraContract,
loadTerraConfig,
makeTerraContractData,
relayVaaToTerra,
TerraEnvironment,
} from "./terra";
let logger: any;
export let logger: any;
let configFile: string = ".env";
if (process.env.SWAP_RELAY_CONFIG) {
@ -44,29 +52,17 @@ require("dotenv").config({ path: configFile });
initLogger();
type OurEnvironment = {
export type OurEnvironment = {
spy_host: string;
spy_filters: string;
eth_provider_url: string;
polygon_provider_url: string;
wallet_private_key: string;
eth_contract_address: string;
polygon_contract_address: string;
eth_token_bridge_address: string;
polygon_token_bridge_address: string;
evm_configs: EvmEnvironment[];
terra_config: TerraEnvironment;
};
type TargetContractData = {
name: string;
contractAddress: string;
tokenBridgeAddress: string;
contract: ethers.Contract;
provider: ethers.providers.StaticJsonRpcProvider;
wallet: ethers.Wallet;
contractWithSigner: ethers.Contract;
};
type Type3Payload = {
export type Type3Payload = {
sourceChainId: number;
targetChainId: number;
contractAddress: string;
relayerFee: ethers.BigNumber;
swapFunctionType: number;
@ -85,8 +81,6 @@ let success: boolean;
let env: OurEnvironment;
[success, env] = loadConfig();
let ethContractData: TargetContractData = null;
let polygonContractData: TargetContractData = null;
let seqMap = new Map<string, number>();
const mutex = new Mutex();
@ -95,14 +89,14 @@ let pendingQueue = new Array<PendingEvent>();
if (success) {
logger.info(
"swap_relay starting up, will listen for signed VAAs from [" +
"swap_relayer starting up, will listen for signed VAAs from [" +
env.spy_host +
"]"
);
try {
ethContractData = makeEthContractData();
polygonContractData = makePolygonContractData();
makeEvmContractData(env.evm_configs);
makeTerraContractData(env.terra_config);
} catch (e: any) {
logger.error("failed to connect to target contracts: %o", e);
success = false;
@ -119,39 +113,23 @@ function loadConfig(): [boolean, OurEnvironment] {
logger.error("Missing environment variable SPY_SERVICE_HOST");
return [false, undefined];
}
if (!process.env.ETH_PROVIDER) {
logger.error("Missing environment variable ETH_PROVIDER");
return [false, undefined];
}
if (!process.env.POLYGON_PROVIDER) {
logger.error("Missing environment variable POLYGON_PROVIDER");
return [false, undefined];
}
if (!process.env.WALLET_PRIVATE_KEY) {
logger.error("Missing environment variable WALLET_PRIVATE_KEY");
return [false, undefined];
}
if (!process.env.ETH_TOKEN_BRIDGE_ADDRESS) {
logger.error("Missing environment variable ETH_TOKEN_BRIDGE_ADDRESS");
return [false, undefined];
}
if (!process.env.POLYGON_TOKEN_BRIDGE_ADDRESS) {
logger.error("Missing environment variable POLYGON_TOKEN_BRIDGE_ADDRESS");
return [false, undefined];
let evm_configs: EvmEnvironment[] = null;
if (process.env.EVM_CHAINS) {
evm_configs = loadEvmConfig();
if (!evm_configs) return [false, undefined];
}
let terra_config = loadTerraConfig();
if (!terra_config) return [false, undefined];
return [
true,
{
spy_host: process.env.SPY_SERVICE_HOST,
spy_filters: process.env.SPY_SERVICE_FILTERS,
eth_provider_url: process.env.ETH_PROVIDER,
polygon_provider_url: process.env.POLYGON_PROVIDER,
wallet_private_key: process.env.WALLET_PRIVATE_KEY,
eth_contract_address: process.env.ETH_CONTRACT_ADDRESS,
polygon_contract_address: process.env.POLYGON_CONTRACT_ADDRESS,
eth_token_bridge_address: process.env.ETH_TOKEN_BRIDGE_ADDRESS,
polygon_token_bridge_address: process.env.POLYGON_TOKEN_BRIDGE_ADDRESS,
evm_configs: evm_configs,
terra_config: terra_config,
},
];
}
@ -200,7 +178,7 @@ async function spy_listen() {
processVaa(vaaBytes);
});
logger.info("swap_relay waiting for transfer signed VAAs");
logger.info("swap_relayer waiting for transfer signed VAAs");
})();
}
@ -221,10 +199,10 @@ async function encodeEmitterAddress(
async function processVaa(vaaBytes: string) {
let receiveTime = new Date();
logger.debug("processVaa");
// logger.debug("processVaa");
const { parse_vaa } = await importCoreWasm();
const parsedVAA = parse_vaa(hexToUint8Array(vaaBytes));
logger.debug("processVaa: parsedVAA: %o", parsedVAA);
// logger.debug("processVaa: parsedVAA: %o", parsedVAA);
let emitter_address: string = uint8ArrayToHex(parsedVAA.emitter_address);
@ -245,11 +223,16 @@ async function processVaa(vaaBytes: string) {
seqMap.set(seqNumKey, parsedVAA.sequence);
let payload_type: number = parsedVAA.payload[0];
let t3Payload: Type3Payload = null;
try {
t3Payload = decodeSignedVAAPayloadType3(parsedVAA, parsedVAA.emitter_chain);
} catch (e) {
logger.error("failed to parse type 3 vaa: %o", e);
return;
}
let t3Payload = decodeSignedVAAPayloadType3(parsedVAA);
if (t3Payload) {
if (isOurContract(t3Payload.contractAddress)) {
if (isOurContract(t3Payload.contractAddress, t3Payload.targetChainId)) {
logger.info(
"enqueuing type 3 vaa: emitter: [" +
parsedVAA.emitter_chain +
@ -257,7 +240,9 @@ async function processVaa(vaaBytes: string) {
emitter_address +
"], seqNum: " +
parsedVAA.sequence +
", contractAddress: [" +
", target: [" +
t3Payload.targetChainId +
":" +
t3Payload.contractAddress +
"], relayerFee: [" +
t3Payload.relayerFee +
@ -277,7 +262,9 @@ async function processVaa(vaaBytes: string) {
emitter_address +
"], seqNum: " +
parsedVAA.sequence +
", contractAddress: [" +
", target: [" +
t3Payload.targetChainId +
":" +
t3Payload.contractAddress +
"], relayerFee: [" +
t3Payload.relayerFee +
@ -288,39 +275,81 @@ async function processVaa(vaaBytes: string) {
"]"
);
}
} else {
logger.debug(
"dropping vaa: emitter: [" +
parsedVAA.emitter_chain +
":" +
emitter_address +
"], seqNum: " +
parsedVAA.sequence +
" payloadType: " +
payload_type
);
// } else {
// logger.debug(
// "dropping vaa: emitter: [" +
// parsedVAA.emitter_chain +
// ":" +
// emitter_address +
// "], seqNum: " +
// parsedVAA.sequence +
// " payloadType: " +
// parsedVAA.payload[0]
// );
}
}
function decodeSignedVAAPayloadType3(parsedVAA: any): Type3Payload {
function decodeSignedVAAPayloadType3(
parsedVAA: any,
sourceChainId: number
): Type3Payload {
const payload = Buffer.from(new Uint8Array(parsedVAA.payload));
const version = payload.readUInt8(0);
if (payload[0] !== 3) return undefined;
if (version !== 3) {
logger.info("decodeSignedVAAPayloadType3: length: " + payload.length);
if (payload.length < 101) {
logger.error(
"decodeSignedVAAPayloadType3: dropping type 3 vaa because the payload is too short to determine the target chain id, length: " +
payload.length
);
return undefined;
}
const targetChainId = payload.readUInt16BE(99);
logger.info("decodeSignedVAAPayloadType3: target ChainId: " + targetChainId);
let contractAddress: string = "";
let swapFunctionType: number = 0;
let swapCurrencyType: number = 0;
if (targetChainId === 3) {
logger.info(
"decodeSignedVAAPayloadType3: terraContractAddr: [" +
payload.slice(67, 67 + 32).toString("hex") +
"]"
);
contractAddress = payload.slice(67, 67 + 32).toString("hex");
} else {
if (payload.length < 262) {
logger.error(
"decodeSignedVAAPayloadType3: dropping type 3 vaa because the payload is too short to extract the contract fields, length: " +
payload.length +
", target chain id: " +
targetChainId
);
return undefined;
}
contractAddress = payload.slice(79, 79 + 20).toString("hex");
swapFunctionType = payload.readUInt8(272);
swapCurrencyType = payload.readUInt8(273);
}
return {
contractAddress: payload.slice(79, 79 + 20).toString("hex"),
sourceChainId: sourceChainId,
targetChainId: targetChainId,
contractAddress: contractAddress,
relayerFee: ethers.BigNumber.from(payload.slice(101, 101 + 32)),
swapFunctionType: payload.readUInt8(260),
swapCurrencyType: payload.readUInt8(261),
swapFunctionType: swapFunctionType,
swapCurrencyType: swapCurrencyType,
};
}
function isOurContract(contractAddress: string): boolean {
function isOurContract(contractAddress: string, chainId: number): boolean {
return (
contractAddress === ethContractData.contractAddress ||
contractAddress === polygonContractData.contractAddress
isEvmContract(contractAddress, chainId) ||
isTerraContract(contractAddress, chainId)
);
}
@ -408,379 +437,13 @@ async function callBack(err: any, result: any) {
// logger.debug("leaving callback.");
}
// Ethereum (Goerli) set up
function makeEthContractData(): TargetContractData {
let overridden: boolean = false;
let contractAddress: string = CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM;
if (env.eth_contract_address) {
contractAddress = env.eth_contract_address;
overridden = true;
}
contractAddress = contractAddress.toLowerCase();
if (contractAddress.search("0x") == 0) {
contractAddress = contractAddress.substring(2);
}
if (overridden) {
logger.info(
"Connecting to Ethereum: overriding contract address: [" +
contractAddress +
"], node: [" +
env.eth_provider_url +
"], token bridge address: [" +
env.eth_token_bridge_address +
"]"
);
} else {
logger.info(
"Connecting to Ethereum: contract address: [" +
contractAddress +
"], node: [" +
env.eth_provider_url +
"], token bridge address: [" +
env.eth_token_bridge_address +
"]"
);
}
const provider = new ethers.providers.StaticJsonRpcProvider(
env.eth_provider_url
);
const contract = new ethers.Contract(
contractAddress,
SWAP_CONTRACT_V3_ABI,
provider
);
const wallet = new ethers.Wallet(env.wallet_private_key, provider);
const contractWithSigner = contract.connect(wallet);
return {
name: "Ethereum",
contractAddress: contractAddress,
tokenBridgeAddress: env.eth_token_bridge_address,
contract: contract,
provider: provider,
wallet: wallet,
contractWithSigner: contractWithSigner,
};
}
// Polygon (Mumbai) set up
function makePolygonContractData(): TargetContractData {
let overridden: boolean = false;
let contractAddress: string = CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON;
if (env.polygon_contract_address) {
contractAddress = env.polygon_contract_address;
overridden = true;
}
contractAddress = contractAddress.toLowerCase();
if (contractAddress.search("0x") == 0) {
contractAddress = contractAddress.substring(2);
}
if (overridden) {
logger.info(
"Connecting to Polygon: overriding contract address: [" +
contractAddress +
"], node: [" +
env.polygon_provider_url +
"], token bridge address: [" +
env.polygon_token_bridge_address +
"]"
);
} else {
logger.info(
"Connecting to Polygon: contract address: [" +
contractAddress +
"], node: [" +
env.polygon_provider_url +
"], token bridge address: [" +
env.polygon_token_bridge_address +
"]"
);
}
const provider = new ethers.providers.StaticJsonRpcProvider(
env.polygon_provider_url
);
const contract = new ethers.Contract(
contractAddress,
SWAP_CONTRACT_V2_ABI,
provider
);
const wallet = new ethers.Wallet(env.wallet_private_key, provider);
const contractWithSigner = contract.connect(wallet);
return {
name: "Polygon",
contractAddress: contractAddress,
tokenBridgeAddress: env.polygon_token_bridge_address,
contract: contract,
provider: provider,
wallet: wallet,
contractWithSigner: contractWithSigner,
};
}
/*
// GOERLI_PROVIDER = Ethereum
// MUMBAI_PROVIDER = Polygon
if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM) {
// Use one of the V3 swap methods.
} else if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON) {
// Use one of the V2 swap methods.
} else {
// Error
}
if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 1) {
// swapExactInFromVaaNative
} else if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 2) {
// swapExactInFromVaaToken
} else if (
t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 1) {
// swapExactOutFromVaaNative
} else if (t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 2) {
// swapExactOutFromVaaToken
} else {
// error
}
*/
async function relayVaa(vaaBytes: string, t3Payload: Type3Payload) {
const signedVaaArray = hexToUint8Array(vaaBytes);
let exactIn: boolean = false;
if (t3Payload.swapFunctionType === 1) {
exactIn = true;
} else if (t3Payload.swapFunctionType !== 2) {
logger.error(
"relayVaa: unsupported swapFunctionType: [" +
t3Payload.swapFunctionType +
"]"
);
}
let native: boolean = false;
if (t3Payload.swapCurrencyType === 1) {
native = true;
} else if (t3Payload.swapCurrencyType !== 2) {
logger.error(
"relayVaa: unsupported swapCurrencyType: [" +
t3Payload.swapCurrencyType +
"]"
);
}
logger.debug(
"relayVaa: contractAddress: [" +
t3Payload.contractAddress +
"], ethContract: [" +
ethContractData.contractAddress +
"], polygonContract[" +
polygonContractData.contractAddress +
"]"
);
if (t3Payload.contractAddress === ethContractData.contractAddress) {
await relayVaaToChain(
t3Payload,
ethContractData,
signedVaaArray,
exactIn,
native
);
} else if (
t3Payload.contractAddress === polygonContractData.contractAddress
) {
await relayVaaToChain(
t3Payload,
polygonContractData,
signedVaaArray,
exactIn,
native
);
} else {
logger.error(
"relayVaa: unexpected contract: [" +
t3Payload.contractAddress +
"], this should not happen!"
);
}
}
async function relayVaaToChain(
t3Payload: Type3Payload,
tcd: TargetContractData,
signedVaaArray: Uint8Array,
exactIn: boolean,
native: boolean
) {
logger.debug(
"relayVaaTo" +
tcd.name +
": checking if already redeemed on " +
tcd.name +
" using tokenBridgeAddress [" +
tcd.tokenBridgeAddress +
"]"
);
if (await isRedeemed(t3Payload, tcd, signedVaaArray)) {
logger.info(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": already transferred"
);
if (t3Payload.targetChainId === 3) {
await relayVaaToTerra(t3Payload, vaaBytes);
return;
}
logger.info(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": submitting redeem request"
);
try {
let receipt: any = null;
if (exactIn) {
if (native) {
receipt = await swap.swapExactInFromVaaNative(
tcd.contractWithSigner,
signedVaaArray
);
} else {
receipt = await swap.swapExactInFromVaaToken(
tcd.contractWithSigner,
signedVaaArray
);
}
} else {
if (native) {
receipt = await swap.swapExactOutFromVaaNative(
tcd.contractWithSigner,
signedVaaArray
);
} else {
receipt = await swap.swapExactOutFromVaaToken(
tcd.contractWithSigner,
signedVaaArray
);
}
}
logger.info(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": success, txHash: " +
receipt.transactionHash
);
} catch (e: any) {
if (await isRedeemed(t3Payload, tcd, signedVaaArray)) {
logger.info(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": relay failed because the vaa has already been redeemed"
);
return;
}
logger.error(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": transaction failed: %o",
e
);
}
if (await isRedeemed(t3Payload, tcd, signedVaaArray)) {
logger.info(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": redeem succeeded"
);
} else {
logger.error(
"relayVaaTo" +
tcd.name +
": contract: [" +
t3Payload.contractAddress +
"], exactIn: " +
exactIn +
", native: " +
native +
": redeem failed!"
);
}
}
async function isRedeemed(
t3Payload: Type3Payload,
tcd: TargetContractData,
signedVaaArray: Uint8Array
): Promise<boolean> {
let redeemed: boolean = false;
try {
redeemed = await getIsTransferCompletedEth(
tcd.tokenBridgeAddress,
tcd.provider,
signedVaaArray
);
} catch (e) {
logger.error(
"relayVaaTo" +
tcd.name +
": failed to check if transfer is already complete, will attempt the transfer, e: %o",
e
);
}
return redeemed;
await relayVaaToEvm(vaaBytes, t3Payload);
}
///////////////////////////////// Start of logger stuff ///////////////////////////////////////////

357
swap_relayer/src/terra.ts Normal file
View File

@ -0,0 +1,357 @@
import {
CHAIN_ID_TERRA,
getIsTransferCompletedTerra,
redeemOnTerra,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import axios from "axios";
import { bech32 } from "bech32";
import { zeroPad } from "ethers/lib/utils";
import { fromUint8Array } from "js-base64";
import * as Terra from "@terra-money/terra.js";
import { logger, OurEnvironment, Type3Payload } from "./index";
export type TerraEnvironment = {
terra_enabled: boolean;
terra_provider_url: string;
terra_chain_id: string;
terra_name: string;
terra_contract_address: string;
terra_token_bridge_address: string;
terra_wallet_private_key: string;
terra_gas_price_url: string;
};
type TerraContractData = {
name: string;
contractAddress: string;
encodedContractAddress: string;
tokenBridgeAddress: string;
lcdConfig: Terra.LCDClientConfig;
lcdClient: Terra.LCDClient;
wallet: Terra.Wallet;
gasPriceUrl: string;
};
let terraContractData: TerraContractData = null;
export function loadTerraConfig(): TerraEnvironment {
let terra_enabled: boolean = false;
if (process.env.TERRA_PROVIDER) {
terra_enabled = true;
if (!process.env.TERRA_CHAIN_ID) {
logger.error("Missing environment variable WALLET_PRIVATE_KEY");
return undefined;
}
if (!process.env.TERRA_NAME) {
logger.error("Missing environment variable WALLET_PRIVATE_KEY");
return undefined;
}
if (!process.env.TERRA_WALLET_PRIVATE_KEY) {
logger.error("Missing environment variable TERRA_WALLET_PRIVATE_KEY");
return undefined;
}
if (!process.env.TERRA_GAS_PRICE_URL) {
logger.error("Missing environment variable TERRA_GAS_PRICE_URL");
return undefined;
}
if (!process.env.TERRA_CONTRACT_ADDRESS) {
logger.error("Missing environment variable TERRA_CONTRACT_ADDRESS");
return undefined;
}
if (!process.env.TERRA_TOKEN_BRIDGE_ADDRESS) {
logger.error("Missing environment variable TERRA_TOKEN_BRIDGE_ADDRESS");
return undefined;
}
}
return {
terra_enabled: terra_enabled,
terra_provider_url: process.env.TERRA_PROVIDER,
terra_chain_id: process.env.TERRA_CHAIN_ID,
terra_name: process.env.TERRA_NAME,
terra_contract_address: process.env.TERRA_CONTRACT_ADDRESS,
terra_token_bridge_address: process.env.TERRA_TOKEN_BRIDGE_ADDRESS,
terra_wallet_private_key: process.env.TERRA_WALLET_PRIVATE_KEY,
terra_gas_price_url: process.env.TERRA_GAS_PRICE_URL,
};
}
export function makeTerraContractData(env: TerraEnvironment) {
if (!env.terra_enabled) return;
let contractAddress: string = env.terra_contract_address.toLowerCase();
if (contractAddress.search("0x") == 0) {
contractAddress = contractAddress.substring(2);
}
let encodedContractAddress: string = Buffer.from(
zeroPad(bech32.fromWords(bech32.decode(contractAddress).words), 32)
).toString("hex");
logger.info(
"Connecting to Terra: contract address: [" +
contractAddress +
"], encoded contract address: [" +
encodedContractAddress +
"], node: [" +
env.terra_provider_url +
"], token bridge address: [" +
env.terra_token_bridge_address +
"], terra chain id: [" +
env.terra_chain_id +
"], terra name: [" +
env.terra_name +
"]"
);
const lcdConfig = {
URL: env.terra_provider_url,
chainID: env.terra_chain_id,
name: env.terra_name,
};
const lcdClient = new Terra.LCDClient(lcdConfig);
const mk = new Terra.MnemonicKey({
mnemonic: env.terra_wallet_private_key,
});
const wallet = lcdClient.wallet(mk);
terraContractData = {
name: "Terra",
contractAddress: contractAddress,
encodedContractAddress: encodedContractAddress,
tokenBridgeAddress: env.terra_token_bridge_address,
lcdConfig: lcdConfig,
lcdClient: lcdClient,
wallet: wallet,
gasPriceUrl: env.terra_gas_price_url,
};
}
export function isTerraContract(
contractAddress: string,
chain_id: number
): boolean {
if (chain_id !== CHAIN_ID_TERRA) return false;
if (!terraContractData) return false;
let retVal: boolean =
terraContractData &&
contractAddress === terraContractData.encodedContractAddress;
logger.debug(
"isTerraContract: comparing [" +
contractAddress +
"] to [" +
terraContractData.encodedContractAddress +
"]: " +
retVal
);
return retVal;
}
export async function relayVaaToTerra(
t3Payload: Type3Payload,
vaaBytes: string
) {
if (!terraContractData) return;
// logger.debug(
// "relayVaaToTerra: checking if already redeemed using tokenBridgeAddress [" +
// terraContractData.tokenBridgeAddress +
// "]"
// );
// if (await isRedeemedOnTerra(terraContractData, vaaBytes)) {
// logger.info(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: completed: already redeemed"
// );
// return;
// }
try {
logger.debug(
"relayVaaToTerra: creating message using contract address [" +
terraContractData.contractAddress +
"]"
);
logger.debug(
"relayVaaToTerra: vaa as hex: [" +
Buffer.from(vaaBytes, "hex").toString("hex") +
"]"
);
logger.debug(
"relayVaaToTerra: vaa as base64: [" +
Buffer.from(vaaBytes, "hex").toString("base64") +
"]"
);
const msg = new Terra.MsgExecuteContract(
terraContractData.wallet.key.accAddress,
terraContractData.contractAddress,
{
submit_vaa: {
data: Buffer.from(vaaBytes, "hex").toString("base64"),
},
}
);
// logger.debug("relayVaaToTerra: getting gas prices");
// const gasPrices = terraContractData.lcdClient.config.gasPrices;
// logger.debug("relayVaaToTerra: estimating fees");
// const feeEstimate = await terraContractData.lcdClient.tx.estimateFee(terraContractData.wallet.key.accAddress, [msg], {
// feeDenoms: ["uluna"],
// gasPrices,
// });
logger.debug("relayVaaToTerra: creating transaction");
const tx = await terraContractData.wallet.createAndSignTx({
msgs: [msg],
memo: "swap relayer",
feeDenoms: ["uluna"],
// gasPrices,
// fee: feeEstimate,
});
logger.info(
"relayVaaToTerra: contract: [" +
t3Payload.contractAddress +
"]: submitting redeem request"
);
const receipt = await terraContractData.lcdClient.tx.broadcast(tx);
logger.info(
"relayVaaToTerra: srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"]: completed: success: %o",
receipt
);
// logger.info(
// "relayVaaToTerra: contract: [" +
// t3Payload.contractAddress +
// "]: success, txHash: " +
// receipt.transactionHash
// );
} catch (e: any) {
// if (await isRedeemedOnTerra(terraContractData, vaaBytes)) {
// logger.info(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: completed: relay failed because the vaa has already been redeemed"
// );
// return;
// }
logger.error(
"relayVaaToTerra: srcChain: " +
t3Payload.sourceChainId +
", targetChain: " +
t3Payload.targetChainId +
", contract: [" +
t3Payload.contractAddress +
"]: completed: transaction failed: %o",
e
);
}
// if (await isRedeemedOnTerra(terraContractData, vaaBytes)) {
// logger.info(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: redeem confirmed"
// );
// } else {
// logger.error(
// "relayVaaToTerra: srcChain: " +
// t3Payload.sourceChainId +
// ", targetChain: " +
// t3Payload.targetChainId +
// ", contract: [" +
// t3Payload.contractAddress +
// "]: completed: failed to confirm redeem!"
// );
// }
}
async function isRedeemedOnTerra(
terraContractData: TerraContractData,
vaaBytes: string
): Promise<boolean> {
let msg: Terra.MsgExecuteContract = null;
let sequenceNumber: number = 0;
try {
msg = new Terra.MsgExecuteContract(
terraContractData.wallet.key.accAddress,
terraContractData.tokenBridgeAddress,
{
submit_vaa: {
data: Buffer.from(vaaBytes, "hex").toString("base64"),
},
}
);
sequenceNumber = await terraContractData.wallet.sequence();
} catch (e) {
logger.error(
"isRedeemedOnTerra: failed to create message or look up sequence number, e: %o",
e
);
return false;
}
try {
await terraContractData.lcdClient.tx.estimateFee(
[{ sequenceNumber: sequenceNumber }],
{
msgs: [msg],
}
);
} catch (e) {
if (e.response.data.error.includes("VaaAlreadyExecuted")) {
return true;
}
logger.error(
"isRedeemedOnTerra: failed to check if transfer is already complete, e: %o",
e
);
}
return false;
}