initial commit

This commit is contained in:
Kevin Peters 2022-01-11 16:29:49 +00:00
commit 061cc544fe
66 changed files with 84881 additions and 0 deletions

95
README.md Normal file
View File

@ -0,0 +1,95 @@
## NativeSwap
This is a non-production example program.
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).
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.
Here is what happens under the hood of this example:
- User generates quote from front-end for native-to-native swap.
- User calls the smart contract with its quote on chain A.
- Smart contract on chain A executes swap from native A to UST. If the swap succeeds, the smart contract will execute a Token Bridge transfer of UST with encoded swap parameters for chain B.
- Guardians sign the Token Bridge transfer.
- The relayer reads the signed VAA and calls the smart contract with the VAA as its only argument.
- Smart contract on chain B completes the UST transfer and decodes the swap parameters from the Wormhole message payload.
- Smart contract on chain B executes swap from UST to native B. If the swap succeeds, the smart contract will send native B to user. Otherwise, it will send UST to user.
The Wormhole message payload for swap parameters are all encoded and decoded on-chain.
We also wrote a front-end UI using a custom class (UniswapToUniswapExecutor) to perform the quotes for "Exact In" (swapping from an exact amount of native A to an estimated amount of native B) and "Exact Out" (swapping from an estimated amount of native A to an exact amount of native B) swaps and execute these swaps based on this quote. This library uses the ABIs of our example smart contracts to execute the swap transactions.
### What's next?
That is up to you! You are not limited to native-to-native multi-chain swaps. Build in your own smart routing with whichever DEX to perform any swap from chain A to chain B. Wormhole messaging and token transfers with payload are generic enough to adapt this example for any of the chains Wormhole currently supports.
### Deployment
Before deploying, you need to install contract and Truffle dependencies:
```bash
npm ci
```
There are two deployment scripts found in the _scripts_ directory:
- _deploy_to_goerli.sh_
- _deploy_to_mumbai.sh_
These will deploy _CrossChainSwapV3_ and _CrossChainSwapV2_ respectively (found in the _contracts_ directory) and automatically write those contract addresses to a Typescript file, which will be compiled with everything else for your front-end library. You can run them like so:
```bash
bash scripts/deploy_to_goerli.sh
bash scripts/deploy_to_mumbai.sh
```
After you deploy your contracts, run the following to build the front-end library:
```bash
npm run build
```
### Running
First compile the example contracts:
```
cd contracts
npm ci
./compile_contracts.sh
```
Then copy sample.env to .env, edit .env and replace YOUR-PROJECT-ID with your Infura Goerli and Mumbai Project IDs and also add your Ethereum wallet's private key.
These are needed to deploy the example contracts.
```
cp .env.sample .env
# make sure to edit .env file
```
Then deploy the example contracts:
```
./deploy_to_goerli.sh
./deploy_to_mumbai.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
```
cd react
cp .env.sample .env
# make sure to edit .env file
```
And finally, start the react app:
```
npm ci
npm run start
```

3
contracts/.env.sample Normal file
View File

@ -0,0 +1,3 @@
GOERLI_PROVIDER=https://goerli.infura.io/v3/YOUR-PROJECT-ID
MUMBAI_PROVIDER=https://polygon-mumbai.infura.io/v3/YOUR-PROJECT-ID
ETH_PRIVATE_KEY=

1
contracts/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.env

10
contracts/compile_contracts.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
set -euo pipefail
npx truffle compile --config truffle-config.ethereum.js
npx truffle compile --config truffle-config.polygon.js
mkdir -p ../ui/src/abi/contracts
cp -r build/contracts/* ../ui/src/abi/contracts

View File

@ -0,0 +1,272 @@
pragma solidity ^0.7.6;
pragma abicoder v2;
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import 'solidity-bytes-utils/contracts/BytesLib.sol';
import './IWormhole.sol';
import './SwapHelper.sol';
interface TokenBridge {
function transferTokensWithPayload(
address token,
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
uint256 arbiterFee,
uint32 nonce,
bytes memory payload
) external payable returns (uint64);
function completeTransferWithPayload(bytes memory encodedVm) external returns (IWormhole.VM memory);
}
contract CrossChainSwapV2 {
using SafeERC20 for IERC20;
using BytesLib for bytes;
uint8 public immutable typeExactIn = 1;
uint8 public immutable typeExactOut = 2;
uint16 public immutable expectedVaaLength = 261;
IUniswapV2Router02 public immutable swapRouter;
address public immutable feeTokenAddress;
address public immutable tokenBridgeAddress;
constructor(address _swapRouterAddress, address _feeTokenAddress, address _tokenBridgeAddress) {
swapRouter = IUniswapV2Router02(_swapRouterAddress);
feeTokenAddress = _feeTokenAddress;
tokenBridgeAddress = _tokenBridgeAddress;
}
function swapExactInFromV3(
bytes calldata encodedVaa
) external returns (uint256[] memory amounts) {
// complete the transfer on the token bridge
IWormhole.VM memory vm = TokenBridge(tokenBridgeAddress).completeTransferWithPayload(encodedVaa);
require(vm.payload.length==expectedVaaLength, "VAA has the wrong number of bytes");
// parse the payload
SwapHelper.DecodedVaaParameters memory payload = SwapHelper.decodeVaaPayload(vm);
require(payload.swapType==typeExactIn, "swap must be type ExactIn");
// create dynamic address array - uniswap won't take fixed size array
address[] memory uniPath = new address[](2);
uniPath[0] = payload.path[0];
uniPath[1] = payload.path[1];
require(uniPath[0]==feeTokenAddress, "tokenIn must be UST");
// pay relayer before attempting to do the swap
// reflect payment in second swap amount
IERC20 feeToken = IERC20(feeTokenAddress);
feeToken.safeTransfer(msg.sender, payload.relayerFee);
uint256 swapAmountLessFees = payload.swapAmount - payload.relayerFee;
// approve the router to spend tokens
TransferHelper.safeApprove(uniPath[0], address(swapRouter), swapAmountLessFees);
// try to perform the swap
try swapRouter.swapExactTokensForTokens(
swapAmountLessFees,
payload.estimatedAmount,
uniPath,
payload.recipientAddress,
payload.deadline
) returns (uint256[] memory amounts) {
return amounts;
} catch {
// swap failed - return UST to recipient
feeToken.safeTransfer(payload.recipientAddress, swapAmountLessFees);
}
}
function _swapExactInBeforeTransfer(
uint256 amountIn,
uint256 amountOutMinimum,
address contractCaller,
address[] calldata path,
uint256 deadline
) internal returns (uint256 amountOut) {
// path[0] is the tokenIn in
IERC20 token = IERC20(path[0]);
token.safeTransferFrom(contractCaller, address(this), amountIn);
// approve the router to spend tokens
TransferHelper.safeApprove(path[0], address(swapRouter), amountIn);
// perform the swap
uint256[] memory amounts = swapRouter.swapExactTokensForTokens(
amountIn,
amountOutMinimum,
path,
address(this),
deadline
);
amountOut = amounts[1];
}
function swapExactInToV3(
SwapHelper.ExactInParameters calldata swapParams,
address[] calldata path,
uint256 relayerFee,
uint16 targetChainId,
bytes32 targetContractAddress,
uint32 nonce
) external {
require(swapParams.amountOutMinimum > relayerFee, "insufficient amountOutMinimum to pay relayer");
require(path[1]==feeTokenAddress, "tokenOut must be UST for first swap");
// peform the first swap
uint256 amountOut = _swapExactInBeforeTransfer(
swapParams.amountIn,
swapParams.amountOutMinimum,
msg.sender,
path[0:2],
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
);
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(feeTokenAddress, tokenBridgeAddress, amountOut);
// send transfer with payload to the TokenBridge
TokenBridge(tokenBridgeAddress).transferTokensWithPayload(
feeTokenAddress, amountOut, targetChainId, targetContractAddress, relayerFee, nonce, payload
);
}
function swapExactOutFromV3(
bytes calldata encodedVaa
) external returns (uint256 amountInUsed) {
// complete the transfer on the token bridge
IWormhole.VM memory vm = TokenBridge(tokenBridgeAddress).completeTransferWithPayload(encodedVaa);
require(vm.payload.length==expectedVaaLength, "VAA has the wrong number of bytes");
// parse the payload
SwapHelper.DecodedVaaParameters memory payload = SwapHelper.decodeVaaPayload(vm);
require(payload.swapType==typeExactOut, "swap must be type ExactOut");
// amountOut is the estimated swap amount for exact out methods
uint256 amountOut = payload.estimatedAmount;
// create dynamic address array - uniswap won't take fixed size array
address[] memory uniPath = new address[](2);
uniPath[0] = payload.path[0];
uniPath[1] = payload.path[1];
require(uniPath[0]==feeTokenAddress, "tokenIn must be UST");
// pay relayer before attempting to do the swap
// reflect payment in second swap amount
IERC20 feeToken = IERC20(feeTokenAddress);
feeToken.safeTransfer(msg.sender, payload.relayerFee);
uint256 maxAmountInLessFees = payload.swapAmount - payload.relayerFee;
// approve the router to spend tokens
TransferHelper.safeApprove(uniPath[0], address(swapRouter), maxAmountInLessFees);
// try to perform the swap
try swapRouter.swapTokensForExactTokens(
amountOut,
maxAmountInLessFees,
uniPath,
payload.recipientAddress,
payload.deadline
) returns (uint256[] memory amounts) {
// amountIn used is first element in array
amountInUsed = amounts[0];
// refund recipient with any UST not used in the swap
if (amountInUsed < maxAmountInLessFees) {
TransferHelper.safeApprove(feeTokenAddress, address(swapRouter), 0);
feeToken.safeTransfer(payload.recipientAddress, maxAmountInLessFees - amountInUsed);
}
return amountInUsed;
} catch {
feeToken.safeTransfer(payload.recipientAddress, maxAmountInLessFees);
}
}
function _swapExactOutBeforeTransfer(
uint256 amountOut,
uint256 amountInMaximum,
address contractCaller,
address[] calldata path,
uint256 deadline
) internal {
// path[0] is the tokenIn
IERC20 token = IERC20(path[0]);
token.safeTransferFrom(contractCaller, address(this), amountInMaximum);
// approve the router to spend tokens
TransferHelper.safeApprove(path[0], address(swapRouter), amountInMaximum);
// perform the swap
uint256[] memory amounts = swapRouter.swapTokensForExactTokens(
amountOut,
amountInMaximum,
path,
address(this),
deadline
);
// amountIn used is first element in array
uint256 amountInUsed = amounts[0];
// refund contractCaller with any amountIn that wasn't spent
if (amountInUsed < amountInMaximum) {
TransferHelper.safeApprove(path[0], address(swapRouter), 0);
token.safeTransfer(contractCaller, amountInMaximum - amountInUsed);
}
}
function swapExactOutToV3(
SwapHelper.ExactOutParameters calldata swapParams,
address[] calldata path,
uint256 relayerFee,
uint16 targetChainId,
bytes32 targetContractAddress,
uint32 nonce
) external {
require(swapParams.amountOut > relayerFee, "insufficient amountOut to pay relayer");
require(path[1]==feeTokenAddress, "tokenOut must be UST for first swap");
// peform the first swap
_swapExactOutBeforeTransfer(
swapParams.amountOut,
swapParams.amountInMaximum,
msg.sender,
path[0:2],
swapParams.deadline
);
// encode payload for second swap
bytes memory payload = abi.encodePacked(
swapParams.targetAmountOut,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactOut
);
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(feeTokenAddress, tokenBridgeAddress, swapParams.amountOut);
// send transfer with payload to the TokenBridge
TokenBridge(tokenBridgeAddress).transferTokensWithPayload(
feeTokenAddress, swapParams.amountOut, targetChainId, targetContractAddress, relayerFee, nonce, payload
);
}
}

View File

@ -0,0 +1,287 @@
pragma solidity ^0.7.6;
pragma abicoder v2;
import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import 'solidity-bytes-utils/contracts/BytesLib.sol';
import './IWormhole.sol';
import './SwapHelper.sol';
interface TokenBridge {
function transferTokensWithPayload(
address token,
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
uint256 arbiterFee,
uint32 nonce,
bytes memory payload
) external payable returns (uint64);
function completeTransferWithPayload(bytes memory encodedVm) external returns (IWormhole.VM memory);
}
contract CrossChainSwapV3 {
using SafeERC20 for IERC20;
using BytesLib for bytes;
uint8 public immutable typeExactIn = 1;
uint8 public immutable typeExactOut = 2;
uint16 public immutable expectedVaaLength = 261;
ISwapRouter public immutable swapRouter;
address public immutable feeTokenAddress;
address public immutable tokenBridgeAddress;
constructor(address _swapRouterAddress, address _feeTokenAddress, address _tokenBridgeAddress) {
swapRouter = ISwapRouter(_swapRouterAddress);
feeTokenAddress = _feeTokenAddress;
tokenBridgeAddress = _tokenBridgeAddress;
}
function swapExactInFromV2(
bytes calldata encodedVaa
) external returns (uint256 amountOut) {
// complete the transfer on the token bridge
IWormhole.VM memory vm = TokenBridge(tokenBridgeAddress).completeTransferWithPayload(encodedVaa);
require(vm.payload.length==expectedVaaLength, "VAA has the wrong number of bytes");
// parse the payload
SwapHelper.DecodedVaaParameters memory payload = SwapHelper.decodeVaaPayload(vm);
require(payload.swapType==typeExactIn, "swap must be type ExactIn");
require(payload.path[0]==feeTokenAddress, "tokenIn must be UST");
// pay relayer before attempting to do the swap
// reflect payment in second swap amount
IERC20 feeToken = IERC20(feeTokenAddress);
feeToken.safeTransfer(msg.sender, payload.relayerFee);
uint256 swapAmountLessFees = payload.swapAmount - payload.relayerFee;
// approve the router to spend tokens based on amountIn
TransferHelper.safeApprove(payload.path[0], address(swapRouter), swapAmountLessFees);
// set swap options with user params
ISwapRouter.ExactInputSingleParams memory params =
ISwapRouter.ExactInputSingleParams({
tokenIn: payload.path[0],
tokenOut: payload.path[1],
fee: payload.poolFee,
recipient: payload.recipientAddress,
deadline: payload.deadline,
amountIn: swapAmountLessFees,
amountOutMinimum: payload.estimatedAmount,
sqrtPriceLimitX96: 0
});
// the call to `exactInputSingle` executes the swap
try swapRouter.exactInputSingle(params) returns (uint256 amountOut) {
return amountOut;
} catch {
// swap failed - return UST to recipient
feeToken.safeTransfer(payload.recipientAddress, swapAmountLessFees);
}
}
function _swapExactInBeforeTransfer(
uint256 amountIn,
uint256 amountOutMinimum,
address contractCaller,
address[] calldata path,
uint256 deadline,
uint24 poolFee
) internal returns (uint256 amountOut) {
// transfer the allowed amount of tokens to this contract
IERC20 token = IERC20(path[0]);
token.safeTransferFrom(contractCaller, address(this), amountIn);
// approve the router to spend tokens based on amountIn
TransferHelper.safeApprove(path[0], address(swapRouter), amountIn);
// set swap options with user params
ISwapRouter.ExactInputSingleParams memory params =
ISwapRouter.ExactInputSingleParams({
tokenIn: path[0],
tokenOut: path[1],
fee: poolFee,
recipient: address(this),
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: amountOutMinimum,
sqrtPriceLimitX96: 0
});
// the call to `exactInputSingle` executes the swap
amountOut = swapRouter.exactInputSingle(params);
}
function swapExactInToV2(
SwapHelper.ExactInParameters calldata swapParams,
address[] calldata path,
uint256 relayerFee,
uint16 targetChainId,
bytes32 targetContractAddress,
uint32 nonce
) external {
// makes sure the relayer is left whole after the second swap
require(swapParams.amountOutMinimum > relayerFee, "insufficient amountOutMinimum to pay relayer");
require(path[1]==feeTokenAddress, "tokenOut must be UST for first swap");
// peform the first swap
uint256 amountOut = _swapExactInBeforeTransfer(
swapParams.amountIn,
swapParams.amountOutMinimum,
msg.sender,
path[0:2],
swapParams.deadline,
swapParams.poolFee
);
// encode payload for second swap
bytes memory payload = abi.encodePacked(
swapParams.targetAmountOutMinimum,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactIn
);
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(feeTokenAddress, tokenBridgeAddress, amountOut);
// send transfer with payload to the TokenBridge
TokenBridge(tokenBridgeAddress).transferTokensWithPayload(
feeTokenAddress, amountOut, targetChainId, targetContractAddress, relayerFee, nonce, payload
);
}
function swapExactOutFromV2(
bytes calldata encodedVaa
) external returns (uint256 amountInUsed) {
// complete the transfer on the token bridge
IWormhole.VM memory vm = TokenBridge(tokenBridgeAddress).completeTransferWithPayload(encodedVaa);
require(vm.payload.length==expectedVaaLength, "VAA has the wrong number of bytes");
// parse the payload
SwapHelper.DecodedVaaParameters memory payload = SwapHelper.decodeVaaPayload(vm);
require(payload.swapType==typeExactOut, "swap must be type ExactOut");
require(payload.path[0]==feeTokenAddress, "tokenIn must be UST");
// amountOut is the estimated swap amount for exact out methods
uint256 amountOut = payload.estimatedAmount;
// pay relayer before attempting to do the swap
// reflect payment in second swap amount
IERC20 feeToken = IERC20(feeTokenAddress);
feeToken.safeTransfer(msg.sender, payload.relayerFee);
uint256 maxAmountInLessFees = payload.swapAmount - payload.relayerFee;
// approve the router to spend swapAmount - which is maxAmountIn in payload
TransferHelper.safeApprove(payload.path[0], address(swapRouter), maxAmountInLessFees);
// set swap options with user params
ISwapRouter.ExactOutputSingleParams memory params =
ISwapRouter.ExactOutputSingleParams({
tokenIn: payload.path[0],
tokenOut: payload.path[1],
fee: payload.poolFee,
recipient: payload.recipientAddress,
deadline: payload.deadline,
amountOut: amountOut,
amountInMaximum: maxAmountInLessFees,
sqrtPriceLimitX96: 0
});
try swapRouter.exactOutputSingle(params) returns (uint256 amountInUsed) {
// refund recipient with any UST not used in the swap
if (amountInUsed < maxAmountInLessFees) {
TransferHelper.safeApprove(feeTokenAddress, address(swapRouter), 0);
feeToken.safeTransfer(payload.recipientAddress, maxAmountInLessFees - amountInUsed);
}
return amountInUsed;
} catch {
feeToken.safeTransfer(payload.recipientAddress, maxAmountInLessFees);
}
}
function _swapExactOutBeforeTransfer(
uint256 amountOut,
uint256 amountInMaximum,
address contractCaller,
address[] calldata path,
uint256 deadline,
uint24 poolFee
) internal {
// transfer the allowed amount of tokens to this contract
IERC20 token = IERC20(path[0]);
token.safeTransferFrom(contractCaller, address(this), amountInMaximum);
// approve the router to spend the specifed amountInMaximum of tokens
TransferHelper.safeApprove(path[0], address(swapRouter), amountInMaximum);
// set swap options with user params
ISwapRouter.ExactOutputSingleParams memory params =
ISwapRouter.ExactOutputSingleParams({
tokenIn: path[0],
tokenOut: path[1],
fee: poolFee,
recipient: address(this),
deadline: deadline,
amountOut: amountOut,
amountInMaximum: amountInMaximum,
sqrtPriceLimitX96: 0
});
// executes the swap returning the amountIn needed to spend to receive the desired amountOut
uint256 amountInUsed = swapRouter.exactOutputSingle(params);
// refund contractCaller with any amountIn that's not spent
if (amountInUsed < amountInMaximum) {
TransferHelper.safeApprove(path[0], address(swapRouter), 0);
token.safeTransfer(contractCaller, amountInMaximum - amountInUsed);
}
}
function swapExactOutToV2(
SwapHelper.ExactOutParameters calldata swapParams,
address[] calldata path,
uint256 relayerFee,
uint16 targetChainId, // make sure target contract address belongs to targeChainId
bytes32 targetContractAddress,
uint32 nonce
) external {
require(swapParams.amountOut > relayerFee, "insufficient amountOut to pay relayer");
require(path[1]==feeTokenAddress, "tokenOut must be UST for first swap");
// peform the first swap
_swapExactOutBeforeTransfer(
swapParams.amountOut,
swapParams.amountInMaximum,
msg.sender,
path[0:2],
swapParams.deadline,
swapParams.poolFee
);
// encode payload for second swap
bytes memory payload = abi.encodePacked(
swapParams.targetAmountOut,
swapParams.targetChainRecipient,
path[2],
path[3],
swapParams.deadline,
swapParams.poolFee,
typeExactOut
);
// approve token bridge to spend feeTokens (UST)
TransferHelper.safeApprove(feeTokenAddress, tokenBridgeAddress, swapParams.amountOut);
// send transfer with payload to the TokenBridge
TokenBridge(tokenBridgeAddress).transferTokensWithPayload(
feeTokenAddress, swapParams.amountOut, targetChainId, targetContractAddress, relayerFee, nonce, payload
);
}
}

View File

@ -0,0 +1,43 @@
// contracts/Messages.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.7.6;
pragma abicoder v2;
import "./Structs.sol";
interface IWormhole is Structs {
event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);
function publishMessage(
uint32 nonce,
bytes memory payload,
uint8 consistencyLevel
) external payable returns (uint64 sequence);
function parseAndVerifyVM(bytes calldata encodedVM) external view returns (Structs.VM memory vm, bool valid, string memory reason);
function verifyVM(Structs.VM memory vm) external view returns (bool valid, string memory reason);
function verifySignatures(bytes32 hash, Structs.Signature[] memory signatures, Structs.GuardianSet memory guardianSet) external pure returns (bool valid, string memory reason) ;
function parseVM(bytes memory encodedVM) external pure returns (Structs.VM memory vm);
function getGuardianSet(uint32 index) external view returns (Structs.GuardianSet memory) ;
function getCurrentGuardianSetIndex() external view returns (uint32) ;
function getGuardianSetExpiry() external view returns (uint32) ;
function governanceActionIsConsumed(bytes32 hash) external view returns (bool) ;
function isInitialized(address impl) external view returns (bool) ;
function chainId() external view returns (uint16) ;
function governanceChainId() external view returns (uint16);
function governanceContract() external view returns (bytes32);
function messageFee() external view returns (uint256) ;
}

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
constructor() public {
owner = msg.sender;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}

View File

@ -0,0 +1,41 @@
// contracts/Structs.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.7.6;
pragma abicoder v2;
interface Structs {
struct Provider {
uint16 chainId;
uint16 governanceChainId;
bytes32 governanceContract;
}
struct GuardianSet {
address[] keys;
uint32 expirationTime;
}
struct Signature {
bytes32 r;
bytes32 s;
uint8 v;
uint8 guardianIndex;
}
struct VM {
uint8 version;
uint32 timestamp;
uint32 nonce;
uint16 emitterChainId;
bytes32 emitterAddress;
uint64 sequence;
uint8 consistencyLevel;
bytes payload;
uint32 guardianSetIndex;
Signature[] signatures;
bytes32 hash;
}
}

View File

@ -0,0 +1,88 @@
pragma solidity ^0.7.6;
pragma abicoder v2;
import './IWormhole.sol';
import 'solidity-bytes-utils/contracts/BytesLib.sol';
library SwapHelper {
using BytesLib for bytes;
struct ExactInParameters {
uint256 amountIn;
uint256 amountOutMinimum;
uint256 targetAmountOutMinimum;
address targetChainRecipient;
uint256 deadline;
uint24 poolFee;
}
struct ExactOutParameters {
uint256 amountOut;
uint256 amountInMaximum;
uint256 targetAmountOut;
address targetChainRecipient;
uint256 deadline;
uint24 poolFee;
}
struct DecodedVaaParameters {
// in order of decoding
uint8 version;
uint256 swapAmount;
address contractAddress;
uint256 relayerFee;
uint256 estimatedAmount;
address recipientAddress;
address[2] path;
uint256 deadline;
uint24 poolFee;
uint8 swapType;
}
function decodeVaaPayload(
IWormhole.VM memory encodedVm
) public view returns (DecodedVaaParameters memory decoded) {
uint index = 0;
decoded.version = encodedVm.payload.toUint8(index);
index += 1;
decoded.swapAmount = encodedVm.payload.toUint256(index);
index += 32;
// skip
index += 46;
decoded.contractAddress = encodedVm.payload.toAddress(index);
index += 20;
// skip
index += 2;
decoded.relayerFee = encodedVm.payload.toUint256(index);
index += 32;
decoded.estimatedAmount = encodedVm.payload.toUint256(index);
index += 32;
decoded.recipientAddress = encodedVm.payload.toAddress(index);
index += 20;
decoded.path[0] = encodedVm.payload.toAddress(index);
index += 20;
decoded.path[1] = encodedVm.payload.toAddress(index);
index += 20;
decoded.deadline = encodedVm.payload.toUint256(index);
index += 32;
// skip
index += 1;
decoded.poolFee = encodedVm.payload.toUint16(index);
index += 2;
decoded.swapType = encodedVm.payload.toUint8(index);
}
}

2
contracts/deploy_to_goerli.sh Executable file
View File

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

2
contracts/deploy_to_mumbai.sh Executable file
View File

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

View File

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

View File

@ -0,0 +1,30 @@
const CrossChainSwapV3 = artifacts.require('CrossChainSwapV3');
const CrossChainSwapV2 = artifacts.require('CrossChainSwapV2');
const BytesDecodingTest = artifacts.require('BytesDecodingTest');
module.exports = function(deployer) {
// CrossChainSwapV3
{
const routerAddress = '0xE592427A0AEce92De3Edee1F18E0157C05861564'
const feeTokenAddress = '0x36Ed51Afc79619b299b238898E72ce482600568a' // wUST
const tokenBridgeAddress = '0xF890982f9310df57d00f659cf4fd87e65adEd8d7';
deployer.deploy(CrossChainSwapV3, routerAddress, feeTokenAddress, tokenBridgeAddress);
}
// CrossChainSwapV2
{
const routerAddress = '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff'; // quickwap
const feeTokenAddress = '0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c'; // wUST
const tokenBridgeAddress = '0x377D55a7928c046E18eEbb61977e714d2a76472a';
deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress);
}
// BytesDecodingTest
deployer.deploy(BytesDecodingTest)
//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,32 @@
const fsp = require("fs/promises");
const CrossChainSwapV3 = artifacts.require("CrossChainSwapV3");
const SwapHelper = artifacts.require("SwapHelper");
const scriptsAddressPath = "../ui/src/addresses";
module.exports = async function (deployer, network) {
const routerAddress = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
const feeTokenAddress = "0x36Ed51Afc79619b299b238898E72ce482600568a"; // wUST
const tokenBridgeAddress = "0xF890982f9310df57d00f659cf4fd87e65adEd8d7";
await deployer.deploy(SwapHelper);
await deployer.link(SwapHelper, CrossChainSwapV3);
await deployer.deploy(
CrossChainSwapV3,
routerAddress,
feeTokenAddress,
tokenBridgeAddress
);
// save the contract address somewhere
await fsp.mkdir(scriptsAddressPath, { recursive: true });
await fsp.writeFile(
`${scriptsAddressPath}/${network}.ts`,
`export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV3.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,32 @@
const fsp = require("fs/promises");
const CrossChainSwapV2 = artifacts.require("CrossChainSwapV2");
const SwapHelper = artifacts.require("SwapHelper");
const scriptsAddressPath = "../ui/src/addresses";
module.exports = async function (deployer, network) {
const routerAddress = "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff"; // quickwap
const feeTokenAddress = "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c"; // wUST
const tokenBridgeAddress = "0x377D55a7928c046E18eEbb61977e714d2a76472a";
await deployer.deploy(SwapHelper);
await deployer.link(SwapHelper, CrossChainSwapV2);
await deployer.deploy(
CrossChainSwapV2,
routerAddress,
feeTokenAddress,
tokenBridgeAddress
);
// 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);
};

32783
contracts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
contracts/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "contracts",
"version": "1.0.0",
"description": "",
"scripts": {},
"author": "",
"license": "ISC",
"dependencies": {
"@truffle/hdwallet-provider": "^2.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-periphery": "^1.3.0",
"dotenv": "^14.2.0",
"solidity-bytes-utils": "github:GNSPS/solidity-bytes-utils#feat/update-0.7.0",
"truffle": "^5.4.29"
}
}

View File

@ -0,0 +1,127 @@
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/ethereum",
/**
* 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.
goerli: {
provider: () =>
new HDWalletProvider(
process.env.ETH_PRIVATE_KEY,
process.env.GOERLI_PROVIDER
),
network_id: 5,
gas: 4465030,
//confirmations: 2, // # of confs to wait between deployments. (default: 0)
//timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
//skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
},
// 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,75 @@
/**
* 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 infuraKey = 'fj4jll3k.....';
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync('.secret').toString().trim();
module.exports = {
contracts_directory: "./contracts",
contracts_build_directory: "./build/contracts",
migrations_directory: "./migrations/development",
/**
* 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: {
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
},
// 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 enabled in this project by default. Enabling Truffle DB surfaces access to the @truffle/db package
// for querying data about the contracts, deployments, and networks in this project
//db: {
// enabled: true
//}
};

View File

@ -0,0 +1,127 @@
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/polygon",
/**
* 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.
mumbai: {
provider: () =>
new HDWalletProvider(
process.env.ETH_PRIVATE_KEY,
process.env.MUMBAI_PROVIDER
),
network_id: 80001,
gas: 4465030,
//confirmations: 2, // # of confs to wait between deployments. (default: 0)
//timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
//skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
},
// 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'
// }
// }
// }
};

2
react/.env.sample Normal file
View File

@ -0,0 +1,2 @@
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

24
react/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*

28
react/craco.config.js Normal file
View File

@ -0,0 +1,28 @@
const { addBeforeLoader, loaderByName } = require("@craco/craco");
module.exports = {
webpack: {
configure: (webpackConfig) => {
const wasmExtensionRegExp = /\.wasm$/;
webpackConfig.resolve.extensions.push(".wasm");
webpackConfig.module.rules.forEach((rule) => {
(rule.oneOf || []).forEach((oneOf) => {
if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) {
oneOf.exclude.push(wasmExtensionRegExp);
}
});
});
const wasmLoader = {
test: /\.wasm$/,
include: /node_modules\/(bridge|token-bridge)/,
loaders: ["wasm-loader"],
};
addBeforeLoader(webpackConfig, loaderByName("file-loader"), wasmLoader);
return webpackConfig;
},
},
};

45837
react/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

57
react/package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "Cross Chain Swap",
"version": "0.1.0",
"private": true,
"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",
"@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"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@craco/craco": "^6.3.0",
"wasm-loader": "^1.3.0"
}
}

BIN
react/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

43
react/public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Cross Chain Swap</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
react/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
react/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "Cross Chain Swap",
"name": "Cross Chain Swap Example Program",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
react/public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

5
react/src/App.tsx Normal file
View File

@ -0,0 +1,5 @@
import Home from "./views/Home";
export default function App() {
return <Home />;
}

View File

@ -0,0 +1,751 @@
[
{
"inputs": [],
"name": "WETH",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountADesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "addLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountTokenDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "addLiquidityETH",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveOut",
"type": "uint256"
}
],
"name": "getAmountIn",
"outputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveOut",
"type": "uint256"
}
],
"name": "getAmountOut",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
}
],
"name": "getAmountsIn",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
}
],
"name": "getAmountsOut",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveB",
"type": "uint256"
}
],
"name": "quote",
"outputs": [
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "removeLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "removeLiquidityETH",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "bool",
"name": "approveMax",
"type": "bool"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "removeLiquidityETHWithPermit",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "bool",
"name": "approveMax",
"type": "bool"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "removeLiquidityWithPermit",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapETHForExactTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactETHForTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountInMax",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapTokensForExactETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountInMax",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapTokensForExactTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

View File

@ -0,0 +1,953 @@
[
{
"inputs": [],
"name": "WETH",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountADesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "addLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountTokenDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "addLiquidityETH",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveOut",
"type": "uint256"
}
],
"name": "getAmountIn",
"outputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveOut",
"type": "uint256"
}
],
"name": "getAmountOut",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
}
],
"name": "getAmountsIn",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
}
],
"name": "getAmountsOut",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveB",
"type": "uint256"
}
],
"name": "quote",
"outputs": [
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "removeLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "removeLiquidityETH",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "removeLiquidityETHSupportingFeeOnTransferTokens",
"outputs": [
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "bool",
"name": "approveMax",
"type": "bool"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "removeLiquidityETHWithPermit",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "bool",
"name": "approveMax",
"type": "bool"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "removeLiquidityETHWithPermitSupportingFeeOnTransferTokens",
"outputs": [
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "bool",
"name": "approveMax",
"type": "bool"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "removeLiquidityWithPermit",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapETHForExactTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactETHForTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactETHForTokensSupportingFeeOnTransferTokens",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForETHSupportingFeeOnTransferTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForTokensSupportingFeeOnTransferTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountInMax",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapTokensForExactETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountInMax",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapTokensForExactTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

1
react/src/abi/contracts/.gitignore vendored Normal file
View File

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

224
react/src/abi/erc20.json Normal file
View File

@ -0,0 +1,224 @@
{
"abi": [
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
]
}

2
react/src/addresses/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,74 @@
import {
Button,
CircularProgress,
makeStyles,
Typography,
} from "@material-ui/core";
import { ReactChild } from "react";
const useStyles = makeStyles((theme) => ({
root: {
position: "relative",
},
button: {
marginTop: theme.spacing(2),
textTransform: "none",
width: "100%",
},
loader: {
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
},
error: {
marginTop: theme.spacing(1),
textAlign: "center",
},
}));
export default function ButtonWithLoader({
disabled,
onClick,
showLoader,
error,
children,
className,
}: {
disabled?: boolean;
onClick: () => void;
showLoader?: boolean;
error?: string;
children: ReactChild;
className?: string;
}) {
const classes = useStyles();
return (
<>
<div className={classes.root}>
<Button
color="primary"
variant="contained"
className={className || classes.button}
disabled={disabled}
onClick={onClick}
>
{children}
</Button>
{showLoader ? (
<CircularProgress
size={24}
color="inherit"
className={className || classes.loader}
/>
) : null}
</div>
{error ? (
<Typography color="error" className={classes.error}>
{error}
</Typography>
) : null}
</>
);
}

View File

@ -0,0 +1,29 @@
import { Typography } from "@material-ui/core";
import React from "react";
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<Typography variant="h5" style={{ textAlign: "center", marginTop: 24 }}>
"An unexpected error has occurred. Please refresh the page."
</Typography>
);
}
return this.props.children;
}
}

View File

@ -0,0 +1,25 @@
import { Typography } from "@material-ui/core";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import ToggleConnectedButton from "./ToggleConnectedButton";
const EthereumSignerKey = () => {
const { connect, disconnect, signerAddress, providerError } =
useEthereumProvider();
return (
<>
<ToggleConnectedButton
connect={connect}
disconnect={disconnect}
connected={!!signerAddress}
pk={signerAddress || ""}
/>
{providerError ? (
<Typography variant="body2" color="error">
{providerError}
</Typography>
) : null}
</>
);
};
export default EthereumSignerKey;

View File

@ -0,0 +1,113 @@
import {
Button,
Dialog,
DialogContent,
DialogTitle,
InputAdornment,
TextField,
} from "@material-ui/core";
import SettingsIcon from "@material-ui/icons/Settings";
import { makeStyles } from "@material-ui/styles";
import { useState } from "react";
const useStyles = makeStyles({
topScrollPaper: {
alignItems: "flex-start",
},
topPaperScrollBody: {
verticalAlign: "top",
},
button: {
float: "right",
"&:hover": {
backgroundColor: "transparent",
},
},
});
const clamp = (value: number, min: number, max: number) => {
if (isNaN(value)) {
return value;
}
return Math.min(Math.max(min, value), max);
};
export default function Settings({
disabled,
slippage,
deadline,
onSlippageChange,
onDeadlineChange,
}: {
disabled: boolean;
slippage: string;
deadline: string;
onSlippageChange: (slippage: string) => void;
onDeadlineChange: (deadline: string) => void;
}) {
const classes = useStyles();
const [dialogIsOpen, setDialogIsOpen] = useState(false);
const dialog = (
<Dialog
open={dialogIsOpen}
aria-labelledby="simple-dialog-title"
onClose={() => setDialogIsOpen(false)}
maxWidth="xs"
scroll="paper"
>
<DialogTitle id="simple-dialog-title">Transaction Settings</DialogTitle>
<DialogContent>
<TextField
variant="outlined"
label="Slippage tolerance"
value={slippage}
fullWidth
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>,
}}
margin="normal"
type="number"
onChange={(event) => {
onSlippageChange(
clamp(parseFloat(event.target.value), 0, 100).toString()
);
}}
></TextField>
<TextField
variant="outlined"
label="Transaction deadline"
value={deadline}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">minutes</InputAdornment>
),
}}
margin="normal"
type="number"
onChange={(event) => {
onDeadlineChange(
clamp(parseFloat(event.target.value), 1, 100).toString()
);
}}
></TextField>
</DialogContent>
</Dialog>
);
return (
<div>
<Button
className={classes.button}
onClick={() => {
setDialogIsOpen(true);
}}
disabled={disabled}
disableRipple
endIcon={<SettingsIcon />}
/>
{dialog}
</div>
);
}

View File

@ -0,0 +1,51 @@
import { Button, makeStyles, Tooltip } from "@material-ui/core";
const useStyles = makeStyles((theme) => ({
button: {
display: "block",
margin: `${theme.spacing(1)}px auto`,
width: "100%",
maxWidth: 400,
},
}));
const ToggleConnectedButton = ({
connect,
disconnect,
connected,
pk,
}: {
connect(): any;
disconnect(): any;
connected: boolean;
pk: string;
}) => {
const classes = useStyles();
const is0x = pk.startsWith("0x");
return connected ? (
<Tooltip title={pk}>
<Button
color="secondary"
variant="contained"
size="small"
onClick={disconnect}
className={classes.button}
>
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
{pk.substr(pk.length - (is0x ? 4 : 3))}
</Button>
</Tooltip>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
className={classes.button}
>
Connect Wallet
</Button>
);
};
export default ToggleConnectedButton;

View File

@ -0,0 +1,63 @@
import {
ListItemIcon,
ListItemText,
makeStyles,
MenuItem,
TextField,
} from "@material-ui/core";
import { TokenInfo } from "../utils/consts";
const useStyles = makeStyles((theme) => ({
select: {
"& .MuiSelect-root": {
display: "flex",
alignItems: "center",
},
},
listItemIcon: {
minWidth: 40,
},
icon: {
height: 24,
maxWidth: 24,
},
}));
const createTokenMenuItem = ({ name, logo }: TokenInfo, classes: any) => (
<MenuItem key={name} value={name}>
<ListItemIcon className={classes.listItemIcon}>
<img src={logo} alt={name} className={classes.icon} />
</ListItemIcon>
<ListItemText>{name}</ListItemText>
</MenuItem>
);
interface TokenSelectProps {
tokens: TokenInfo[];
value: string;
onChange: (event: any) => void;
disabled: boolean;
}
export default function TokenSelect({
tokens,
value,
onChange,
disabled,
}: TokenSelectProps) {
const classes = useStyles();
return (
<TextField
value={value}
onChange={onChange}
select
variant="outlined"
fullWidth
className={classes.select}
disabled={disabled}
>
{tokens.map((token) => createTokenMenuItem(token, classes))}
</TextField>
);
}

View File

@ -0,0 +1,158 @@
import detectEthereumProvider from "@metamask/detect-provider";
import { BigNumber, ethers } from "ethers";
import React, {
ReactChildren,
useCallback,
useContext,
useMemo,
useState,
} from "react";
export type Provider = ethers.providers.Web3Provider | undefined;
export type Signer = ethers.Signer | undefined;
interface IEthereumProviderContext {
connect(): void;
disconnect(): void;
provider: Provider;
chainId: number | undefined;
signer: Signer;
signerAddress: string | undefined;
providerError: string | null;
}
const EthereumProviderContext = React.createContext<IEthereumProviderContext>({
connect: () => {},
disconnect: () => {},
provider: undefined,
chainId: undefined,
signer: undefined,
signerAddress: undefined,
providerError: null,
});
export const EthereumProviderProvider = ({
children,
}: {
children: ReactChildren;
}) => {
const [providerError, setProviderError] = useState<string | null>(null);
const [provider, setProvider] = useState<Provider>(undefined);
const [chainId, setChainId] = useState<number | undefined>(undefined);
const [signer, setSigner] = useState<Signer>(undefined);
const [signerAddress, setSignerAddress] = useState<string | undefined>(
undefined
);
const connect = useCallback(() => {
setProviderError(null);
detectEthereumProvider()
.then((detectedProvider) => {
if (detectedProvider) {
const provider = new ethers.providers.Web3Provider(
// @ts-ignore
detectedProvider,
"any"
);
provider
.send("eth_requestAccounts", [])
.then(() => {
setProviderError(null);
setProvider(provider);
provider
.getNetwork()
.then((network) => {
setChainId(network.chainId);
})
.catch(() => {
setProviderError(
"An error occurred while getting the network"
);
});
const signer = provider.getSigner();
setSigner(signer);
signer
.getAddress()
.then((address) => {
setSignerAddress(address);
})
.catch(() => {
setProviderError(
"An error occurred while getting the signer address"
);
});
// TODO: try using ethers directly
// @ts-ignore
if (detectedProvider && detectedProvider.on) {
// @ts-ignore
detectedProvider.on("chainChanged", (chainId) => {
try {
setChainId(BigNumber.from(chainId).toNumber());
} catch (e) {}
});
// @ts-ignore
detectedProvider.on("accountsChanged", (accounts) => {
try {
const signer = provider.getSigner();
setSigner(signer);
signer
.getAddress()
.then((address) => {
setSignerAddress(address);
})
.catch(() => {
setProviderError(
"An error occurred while getting the signer address"
);
});
} catch (e) {}
});
}
})
.catch(() => {
setProviderError(
"An error occurred while requesting eth accounts"
);
});
} else {
setProviderError("Please install MetaMask");
}
})
.catch(() => {
setProviderError("Please install MetaMask");
});
}, []);
const disconnect = useCallback(() => {
setProviderError(null);
setProvider(undefined);
setChainId(undefined);
setSigner(undefined);
setSignerAddress(undefined);
}, []);
const contextValue = useMemo(
() => ({
connect,
disconnect,
provider,
chainId,
signer,
signerAddress,
providerError,
}),
[
connect,
disconnect,
provider,
chainId,
signer,
signerAddress,
providerError,
]
);
return (
<EthereumProviderContext.Provider value={contextValue}>
{children}
</EthereumProviderContext.Provider>
);
};
export const useEthereumProvider = () => {
return useContext(EthereumProviderContext);
};

View File

@ -0,0 +1,105 @@
import { ChainId, CHAIN_ID_SOLANA, isEVMChain } from "@certusone/wormhole-sdk";
import { hexlify, hexStripZeros } from "@ethersproject/bytes";
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 = "",
forceNetworkSwitch: () => void,
walletAddress?: string
) => ({
isReady,
statusMessage,
forceNetworkSwitch,
walletAddress,
});
function useIsWalletReady(
chainId: ChainId,
enableNetworkAutoswitch: boolean = true
): {
isReady: boolean;
statusMessage: string;
walletAddress?: string;
forceNetworkSwitch: () => void;
} {
const autoSwitch = enableNetworkAutoswitch;
// const solanaWallet = useSolanaWallet();
// const solPK = solanaWallet?.publicKey;
const {
provider,
signerAddress,
chainId: evmChainId,
} = useEthereumProvider();
const hasEthInfo = !!provider && !!signerAddress;
const correctEvmNetwork = getEvmChainId(chainId);
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
const forceNetworkSwitch = useCallback(() => {
if (provider && correctEvmNetwork) {
if (!isEVMChain(chainId)) {
return;
}
try {
provider.send("wallet_switchEthereumChain", [
{ chainId: hexStripZeros(hexlify(correctEvmNetwork)) },
]);
} catch (e) {}
}
}, [provider, correctEvmNetwork, chainId]);
return useMemo(() => {
//if (chainId === CHAIN_ID_SOLANA && solPK) {
// return createWalletStatus(
// true,
// undefined,
// forceNetworkSwitch,
// solPK.toString()
// );
//}
if (isEVMChain(chainId) && hasEthInfo && signerAddress) {
if (hasCorrectEvmNetwork) {
return createWalletStatus(
true,
undefined,
forceNetworkSwitch,
signerAddress
);
} else {
if (provider && correctEvmNetwork && autoSwitch) {
forceNetworkSwitch();
}
return createWalletStatus(
false,
`Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${correctEvmNetwork}`,
forceNetworkSwitch,
undefined
);
}
}
return createWalletStatus(
false,
"Wallet not connected",
forceNetworkSwitch,
undefined
);
}, [
chainId,
autoSwitch,
forceNetworkSwitch,
// solPK,
hasEthInfo,
correctEvmNetwork,
hasCorrectEvmNetwork,
provider,
signerAddress,
]);
}
export default useIsWalletReady;

13
react/src/icons/eth.svg Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.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 1920 1920" enable-background="new 0 0 1920 1920" xml:space="preserve">
<g>
<polygon fill="#8A92B2" points="959.8,80.7 420.1,976.3 959.8,731 "/>
<polygon fill="#62688F" points="959.8,731 420.1,976.3 959.8,1295.4 "/>
<polygon fill="#62688F" points="1499.6,976.3 959.8,80.7 959.8,731 "/>
<polygon fill="#454A75" points="959.8,1295.4 1499.6,976.3 959.8,731 "/>
<polygon fill="#8A92B2" points="420.1,1078.7 959.8,1839.3 959.8,1397.6 "/>
<polygon fill="#62688F" points="959.8,1397.6 959.8,1839.3 1499.9,1078.7 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 807 B

View File

@ -0,0 +1,16 @@
<?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 38.4 33.5" style="enable-background:new 0 0 38.4 33.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#8247E5;}
</style>
<g>
<path class="st0" d="M29,10.2c-0.7-0.4-1.6-0.4-2.4,0L21,13.5l-3.8,2.1l-5.5,3.3c-0.7,0.4-1.6,0.4-2.4,0L5,16.3
c-0.7-0.4-1.2-1.2-1.2-2.1v-5c0-0.8,0.4-1.6,1.2-2.1l4.3-2.5c0.7-0.4,1.6-0.4,2.4,0L16,7.2c0.7,0.4,1.2,1.2,1.2,2.1v3.3l3.8-2.2V7
c0-0.8-0.4-1.6-1.2-2.1l-8-4.7c-0.7-0.4-1.6-0.4-2.4,0L1.2,5C0.4,5.4,0,6.2,0,7v9.4c0,0.8,0.4,1.6,1.2,2.1l8.1,4.7
c0.7,0.4,1.6,0.4,2.4,0l5.5-3.2l3.8-2.2l5.5-3.2c0.7-0.4,1.6-0.4,2.4,0l4.3,2.5c0.7,0.4,1.2,1.2,1.2,2.1v5c0,0.8-0.4,1.6-1.2,2.1
L29,28.8c-0.7,0.4-1.6,0.4-2.4,0l-4.3-2.5c-0.7-0.4-1.2-1.2-1.2-2.1V21l-3.8,2.2v3.3c0,0.8,0.4,1.6,1.2,2.1l8.1,4.7
c0.7,0.4,1.6,0.4,2.4,0l8.1-4.7c0.7-0.4,1.2-1.2,1.2-2.1V17c0-0.8-0.4-1.6-1.2-2.1L29,10.2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<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>

After

Width:  |  Height:  |  Size: 993 B

23
react/src/index.js Normal file
View File

@ -0,0 +1,23 @@
import { CssBaseline } from "@material-ui/core";
import { ThemeProvider } from "@material-ui/core/styles";
import { SnackbarProvider } from "notistack";
import ReactDOM from "react-dom";
import App from "./App";
import ErrorBoundary from "./components/ErrorBoundary";
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
import { theme } from "./muiTheme";
ReactDOM.render(
<ErrorBoundary>
<ThemeProvider theme={theme}>
<CssBaseline>
<EthereumProviderProvider>
<SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
</EthereumProviderProvider>
</CssBaseline>
</ThemeProvider>
</ErrorBoundary>,
document.getElementById("root")
);

160
react/src/muiTheme.js Normal file
View File

@ -0,0 +1,160 @@
import { createTheme, responsiveFontSizes } from "@material-ui/core";
export const COLORS = {
blue: "#1975e6",
blueWithTransparency: "rgba(25, 117, 230, 0.8)",
gray: "#4e4e54",
green: "#0ac2af",
greenWithTransparency: "rgba(10, 194, 175, 0.8)",
lightGreen: "rgba(51, 242, 223, 1)",
lightBlue: "#83b9fc",
nearBlack: "#000008",
nearBlackWithMinorTransparency: "rgba(0,0,0,.25)",
red: "#aa0818",
darkRed: "#810612",
};
export const theme = responsiveFontSizes(
createTheme({
palette: {
type: "dark",
background: {
default: COLORS.nearBlack,
paper: COLORS.nearBlack,
},
divider: COLORS.gray,
text: {
primary: "rgba(255,255,255,0.98)",
},
primary: {
main: COLORS.blueWithTransparency,
light: COLORS.lightBlue,
},
secondary: {
main: COLORS.greenWithTransparency,
light: COLORS.lightGreen,
},
error: {
main: COLORS.red,
},
},
typography: {
fontFamily: "'Sora', sans-serif",
h1: {
fontWeight: "200",
},
h2: {
fontWeight: "200",
},
h4: {
fontWeight: "500",
},
},
overrides: {
MuiCssBaseline: {
"@global": {
"*": {
scrollbarWidth: "thin",
scrollbarColor: `${COLORS.gray} ${COLORS.nearBlackWithMinorTransparency}`,
},
"*::-webkit-scrollbar": {
width: "8px",
height: "8px",
backgroundColor: COLORS.nearBlackWithMinorTransparency,
},
"*::-webkit-scrollbar-thumb": {
backgroundColor: COLORS.gray,
borderRadius: "4px",
},
"*::-webkit-scrollbar-corner": {
// this hides an annoying white box which appears when both scrollbars are present
backgroundColor: "transparent",
},
},
},
MuiAccordion: {
root: {
backgroundColor: COLORS.nearBlackWithMinorTransparency,
"&:before": {
display: "none",
},
},
rounded: {
"&:first-child": {
borderTopLeftRadius: "16px",
borderTopRightRadius: "16px",
},
"&:last-child": {
borderBottomLeftRadius: "16px",
borderBottomRightRadius: "16px",
},
},
},
MuiAlert: {
root: {
borderRadius: "8px",
border: "1px solid",
},
},
MuiButton: {
root: {
borderRadius: "5px",
textTransform: "none",
},
},
MuiLink: {
root: {
color: COLORS.lightBlue,
},
},
MuiPaper: {
rounded: {
borderRadius: "16px",
},
},
MuiStepper: {
root: {
backgroundColor: "transparent",
padding: 0,
},
},
MuiStep: {
root: {
backgroundColor: COLORS.nearBlackWithMinorTransparency,
borderRadius: "16px",
padding: 16,
},
},
MuiStepConnector: {
lineVertical: {
borderLeftWidth: 0,
},
},
MuiStepContent: {
root: {
borderLeftWidth: 0,
},
},
MuiStepLabel: {
label: {
fontSize: 16,
fontWeight: "300",
"&.MuiStepLabel-active": {
fontWeight: "300",
},
"&.MuiStepLabel-completed": {
fontWeight: "300",
},
},
},
MuiTab: {
root: {
fontSize: 18,
fontWeight: "300",
padding: 12,
textTransform: "none",
},
},
},
})
);

1
react/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,322 @@
import { ethers } from "ethers";
import { UniEvmToken } from "./uniswap-core";
import { QuickswapRouter } from "./quickswap";
import { SingleAmmSwapRouter as UniswapV3Router } from "./uniswap-v3";
import {
ETH_NETWORK_CHAIN_ID,
POLYGON_NETWORK_CHAIN_ID,
} from "../utils/consts";
export { PROTOCOL as PROTOCOL_UNISWAP_V2 } from "./uniswap-v2";
export { PROTOCOL as PROTOCOL_UNISWAP_V3 } from "./uniswap-v3";
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);
}
case POLYGON_NETWORK_CHAIN_ID: {
return new QuickswapRouter(provider);
}
default: {
throw Error("unrecognized chain id");
}
}
}
export function getUstAddress(id: number): string {
switch (id) {
case ETH_NETWORK_CHAIN_ID: {
return "0x36Ed51Afc79619b299b238898E72ce482600568a";
}
case POLYGON_NETWORK_CHAIN_ID: {
return "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c";
}
default: {
throw Error("unrecognized chain id");
}
}
}
function splitSlippageInHalf(totalSlippage: string): string {
const divisor = ethers.FixedNumber.from("2");
return ethers.FixedNumber.from(totalSlippage)
.divUnsafe(divisor)
.round(4)
.toString();
}
interface RelayerFee {
amount: ethers.BigNumber;
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;
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;
relayerFee: RelayerFee;
}
export class UniswapToUniswapQuoter {
// providers
srcProvider: ethers.providers.Provider;
dstProvider: ethers.providers.Provider;
// networks
srcNetwork: ethers.providers.Network;
dstNetwork: ethers.providers.Network;
// routers
srcRouter: UniswapV3Router | QuickswapRouter;
dstRouter: UniswapV3Router | QuickswapRouter;
// tokens
srcTokenIn: UniEvmToken;
srcTokenOut: UniEvmToken;
dstTokenIn: UniEvmToken;
dstTokenOut: UniEvmToken;
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];
}
async computeAndVerifySrcPoolAddress(): Promise<string> {
return this.srcRouter.computeAndVerifyPoolAddress(
this.srcTokenIn,
this.srcTokenOut
);
}
async computeAndVerifyDstPoolAddress(): Promise<string> {
return this.dstRouter.computeAndVerifyPoolAddress(
this.dstTokenIn,
this.dstTokenOut
);
}
async computeExactInParameters(
amountIn: string,
slippage: string,
relayerFeeUst: string
): Promise<ExactInCrossParameters> {
const singleSlippage = splitSlippageInHalf(slippage);
// src quote
const srcRouter = this.srcRouter;
const srcTokenIn = this.srcTokenIn;
const srcTokenOut = this.srcTokenOut;
const srcMinAmountOut = await srcRouter.fetchQuoteAmountOut(
srcTokenIn,
srcTokenOut,
amountIn,
singleSlippage
);
// dst quote
const dstRouter = this.dstRouter;
const dstAmountIn = this.srcTokenOut.formatAmount(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(
dstAmountIn,
relayerFeeUst
);
const dstMinAmountOut = await dstRouter.fetchQuoteAmountOut(
dstTokenIn,
dstTokenOut,
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()],
};
const params: ExactInCrossParameters = {
src: srcParameters,
dst: dstParameters,
relayerFee: {
amount: dstTokenIn.computeUnitAmount(relayerFeeUst),
tokenAddress: this.dstTokenIn.getAddress(),
},
};
return params;
}
async computeExactOutParameters(
amountOut: string,
slippage: string,
relayerFeeUst: string
): Promise<ExactOutCrossParameters> {
const singleSlippage = splitSlippageInHalf(slippage);
// dst quote first
const dstRouter = this.dstRouter;
const dstTokenIn = this.dstTokenIn;
const dstTokenOut = this.dstTokenOut;
const dstMaxAmountIn = await dstRouter.fetchQuoteAmountIn(
dstTokenIn,
dstTokenOut,
amountOut,
singleSlippage
);
// src quote
const srcRouter = this.srcRouter;
const srcAmountOut = this.dstTokenIn.formatAmount(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(
srcAmountOut,
relayerFeeUst
);
const srcMaxAmountIn = await srcRouter.fetchQuoteAmountIn(
srcTokenIn,
srcTokenOut,
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()],
};
const params: ExactOutCrossParameters = {
src: srcParameters,
dst: dstParameters,
relayerFee: {
amount: dstTokenIn.computeUnitAmount(relayerFeeUst),
tokenAddress: this.dstTokenIn.getAddress(),
},
};
return params;
}
setDeadlines(deadline: string): void {
this.srcRouter.setDeadline(deadline);
this.dstRouter.setDeadline(deadline);
return;
}
estimateUstFee(gasPriceInNativeCurrency: string): string {
return "0";
}
}

105
react/src/route/evm.ts Normal file
View File

@ -0,0 +1,105 @@
import { ethers } from "ethers";
import { GenericToken } from "./generic";
// erc20 spec
import { abi as Erc20Abi } from "../abi/erc20.json";
import {
TransactionReceipt,
TransactionRequest,
TransactionResponse,
} from "@ethersproject/abstract-provider";
import { APPROVAL_GAS_LIMIT } from "../utils/consts";
export class EvmToken extends GenericToken {
token: ethers.Contract;
decimals: number;
async initialize(provider: ethers.providers.Provider, tokenAddress: string) {
this.token = await makeErc20Contract(provider, tokenAddress);
this.decimals = await this.token.decimals();
}
static async create(
provider: ethers.providers.Provider,
tokenAddress: string
): Promise<EvmToken> {
const o = new EvmToken();
await o.initialize(provider, tokenAddress);
return o;
}
getAddress(): string {
return this.token.address;
}
getDecimals(): number {
return this.decimals;
}
getContract(): ethers.Contract {
return this.token;
}
async getBalanceOf(signer: ethers.Wallet) {
const decimals = this.getDecimals();
const balanceBeforeDecimals = await this.token.balanceOf(signer.address);
return ethers.utils.formatUnits(balanceBeforeDecimals.toString(), decimals);
}
computeUnitAmount(amount: string): ethers.BigNumber {
return ethers.utils.parseUnits(amount, this.getDecimals());
}
formatAmount(unitAmount: ethers.BigNumber): string {
return ethers.utils.formatUnits(unitAmount, this.getDecimals());
}
addAmounts(left: string, right: string): string {
const sum = ethers.FixedNumber.from(left).addUnsafe(
ethers.FixedNumber.from(right)
);
return sum.round(this.getDecimals()).toString();
}
subtractAmounts(left: string, right: string): string {
const sum = ethers.FixedNumber.from(left).subUnsafe(
ethers.FixedNumber.from(right)
);
return sum.round(this.getDecimals()).toString();
}
}
export async function makeErc20Contract(
provider: ethers.providers.Provider,
tokenAddress: string
): Promise<ethers.Contract> {
return new ethers.Contract(tokenAddress, Erc20Abi, provider);
}
export async function approveContractTokenSpend(
provider: ethers.providers.Provider,
signer: ethers.Wallet,
tokenContract: ethers.Contract,
smartContractAddress: string,
swapAmount: ethers.BigNumber
): Promise<TransactionReceipt> {
// build transaction for token spending
const unsignedTx: TransactionRequest =
await tokenContract.populateTransaction.approve(
smartContractAddress,
swapAmount
);
const nonce = await provider.getTransactionCount(signer.address, "latest");
const gasPrice = await signer.getGasPrice();
const parsedGasPrice = ethers.utils.hexlify(parseInt(gasPrice.toString()));
unsignedTx.nonce = nonce;
unsignedTx.gasLimit = ethers.BigNumber.from(APPROVAL_GAS_LIMIT);
unsignedTx.gasPrice = ethers.BigNumber.from(parsedGasPrice);
// sign and send transaction
const tx: TransactionResponse = await signer.sendTransaction(unsignedTx);
return tx.wait();
}

View File

@ -0,0 +1,24 @@
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 abstract class GenericToken {
abstract getAddress(): string;
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,12 @@
import { ethers } from "ethers";
import { QUICKSWAP_FACTORY_ADDRESS } from "../utils/consts";
import { SingleAmmSwapRouter } from "./uniswap-v2";
export { PROTOCOL } from "./uniswap-v2";
export class QuickswapRouter extends SingleAmmSwapRouter {
constructor(provider: ethers.providers.Provider) {
super(provider);
super.setFactoryAddress(QUICKSWAP_FACTORY_ADDRESS);
}
}

View File

@ -0,0 +1,133 @@
import { ethers } from "ethers";
import { CurrencyAmount, Token } from "@uniswap/sdk-core";
import { EvmToken } from "./evm";
export function computeTradeDeadline(deadline: string): ethers.BigNumber {
return ethers.BigNumber.from(Math.floor(Date.now() / 1000)).add(deadline);
}
export class UniEvmToken {
erc20: EvmToken;
uniToken: Token;
constructor(chainId: number, erc20: EvmToken) {
this.erc20 = erc20;
const address = this.getAddress();
const decimals = this.getDecimals();
this.uniToken = new Token(chainId, address, decimals);
}
getUniToken(): Token {
return this.uniToken;
}
getEvmToken(): EvmToken {
return this.erc20;
}
getDecimals(): number {
return this.erc20.getDecimals();
}
getContract(): ethers.Contract {
return this.erc20.getContract();
}
getAddress(): string {
return this.erc20.getAddress();
}
async getBalanceOf(signer: ethers.Wallet) {
return this.erc20.getBalanceOf(signer);
}
computeUnitAmount(amount: string): ethers.BigNumber {
return this.erc20.computeUnitAmount(amount);
}
formatAmount(unitAmount: ethers.BigNumber): string {
return this.erc20.formatAmount(unitAmount);
}
computeCurrencyAmount(amount: string): CurrencyAmount<Token> {
const unitAmount = this.computeUnitAmount(amount);
return CurrencyAmount.fromRawAmount(
this.getUniToken(),
unitAmount.toString()
);
}
addAmounts(left: string, right: string): string {
return this.erc20.addAmounts(left, right);
}
subtractAmounts(left: string, right: string): string {
return this.erc20.subtractAmounts(left, right);
}
}
export async function makeUniEvmToken(
provider: ethers.providers.Provider,
chainId: number,
tokenAddress: string
): Promise<UniEvmToken> {
const erc20 = await EvmToken.create(provider, tokenAddress);
return new UniEvmToken(chainId, erc20);
}
export abstract class UniswapRouterCore {
provider: ethers.providers.Provider;
// params
deadline: string = "";
constructor(provider: ethers.providers.Provider) {
this.provider = provider;
}
public async makeToken(tokenAddress: string): Promise<UniEvmToken> {
const network = await this.provider.getNetwork();
return makeUniEvmToken(this.provider, network.chainId, tokenAddress);
}
abstract computePoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): string;
abstract computeAndVerifyPoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): Promise<string>;
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;
public getPoolFee(): string {
return "";
}
public setDeadline(deadline: string): void {
this.deadline = deadline;
}
public getTradeDeadline(): ethers.BigNumber {
return computeTradeDeadline(this.deadline);
}
}

View File

@ -0,0 +1,151 @@
import { ethers } from "ethers";
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";
export const PROTOCOL = "UniswapV2";
export class SingleAmmSwapRouter extends UniswapRouterCore {
factoryAddress: string;
pairContract: ethers.Contract;
pair: Pair;
setFactoryAddress(factoryAddress: string) {
this.factoryAddress = factoryAddress;
return;
}
computePoolAddress(tokenIn: UniEvmToken, tokenOut: UniEvmToken): string {
if (this.factoryAddress === undefined) {
throw Error("factoryAddress is undefined. use setFactoryAddress");
}
return computePairAddress({
factoryAddress: this.factoryAddress,
tokenA: tokenIn.getUniToken(),
tokenB: tokenOut.getUniToken(),
});
}
async computeAndVerifyPoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): Promise<string> {
const pairAddress = this.computePoolAddress(tokenIn, tokenOut);
// verify by attempting to call factory()
const poolContract = new ethers.Contract(
pairAddress,
IUniswapV2PairABI,
this.provider
);
await poolContract.factory();
return pairAddress;
}
async createPool(tokenIn: UniEvmToken, tokenOut: UniEvmToken): Promise<Pair> {
const pairAddress = this.computePoolAddress(tokenIn, tokenOut);
const pairContract = new ethers.Contract(
pairAddress,
IUniswapV2PairABI,
this.provider
);
const [token0, reserves] = await Promise.all([
pairContract.token0(),
pairContract.getReserves(),
]);
const reserve0 = reserves._reserve0.toString();
const reserve1 = reserves._reserve1.toString();
if (token0.toLowerCase() === tokenIn.getAddress().toLowerCase()) {
return new Pair(
CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), reserve0),
CurrencyAmount.fromRawAmount(tokenOut.getUniToken(), reserve1)
);
}
return new Pair(
CurrencyAmount.fromRawAmount(tokenOut.getUniToken(), reserve0),
CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), reserve1)
);
}
async fetchQuoteAmountOut(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountIn: string,
slippage: string
): Promise<ethers.BigNumber> {
// create pool
const pair = await this.createPool(tokenIn, tokenOut);
// let's get that quote
const route = new Route(
[pair],
tokenIn.getUniToken(),
tokenOut.getUniToken()
);
const currencyAmountIn = tokenIn.computeCurrencyAmount(amountIn);
const quote = new Trade(route, currencyAmountIn, TradeType.EXACT_INPUT);
const decimals = tokenOut.getDecimals();
const minAmountOut = ethers.FixedNumber.from(
quote.outputAmount.toSignificant(decimals)
);
// calculate output amount with slippage
const slippageMultiplier = ethers.FixedNumber.from("1").subUnsafe(
ethers.FixedNumber.from(slippage)
);
const minAmountOutWithSlippage = minAmountOut
.mulUnsafe(slippageMultiplier)
.round(decimals);
return tokenOut.computeUnitAmount(minAmountOutWithSlippage.toString());
}
async fetchQuoteAmountIn(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountOut: string,
slippage: string
): Promise<ethers.BigNumber> {
// create pool
const pair = await this.createPool(tokenIn, tokenOut);
// let's get that quote
const route = new Route(
[pair],
tokenIn.getUniToken(),
tokenOut.getUniToken()
);
const currencyAmountOut = tokenOut.computeCurrencyAmount(amountOut);
const quote = new Trade(route, currencyAmountOut, TradeType.EXACT_OUTPUT);
const decimals = tokenIn.getDecimals();
const maxAmountIn = ethers.FixedNumber.from(
quote.inputAmount.toSignificant(decimals)
);
const slippageDivisor = ethers.FixedNumber.from("1").subUnsafe(
ethers.FixedNumber.from(slippage)
);
const maxAmountInWithSlippage = maxAmountIn
.divUnsafe(slippageDivisor)
.round(decimals);
return tokenIn.computeUnitAmount(maxAmountInWithSlippage.toString());
}
getProtocol(): string {
return PROTOCOL;
}
}

View File

@ -0,0 +1,218 @@
import { ethers } from "ethers";
import JSBI from "jsbi";
import { CurrencyAmount, Token, TradeType } from "@uniswap/sdk-core";
import { abi as IUniswapV3PoolABI } from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json";
import {
computePoolAddress,
FeeAmount,
nearestUsableTick,
Pool,
Route,
TickMath,
TICK_SPACINGS,
Trade,
} from "@uniswap/v3-sdk";
import { UniEvmToken, UniswapRouterCore } from "./uniswap-core";
import { UNISWAP_V3_FACTORY_ADDRESS } from "../utils/consts";
export const PROTOCOL = "UniswapV3";
export class SingleAmmSwapRouter extends UniswapRouterCore {
poolContract: ethers.Contract;
pool: Pool;
poolFee: FeeAmount;
constructor(provider: ethers.providers.Provider) {
super(provider);
// set fee amount for our example
this.poolFee = FeeAmount.MEDIUM;
}
getPoolFee(): string {
return this.poolFee.toString();
}
computePoolAddress(tokenIn: UniEvmToken, tokenOut: UniEvmToken): string {
return computePoolAddress({
factoryAddress: UNISWAP_V3_FACTORY_ADDRESS,
fee: this.poolFee,
tokenA: tokenIn.getUniToken(),
tokenB: tokenOut.getUniToken(),
});
}
async computeAndVerifyPoolAddress(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken
): Promise<string> {
const pairAddress = this.computePoolAddress(tokenIn, tokenOut);
// verify by attempting to call factory()
const poolContract = new ethers.Contract(
pairAddress,
IUniswapV3PoolABI,
this.provider
);
await poolContract.factory();
return pairAddress;
}
async createPool(tokenIn: UniEvmToken, tokenOut: UniEvmToken): Promise<Pool> {
const poolAddress = this.computePoolAddress(tokenIn, tokenOut);
const poolContract = new ethers.Contract(
poolAddress,
IUniswapV3PoolABI,
this.provider
);
this.poolContract = poolContract;
const [liquidity, slot] = await Promise.all([
poolContract.liquidity(),
poolContract.slot0(),
]);
// grab necessary data from slot
const sqrtPriceX96 = slot[0];
const tick = slot[1];
// create JSBI version of liquidity numbers
const bigLiq = JSBI.BigInt(liquidity);
const negBigLiq = JSBI.multiply(bigLiq, JSBI.BigInt(-1));
const tickConstructorArgs = [
{
index: nearestUsableTick(
TickMath.MIN_TICK,
TICK_SPACINGS[this.poolFee]
),
liquidityNet: bigLiq,
liquidityGross: bigLiq,
},
{
index: nearestUsableTick(
TickMath.MAX_TICK,
TICK_SPACINGS[this.poolFee]
),
liquidityNet: negBigLiq,
liquidityGross: bigLiq,
},
];
return new Pool(
tokenIn.getUniToken(),
tokenOut.getUniToken(),
this.poolFee,
sqrtPriceX96.toString(), //note the description discrepancy - sqrtPriceX96 and sqrtRatioX96 are interchangable values
liquidity,
tick,
tickConstructorArgs
);
}
async computeTradeExactIn(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amount: string
): Promise<Trade<Token, Token, TradeType.EXACT_INPUT>> {
// create pool
const pool = await this.createPool(tokenIn, tokenOut);
// let's get that quote
const amountIn = tokenIn.computeUnitAmount(amount);
const route = new Route(
[pool],
tokenIn.getUniToken(),
tokenOut.getUniToken()
);
return Trade.fromRoute(
route,
CurrencyAmount.fromRawAmount(tokenIn.getUniToken(), amountIn.toString()),
TradeType.EXACT_INPUT
);
}
async computeTradeExactOut(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amount: string
): Promise<Trade<Token, Token, TradeType.EXACT_OUTPUT>> {
// create pool
const pool = await this.createPool(tokenIn, tokenOut);
// let's get that quote
const amountOut = tokenOut.computeUnitAmount(amount);
const route = new Route(
[pool],
tokenIn.getUniToken(),
tokenOut.getUniToken()
);
return Trade.fromRoute(
route,
CurrencyAmount.fromRawAmount(
tokenOut.getUniToken(),
amountOut.toString()
),
TradeType.EXACT_OUTPUT
);
}
async fetchQuoteAmountOut(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountIn: string,
slippage: string
): Promise<ethers.BigNumber> {
// get the quote
const trade = await this.computeTradeExactIn(tokenIn, tokenOut, amountIn);
const decimals = tokenOut.getDecimals();
// calculate output amount with slippage
const minAmountOut = ethers.FixedNumber.from(
trade.outputAmount.toSignificant(decimals)
);
const slippageMultiplier = ethers.FixedNumber.from("1").subUnsafe(
ethers.FixedNumber.from(slippage)
);
const minAmountOutWithSlippage = minAmountOut
.mulUnsafe(slippageMultiplier)
.round(decimals);
return tokenOut.computeUnitAmount(minAmountOutWithSlippage.toString());
}
async fetchQuoteAmountIn(
tokenIn: UniEvmToken,
tokenOut: UniEvmToken,
amountOut: string,
slippage: string
): Promise<ethers.BigNumber> {
// get the quote
const trade = await this.computeTradeExactOut(tokenIn, tokenOut, amountOut);
const decimals = tokenIn.getDecimals();
// calculate output amount with slippage
const maxAmountIn = ethers.FixedNumber.from(
trade.inputAmount.toSignificant(decimals)
);
const slippageDivisor = ethers.FixedNumber.from("1").subUnsafe(
ethers.FixedNumber.from(slippage)
);
const maxAmountInWithSlippage = maxAmountIn
.divUnsafe(slippageDivisor)
.round(decimals);
return tokenIn.computeUnitAmount(maxAmountInWithSlippage.toString());
}
getProtocol(): string {
return PROTOCOL;
}
}

View File

@ -0,0 +1,585 @@
//@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,
getEmitterAddressEth,
hexToUint8Array,
nativeToHexString,
parseSequenceFromLogEth,
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,
ExactInCrossParameters,
ExactOutCrossParameters,
QuoteType,
UniswapToUniswapQuoter,
} from "../route/cross-quote";
import {
TOKEN_BRIDGE_ADDRESS_POLYGON,
CORE_BRIDGE_ADDRESS_ETHEREUM,
CORE_BRIDGE_ADDRESS_POLYGON,
TOKEN_BRIDGE_ADDRESS_ETHEREUM,
WORMHOLE_RPC_HOSTS,
POLYGON_NETWORK_CHAIN_ID,
ETH_NETWORK_CHAIN_ID,
WETH_TOKEN_INFO,
WMATIC_TOKEN_INFO,
} from "../utils/consts";
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";
interface SwapContractParameters {
address: string;
}
interface WormholeParameters {
chainId: ChainId;
coreBridgeAddress: string;
tokenBridgeAddress: string;
}
interface ExecutionParameters {
crossChainSwap: SwapContractParameters;
wormhole: WormholeParameters;
}
const EXECUTION_PARAMETERS_ETHEREUM: ExecutionParameters = {
crossChainSwap: {
address: CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM,
},
wormhole: {
chainId: WORMHOLE_CHAIN_ID_ETHEREUM,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_ETHEREUM,
tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_ETHEREUM,
},
};
const EXECUTION_PARAMETERS_POLYGON: ExecutionParameters = {
crossChainSwap: {
address: CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON,
},
wormhole: {
chainId: WORMHOLE_CHAIN_ID_POLYGON,
coreBridgeAddress: CORE_BRIDGE_ADDRESS_POLYGON,
tokenBridgeAddress: TOKEN_BRIDGE_ADDRESS_POLYGON,
},
};
const CROSSCHAINSWAP_GAS_PARAMETERS_UNISWAP_V3 = {
gasLimit: "550000",
maxFeePerGas: "250000000000",
maxPriorityFeePerGas: "1690000000",
};
const CROSSCHAINSWAP_GAS_PARAMETERS_UNISWAP_V2 = {
gasLimit: "350000",
maxFeePerGas: "250000000000",
maxPriorityFeePerGas: "1690000000",
};
function makeExecutionParameters(id: number): ExecutionParameters {
switch (id) {
case ETH_NETWORK_CHAIN_ID: {
return EXECUTION_PARAMETERS_ETHEREUM;
}
case POLYGON_NETWORK_CHAIN_ID: {
return EXECUTION_PARAMETERS_POLYGON;
}
default: {
throw Error("unrecognized chain id");
}
}
}
async function approveContractTokenSpend(
provider: ethers.providers.Provider,
signer: ethers.Signer,
tokenContract: ethers.Contract,
swapContractAddress: string,
amount: ethers.BigNumber
): Promise<TransactionReceipt> {
// build transaction for token spending
const unsignedTx = await tokenContract.populateTransaction.approve(
swapContractAddress,
amount
);
// TODO: pass this in?
const address = await signer.getAddress();
console.log("address", address);
console.log("signer", signer);
// gas calcs
const gas_limit = "0x100000";
const gasPrice = await signer.getGasPrice();
const parsedGasPrice = ethers.utils.hexlify(parseInt(gasPrice.toString()));
console.log("gettingTranscationCount", provider);
unsignedTx.nonce = await provider.getTransactionCount(address, "latest");
unsignedTx.gasLimit = ethers.BigNumber.from(ethers.utils.hexlify(gas_limit));
unsignedTx.gasPrice = ethers.BigNumber.from(parsedGasPrice);
console.log("done gettingTranscationCount");
// sign and send transaction
const tx = await signer.sendTransaction(unsignedTx);
return tx.wait();
}
function makeCrossChainSwapV3Contract(
contractAddress: string,
provider: ethers.providers.Provider
): ethers.Contract {
return new ethers.Contract(contractAddress, SWAP_CONTRACT_V3_ABI, provider);
}
function makeCrossChainSwapV2Contract(
contractAddress: string,
provider: ethers.providers.Provider
): ethers.Contract {
return new ethers.Contract(contractAddress, SWAP_CONTRACT_V2_ABI, provider);
}
function makeCrossChainSwapContract(
provider: ethers.providers.Provider,
protocol: string,
contractAddress: string
): ethers.Contract {
if (protocol === PROTOCOL_UNISWAP_V2) {
return makeCrossChainSwapV2Contract(contractAddress, provider);
} else {
return makeCrossChainSwapV3Contract(contractAddress, provider);
}
}
function addressToBytes32(
address: string,
wormholeChainId: ChainId
): Uint8Array {
return hexToUint8Array(nativeToHexString(address, wormholeChainId));
}
async function approveAndSwapExactIn(
srcProvider: ethers.providers.Provider,
srcWallet: ethers.Signer,
srcTokenIn: UniEvmToken,
quoteParams: ExactInCrossParameters,
srcExecutionParams: ExecutionParameters,
dstExecutionParams: ExecutionParameters
): Promise<TransactionReceipt> {
const swapContractParams = srcExecutionParams.crossChainSwap;
const protocol = quoteParams.src.protocol;
const swapContract = makeCrossChainSwapContract(
srcProvider,
protocol,
swapContractParams.address
);
const contractWithSigner = swapContract.connect(srcWallet);
// approve and swap this amount
const amountIn = quoteParams.src.amountIn;
// approve swap contract to spend our tokens
console.info("approving contract to spend token in");
await approveContractTokenSpend(
srcProvider,
srcWallet,
srcTokenIn.getContract(),
swapContract.address,
amountIn
);
const address = await srcWallet.getAddress();
const swapParams = [
amountIn,
quoteParams.src.minAmountOut,
quoteParams.dst.minAmountOut,
// srcWallet.address,
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 dstContractAddress = addressToBytes32(
dstExecutionParams.crossChainSwap.address,
dstWormholeChainId
);
const bridgeNonce = 69;
// do the swap
if (protocol === PROTOCOL_UNISWAP_V2) {
console.info("swapExactInToV3");
const tx = await contractWithSigner.swapExactInToV3(
swapParams,
pathArray,
quoteParams.relayerFee.amount,
dstWormholeChainId,
dstContractAddress,
bridgeNonce,
CROSSCHAINSWAP_GAS_PARAMETERS_UNISWAP_V2
);
return tx.wait();
} else {
console.info("swapExactInToV2");
const tx = await contractWithSigner.swapExactInToV2(
swapParams,
pathArray,
quoteParams.relayerFee.amount,
dstWormholeChainId,
dstContractAddress,
bridgeNonce,
CROSSCHAINSWAP_GAS_PARAMETERS_UNISWAP_V3
);
return tx.wait();
}
}
async function swapExactInFromVaa(
dstProvider: ethers.providers.Provider,
dstWallet: ethers.Signer,
dstExecutionParams: ExecutionParameters,
dstProtocol: string,
signedVAA: Uint8Array
): Promise<TransactionReceipt> {
const swapContractParams = dstExecutionParams.crossChainSwap;
const swapContract = makeCrossChainSwapContract(
dstProvider,
dstProtocol,
swapContractParams.address
);
const contractWithSigner = swapContract.connect(dstWallet);
if (dstProtocol === PROTOCOL_UNISWAP_V3) {
console.info("swapExactInFromV2");
const tx = await contractWithSigner.swapExactInFromV2(
signedVAA,
CROSSCHAINSWAP_GAS_PARAMETERS_UNISWAP_V3
);
return tx.wait();
} else {
console.info("swapExactInFromV3");
const tx = await contractWithSigner.swapExactInFromV3(
signedVAA,
CROSSCHAINSWAP_GAS_PARAMETERS_UNISWAP_V2
);
return tx.wait();
}
}
interface CrossChainSwapTokens {
srcIn: UniEvmToken;
srcOut: UniEvmToken;
dstIn: UniEvmToken;
dstOut: UniEvmToken;
}
interface VaaSearchParams {
sequence: string;
emitterAddress: string;
}
export function makeProvider(tokenAddress: string) {
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: {
throw Error("unrecognized token address");
}
}
}
export class UniswapToUniswapExecutor {
// quoting
quoter: UniswapToUniswapQuoter;
cachedExactInParams: ExactInCrossParameters;
cachedExactOutParams: ExactOutCrossParameters;
quoteType: QuoteType;
tokens: CrossChainSwapTokens;
// swapping
slippage: string;
relayerFeeAmount: string;
srcExecutionParams: ExecutionParameters;
dstExecutionParams: ExecutionParameters;
// vaa handling
transportFactory: grpc.TransportFactory;
vaaSearchParams: VaaSearchParams;
vaaBytes: Uint8Array;
srcReceipt: TransactionReceipt;
dstReceipt: TransactionReceipt;
async initialize(
tokenInAddress: string,
tokenOutAddress: string
): Promise<void> {
this.clearState();
const srcProvider = makeProvider(tokenInAddress);
const dstProvider = makeProvider(tokenOutAddress);
this.quoter = new UniswapToUniswapQuoter(srcProvider, dstProvider);
await this.quoter.initialize();
await this.makeTokens(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.dstExecutionParams = makeExecutionParameters(
this.quoter.dstNetwork.chainId
);
}
setSlippage(slippage: string): void {
this.slippage = slippage;
}
setRelayerFee(amount: string): void {
this.relayerFeeAmount = amount;
}
areSwapParametersUndefined(): boolean {
return this.slippage === undefined || this.relayerFeeAmount === undefined;
}
setDeadlines(deadline: string): void {
this.quoter.setDeadlines(deadline);
}
async makeTokens(
tokenInAddress: string,
tokenOutAddress: string
): Promise<void> {
const quoter = this.quoter;
const [srcTokenIn, srcTokenOut] = await quoter.makeSrcTokens(
tokenInAddress
);
const [dstTokenIn, dstTokenOut] = await quoter.makeDstTokens(
tokenOutAddress
);
this.tokens = {
srcIn: srcTokenIn,
srcOut: srcTokenOut,
dstIn: dstTokenIn,
dstOut: dstTokenOut,
};
}
getTokens(): CrossChainSwapTokens {
return this.tokens;
}
async computeAndVerifySrcPoolAddress(): Promise<string> {
return this.quoter.computeAndVerifySrcPoolAddress();
}
async computeAndVerifyDstPoolAddress(): Promise<string> {
return this.quoter.computeAndVerifyDstPoolAddress();
}
async computeQuoteExactIn(amountIn: string): Promise<ExactInCrossParameters> {
if (this.areSwapParametersUndefined()) {
throw Error("undefined swap parameters");
}
this.clearCachedParams();
this.cachedExactInParams = await this.quoter.computeExactInParameters(
amountIn,
this.slippage,
this.relayerFeeAmount
);
this.quoteType = QuoteType.ExactIn;
return this.cachedExactInParams;
}
async computeQuoteExactOut(
amountOut: string
): Promise<ExactOutCrossParameters> {
if (this.areSwapParametersUndefined()) {
throw Error("undefined swap parameters");
}
this.clearCachedParams();
this.cachedExactOutParams = await this.quoter.computeExactOutParameters(
amountOut,
this.slippage,
this.relayerFeeAmount
);
this.quoteType = QuoteType.ExactOut;
return this.cachedExactOutParams;
}
clearCachedParams(): void {
this.cachedExactInParams = undefined;
this.cachedExactOutParams = undefined;
this.quoteType = undefined;
}
getSrcProvider(): ethers.providers.Provider {
return this.quoter.srcProvider;
}
getDstProvider(): ethers.providers.Provider {
return this.quoter.dstProvider;
}
async approveAndSwapExactIn(
wallet: ethers.Signer
): Promise<TransactionReceipt> {
return approveAndSwapExactIn(
this.getSrcProvider(),
wallet,
this.tokens.srcIn,
this.cachedExactInParams,
this.srcExecutionParams,
this.dstExecutionParams
);
}
async approveAndSwapExactOut(
wallet: ethers.Wallet
): Promise<TransactionReceipt> {
throw Error("ExactOut not supported yet");
}
async approveAndSwap(wallet: ethers.Signer): Promise<TransactionReceipt> {
const quoteType = this.quoteType;
if (quoteType === QuoteType.ExactIn) {
this.srcReceipt = await this.approveAndSwapExactIn(wallet);
} else if (quoteType === QuoteType.ExactOut) {
this.srcReceipt = await this.approveAndSwapExactOut(wallet);
} else {
throw Error("no quote found");
}
this.fetchAndSetEmitterAndSequence();
return this.srcReceipt;
}
fetchAndSetEmitterAndSequence(): void {
const receipt = this.srcReceipt;
if (receipt === undefined) {
throw Error("no swap receipt found");
}
const wormholeParams = this.srcExecutionParams.wormhole;
this.vaaSearchParams = {
sequence: parseSequenceFromLogEth(
receipt,
wormholeParams.coreBridgeAddress
),
emitterAddress: getEmitterAddressEth(wormholeParams.tokenBridgeAddress),
};
return;
}
async fetchSignedVaaFromSwap(): Promise<void> {
if (this.vaaBytes !== undefined) {
// console.warn("vaaBytes are defined");
return;
}
const vaaSearchParams = this.vaaSearchParams;
if (vaaSearchParams === undefined) {
throw Error("no vaa search params found");
}
const sequence = vaaSearchParams.sequence;
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
// TODO: this is where we passed the transport
);
// grab vaaBytes
this.vaaBytes = vaaResponse.vaaBytes;
return;
}
async fetchVaaAndSwap(wallet: ethers.Signer): Promise<TransactionReceipt> {
await this.fetchSignedVaaFromSwap();
const quoteType = this.quoteType;
if (quoteType === QuoteType.ExactIn) {
this.dstReceipt = await this.swapExactInFromVaa(wallet);
} else if (quoteType === QuoteType.ExactOut) {
this.dstReceipt = await this.swapExactOutFromVaa(wallet);
} else {
throw Error("no quote found");
}
// console.info("clearing state");
this.clearState();
return this.dstReceipt;
}
async swapExactInFromVaa(wallet: ethers.Signer): Promise<TransactionReceipt> {
return swapExactInFromVaa(
this.getDstProvider(),
wallet,
this.dstExecutionParams,
this.cachedExactInParams.dst.protocol,
this.vaaBytes
);
}
async swapExactOutFromVaa(
wallet: ethers.Wallet
): Promise<TransactionReceipt> {
throw Error("ExactOut not supported yet");
}
clearState(): void {
// TODO: after the whole swap, clear the state of everything
this.vaaBytes = undefined;
// clear src receipt only
this.srcReceipt = undefined;
// clear params
this.cachedExactInParams = undefined;
this.cachedExactOutParams = undefined;
this.quoteType = undefined;
return;
}
}

70
react/src/utils/consts.ts Normal file
View File

@ -0,0 +1,70 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_POLYGON,
} from "@certusone/wormhole-sdk";
import ethIcon from "../icons/eth.svg";
import polygonIcon from "../icons/polygon.svg";
export interface TokenInfo {
id: string;
name: string;
address: string;
chainId: ChainId;
logo: string;
}
export const WMATIC_TOKEN_INFO: TokenInfo = {
id: "WMATIC",
name: "WMATIC",
address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889",
chainId: CHAIN_ID_POLYGON,
logo: polygonIcon,
};
export const WETH_TOKEN_INFO: TokenInfo = {
id: "WETH",
name: "WETH",
address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
chainId: CHAIN_ID_ETH,
logo: ethIcon,
};
export const TOKEN_INFOS = [WMATIC_TOKEN_INFO, WETH_TOKEN_INFO];
export const ETH_NETWORK_CHAIN_ID = 5;
export const POLYGON_NETWORK_CHAIN_ID = 80001;
export const getEvmChainId = (chainId: ChainId) =>
chainId === CHAIN_ID_ETH
? ETH_NETWORK_CHAIN_ID
: chainId === CHAIN_ID_POLYGON
? POLYGON_NETWORK_CHAIN_ID
: undefined;
export const RELAYER_FEE_UST = "0.0001";
export const WORMHOLE_RPC_HOSTS = [
"https://wormhole-v2-testnet-api.certus.one",
];
export const CORE_BRIDGE_ADDRESS_ETHEREUM =
"0x706abc4E45D419950511e474C7B9Ed348A4a716c";
export const CORE_BRIDGE_ADDRESS_POLYGON =
"0x0CBE91CF822c73C2315FB05100C2F714765d5c20";
export const TOKEN_BRIDGE_ADDRESS_ETHEREUM =
"0xF890982f9310df57d00f659cf4fd87e65adEd8d7";
export const TOKEN_BRIDGE_ADDRESS_POLYGON =
"0x377D55a7928c046E18eEbb61977e714d2a76472a";
export const QUICKSWAP_FACTORY_ADDRESS =
"0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32";
export const UNISWAP_V3_FACTORY_ADDRESS =
"0x1F98431c8aD98523631AE4a59f267346ea31F984";
export const APPROVAL_GAS_LIMIT = "100000";

View File

@ -0,0 +1,11 @@
const MM_ERR_WITH_INFO_START =
"VM Exception while processing transaction: revert ";
const parseError = (e: any) =>
e?.data?.message?.startsWith(MM_ERR_WITH_INFO_START)
? e.data.message.replace(MM_ERR_WITH_INFO_START, "")
: e?.response?.data?.error // terra error
? e.response.data.error
: e?.message
? e.message
: "An unknown error occurred";
export default parseError;

335
react/src/views/Home.tsx Normal file
View File

@ -0,0 +1,335 @@
import {
Container,
makeStyles,
Paper,
TextField,
Typography,
} from "@material-ui/core";
import { ChainId } from "@certusone/wormhole-sdk";
import { useCallback, useEffect, useState } from "react";
import ButtonWithLoader from "../components/ButtonWithLoader";
import EthereumSignerKey from "../components/EthereumSignerKey";
import TokenSelect from "../components/TokenSelect";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import {
getEvmChainId,
RELAYER_FEE_UST,
TOKEN_INFOS,
WETH_TOKEN_INFO,
WMATIC_TOKEN_INFO,
} from "../utils/consts";
import { COLORS } from "../muiTheme";
import Wormhole from "../icons/wormhole-network.svg";
import { UniswapToUniswapExecutor } from "../swapper/swapper";
import { Web3Provider } from "@ethersproject/providers";
import { hexlify, hexStripZeros } from "ethers/lib/utils";
import { useDebouncedCallback } from "use-debounce";
import { useSnackbar } from "notistack";
import { Alert } from "@material-ui/lab";
import parseError from "../utils/parseError";
import Settings from "../components/Settings";
const useStyles = makeStyles((theme) => ({
bg: {
background:
"linear-gradient(160deg, rgba(69,74,117,.1) 0%, rgba(138,146,178,.1) 33%, rgba(69,74,117,.1) 66%, rgba(98,104,143,.1) 100%), linear-gradient(45deg, rgba(153,69,255,.1) 0%, rgba(121,98,231,.1) 20%, rgba(0,209,140,.1) 100%)",
display: "flex",
flexDirection: "column",
minHeight: "100vh",
},
centeredContainer: {
textAlign: "center",
width: "100%",
},
mainPaper: {
padding: "2rem",
backgroundColor: COLORS.nearBlackWithMinorTransparency,
},
numberField: {
flexGrow: 1,
"& > * > .MuiInputBase-input": {
textAlign: "center",
height: "100%",
flexGrow: "1",
fontSize: "3rem",
fontFamily: "Roboto Mono, monospace",
caretShape: "block",
"&::-webkit-outer-spin-button, &::-webkit-inner-spin-button": {
"-webkit-appearance": "none",
"-moz-appearance": "none",
margin: 0,
},
"&[type=number]": {
"-webkit-appearance": "textfield",
"-moz-appearance": "textfield",
},
},
"& > * > input::-webkit-inner-spin-button": {
webkitAppearance: "none",
margin: "0",
},
},
gradientButton: {
backgroundImage: `linear-gradient(45deg, ${COLORS.blue} 0%, ${COLORS.nearBlack}20 50%, ${COLORS.blue}30 62%, ${COLORS.nearBlack}50 120%)`,
transition: "0.75s",
backgroundSize: "200% auto",
boxShadow: "0 0 20px #222",
"&:hover": {
backgroundPosition:
"right center" /* change the direction of the change here */,
},
width: "100%",
height: "3rem",
marginTop: "1rem",
},
disabled: {
background: COLORS.gray,
},
spacer: {
height: "1rem",
},
titleBar: {
marginTop: "10rem",
"& > *": {
margin: ".5rem",
alignSelf: "flex-end",
},
},
tokenSelectWrapper: {
display: "flex",
alignItems: "center",
},
wormholeIcon: {
height: 60,
filter: "contrast(0)",
transition: "filter 0.5s",
"&:hover": {
filter: "contrast(1)",
},
verticalAlign: "middle",
margin: "1rem",
display: "inline-block",
},
}));
const switchProviderNetwork = async (
provider: Web3Provider,
chainId: ChainId
) => {
const evmChainId = getEvmChainId(chainId);
if (evmChainId === undefined) {
throw new Error("Unknown chainId");
}
await provider.send("wallet_switchEthereumChain", [
{ chainId: hexStripZeros(hexlify(evmChainId)) },
]);
const network = await provider.getNetwork();
if (network.chainId !== evmChainId) {
throw new Error("Could not switch network");
}
};
export default function Home() {
const classes = useStyles();
const [sourceTokenInfo, setSourceTokenInfo] = useState(WMATIC_TOKEN_INFO);
const [targetTokenInfo, setTargetTokenInfo] = useState(WETH_TOKEN_INFO);
const [amountIn, setAmountIn] = useState("0.0");
const [amountOut, setAmountOut] = useState("0.0");
const [deadline, setDeadline] = useState("30");
const [slippage, setSlippage] = useState("1");
const [executor, setExecutor] = useState<UniswapToUniswapExecutor | null>(
null
);
const [isSwapping, setIsSwapping] = useState(false);
const [isComputingQuote, setIsComputingQuote] = useState(false);
const [hasQuote, setHasQuote] = useState(false);
const { provider, signer } = useEthereumProvider();
const { enqueueSnackbar } = useSnackbar();
const computeQuote = useCallback(() => {
(async () => {
setHasQuote(false);
setIsComputingQuote(true);
try {
if (
parseFloat(amountIn) > 0 &&
!isNaN(parseFloat(deadline)) &&
!isNaN(parseFloat(slippage))
) {
setAmountOut("0.0");
const executor = new UniswapToUniswapExecutor();
await executor.initialize(
sourceTokenInfo.address,
targetTokenInfo.address
);
await executor.computeAndVerifySrcPoolAddress().catch((e) => {
throw new Error("failed to verify source pool address");
});
await executor.computeAndVerifyDstPoolAddress().catch((e) => {
throw new Error("failed to verify dest pool address");
});
executor.setDeadlines((parseFloat(deadline) * 60).toString());
executor.setSlippage((parseFloat(slippage) / 100).toString());
executor.setRelayerFee(RELAYER_FEE_UST);
const quote = await executor.computeQuoteExactIn(amountIn);
setExecutor(executor);
setAmountOut(
parseFloat(
executor.tokens.dstOut.formatAmount(quote.dst.minAmountOut)
).toFixed(8)
);
setHasQuote(true);
} else {
setAmountOut("0.0");
}
} catch (e) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
}
setIsComputingQuote(false);
})();
}, [
sourceTokenInfo,
targetTokenInfo,
amountIn,
deadline,
slippage,
enqueueSnackbar,
]);
const debouncedComputeQuote = useDebouncedCallback(computeQuote, 1000);
useEffect(() => {
debouncedComputeQuote();
}, [
sourceTokenInfo,
targetTokenInfo,
amountIn,
deadline,
slippage,
debouncedComputeQuote,
]);
const handleAmountChange = useCallback((event) => {
setAmountIn(event.target.value);
}, []);
const handleSlippageChange = useCallback((slippage) => {
setSlippage(slippage);
}, []);
const handleDeadlineChange = useCallback((deadline) => {
setDeadline(deadline);
}, []);
const handleSourceChange = useCallback((event) => {
if (event.target.value === WMATIC_TOKEN_INFO.name) {
setSourceTokenInfo(WMATIC_TOKEN_INFO);
setTargetTokenInfo(WETH_TOKEN_INFO);
} else {
setSourceTokenInfo(WETH_TOKEN_INFO);
setTargetTokenInfo(WMATIC_TOKEN_INFO);
}
setAmountIn("0.0");
setAmountOut("0.0");
}, []);
const handleSwapClick = useCallback(async () => {
if (provider && signer && executor) {
try {
setIsSwapping(true);
await switchProviderNetwork(provider, sourceTokenInfo.chainId);
const sourceReceipt = await executor.approveAndSwap(signer);
console.info(`src transaction: ${sourceReceipt.transactionHash}`);
await switchProviderNetwork(provider, targetTokenInfo.chainId);
const targetReceipt = await executor.fetchVaaAndSwap(signer);
console.info(`dst transaction: ${targetReceipt.transactionHash}`);
enqueueSnackbar(null, {
content: <Alert severity="success">Success!</Alert>,
});
} catch (e: any) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
}
setIsSwapping(false);
}
}, [
provider,
signer,
executor,
enqueueSnackbar,
sourceTokenInfo,
targetTokenInfo,
]);
const readyToSwap = provider && signer && hasQuote;
return (
<div className={classes.bg}>
<Container className={classes.centeredContainer} maxWidth="sm">
<div className={classes.titleBar}></div>
<Typography variant="h4" color="textSecondary">
Cross Chain Swap Demo
</Typography>
<div className={classes.spacer} />
<Paper className={classes.mainPaper}>
<Settings
disabled={isSwapping || isComputingQuote}
slippage={slippage}
deadline={deadline}
onSlippageChange={handleSlippageChange}
onDeadlineChange={handleDeadlineChange}
/>
<TokenSelect
tokens={TOKEN_INFOS}
value={sourceTokenInfo.name}
onChange={handleSourceChange}
disabled={isSwapping || isComputingQuote}
></TokenSelect>
<Typography variant="subtitle1">Send</Typography>
<TextField
type="number"
value={amountIn}
disabled={isSwapping || isComputingQuote}
InputProps={{ disableUnderline: true }}
className={classes.numberField}
onChange={handleAmountChange}
></TextField>
<div className={classes.spacer} />
<TokenSelect
tokens={TOKEN_INFOS}
value={targetTokenInfo.name}
onChange={() => {}}
disabled={true}
></TokenSelect>
<Typography variant="subtitle1">Receive (estimated)</Typography>
<TextField
type="number"
value={amountOut}
autoFocus={true}
InputProps={{ disableUnderline: true }}
className={classes.numberField}
inputProps={{ readOnly: true }}
></TextField>
{!isSwapping && <EthereumSignerKey />}
<ButtonWithLoader
disabled={!readyToSwap || isSwapping}
showLoader={isSwapping}
onClick={handleSwapClick}
>
Swap
</ButtonWithLoader>
</Paper>
<div className={classes.spacer} />
<Typography variant="subtitle1" color="textSecondary">
{"powered by wormhole"}
</Typography>
<img src={Wormhole} alt="Wormhole" className={classes.wormholeIcon} />
</Container>
</div>
);
}

21
react/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"strictPropertyInitialization": false,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}