From 5f5a2a56f5f8e8a4417a0808058e2044e4c1f133 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Fri, 20 Aug 2021 14:15:29 -0400 Subject: [PATCH] bridge_ui: notistack, hooks, cleanup Change-Id: Ia553e514afee655c6cd8e26320e539fc59041e49 --- bridge_ui/package-lock.json | 28 ++ bridge_ui/package.json | 1 + bridge_ui/src/components/Attest/Create.tsx | 59 +-- bridge_ui/src/components/Attest/Send.tsx | 91 +---- bridge_ui/src/components/SolanaWalletKey.tsx | 4 +- bridge_ui/src/components/Transfer/Redeem.tsx | 82 +--- bridge_ui/src/components/Transfer/Send.tsx | 188 +--------- bridge_ui/src/components/Transfer/Source.tsx | 2 +- bridge_ui/src/hooks/useFetchTargetAsset.ts | 8 +- bridge_ui/src/hooks/useGetBalanceEffect.ts | 1 - bridge_ui/src/hooks/useHandleAttest.ts | 198 ++++++++++ bridge_ui/src/hooks/useHandleCreateWrapped.ts | 160 ++++++++ bridge_ui/src/hooks/useHandleRedeem.ts | 190 ++++++++++ bridge_ui/src/hooks/useHandleTransfer.ts | 354 ++++++++++++++++++ bridge_ui/src/index.js | 21 +- bridge_ui/src/utils/attestFrom.ts | 103 ----- bridge_ui/src/utils/createWrappedOn.ts | 65 ---- bridge_ui/src/utils/parseError.ts | 9 + bridge_ui/src/utils/redeemOn.ts | 80 ---- bridge_ui/src/utils/solana.ts | 9 - bridge_ui/src/utils/terra.ts | 11 +- bridge_ui/src/utils/transferFrom.ts | 167 --------- sdk/js/src/bridge/parseSequenceFromLog.ts | 1 - sdk/js/src/token_bridge/redeem.ts | 1 - 24 files changed, 982 insertions(+), 851 deletions(-) create mode 100644 bridge_ui/src/hooks/useHandleAttest.ts create mode 100644 bridge_ui/src/hooks/useHandleCreateWrapped.ts create mode 100644 bridge_ui/src/hooks/useHandleRedeem.ts create mode 100644 bridge_ui/src/hooks/useHandleTransfer.ts delete mode 100644 bridge_ui/src/utils/attestFrom.ts delete mode 100644 bridge_ui/src/utils/createWrappedOn.ts create mode 100644 bridge_ui/src/utils/parseError.ts delete mode 100644 bridge_ui/src/utils/redeemOn.ts delete mode 100644 bridge_ui/src/utils/transferFrom.ts diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json index 81508cb1..b326bd57 100644 --- a/bridge_ui/package-lock.json +++ b/bridge_ui/package-lock.json @@ -31,6 +31,7 @@ "@terra-money/wallet-provider": "^1.4.0-alpha.1", "ethers": "^5.4.1", "js-base64": "^3.6.1", + "notistack": "^1.0.10", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.4", @@ -26881,6 +26882,24 @@ "node": ">=4" } }, + "node_modules/notistack": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-1.0.10.tgz", + "integrity": "sha512-z0y4jJaVtOoH3kc3GtNUlhNTY+5LE04QDeLVujX3VPhhzg67zw055mZjrBF+nzpv3V9aiPNph1EgRU4+t8kQTQ==", + "dependencies": { + "clsx": "^1.1.0", + "hoist-non-react-statics": "^3.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "@material-ui/core": "^4.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, "node_modules/npm-bundled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", @@ -61189,6 +61208,15 @@ "sort-keys": "^1.0.0" } }, + "notistack": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-1.0.10.tgz", + "integrity": "sha512-z0y4jJaVtOoH3kc3GtNUlhNTY+5LE04QDeLVujX3VPhhzg67zw055mZjrBF+nzpv3V9aiPNph1EgRU4+t8kQTQ==", + "requires": { + "clsx": "^1.1.0", + "hoist-non-react-statics": "^3.3.0" + } + }, "npm-bundled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", diff --git a/bridge_ui/package.json b/bridge_ui/package.json index 174e2419..ea33caa5 100644 --- a/bridge_ui/package.json +++ b/bridge_ui/package.json @@ -25,6 +25,7 @@ "@terra-money/wallet-provider": "^1.4.0-alpha.1", "ethers": "^5.4.1", "js-base64": "^3.6.1", + "notistack": "^1.0.10", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.4", diff --git a/bridge_ui/src/components/Attest/Create.tsx b/bridge_ui/src/components/Attest/Create.tsx index 87087d38..29896aac 100644 --- a/bridge_ui/src/components/Attest/Create.tsx +++ b/bridge_ui/src/components/Attest/Create.tsx @@ -1,25 +1,5 @@ -import { - CHAIN_ID_TERRA, - CHAIN_ID_ETH, - CHAIN_ID_SOLANA, -} from "@certusone/wormhole-sdk"; import { Button, CircularProgress, makeStyles } from "@material-ui/core"; -import { useCallback } from "react"; -import { useConnectedWallet } from "@terra-money/wallet-provider"; -import { useDispatch, useSelector } from "react-redux"; -import { useEthereumProvider } from "../../contexts/EthereumProviderContext"; -import { useSolanaWallet } from "../../contexts/SolanaWalletContext"; -import useAttestSignedVAA from "../../hooks/useAttestSignedVAA"; -import { reset, setIsCreating } from "../../store/attestSlice"; -import { - selectAttestIsCreating, - selectAttestTargetChain, -} from "../../store/selectors"; -import { - createWrappedOnEth, - createWrappedOnSolana, - createWrappedOnTerra, -} from "../../utils/createWrappedOn"; +import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped"; const useStyles = makeStyles((theme) => ({ transferButton: { @@ -30,49 +10,20 @@ const useStyles = makeStyles((theme) => ({ })); function Create() { - const dispatch = useDispatch(); const classes = useStyles(); - const targetChain = useSelector(selectAttestTargetChain); - const solanaWallet = useSolanaWallet(); - const solPK = solanaWallet?.publicKey; - const signedVAA = useAttestSignedVAA(); - const isCreating = useSelector(selectAttestIsCreating); - const { signer } = useEthereumProvider(); - const terraWallet = useConnectedWallet(); - const handleCreateClick = useCallback(() => { - if (targetChain === CHAIN_ID_SOLANA && signedVAA) { - (async () => { - dispatch(setIsCreating(true)); - await createWrappedOnSolana(solanaWallet, solPK?.toString(), signedVAA); - dispatch(reset()); - })(); - } - if (targetChain === CHAIN_ID_ETH && signedVAA) { - (async () => { - dispatch(setIsCreating(true)); - await createWrappedOnEth(signer, signedVAA); - dispatch(reset()); - })(); - } - if (targetChain === CHAIN_ID_TERRA && signedVAA) { - (async () => { - dispatch(setIsCreating(true)); - createWrappedOnTerra(terraWallet, signedVAA); - })(); - } - }, [dispatch, targetChain, solanaWallet, solPK, signedVAA, signer]); + const { handleClick, disabled, showLoader } = useHandleCreateWrapped(); return (
- {isCreating ? ( + {showLoader ? ( ({ transferButton: { @@ -32,67 +9,9 @@ const useStyles = makeStyles((theme) => ({ }, })); -// TODO: move attest to its own workflow - function Send() { const classes = useStyles(); - const dispatch = useDispatch(); - const sourceChain = useSelector(selectAttestSourceChain); - const sourceAsset = useSelector(selectAttestSourceAsset); - const isTargetComplete = useSelector(selectAttestIsTargetComplete); - const isSending = useSelector(selectAttestIsSending); - const isSendComplete = useSelector(selectAttestIsSendComplete); - const { signer } = useEthereumProvider(); - const solanaWallet = useSolanaWallet(); - const terraWallet = useConnectedWallet(); - const solPK = solanaWallet?.publicKey; - // TODO: dynamically get "to" wallet - const handleAttestClick = useCallback(() => { - if (sourceChain === CHAIN_ID_ETH) { - //TODO: just for testing, this should eventually use the store to communicate between steps - (async () => { - dispatch(setIsSending(true)); - try { - const vaaBytes = await attestFromEth(signer, sourceAsset); - console.log("bytes in attest", vaaBytes); - vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); - } catch (e) { - console.error(e); - dispatch(setIsSending(false)); - } - })(); - } else if (sourceChain === CHAIN_ID_SOLANA) { - //TODO: just for testing, this should eventually use the store to communicate between steps - (async () => { - dispatch(setIsSending(true)); - try { - const vaaBytes = await attestFromSolana( - solanaWallet, - solPK?.toString(), - sourceAsset - ); - console.log("bytes in attest", vaaBytes); - vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); - } catch (e) { - console.error(e); - dispatch(setIsSending(false)); - } - })(); - } else if (sourceChain === CHAIN_ID_TERRA) { - //TODO: just for testing, this should eventually use the store to communicate between steps - (async () => { - dispatch(setIsSending(true)); - try { - const vaaBytes = await attestFromTerra(terraWallet, sourceAsset); - console.log("bytes in attest", vaaBytes); - vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); - } catch (e) { - console.error(e); - dispatch(setIsSending(false)); - } - })(); - } - }, [dispatch, sourceChain, signer, solanaWallet, solPK, sourceAsset]); + const { handleClick, disabled, showLoader } = useHandleAttest(); return ( <>
@@ -100,12 +19,12 @@ function Send() { color="primary" variant="contained" className={classes.transferButton} - onClick={handleAttestClick} - disabled={!isTargetComplete || isSending || isSendComplete} + onClick={handleClick} + disabled={disabled} > Attest - {isSending ? ( + {showLoader ? ( { diff --git a/bridge_ui/src/components/Transfer/Redeem.tsx b/bridge_ui/src/components/Transfer/Redeem.tsx index db716842..fee294a1 100644 --- a/bridge_ui/src/components/Transfer/Redeem.tsx +++ b/bridge_ui/src/components/Transfer/Redeem.tsx @@ -1,28 +1,5 @@ -import { - CHAIN_ID_TERRA, - CHAIN_ID_ETH, - CHAIN_ID_SOLANA, -} from "@certusone/wormhole-sdk"; import { Button, CircularProgress, makeStyles } from "@material-ui/core"; -import { useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { useConnectedWallet } from "@terra-money/wallet-provider"; -import { useEthereumProvider } from "../../contexts/EthereumProviderContext"; -import { useSolanaWallet } from "../../contexts/SolanaWalletContext"; -import useTransferSignedVAA from "../../hooks/useTransferSignedVAA"; -import { - selectTransferIsRedeeming, - selectTransferIsSourceAssetWormholeWrapped, - selectTransferOriginChain, - selectTransferTargetAsset, - selectTransferTargetChain, -} from "../../store/selectors"; -import { reset, setIsRedeeming } from "../../store/transferSlice"; -import { - redeemOnEth, - redeemOnSolana, - redeemOnTerra, -} from "../../utils/redeemOn"; +import { useHandleRedeem } from "../../hooks/useHandleRedeem"; const useStyles = makeStyles((theme) => ({ transferButton: { @@ -33,69 +10,20 @@ const useStyles = makeStyles((theme) => ({ })); function Redeem() { - const dispatch = useDispatch(); const classes = useStyles(); - const isSourceAssetWormholeWrapped = useSelector( - selectTransferIsSourceAssetWormholeWrapped - ); - const originChain = useSelector(selectTransferOriginChain); - const targetChain = useSelector(selectTransferTargetChain); - const targetAsset = useSelector(selectTransferTargetAsset); - const solanaWallet = useSolanaWallet(); - const solPK = solanaWallet?.publicKey; - const { signer } = useEthereumProvider(); - const terraWallet = useConnectedWallet(); - const signedVAA = useTransferSignedVAA(); - const isRedeeming = useSelector(selectTransferIsRedeeming); - const handleRedeemClick = useCallback(() => { - if (targetChain === CHAIN_ID_ETH && signedVAA) { - (async () => { - dispatch(setIsRedeeming(true)); - await redeemOnEth(signer, signedVAA); - dispatch(reset()); - })(); - } - if (targetChain === CHAIN_ID_SOLANA && signedVAA) { - (async () => { - dispatch(setIsRedeeming(true)); - await redeemOnSolana( - solanaWallet, - solPK?.toString(), - signedVAA, - !!isSourceAssetWormholeWrapped && originChain === CHAIN_ID_SOLANA, - targetAsset || undefined - ); - dispatch(reset()); - })(); - } - if (targetChain === CHAIN_ID_TERRA && signedVAA) { - dispatch(setIsRedeeming(true)); - redeemOnTerra(terraWallet, signedVAA); - } - }, [ - dispatch, - terraWallet, - targetChain, - signer, - signedVAA, - solanaWallet, - solPK, - isSourceAssetWormholeWrapped, - originChain, - targetAsset, - ]); + const { handleClick, disabled, showLoader } = useHandleRedeem(); return (
- {isRedeeming ? ( + {showLoader ? ( ({ transferButton: { @@ -46,150 +9,9 @@ const useStyles = makeStyles((theme) => ({ }, })); -// TODO: move attest to its own workflow - function Send() { const classes = useStyles(); - const dispatch = useDispatch(); - const sourceChain = useSelector(selectTransferSourceChain); - const sourceAsset = useSelector(selectTransferSourceAsset); - const originChain = useSelector(selectTransferOriginChain); - const originAsset = useSelector(selectTransferOriginAsset); - const amount = useSelector(selectTransferAmount); - const targetChain = useSelector(selectTransferTargetChain); - const targetAsset = useSelector(selectTransferTargetAsset); - const isTargetComplete = useSelector(selectTransferIsTargetComplete); - const isSending = useSelector(selectTransferIsSending); - const isSendComplete = useSelector(selectTransferIsSendComplete); - const { signer, signerAddress } = useEthereumProvider(); - const terraWallet = useConnectedWallet(); - const solanaWallet = useSolanaWallet(); - const solPK = solanaWallet?.publicKey; - const sourceParsedTokenAccount = useSelector( - selectTransferSourceParsedTokenAccount - ); - const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey; - const decimals = sourceParsedTokenAccount?.decimals; - const targetParsedTokenAccount = useSelector( - selectTransferTargetParsedTokenAccount - ); - // TODO: we probably shouldn't get here if we don't have this public key - // TODO: also this is just for solana... send help(ers) - const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey; - console.log( - "Sending to:", - targetTokenAccountPublicKey, - targetTokenAccountPublicKey && - new PublicKey(targetTokenAccountPublicKey).toBytes() - ); - // TODO: AVOID THIS DANGEROUS CACOPHONY - const tpkRef = useRef(undefined); - useEffect(() => { - (async () => { - if (targetChain === CHAIN_ID_SOLANA) { - tpkRef.current = targetTokenAccountPublicKey - ? zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32) // use the target's TokenAccount if it exists - : solPK && targetAsset // otherwise, use the associated token account (which we create in the case it doesn't exist) - ? zeroPad( - ( - await Token.getAssociatedTokenAddress( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - new PublicKey(targetAsset), - solPK - ) - ).toBytes(), - 32 - ) - : undefined; - } else tpkRef.current = undefined; - })(); - }, [targetChain, solPK, targetAsset, targetTokenAccountPublicKey]); - // TODO: dynamically get "to" wallet - const handleTransferClick = useCallback(() => { - // TODO: we should separate state for transaction vs fetching vaa - // TODO: more generic way of calling these - if (sourceChain === CHAIN_ID_ETH && decimals) { - //TODO: just for testing, this should eventually use the store to communicate between steps - (async () => { - dispatch(setIsSending(true)); - try { - console.log("actually sending", tpkRef.current); - const vaaBytes = await transferFromEth( - signer, - sourceAsset, - decimals, - amount, - targetChain, - tpkRef.current - ); - console.log("bytes in transfer", vaaBytes); - vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); - } catch (e) { - console.error(e); - dispatch(setIsSending(false)); - } - })(); - } - if (sourceChain === CHAIN_ID_SOLANA && decimals) { - //TODO: just for testing, this should eventually use the store to communicate between steps - (async () => { - dispatch(setIsSending(true)); - try { - const vaaBytes = await transferFromSolana( - solanaWallet, - solPK?.toString(), - sourceTokenPublicKey, - sourceAsset, - amount, //TODO: avoid decimals, pass in parsed amount - decimals, - signerAddress, - targetChain, - originAsset, - originChain - ); - console.log("bytes in transfer", vaaBytes); - vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); - } catch (e) { - console.error(e); - dispatch(setIsSending(false)); - } - })(); - } - if (sourceChain === CHAIN_ID_TERRA && decimals) { - (async () => { - dispatch(setIsSending(true)); - try { - const vaaBytes = await transferFromTerra( - terraWallet, - sourceAsset, - amount, - "", - targetChain - ); - console.log("bytes in transfer", vaaBytes); - vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); - } catch (e) { - console.error(e); - dispatch(setIsSending(false)); - } - })(); - } - }, [ - dispatch, - sourceChain, - signer, - signerAddress, - solanaWallet, - solPK, - sourceTokenPublicKey, - sourceAsset, - amount, - decimals, - targetChain, - originAsset, - originChain, - ]); + const { handleClick, disabled, showLoader } = useHandleTransfer(); return ( <>
@@ -197,12 +19,12 @@ function Send() { color="primary" variant="contained" className={classes.transferButton} - onClick={handleTransferClick} - disabled={!isTargetComplete || isSending || isSendComplete} + onClick={handleClick} + disabled={disabled} > Transfer - {isSending ? ( + {showLoader ? ( { diff --git a/bridge_ui/src/hooks/useFetchTargetAsset.ts b/bridge_ui/src/hooks/useFetchTargetAsset.ts index d63ea888..525ce79b 100644 --- a/bridge_ui/src/hooks/useFetchTargetAsset.ts +++ b/bridge_ui/src/hooks/useFetchTargetAsset.ts @@ -26,14 +26,9 @@ function useFetchTargetAsset() { ); const originChain = useSelector(selectTransferOriginChain); const originAsset = useSelector(selectTransferOriginAsset); - console.log( - "WH Wrapped?", - isSourceAssetWormholeWrapped, - originChain, - originAsset - ); const targetChain = useSelector(selectTransferTargetChain); const { provider } = useEthereumProvider(); + // TODO: this may not cover wrapped to wrapped, should always use origin? useEffect(() => { if (isSourceAssetWormholeWrapped && originChain === targetChain) { dispatch(setTargetAsset(originAsset)); @@ -57,7 +52,6 @@ function useFetchTargetAsset() { try { const asset = await getForeignAssetSol(sourceChain, sourceAsset); if (!cancelled) { - console.log("solana target asset", asset); dispatch(setTargetAsset(asset)); } } catch (e) { diff --git a/bridge_ui/src/hooks/useGetBalanceEffect.ts b/bridge_ui/src/hooks/useGetBalanceEffect.ts index e67ee6c4..e0d2734e 100644 --- a/bridge_ui/src/hooks/useGetBalanceEffect.ts +++ b/bridge_ui/src/hooks/useGetBalanceEffect.ts @@ -96,7 +96,6 @@ function useGetBalanceEffect(sourceOrTarget: "source" | "target") { .getParsedTokenAccountsByOwner(solPK, { mint }) .then(({ value }) => { if (!cancelled) { - console.log("parsed token accounts", value); if (value.length) { // TODO: allow selection between these target accounts dispatch( diff --git a/bridge_ui/src/hooks/useHandleAttest.ts b/bridge_ui/src/hooks/useHandleAttest.ts new file mode 100644 index 00000000..61536224 --- /dev/null +++ b/bridge_ui/src/hooks/useHandleAttest.ts @@ -0,0 +1,198 @@ +import { + attestFromEth, + attestFromSolana, + attestFromTerra, + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + CHAIN_ID_TERRA, + getEmitterAddressEth, + getEmitterAddressSolana, + getEmitterAddressTerra, + parseSequenceFromLogEth, + parseSequenceFromLogSolana, + parseSequenceFromLogTerra, +} from "@certusone/wormhole-sdk"; +import { WalletContextState } from "@solana/wallet-adapter-react"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { + ConnectedWallet, + useConnectedWallet, +} from "@terra-money/wallet-provider"; +import { useSnackbar } from "notistack"; +import { useCallback, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Signer } from "../../../sdk/js/node_modules/ethers/lib"; +import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { useSolanaWallet } from "../contexts/SolanaWalletContext"; +import { setIsSending, setSignedVAAHex } from "../store/attestSlice"; +import { + selectAttestIsSendComplete, + selectAttestIsSending, + selectAttestIsTargetComplete, + selectAttestSourceAsset, + selectAttestSourceChain, +} from "../store/selectors"; +import { uint8ArrayToHex } from "../utils/array"; +import { + ETH_BRIDGE_ADDRESS, + ETH_TOKEN_BRIDGE_ADDRESS, + SOLANA_HOST, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + TERRA_TOKEN_BRIDGE_ADDRESS, +} from "../utils/consts"; +import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry"; +import parseError from "../utils/parseError"; +import { signSendAndConfirm } from "../utils/solana"; +import { waitForTerraExecution } from "../utils/terra"; + +async function eth( + dispatch: any, + enqueueSnackbar: any, + signer: Signer, + sourceAsset: string +) { + dispatch(setIsSending(true)); + try { + const receipt = await attestFromEth( + ETH_TOKEN_BRIDGE_ADDRESS, + signer, + sourceAsset + ); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS); + const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS); + enqueueSnackbar("Fetching VAA", { variant: "info" }); + const { vaaBytes } = await getSignedVAAWithRetry( + CHAIN_ID_ETH, + emitterAddress, + sequence + ); + dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); + enqueueSnackbar("Fetched Signed VAA", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsSending(false)); + } +} + +async function solana( + dispatch: any, + enqueueSnackbar: any, + solPK: PublicKey, + sourceAsset: string, + wallet: WalletContextState +) { + dispatch(setIsSending(true)); + try { + // TODO: share connection in context? + const connection = new Connection(SOLANA_HOST, "confirmed"); + const transaction = await attestFromSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + solPK.toString(), + sourceAsset + ); + const txid = await signSendAndConfirm(wallet, connection, transaction); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + const info = await connection.getTransaction(txid); + if (!info) { + // TODO: error state + throw new Error("An error occurred while fetching the transaction info"); + } + const sequence = parseSequenceFromLogSolana(info); + const emitterAddress = await getEmitterAddressSolana( + SOL_TOKEN_BRIDGE_ADDRESS + ); + enqueueSnackbar("Fetching VAA", { variant: "info" }); + const { vaaBytes } = await getSignedVAAWithRetry( + CHAIN_ID_SOLANA, + emitterAddress, + sequence + ); + dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); + enqueueSnackbar("Fetched Signed VAA", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsSending(false)); + } +} + +async function terra( + dispatch: any, + enqueueSnackbar: any, + wallet: ConnectedWallet, + asset: string +) { + dispatch(setIsSending(true)); + try { + const infoMaybe = await attestFromTerra( + TERRA_TOKEN_BRIDGE_ADDRESS, + wallet, + asset + ); + const info = await waitForTerraExecution(wallet, infoMaybe); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + const sequence = parseSequenceFromLogTerra(info); + const emitterAddress = await getEmitterAddressTerra( + TERRA_TOKEN_BRIDGE_ADDRESS + ); + enqueueSnackbar("Fetching VAA", { variant: "info" }); + const { vaaBytes } = await getSignedVAAWithRetry( + CHAIN_ID_TERRA, + emitterAddress, + sequence + ); + dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); + enqueueSnackbar("Fetched Signed VAA", { variant: "success" }); + } catch (e) { + console.error(e); + dispatch(setIsSending(false)); + } +} + +export function useHandleAttest() { + const dispatch = useDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const sourceChain = useSelector(selectAttestSourceChain); + const sourceAsset = useSelector(selectAttestSourceAsset); + const isTargetComplete = useSelector(selectAttestIsTargetComplete); + const isSending = useSelector(selectAttestIsSending); + const isSendComplete = useSelector(selectAttestIsSendComplete); + const { signer } = useEthereumProvider(); + const solanaWallet = useSolanaWallet(); + const solPK = solanaWallet?.publicKey; + const terraWallet = useConnectedWallet(); + const disabled = !isTargetComplete || isSending || isSendComplete; + const handleAttestClick = useCallback(() => { + if (sourceChain === CHAIN_ID_ETH && !!signer) { + eth(dispatch, enqueueSnackbar, signer, sourceAsset); + } else if (sourceChain === CHAIN_ID_SOLANA && !!solanaWallet && !!solPK) { + solana(dispatch, enqueueSnackbar, solPK, sourceAsset, solanaWallet); + } else if (sourceChain === CHAIN_ID_TERRA && !!terraWallet) { + terra(dispatch, enqueueSnackbar, terraWallet, sourceAsset); + } else { + // enqueueSnackbar("Attesting from this chain is not yet supported", { + // variant: "error", + // }); + } + }, [ + dispatch, + enqueueSnackbar, + sourceChain, + signer, + solanaWallet, + solPK, + terraWallet, + sourceAsset, + ]); + return useMemo( + () => ({ + handleClick: handleAttestClick, + disabled, + showLoader: isSending, + }), + [handleAttestClick, disabled, isSending] + ); +} diff --git a/bridge_ui/src/hooks/useHandleCreateWrapped.ts b/bridge_ui/src/hooks/useHandleCreateWrapped.ts new file mode 100644 index 00000000..e8cb559a --- /dev/null +++ b/bridge_ui/src/hooks/useHandleCreateWrapped.ts @@ -0,0 +1,160 @@ +import { + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + CHAIN_ID_TERRA, + createWrappedOnEth, + createWrappedOnSolana, + createWrappedOnTerra, + postVaaSolana, +} from "@certusone/wormhole-sdk"; +import { WalletContextState } from "@solana/wallet-adapter-react"; +import { Connection } from "@solana/web3.js"; +import { + ConnectedWallet, + useConnectedWallet, +} from "@terra-money/wallet-provider"; +import { Signer } from "ethers"; +import { useSnackbar } from "notistack"; +import { useCallback, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { useSolanaWallet } from "../contexts/SolanaWalletContext"; +import useAttestSignedVAA from "../hooks/useAttestSignedVAA"; +import { reset, setIsCreating } from "../store/attestSlice"; +import { + selectAttestIsCreating, + selectAttestTargetChain, +} from "../store/selectors"; +import { + ETH_TOKEN_BRIDGE_ADDRESS, + SOLANA_HOST, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + TERRA_TOKEN_BRIDGE_ADDRESS, +} from "../utils/consts"; +import parseError from "../utils/parseError"; +import { signSendAndConfirm } from "../utils/solana"; + +async function eth( + dispatch: any, + enqueueSnackbar: any, + signer: Signer, + signedVAA: Uint8Array +) { + dispatch(setIsCreating(true)); + try { + await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA); + dispatch(reset()); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsCreating(false)); + } +} + +async function solana( + dispatch: any, + enqueueSnackbar: any, + wallet: WalletContextState, + payerAddress: string, // TODO: we may not need this since we have wallet + signedVAA: Uint8Array +) { + dispatch(setIsCreating(true)); + try { + // TODO: share connection in context? + const connection = new Connection(SOLANA_HOST, "confirmed"); + await postVaaSolana( + connection, + wallet.signTransaction, + SOL_BRIDGE_ADDRESS, + payerAddress, + Buffer.from(signedVAA) + ); + const transaction = await createWrappedOnSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + signedVAA + ); + await signSendAndConfirm(wallet, connection, transaction); + dispatch(reset()); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsCreating(false)); + } +} + +async function terra( + dispatch: any, + enqueueSnackbar: any, + wallet: ConnectedWallet, + signedVAA: Uint8Array +) { + dispatch(setIsCreating(true)); + try { + await createWrappedOnTerra(TERRA_TOKEN_BRIDGE_ADDRESS, wallet, signedVAA); + dispatch(reset()); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsCreating(false)); + } +} + +export function useHandleCreateWrapped() { + const dispatch = useDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const targetChain = useSelector(selectAttestTargetChain); + const solanaWallet = useSolanaWallet(); + const solPK = solanaWallet?.publicKey; + const signedVAA = useAttestSignedVAA(); + const isCreating = useSelector(selectAttestIsCreating); + const { signer } = useEthereumProvider(); + const terraWallet = useConnectedWallet(); + const handleCreateClick = useCallback(() => { + if (targetChain === CHAIN_ID_ETH && !!signer && !!signedVAA) { + eth(dispatch, enqueueSnackbar, signer, signedVAA); + } else if ( + targetChain === CHAIN_ID_SOLANA && + !!solanaWallet && + !!solPK && + !!signedVAA + ) { + solana( + dispatch, + enqueueSnackbar, + solanaWallet, + solPK.toString(), + signedVAA + ); + } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && !!signedVAA) { + terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); + } else { + // enqueueSnackbar( + // "Creating wrapped tokens on this chain is not yet supported", + // { + // variant: "error", + // } + // ); + } + }, [ + dispatch, + enqueueSnackbar, + targetChain, + solanaWallet, + solPK, + terraWallet, + signedVAA, + signer, + ]); + return useMemo( + () => ({ + handleClick: handleCreateClick, + disabled: !!isCreating, + showLoader: !!isCreating, + }), + [handleCreateClick, isCreating] + ); +} diff --git a/bridge_ui/src/hooks/useHandleRedeem.ts b/bridge_ui/src/hooks/useHandleRedeem.ts new file mode 100644 index 00000000..0ee3caff --- /dev/null +++ b/bridge_ui/src/hooks/useHandleRedeem.ts @@ -0,0 +1,190 @@ +import { + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + CHAIN_ID_TERRA, + postVaaSolana, + redeemOnEth, + redeemOnSolana, +} from "@certusone/wormhole-sdk"; +import { WalletContextState } from "@solana/wallet-adapter-react"; +import { Connection } from "@solana/web3.js"; +import { MsgExecuteContract } from "@terra-money/terra.js"; +import { + ConnectedWallet, + useConnectedWallet, +} from "@terra-money/wallet-provider"; +import { Signer } from "ethers"; +import { fromUint8Array } from "js-base64"; +import { useSnackbar } from "notistack"; +import { useCallback, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { useSolanaWallet } from "../contexts/SolanaWalletContext"; +import useTransferSignedVAA from "../hooks/useTransferSignedVAA"; +import { + selectTransferIsRedeeming, + selectTransferIsSourceAssetWormholeWrapped, + selectTransferOriginChain, + selectTransferTargetAsset, + selectTransferTargetChain, +} from "../store/selectors"; +import { reset, setIsRedeeming } from "../store/transferSlice"; +import { + ETH_TOKEN_BRIDGE_ADDRESS, + SOLANA_HOST, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + TERRA_TOKEN_BRIDGE_ADDRESS, +} from "../utils/consts"; +import parseError from "../utils/parseError"; +import { signSendAndConfirm } from "../utils/solana"; + +async function eth( + dispatch: any, + enqueueSnackbar: any, + signer: Signer, + signedVAA: Uint8Array +) { + dispatch(setIsRedeeming(true)); + try { + await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA); + dispatch(reset()); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsRedeeming(false)); + } +} + +async function solana( + dispatch: any, + enqueueSnackbar: any, + wallet: WalletContextState, + payerAddress: string, //TODO: we may not need this since we have wallet + signedVAA: Uint8Array, + isSolanaNative: boolean, + mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist +) { + dispatch(setIsRedeeming(true)); + try { + // TODO: share connection in context? + const connection = new Connection(SOLANA_HOST, "confirmed"); + await postVaaSolana( + connection, + wallet.signTransaction, + SOL_BRIDGE_ADDRESS, + payerAddress, + Buffer.from(signedVAA) + ); + // TODO: how do we retry in between these steps + const transaction = await redeemOnSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + signedVAA, + isSolanaNative, + mintAddress + ); + await signSendAndConfirm(wallet, connection, transaction); + dispatch(reset()); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsRedeeming(false)); + } +} + +async function terra( + dispatch: any, + enqueueSnackbar: any, + wallet: ConnectedWallet, + signedVAA: Uint8Array +) { + dispatch(setIsRedeeming(true)); + try { + await wallet.post({ + msgs: [ + new MsgExecuteContract( + wallet.terraAddress, + TERRA_TOKEN_BRIDGE_ADDRESS, + { + submit_vaa: { + data: fromUint8Array(signedVAA), + }, + }, + { uluna: 1000 } + ), + ], + memo: "Complete Transfer", + }); + dispatch(reset()); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + } catch (e) { + enqueueSnackbar(parseError(e), { variant: "error" }); + dispatch(setIsRedeeming(false)); + } +} + +export function useHandleRedeem() { + const dispatch = useDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const isSourceAssetWormholeWrapped = useSelector( + selectTransferIsSourceAssetWormholeWrapped + ); + const originChain = useSelector(selectTransferOriginChain); + const targetChain = useSelector(selectTransferTargetChain); + const targetAsset = useSelector(selectTransferTargetAsset); + const solanaWallet = useSolanaWallet(); + const solPK = solanaWallet?.publicKey; + const { signer } = useEthereumProvider(); + const terraWallet = useConnectedWallet(); + const signedVAA = useTransferSignedVAA(); + const isRedeeming = useSelector(selectTransferIsRedeeming); + const handleRedeemClick = useCallback(() => { + if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) { + eth(dispatch, enqueueSnackbar, signer, signedVAA); + } else if ( + targetChain === CHAIN_ID_SOLANA && + !!solanaWallet && + !!solPK && + signedVAA + ) { + solana( + dispatch, + enqueueSnackbar, + solanaWallet, + solPK.toString(), + signedVAA, + !!isSourceAssetWormholeWrapped && originChain === CHAIN_ID_SOLANA, + targetAsset || undefined + ); + } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) { + terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); + } else { + // enqueueSnackbar("Redeeming on this chain is not yet supported", { + // variant: "error", + // }); + } + }, [ + dispatch, + enqueueSnackbar, + targetChain, + signer, + signedVAA, + solanaWallet, + solPK, + terraWallet, + isSourceAssetWormholeWrapped, + originChain, + targetAsset, + ]); + return useMemo( + () => ({ + handleClick: handleRedeemClick, + disabled: !!isRedeeming, + showLoader: !!isRedeeming, + }), + [handleRedeemClick, isRedeeming] + ); +} diff --git a/bridge_ui/src/hooks/useHandleTransfer.ts b/bridge_ui/src/hooks/useHandleTransfer.ts new file mode 100644 index 00000000..7bdc9249 --- /dev/null +++ b/bridge_ui/src/hooks/useHandleTransfer.ts @@ -0,0 +1,354 @@ +import { + ChainId, + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + CHAIN_ID_TERRA, + getEmitterAddressEth, + getEmitterAddressSolana, + parseSequenceFromLogEth, + parseSequenceFromLogSolana, + parseSequenceFromLogTerra, + transferFromEth, + transferFromSolana, +} from "@certusone/wormhole-sdk"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { WalletContextState } from "@solana/wallet-adapter-react"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { MsgExecuteContract } from "@terra-money/terra.js"; +import { + ConnectedWallet, + useConnectedWallet, +} from "@terra-money/wallet-provider"; +import { Signer } from "ethers"; +import { arrayify, parseUnits, zeroPad } from "ethers/lib/utils"; +import { useSnackbar } from "notistack"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { useSolanaWallet } from "../contexts/SolanaWalletContext"; +import { + selectTransferAmount, + selectTransferIsSendComplete, + selectTransferIsSending, + selectTransferIsTargetComplete, + selectTransferOriginAsset, + selectTransferOriginChain, + selectTransferSourceAsset, + selectTransferSourceChain, + selectTransferSourceParsedTokenAccount, + selectTransferTargetAsset, + selectTransferTargetChain, + selectTransferTargetParsedTokenAccount, +} from "../store/selectors"; +import { setIsSending, setSignedVAAHex } from "../store/transferSlice"; +import { hexToUint8Array, uint8ArrayToHex } from "../utils/array"; +import { + ETH_BRIDGE_ADDRESS, + ETH_TOKEN_BRIDGE_ADDRESS, + SOLANA_HOST, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + TERRA_TOKEN_BRIDGE_ADDRESS, +} from "../utils/consts"; +import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry"; +import { signSendAndConfirm } from "../utils/solana"; + +async function eth( + dispatch: any, + enqueueSnackbar: any, + signer: Signer, + tokenAddress: string, + decimals: number, + amount: string, + recipientChain: ChainId, + recipientAddress: Uint8Array +) { + dispatch(setIsSending(true)); + try { + const amountParsed = parseUnits(amount, decimals); + const receipt = await transferFromEth( + ETH_TOKEN_BRIDGE_ADDRESS, + signer, + tokenAddress, + amountParsed, + recipientChain, + recipientAddress + ); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS); + const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS); + enqueueSnackbar("Fetching VAA", { variant: "info" }); + const { vaaBytes } = await getSignedVAAWithRetry( + CHAIN_ID_ETH, + emitterAddress, + sequence.toString() + ); + dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); + enqueueSnackbar("Fetched Signed VAA", { variant: "success" }); + } catch (e) { + console.error(e); + dispatch(setIsSending(false)); + } +} + +async function solana( + dispatch: any, + enqueueSnackbar: any, + wallet: WalletContextState, + payerAddress: string, //TODO: we may not need this since we have wallet + fromAddress: string, + mintAddress: string, + amount: string, + decimals: number, + targetAddressStr: string, + targetChain: ChainId, + originAddressStr?: string, + originChain?: ChainId +) { + dispatch(setIsSending(true)); + try { + //TODO: check if token attestation exists on the target chain + // TODO: share connection in context? + const connection = new Connection(SOLANA_HOST, "confirmed"); + const targetAddress = zeroPad(arrayify(targetAddressStr), 32); + const amountParsed = parseUnits(amount, decimals).toBigInt(); + const originAddress = originAddressStr + ? zeroPad(hexToUint8Array(originAddressStr), 32) + : undefined; + const transaction = await transferFromSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + fromAddress, + mintAddress, + amountParsed, + targetAddress, + targetChain, + originAddress, + originChain + ); + const txid = await signSendAndConfirm(wallet, connection, transaction); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + const info = await connection.getTransaction(txid); + if (!info) { + throw new Error("An error occurred while fetching the transaction info"); + } + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + const sequence = parseSequenceFromLogSolana(info); + const emitterAddress = await getEmitterAddressSolana( + SOL_TOKEN_BRIDGE_ADDRESS + ); + enqueueSnackbar("Fetching VAA", { variant: "info" }); + const { vaaBytes } = await getSignedVAAWithRetry( + CHAIN_ID_SOLANA, + emitterAddress, + sequence + ); + + dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); + enqueueSnackbar("Fetched Signed VAA", { variant: "success" }); + } catch (e) { + console.error(e); + dispatch(setIsSending(false)); + } +} + +async function terra( + dispatch: any, + enqueueSnackbar: any, + wallet: ConnectedWallet, + asset: string, + amount: string, + targetAddressStr: string, + targetChain: ChainId +) { + dispatch(setIsSending(true)); + try { + // TODO: SDK + const result = await wallet.post({ + msgs: [ + new MsgExecuteContract( + wallet.terraAddress, + TERRA_TOKEN_BRIDGE_ADDRESS, + { + initiate_transfer: { + asset: asset, + amount: amount, + recipient_chain: targetChain, + recipient: targetAddressStr, + fee: 1000, + nonce: 0, + }, + }, + { uluna: 1000 } + ), + ], + memo: "Complete Transfer", + }); + enqueueSnackbar("Transaction confirmed", { variant: "success" }); + console.log(result); + const sequence = parseSequenceFromLogTerra(result); + console.log(sequence); + const emitterAddress = await getEmitterAddressSolana( + SOL_TOKEN_BRIDGE_ADDRESS + ); + console.log(emitterAddress); + enqueueSnackbar("Fetching VAA", { variant: "info" }); + const { vaaBytes } = await getSignedVAAWithRetry( + CHAIN_ID_TERRA, + emitterAddress, + sequence + ); + enqueueSnackbar("Fetched Signed VAA", { variant: "success" }); + dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); + } catch (e) { + console.error(e); + dispatch(setIsSending(false)); + } +} + +export function useHandleTransfer() { + const dispatch = useDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const sourceChain = useSelector(selectTransferSourceChain); + const sourceAsset = useSelector(selectTransferSourceAsset); + const originChain = useSelector(selectTransferOriginChain); + const originAsset = useSelector(selectTransferOriginAsset); + const amount = useSelector(selectTransferAmount); + const targetChain = useSelector(selectTransferTargetChain); + const targetAsset = useSelector(selectTransferTargetAsset); + const isTargetComplete = useSelector(selectTransferIsTargetComplete); + const isSending = useSelector(selectTransferIsSending); + const isSendComplete = useSelector(selectTransferIsSendComplete); + const { signer, signerAddress } = useEthereumProvider(); + const solanaWallet = useSolanaWallet(); + const solPK = solanaWallet?.publicKey; + const terraWallet = useConnectedWallet(); + const sourceParsedTokenAccount = useSelector( + selectTransferSourceParsedTokenAccount + ); + const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey; + const decimals = sourceParsedTokenAccount?.decimals; + const targetParsedTokenAccount = useSelector( + selectTransferTargetParsedTokenAccount + ); + const disabled = !isTargetComplete || isSending || isSendComplete; + // TODO: we probably shouldn't get here if we don't have this public key + // TODO: also this is just for solana... send help(ers) + const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey; + // TODO: AVOID THIS DANGEROUS CACOPHONY + const tpkRef = useRef(undefined); + useEffect(() => { + (async () => { + if (targetChain === CHAIN_ID_SOLANA) { + tpkRef.current = targetTokenAccountPublicKey + ? zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32) // use the target's TokenAccount if it exists + : solPK && targetAsset // otherwise, use the associated token account (which we create in the case it doesn't exist) + ? zeroPad( + ( + await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + new PublicKey(targetAsset), + solPK + ) + ).toBytes(), + 32 + ) + : undefined; + } else tpkRef.current = undefined; + })(); + }, [targetChain, solPK, targetAsset, targetTokenAccountPublicKey]); + // TODO: dynamically get "to" wallet + const handleTransferClick = useCallback(() => { + // TODO: we should separate state for transaction vs fetching vaa + // TODO: more generic way of calling these + if ( + sourceChain === CHAIN_ID_ETH && + !!signer && + decimals !== undefined && + !!tpkRef.current + ) { + eth( + dispatch, + enqueueSnackbar, + signer, + sourceAsset, + decimals, + amount, + targetChain, + tpkRef.current + ); + } else if ( + sourceChain === CHAIN_ID_SOLANA && + !!solanaWallet && + !!solPK && + !!sourceTokenPublicKey && + !!signerAddress && + decimals !== undefined + ) { + solana( + dispatch, + enqueueSnackbar, + solanaWallet, + solPK.toString(), + sourceTokenPublicKey, + sourceAsset, + amount, //TODO: avoid decimals, pass in parsed amount + decimals, + signerAddress, + targetChain, + originAsset, + originChain + ); + } else if ( + sourceChain === CHAIN_ID_TERRA && + !!terraWallet && + decimals !== undefined && + !!signerAddress + ) { + terra( + dispatch, + enqueueSnackbar, + terraWallet, + sourceAsset, + amount, + signerAddress, // TODO: only works for Eth + targetChain + ); + } else { + // enqueueSnackbar("Transfers from this chain are not yet supported", { + // variant: "error", + // }); + } + }, [ + dispatch, + enqueueSnackbar, + sourceChain, + signer, + signerAddress, + solanaWallet, + solPK, + terraWallet, + sourceTokenPublicKey, + sourceAsset, + amount, + decimals, + targetChain, + originAsset, + originChain, + ]); + return useMemo( + () => ({ + handleClick: handleTransferClick, + disabled, + showLoader: isSending, + }), + [handleTransferClick, disabled, isSending] + ); +} diff --git a/bridge_ui/src/index.js b/bridge_ui/src/index.js index 39c2dc77..e33732f0 100644 --- a/bridge_ui/src/index.js +++ b/bridge_ui/src/index.js @@ -1,5 +1,6 @@ import { CssBaseline } from "@material-ui/core"; import { ThemeProvider } from "@material-ui/core/styles"; +import { SnackbarProvider } from "notistack"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import { HashRouter } from "react-router-dom"; @@ -16,15 +17,17 @@ ReactDOM.render( - - - - - - - - - + + + + + + + + + + + , document.getElementById("root") diff --git a/bridge_ui/src/utils/attestFrom.ts b/bridge_ui/src/utils/attestFrom.ts deleted file mode 100644 index 57c5ac50..00000000 --- a/bridge_ui/src/utils/attestFrom.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - attestFromEth as attestEthTx, - attestFromSolana as attestSolanaTx, - attestFromTerra as attestTerraTx, - CHAIN_ID_ETH, - CHAIN_ID_SOLANA, - CHAIN_ID_TERRA, - getEmitterAddressEth, - getEmitterAddressSolana, - getEmitterAddressTerra, - parseSequenceFromLogEth, - parseSequenceFromLogSolana, - parseSequenceFromLogTerra, -} from "@certusone/wormhole-sdk"; -import { WalletContextState } from "@solana/wallet-adapter-react"; -import { Connection } from "@solana/web3.js"; -import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider"; -import { ethers } from "ethers"; -import { - ETH_BRIDGE_ADDRESS, - ETH_TOKEN_BRIDGE_ADDRESS, - TERRA_TOKEN_BRIDGE_ADDRESS, - SOLANA_HOST, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, -} from "./consts"; -import { getSignedVAAWithRetry } from "./getSignedVAAWithRetry"; -import { signSendConfirmAndGet } from "./solana"; -import { waitForTerraExecution } from "./terra"; - -export async function attestFromEth( - signer: ethers.Signer | undefined, - tokenAddress: string -) { - if (!signer) return; - const receipt = await attestEthTx( - ETH_TOKEN_BRIDGE_ADDRESS, - signer, - tokenAddress - ); - const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS); - const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS); - const { vaaBytes } = await getSignedVAAWithRetry( - CHAIN_ID_ETH, - emitterAddress, - sequence - ); - return vaaBytes; -} - -export async function attestFromSolana( - wallet: WalletContextState, - payerAddress: string | undefined, //TODO: we may not need this since we have wallet - mintAddress: string -) { - if (!wallet || !wallet.publicKey || !payerAddress) return; - // TODO: share connection in context? - const connection = new Connection(SOLANA_HOST, "confirmed"); - const transaction = await attestSolanaTx( - connection, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - payerAddress, - mintAddress - ); - const info = await signSendConfirmAndGet(wallet, connection, transaction); - if (!info) { - throw new Error("An error occurred while fetching the transaction info"); - } - const sequence = parseSequenceFromLogSolana(info); - const emitterAddress = await getEmitterAddressSolana( - SOL_TOKEN_BRIDGE_ADDRESS - ); - const { vaaBytes } = await getSignedVAAWithRetry( - CHAIN_ID_SOLANA, - emitterAddress, - sequence - ); - return vaaBytes; -} - -export async function attestFromTerra( - wallet: TerraConnectedWallet | undefined, - asset: string | undefined -) { - if (!wallet || !asset) return; - const infoMaybe = await attestTerraTx( - TERRA_TOKEN_BRIDGE_ADDRESS, - wallet, - asset - ); - const info = await waitForTerraExecution(wallet, infoMaybe); - const sequence = parseSequenceFromLogTerra(info); - const emitterAddress = await getEmitterAddressTerra( - TERRA_TOKEN_BRIDGE_ADDRESS - ); - const result = await getSignedVAAWithRetry( - CHAIN_ID_TERRA, - emitterAddress, - sequence - ); - return result && result.vaaBytes; -} diff --git a/bridge_ui/src/utils/createWrappedOn.ts b/bridge_ui/src/utils/createWrappedOn.ts deleted file mode 100644 index b91dc0d3..00000000 --- a/bridge_ui/src/utils/createWrappedOn.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - postVaaSolana, - createWrappedOnEth as createWrappedOnEthTx, - createWrappedOnSolana as createWrappedOnSolanaTx, - createWrappedOnTerra as createWrappedOnTerraTx, -} from "@certusone/wormhole-sdk"; -import { Connection } from "@solana/web3.js"; -import { ethers } from "ethers"; -import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider"; -import { - ETH_TOKEN_BRIDGE_ADDRESS, - SOLANA_HOST, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - TERRA_TOKEN_BRIDGE_ADDRESS, - TERRA_BRIDGE_ADDRESS, -} from "./consts"; -import { signSendAndConfirm } from "./solana"; -import { WalletContextState } from "@solana/wallet-adapter-react"; - -export async function createWrappedOnEth( - signer: ethers.Signer | undefined, - signedVAA: Uint8Array -) { - if (!signer) return; - await createWrappedOnEthTx(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA); -} - -export async function createWrappedOnTerra( - wallet: TerraConnectedWallet | undefined, - signedVAA: Uint8Array -) { - if (!wallet) return; - await createWrappedOnTerraTx( - TERRA_TOKEN_BRIDGE_ADDRESS, - wallet, - signedVAA - ); -} - -export async function createWrappedOnSolana( - wallet: WalletContextState | undefined, - payerAddress: string | undefined, //TODO: we may not need this since we have wallet - signedVAA: Uint8Array -) { - if (!wallet || !wallet.publicKey || !payerAddress) return; - // TODO: share connection in context? - const connection = new Connection(SOLANA_HOST, "confirmed"); - await postVaaSolana( - connection, - wallet.signTransaction, - SOL_BRIDGE_ADDRESS, - payerAddress, - Buffer.from(signedVAA) - ); - - const transaction = await createWrappedOnSolanaTx( - connection, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - payerAddress, - signedVAA - ); - await signSendAndConfirm(wallet, connection, transaction); -} diff --git a/bridge_ui/src/utils/parseError.ts b/bridge_ui/src/utils/parseError.ts new file mode 100644 index 00000000..4fa63a89 --- /dev/null +++ b/bridge_ui/src/utils/parseError.ts @@ -0,0 +1,9 @@ +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?.message + ? e.message + : "An unknown error occurred"; +export default parseError; diff --git a/bridge_ui/src/utils/redeemOn.ts b/bridge_ui/src/utils/redeemOn.ts deleted file mode 100644 index 5970cb90..00000000 --- a/bridge_ui/src/utils/redeemOn.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - postVaaSolana, - redeemOnEth as redeemOnEthTx, - redeemOnSolana as redeemOnSolanaTx, -} from "@certusone/wormhole-sdk"; -import { Connection } from "@solana/web3.js"; -import { ethers } from "ethers"; -import { fromUint8Array } from "js-base64"; -import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider"; -import { MsgExecuteContract } from "@terra-money/terra.js"; -import { - ETH_TOKEN_BRIDGE_ADDRESS, - SOLANA_HOST, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - TERRA_TOKEN_BRIDGE_ADDRESS, -} from "./consts"; -import { signSendAndConfirm } from "./solana"; -import { WalletContextState } from "@solana/wallet-adapter-react"; - -export async function redeemOnEth( - signer: ethers.Signer | undefined, - signedVAA: Uint8Array -) { - if (!signer) return; - await redeemOnEthTx(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA); -} - -export async function redeemOnSolana( - wallet: WalletContextState | undefined, - payerAddress: string | undefined, //TODO: we may not need this since we have wallet - signedVAA: Uint8Array, - isSolanaNative: boolean, - mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist -) { - if (!wallet || !wallet.publicKey || !payerAddress) return; - // TODO: share connection in context? - const connection = new Connection(SOLANA_HOST, "confirmed"); - await postVaaSolana( - connection, - wallet.signTransaction, - SOL_BRIDGE_ADDRESS, - payerAddress, - Buffer.from(signedVAA) - ); - - const transaction = await redeemOnSolanaTx( - connection, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - payerAddress, - signedVAA, - isSolanaNative, - mintAddress - ); - await signSendAndConfirm(wallet, connection, transaction); -} - -export async function redeemOnTerra( - wallet: TerraConnectedWallet | undefined, - signedVAA: Uint8Array -) { - if (!wallet) return; - wallet && - (await wallet.post({ - msgs: [ - new MsgExecuteContract( - wallet.terraAddress, - TERRA_TOKEN_BRIDGE_ADDRESS, - { - submit_vaa: { - data: fromUint8Array(signedVAA), - }, - }, - { uluna: 1000 } - ), - ], - memo: "Complete Transfer", - })); -} diff --git a/bridge_ui/src/utils/solana.ts b/bridge_ui/src/utils/solana.ts index 89bcc729..e7f129d6 100644 --- a/bridge_ui/src/utils/solana.ts +++ b/bridge_ui/src/utils/solana.ts @@ -11,12 +11,3 @@ export async function signSendAndConfirm( await connection.confirmTransaction(txid); return txid; } - -export async function signSendConfirmAndGet( - wallet: WalletContextState, - connection: Connection, - transaction: Transaction -) { - const txid = await signSendAndConfirm(wallet, connection, transaction); - return await connection.getTransaction(txid); -} diff --git a/bridge_ui/src/utils/terra.ts b/bridge_ui/src/utils/terra.ts index ae5321bf..0c66c2a5 100644 --- a/bridge_ui/src/utils/terra.ts +++ b/bridge_ui/src/utils/terra.ts @@ -1,5 +1,8 @@ -import { TxResult, ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider"; -import { TxInfo, LCDClient } from "@terra-money/terra.js"; +import { + TxResult, + ConnectedWallet as TerraConnectedWallet, +} from "@terra-money/wallet-provider"; +import { LCDClient } from "@terra-money/terra.js"; // TODO: Loop txInfo for timed out transactions. // lcd.tx.txInfo(transaction.result.txhash); @@ -7,9 +10,9 @@ export async function waitForTerraExecution( wallet: TerraConnectedWallet, transaction: TxResult ) { - const lcd = new LCDClient({ + new LCDClient({ URL: wallet.network.lcd, - chainID: "columbus-4", + chainID: "columbus-4", }); return transaction; } diff --git a/bridge_ui/src/utils/transferFrom.ts b/bridge_ui/src/utils/transferFrom.ts deleted file mode 100644 index da25320e..00000000 --- a/bridge_ui/src/utils/transferFrom.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - ChainId, - CHAIN_ID_ETH, - CHAIN_ID_SOLANA, - CHAIN_ID_TERRA, - getEmitterAddressEth, - getEmitterAddressSolana, - parseSequenceFromLogEth, - parseSequenceFromLogSolana, - parseSequenceFromLogTerra, - transferFromEth as transferFromEthTx, - transferFromSolana as transferFromSolanaTx, -} from "@certusone/wormhole-sdk"; -import { fromUint8Array } from "js-base64"; -import { - ConnectedWallet as TerraConnectedWallet, - TxResult, -} from "@terra-money/wallet-provider"; -import { Connection } from "@solana/web3.js"; -import { MsgExecuteContract } from "@terra-money/terra.js"; -import { ethers } from "ethers"; -import { arrayify, parseUnits, zeroPad } from "ethers/lib/utils"; -import { hexToUint8Array } from "./array"; -import { - ETH_BRIDGE_ADDRESS, - ETH_TOKEN_BRIDGE_ADDRESS, - SOLANA_HOST, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - TERRA_TOKEN_BRIDGE_ADDRESS, - TERRA_TEST_TOKEN_ADDRESS, -} from "./consts"; -import { getSignedVAAWithRetry } from "./getSignedVAAWithRetry"; -import { signSendConfirmAndGet } from "./solana"; -import { WalletContextState } from "@solana/wallet-adapter-react"; - -// TODO: overall better input checking and error handling -export async function transferFromEth( - signer: ethers.Signer | undefined, - tokenAddress: string, - decimals: number, - amount: string, - recipientChain: ChainId, - recipientAddress: Uint8Array | undefined -) { - if (!signer || !recipientAddress) return; - //TODO: check if token attestation exists on the target chain - const amountParsed = parseUnits(amount, decimals); - const receipt = await transferFromEthTx( - ETH_TOKEN_BRIDGE_ADDRESS, - signer, - tokenAddress, - amountParsed, - recipientChain, - recipientAddress - ); - const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS); - const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS); - const { vaaBytes } = await getSignedVAAWithRetry( - CHAIN_ID_ETH, - emitterAddress, - sequence.toString() - ); - return vaaBytes; -} - -export async function transferFromSolana( - wallet: WalletContextState | undefined, - payerAddress: string | undefined, //TODO: we may not need this since we have wallet - fromAddress: string | undefined, - mintAddress: string, - amount: string, - decimals: number, - targetAddressStr: string | undefined, - targetChain: ChainId, - originAddressStr?: string, - originChain?: ChainId -) { - if ( - !wallet || - !wallet.publicKey || - !payerAddress || - !fromAddress || - !targetAddressStr || - (originChain && !originAddressStr) - ) - return; - // TODO: share connection in context? - const connection = new Connection(SOLANA_HOST, "confirmed"); - const targetAddress = zeroPad(arrayify(targetAddressStr), 32); - const amountParsed = parseUnits(amount, decimals).toBigInt(); - const originAddress = originAddressStr - ? zeroPad(hexToUint8Array(originAddressStr), 32) - : undefined; - const transaction = await transferFromSolanaTx( - connection, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - payerAddress, - fromAddress, - mintAddress, - amountParsed, - targetAddress, - targetChain, - originAddress, - originChain - ); - const info = await signSendConfirmAndGet(wallet, connection, transaction); - if (!info) { - throw new Error("An error occurred while fetching the transaction info"); - } - const sequence = parseSequenceFromLogSolana(info); - const emitterAddress = await getEmitterAddressSolana( - SOL_TOKEN_BRIDGE_ADDRESS - ); - const { vaaBytes } = await getSignedVAAWithRetry( - CHAIN_ID_SOLANA, - emitterAddress, - sequence - ); - return vaaBytes; -} - -export async function transferFromTerra( - wallet: TerraConnectedWallet | undefined, - asset: string, - amount: string, - targetAddressStr: string | undefined, - targetChain: ChainId -) { - if (!wallet) return; - const result: TxResult = - wallet && - (await wallet.post({ - msgs: [ - new MsgExecuteContract( - wallet.terraAddress, - TERRA_TOKEN_BRIDGE_ADDRESS, - { - initiate_transfer: { - asset: TERRA_TEST_TOKEN_ADDRESS, - amount: amount, - recipient_chain: targetChain, - recipient: targetAddressStr, - fee: 1000, - nonce: 0, - }, - }, - { uluna: 1000 } - ), - ], - memo: "Complete Transfer", - })); - console.log(result); - const sequence = parseSequenceFromLogTerra(result); - console.log(sequence); - const emitterAddress = await getEmitterAddressSolana( - SOL_TOKEN_BRIDGE_ADDRESS - ); - console.log(emitterAddress); - const { vaaBytes } = await getSignedVAAWithRetry( - CHAIN_ID_TERRA, - emitterAddress, - sequence - ); - return vaaBytes; -} diff --git a/sdk/js/src/bridge/parseSequenceFromLog.ts b/sdk/js/src/bridge/parseSequenceFromLog.ts index 95bd08cd..5655e781 100644 --- a/sdk/js/src/bridge/parseSequenceFromLog.ts +++ b/sdk/js/src/bridge/parseSequenceFromLog.ts @@ -9,7 +9,6 @@ export function parseSequenceFromLogEth( ): string { // TODO: dangerous!(?) const bridgeLog = receipt.logs.filter((l) => { - console.log(l.address, bridgeAddress); return l.address === bridgeAddress; })[0]; const { diff --git a/sdk/js/src/token_bridge/redeem.ts b/sdk/js/src/token_bridge/redeem.ts index 3328a979..ce54e798 100644 --- a/sdk/js/src/token_bridge/redeem.ts +++ b/sdk/js/src/token_bridge/redeem.ts @@ -32,7 +32,6 @@ export async function redeemOnSolana( await import("../solana/token/token_bridge"); const ixs = []; if (isSolanaNative) { - console.log("COMPLETE TRANSFER NATIVE"); ixs.push( ixFromRust( complete_transfer_native_ix(