diff --git a/bridge_ui/src/App.js b/bridge_ui/src/App.js index 78572152..353af4bf 100644 --- a/bridge_ui/src/App.js +++ b/bridge_ui/src/App.js @@ -1,21 +1,6 @@ -import { - AppBar, - Button, - Grid, - Link, - makeStyles, - MenuItem, - TextField, - Toolbar, - Typography, -} from "@material-ui/core"; -import { useCallback } from "react"; -import EthereumSignerKey from "./components/EthereumSignerKey"; -import SolanaWalletKey from "./components/SolanaWalletKey"; -import { useEthereumProvider } from "./contexts/EthereumProviderContext"; -import { Bridge__factory } from "./ethers-contracts"; +import { AppBar, Link, makeStyles, Toolbar } from "@material-ui/core"; +import Transfer from "./components/Transfer"; import wormholeLogo from "./icons/wormhole.svg"; -import { ETH_TOKEN_BRIDGE_ADDRESS } from "./utils/consts"; const useStyles = makeStyles((theme) => ({ appBar: { @@ -47,34 +32,10 @@ const useStyles = makeStyles((theme) => ({ content: { margin: theme.spacing(10.5, 8), }, - transferBox: { - width: 540, - margin: "auto", - border: `.5px solid ${theme.palette.divider}`, - padding: theme.spacing(5.5, 12), - }, - arrow: { - display: "flex", - alignItems: "center", - justifyContent: "center", - }, - transferField: { - marginTop: theme.spacing(5), - }, - transferButton: { - marginTop: theme.spacing(7.5), - textTransform: "none", - width: "100%", - }, })); function App() { const classes = useStyles(); - const provider = useEthereumProvider(); - const handleClick = useCallback(() => { - const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, provider); - bridge.chainId().then((n) => console.log(n)); - }, [provider]); return ( <> @@ -88,48 +49,7 @@ function App() {
-
- - - To - - Ethereum - Solana - - - - - → - - - From - - Ethereum - Solana - - - - - - - -
+
); diff --git a/bridge_ui/src/components/Transfer.tsx b/bridge_ui/src/components/Transfer.tsx new file mode 100644 index 00000000..239e2684 --- /dev/null +++ b/bridge_ui/src/components/Transfer.tsx @@ -0,0 +1,187 @@ +import { + Button, + Grid, + makeStyles, + MenuItem, + TextField, + Typography, +} from "@material-ui/core"; +import { useCallback, useState } from "react"; +import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { useSolanaWallet } from "../contexts/SolanaWalletContext"; +import useEthereumBalance from "../hooks/useEthereumBalance"; +import { + ChainId, + CHAINS, + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + ETH_TEST_TOKEN_ADDRESS, +} from "../utils/consts"; +import transferFrom from "../utils/transferFrom"; +import EthereumSignerKey from "./EthereumSignerKey"; +import SolanaWalletKey from "./SolanaWalletKey"; + +const useStyles = makeStyles((theme) => ({ + transferBox: { + width: 540, + margin: "auto", + border: `.5px solid ${theme.palette.divider}`, + padding: theme.spacing(5.5, 12), + }, + arrow: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + transferField: { + marginTop: theme.spacing(5), + }, + transferButton: { + marginTop: theme.spacing(7.5), + textTransform: "none", + width: "100%", + }, +})); + +// TODO: loaders and such, navigation block? +// TODO: refresh displayed token amount after transfer somehow, could be resolved by having different components appear +// TODO: warn if amount exceeds balance + +function Transfer() { + const classes = useStyles(); + //TODO: don't attempt to connect to any wallets until the user clicks a connect button + const [fromChain, setFromChain] = useState(CHAIN_ID_ETH); + const [toChain, setToChain] = useState(CHAIN_ID_SOLANA); + const [assetAddress, setAssetAddress] = useState(ETH_TEST_TOKEN_ADDRESS); + const [amount, setAmount] = useState(""); + const handleFromChange = useCallback( + (event) => { + setFromChain(event.target.value); + if (toChain === event.target.value) { + setToChain(fromChain); + } + }, + [fromChain, toChain] + ); + const handleToChange = useCallback( + (event) => { + setToChain(event.target.value); + if (fromChain === event.target.value) { + setFromChain(toChain); + } + }, + [fromChain, toChain] + ); + const handleAssetChange = useCallback((event) => { + setAssetAddress(event.target.value); + }, []); + const handleAmountChange = useCallback((event) => { + setAmount(event.target.value); + }, []); + const provider = useEthereumProvider(); + const ethBalance = useEthereumBalance(assetAddress, provider); + const { wallet } = useSolanaWallet(); + const solPK = wallet?.publicKey?.toBytes(); + // TODO: dynamically get "to" wallet + const handleClick = useCallback(() => { + if (transferFrom[fromChain]) { + transferFrom[fromChain](provider, assetAddress, amount, toChain, solPK); + } + }, [fromChain, provider, solPK, assetAddress, amount, toChain]); + // update this as we develop, just setting expectations with the button state + const isTransferImplemented = !!transferFrom[fromChain]; + const isProviderConnected = !!provider; + const isRecipientAvailable = !!solPK; + const isAddressDefined = !!assetAddress; + const isAmountPositive = Number(amount) > 0; // TODO: this needs per-chain, bn parsing + const isBalanceAtLeastAmount = Number(ethBalance) >= Number(amount); // TODO: ditto + const canAttemptTransfer = + isTransferImplemented && + isProviderConnected && + isRecipientAvailable && + isAddressDefined && + isAmountPositive && + isBalanceAtLeastAmount; + return ( +
+ + + To + + {CHAINS.map(({ id, name }) => ( + + {name} + + ))} + + + {ethBalance} + + + → + + + From + + {CHAINS.map(({ id, name }) => ( + + {name} + + ))} + + + + + + + + {canAttemptTransfer ? null : ( + + {!isTransferImplemented + ? `Transfer is not yet implemented for ${CHAINS[fromChain]}` + : !isProviderConnected + ? "The source wallet is not connected" + : !isRecipientAvailable + ? "The receiving wallet is not connected" + : !isAddressDefined + ? "Please provide an asset address" + : !isAmountPositive + ? "The amount must be positive" + : !isBalanceAtLeastAmount + ? "The amount may not be greater than the balance" + : !isBalanceAtLeastAmount + ? "The amount may not be greater than the balance" + : ""} + + )} +
+ ); +} + +export default Transfer; diff --git a/bridge_ui/src/hooks/useEthereumBalance.ts b/bridge_ui/src/hooks/useEthereumBalance.ts new file mode 100644 index 00000000..336716e1 --- /dev/null +++ b/bridge_ui/src/hooks/useEthereumBalance.ts @@ -0,0 +1,37 @@ +import { ethers } from "ethers"; +import { formatUnits } from "ethers/lib/utils"; +import { useEffect, useState } from "react"; +import { TokenImplementation__factory } from "../ethers-contracts"; + +function useEthereumBalance(address: string, provider?: ethers.providers.Web3Provider) { + //TODO: should this check allowance too or subtract allowance? + const [balance, setBalance] = useState('') + useEffect(()=>{ + if (!address || !provider) { + setBalance('') + return + } + let cancelled = false + const token = TokenImplementation__factory.connect(address, provider); + token.decimals().then((decimals) => { + console.log(decimals); + provider + ?.getSigner() + .getAddress() + .then((pk) => { + console.log(pk) + token.balanceOf(pk).then((n) => { + if (!cancelled) { + setBalance(formatUnits(n,decimals)) + } + }); + }); + }); + return () => { + cancelled = true + } + },[address, provider]) + return balance +} + +export default useEthereumBalance \ No newline at end of file diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 0c78dd2b..f0e431fd 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -1,2 +1,32 @@ +export type ChainId = 1 | 2 | 3 | 4 +export const CHAIN_ID_SOLANA: ChainId = 1 +export const CHAIN_ID_ETH: ChainId = 2 +export const CHAIN_ID_TERRA: ChainId = 3 +export const CHAIN_ID_BSC: ChainId = 4 +export interface ChainInfo { + id: ChainId + name: string +} +export const CHAINS = [ + { + id: CHAIN_ID_BSC, + name: 'Binance Smart Chain' + }, + { + id: CHAIN_ID_ETH, + name: 'Ethereum' + }, + { + id: CHAIN_ID_SOLANA, + name: 'Solana' + }, + { + id: CHAIN_ID_TERRA, + name: 'Terra' + }, +] export const SOLANA_HOST = 'http://localhost:8899' -export const ETH_TOKEN_BRIDGE_ADDRESS = "0x254dffcd3277c0b1660f6d42efbb754edababc2b" \ No newline at end of file +export const ETH_TEST_TOKEN_ADDRESS = "0x0290FB167208Af455bB137780163b7B7a9a10C16" +export const ETH_TOKEN_BRIDGE_ADDRESS = "0xe982e462b094850f12af94d21d470e21be9d0e9c" +export const SOL_TEST_TOKEN_ADDRESS = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ" +export const SOL_TOKEN_BRIDGE_ADDRESS = "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE" \ No newline at end of file diff --git a/bridge_ui/src/utils/transferFrom.ts b/bridge_ui/src/utils/transferFrom.ts new file mode 100644 index 00000000..8e23dbd6 --- /dev/null +++ b/bridge_ui/src/utils/transferFrom.ts @@ -0,0 +1,66 @@ +import { ethers } from "ethers"; +import { formatUnits, parseUnits } from "ethers/lib/utils"; +import { Bridge__factory, TokenImplementation__factory } from "../ethers-contracts"; +import { ChainId, CHAIN_ID_ETH, ETH_TOKEN_BRIDGE_ADDRESS } from "./consts"; + +// TODO: this should probably be extended from the context somehow so that the signatures match +// TODO: allow for / handle cancellation? +// TODO: overall better input checking and error handling +function transferFromEth(provider: ethers.providers.Web3Provider | undefined, tokenAddress: string, amount: string, recipientChain: ChainId, recipientAddress: Uint8Array | undefined) { + if (!provider || !recipientAddress) return; + const signer = provider.getSigner(); + if (!signer) return; + //TODO: check if token attestation exists on the target chain + //TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain? + //TODO: more catches + const amountParsed = parseUnits(amount, 18); + signer.getAddress().then((signerAddress) => { + console.log("Signer:", signerAddress); + console.log("Token:", tokenAddress) + const token = TokenImplementation__factory.connect( + tokenAddress, + signer + ); + token + .allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS) + .then((allowance) => { + console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't? + token + .approve(ETH_TOKEN_BRIDGE_ADDRESS, amountParsed) + .then((transaction) => { + console.log(transaction); + const fee = 0; // for now, this won't do anything, we may add later + const nonceConst = Math.random() * 100000; + const nonceBuffer = Buffer.alloc(4); + nonceBuffer.writeUInt32LE(nonceConst, 0); + console.log("Initiating transfer"); + console.log("Amount:", formatUnits(amountParsed, 18)); + console.log("To chain:", recipientChain); + console.log("To address:", recipientAddress); + console.log("Fees:", fee); + console.log("Nonce:", nonceBuffer); + const bridge = Bridge__factory.connect( + ETH_TOKEN_BRIDGE_ADDRESS, + signer + ); + bridge + .transferTokens( + tokenAddress, + amountParsed, + recipientChain, + recipientAddress, + fee, + nonceBuffer + ) + .then((v) => console.log("Success:", v)) + .catch((r) => console.error(r)); //TODO: integrate toast messages + }); + }); + }); +} + +const transferFrom = { + [CHAIN_ID_ETH]: transferFromEth +} + +export default transferFrom \ No newline at end of file