From 78cdcb13aef3420506c11ebceb7c9b731f35c5b5 Mon Sep 17 00:00:00 2001 From: Chase Moran Date: Mon, 1 Nov 2021 19:34:35 -0400 Subject: [PATCH] bridge_ui: add ability to update attestations Change-Id: Iedb0418d2a3b24a979af99107ef8a4ca8c3a4619 --- bridge_ui/src/components/Attest/Create.tsx | 66 ++++++-- bridge_ui/src/hooks/useFetchForeignAsset.ts | 152 ++++++++++++++++++ .../src/hooks/useHandleCreateWrapped.tsx | 60 +++++-- sdk/js/src/token_bridge/index.ts | 1 + sdk/js/src/token_bridge/updateWrapped.ts | 27 ++++ sdk/js/src/utils/array.ts | 27 +++- 6 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 bridge_ui/src/hooks/useFetchForeignAsset.ts create mode 100644 sdk/js/src/token_bridge/updateWrapped.ts diff --git a/bridge_ui/src/components/Attest/Create.tsx b/bridge_ui/src/components/Attest/Create.tsx index 0fecefc2c..77a7f6335 100644 --- a/bridge_ui/src/components/Attest/Create.tsx +++ b/bridge_ui/src/components/Attest/Create.tsx @@ -1,27 +1,71 @@ +import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; +import { CircularProgress, makeStyles } from "@material-ui/core"; import { useSelector } from "react-redux"; +import useFetchForeignAsset from "../../hooks/useFetchForeignAsset"; import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped"; import useIsWalletReady from "../../hooks/useIsWalletReady"; -import { selectAttestTargetChain } from "../../store/selectors"; +import { + selectAttestSourceAsset, + selectAttestSourceChain, + selectAttestTargetChain, +} from "../../store/selectors"; import ButtonWithLoader from "../ButtonWithLoader"; import KeyAndBalance from "../KeyAndBalance"; import WaitingForWalletMessage from "./WaitingForWalletMessage"; +const useStyles = makeStyles((theme) => ({ + alignCenter: { + margin: "0 auto", + display: "block", + textAlign: "center", + }, + spacer: { + height: theme.spacing(2), + }, +})); + function Create() { - const { handleClick, disabled, showLoader } = useHandleCreateWrapped(); + const classes = useStyles(); const targetChain = useSelector(selectAttestTargetChain); + const originAsset = useSelector(selectAttestSourceAsset); + const originChain = useSelector(selectAttestSourceChain); const { isReady, statusMessage } = useIsWalletReady(targetChain); + const foreignAssetInfo = useFetchForeignAsset( + originChain, + originAsset, + targetChain + ); + const shouldUpdate = + targetChain !== CHAIN_ID_SOLANA && foreignAssetInfo.data?.doesExist; + const error = foreignAssetInfo.error || statusMessage; + const { handleClick, disabled, showLoader } = useHandleCreateWrapped( + shouldUpdate || false + ); + + console.log("foreign asset info", foreignAssetInfo); + return ( <> - - Create - - + + {foreignAssetInfo.isFetching ? ( + <> +
+ + + ) : ( + <> + + {shouldUpdate ? "Update" : "Create"} + + + + )} ); } diff --git a/bridge_ui/src/hooks/useFetchForeignAsset.ts b/bridge_ui/src/hooks/useFetchForeignAsset.ts new file mode 100644 index 000000000..9a4c93ecd --- /dev/null +++ b/bridge_ui/src/hooks/useFetchForeignAsset.ts @@ -0,0 +1,152 @@ +import { + ChainId, + CHAIN_ID_TERRA, + getForeignAssetEth, + getForeignAssetSolana, + getForeignAssetTerra, + nativeToHexString, + hexToUint8Array, +} from "@certusone/wormhole-sdk"; +import { Connection } from "@solana/web3.js"; +import { LCDClient } from "@terra-money/terra.js"; +import { ethers } from "ethers"; +import { useEffect, useMemo, useState } from "react"; +import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { DataWrapper } from "../store/helpers"; +import { + getEvmChainId, + getTokenBridgeAddressForChain, + SOLANA_HOST, + SOL_TOKEN_BRIDGE_ADDRESS, + TERRA_HOST, + TERRA_TOKEN_BRIDGE_ADDRESS, +} from "../utils/consts"; +import { isEVMChain } from "../utils/ethereum"; +import useIsWalletReady from "./useIsWalletReady"; + +export type ForeignAssetInfo = { + doesExist: boolean; + address: string | null; +}; + +function useFetchForeignAsset( + originChain: ChainId, + originAsset: string, + foreignChain: ChainId +): DataWrapper { + const { provider, chainId: evmChainId } = useEthereumProvider(); + const { isReady, statusMessage } = useIsWalletReady(foreignChain); + const correctEvmNetwork = getEvmChainId(foreignChain); + const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork; + + const [assetAddress, setAssetAddress] = useState(null); + const [doesExist, setDoesExist] = useState(false); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const originAssetHex = useMemo( + () => nativeToHexString(originAsset, originChain), + [originAsset, originChain] + ); + + const argumentError = useMemo( + () => + !foreignChain || + !originAssetHex || + foreignChain === originChain || + (isEVMChain(foreignChain) && !isReady) || + (isEVMChain(foreignChain) && !hasCorrectEvmNetwork), + [isReady, foreignChain, originChain, hasCorrectEvmNetwork, originAssetHex] + ); + + useEffect(() => { + if (argumentError || !originAssetHex) { + return; + } + + let cancelled = false; + setIsLoading(true); + setAssetAddress(null); + setError(""); + setDoesExist(false); + const getterFunc: () => Promise = isEVMChain(foreignChain) + ? () => + getForeignAssetEth( + getTokenBridgeAddressForChain(foreignChain), + provider as any, //why does this typecheck work elsewhere? + originChain, + hexToUint8Array(originAssetHex) + ) + : foreignChain === CHAIN_ID_TERRA + ? () => { + const lcd = new LCDClient(TERRA_HOST); + return getForeignAssetTerra( + TERRA_TOKEN_BRIDGE_ADDRESS, + lcd, + originChain, + hexToUint8Array(originAssetHex) + ); + } + : () => { + const connection = new Connection(SOLANA_HOST, "confirmed"); + return getForeignAssetSolana( + connection, + SOL_TOKEN_BRIDGE_ADDRESS, + originChain, + hexToUint8Array(originAssetHex) + ); + }; + + const promise = getterFunc(); + + promise + .then((result) => { + if (!cancelled) { + if ( + result && + !( + isEVMChain(foreignChain) && + result === ethers.constants.AddressZero + ) + ) { + setDoesExist(true); + setIsLoading(false); + setAssetAddress(result); + } else { + setDoesExist(false); + setIsLoading(false); + setAssetAddress(null); + } + } + }) + .catch((e) => { + if (!cancelled) { + setError("Could not retrieve the foreign asset."); + setIsLoading(false); + } + }); + }, [argumentError, foreignChain, originAssetHex, originChain, provider]); + + const compoundError = useMemo(() => { + return error + ? error + : !isReady + ? statusMessage + : argumentError + ? "Invalid arguments." + : ""; + }, [error, isReady, statusMessage, argumentError]); + + const output: DataWrapper = useMemo( + () => ({ + error: compoundError, + isFetching: isLoading, + data: { address: assetAddress, doesExist }, + receivedAt: null, + }), + [compoundError, isLoading, assetAddress, doesExist] + ); + + return output; +} + +export default useFetchForeignAsset; diff --git a/bridge_ui/src/hooks/useHandleCreateWrapped.tsx b/bridge_ui/src/hooks/useHandleCreateWrapped.tsx index 8b875acc1..5fb2ac40b 100644 --- a/bridge_ui/src/hooks/useHandleCreateWrapped.tsx +++ b/bridge_ui/src/hooks/useHandleCreateWrapped.tsx @@ -5,6 +5,8 @@ import { createWrappedOnEth, createWrappedOnSolana, createWrappedOnTerra, + updateWrappedOnEth, + updateWrappedOnTerra, postVaaSolana, } from "@certusone/wormhole-sdk"; import { WalletContextState } from "@solana/wallet-adapter-react"; @@ -43,15 +45,22 @@ async function evm( enqueueSnackbar: any, signer: Signer, signedVAA: Uint8Array, - chainId: ChainId + chainId: ChainId, + shouldUpdate: boolean ) { dispatch(setIsCreating(true)); try { - const receipt = await createWrappedOnEth( - getTokenBridgeAddressForChain(chainId), - signer, - signedVAA - ); + const receipt = shouldUpdate + ? await updateWrappedOnEth( + getTokenBridgeAddressForChain(chainId), + signer, + signedVAA + ) + : await createWrappedOnEth( + getTokenBridgeAddressForChain(chainId), + signer, + signedVAA + ); dispatch( setCreateTx({ id: receipt.transactionHash, block: receipt.blockNumber }) ); @@ -71,7 +80,8 @@ async function solana( enqueueSnackbar: any, wallet: WalletContextState, payerAddress: string, // TODO: we may not need this since we have wallet - signedVAA: Uint8Array + signedVAA: Uint8Array, + shouldUpdate: boolean //TODO utilize ) { dispatch(setIsCreating(true)); try { @@ -111,15 +121,22 @@ async function terra( dispatch: any, enqueueSnackbar: any, wallet: ConnectedWallet, - signedVAA: Uint8Array + signedVAA: Uint8Array, + shouldUpdate: boolean ) { dispatch(setIsCreating(true)); try { - const msg = await createWrappedOnTerra( - TERRA_TOKEN_BRIDGE_ADDRESS, - wallet.terraAddress, - signedVAA - ); + const msg = shouldUpdate + ? await updateWrappedOnTerra( + TERRA_TOKEN_BRIDGE_ADDRESS, + wallet.terraAddress, + signedVAA + ) + : await createWrappedOnTerra( + TERRA_TOKEN_BRIDGE_ADDRESS, + wallet.terraAddress, + signedVAA + ); const result = await postWithFees( wallet, [msg], @@ -139,7 +156,7 @@ async function terra( } } -export function useHandleCreateWrapped() { +export function useHandleCreateWrapped(shouldUpdate: boolean) { const dispatch = useDispatch(); const { enqueueSnackbar } = useSnackbar(); const targetChain = useSelector(selectAttestTargetChain); @@ -151,7 +168,14 @@ export function useHandleCreateWrapped() { const terraWallet = useConnectedWallet(); const handleCreateClick = useCallback(() => { if (isEVMChain(targetChain) && !!signer && !!signedVAA) { - evm(dispatch, enqueueSnackbar, signer, signedVAA, targetChain); + evm( + dispatch, + enqueueSnackbar, + signer, + signedVAA, + targetChain, + shouldUpdate + ); } else if ( targetChain === CHAIN_ID_SOLANA && !!solanaWallet && @@ -163,10 +187,11 @@ export function useHandleCreateWrapped() { enqueueSnackbar, solanaWallet, solPK.toString(), - signedVAA + signedVAA, + shouldUpdate ); } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && !!signedVAA) { - terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); + terra(dispatch, enqueueSnackbar, terraWallet, signedVAA, shouldUpdate); } else { // enqueueSnackbar( // "Creating wrapped tokens on this chain is not yet supported", @@ -184,6 +209,7 @@ export function useHandleCreateWrapped() { terraWallet, signedVAA, signer, + shouldUpdate, ]); return useMemo( () => ({ diff --git a/sdk/js/src/token_bridge/index.ts b/sdk/js/src/token_bridge/index.ts index 1dfa4f2af..315596d1f 100644 --- a/sdk/js/src/token_bridge/index.ts +++ b/sdk/js/src/token_bridge/index.ts @@ -5,3 +5,4 @@ export * from "./getIsWrappedAsset"; export * from "./getOriginalAsset"; export * from "./redeem"; export * from "./transfer"; +export * from "./updateWrapped"; diff --git a/sdk/js/src/token_bridge/updateWrapped.ts b/sdk/js/src/token_bridge/updateWrapped.ts new file mode 100644 index 000000000..c219004b9 --- /dev/null +++ b/sdk/js/src/token_bridge/updateWrapped.ts @@ -0,0 +1,27 @@ +import { MsgExecuteContract } from "@terra-money/terra.js"; +import { ethers } from "ethers"; +import { fromUint8Array } from "js-base64"; +import { Bridge__factory } from "../ethers-contracts"; + +export async function updateWrappedOnEth( + tokenBridgeAddress: string, + signer: ethers.Signer, + signedVAA: Uint8Array +) { + const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); + const v = await bridge.updateWrapped(signedVAA); + const receipt = await v.wait(); + return receipt; +} + +export async function updateWrappedOnTerra( + tokenBridgeAddress: string, + walletAddress: string, + signedVAA: Uint8Array +) { + return new MsgExecuteContract(walletAddress, tokenBridgeAddress, { + submit_vaa: { + data: fromUint8Array(signedVAA), + }, + }); +} diff --git a/sdk/js/src/utils/array.ts b/sdk/js/src/utils/array.ts index fe85db710..7d4419942 100644 --- a/sdk/js/src/utils/array.ts +++ b/sdk/js/src/utils/array.ts @@ -6,9 +6,15 @@ import { CHAIN_ID_TERRA, CHAIN_ID_POLYGON, } from "./consts"; -import { humanAddress } from "../terra"; +import { humanAddress, canonicalAddress } from "../terra"; import { PublicKey } from "@solana/web3.js"; import { hexValue, hexZeroPad, stripZeros } from "ethers/lib/utils"; +import { arrayify, zeroPad } from "@ethersproject/bytes"; + +export const isEVMChain = (chainId: ChainId) => + chainId === CHAIN_ID_ETH || + chainId === CHAIN_ID_BSC || + chainId === CHAIN_ID_POLYGON; export const isHexNativeTerra = (h: string) => h.startsWith("01"); export const nativeTerraHexToDenom = (h: string) => @@ -33,3 +39,22 @@ export const hexToNativeString = (h: string | undefined, c: ChainId) => { } catch (e) {} return undefined; }; + +export const nativeToHexString = ( + address: string | undefined, + chain: ChainId +) => { + if (!address || !chain) { + return null; + } + + if (isEVMChain(chain)) { + return uint8ArrayToHex(zeroPad(arrayify(address), 32)); + } else if (chain === CHAIN_ID_SOLANA) { + return uint8ArrayToHex(zeroPad(new PublicKey(address).toBytes(), 32)); + } else if (chain === CHAIN_ID_TERRA) { + return uint8ArrayToHex(zeroPad(canonicalAddress(address), 32)); + } else { + return null; + } +};