bridge_ui: add ability to update attestations

Change-Id: Iedb0418d2a3b24a979af99107ef8a4ca8c3a4619
This commit is contained in:
Chase Moran 2021-11-01 19:34:35 -04:00 committed by Evan Gray
parent 10663cd72e
commit 78cdcb13ae
6 changed files with 304 additions and 29 deletions

View File

@ -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 (
<>
<KeyAndBalance chainId={targetChain} />
<ButtonWithLoader
disabled={!isReady || disabled}
onClick={handleClick}
showLoader={showLoader}
error={statusMessage}
>
Create
</ButtonWithLoader>
<WaitingForWalletMessage />
{foreignAssetInfo.isFetching ? (
<>
<div className={classes.spacer} />
<CircularProgress className={classes.alignCenter} />
</>
) : (
<>
<ButtonWithLoader
disabled={!isReady || disabled}
onClick={handleClick}
showLoader={showLoader}
error={error}
>
{shouldUpdate ? "Update" : "Create"}
</ButtonWithLoader>
<WaitingForWalletMessage />
</>
)}
</>
);
}

View File

@ -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<ForeignAssetInfo> {
const { provider, chainId: evmChainId } = useEthereumProvider();
const { isReady, statusMessage } = useIsWalletReady(foreignChain);
const correctEvmNetwork = getEvmChainId(foreignChain);
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
const [assetAddress, setAssetAddress] = useState<string | null>(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<string | null> = 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<ForeignAssetInfo> = useMemo(
() => ({
error: compoundError,
isFetching: isLoading,
data: { address: assetAddress, doesExist },
receivedAt: null,
}),
[compoundError, isLoading, assetAddress, doesExist]
);
return output;
}
export default useFetchForeignAsset;

View File

@ -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(
() => ({

View File

@ -5,3 +5,4 @@ export * from "./getIsWrappedAsset";
export * from "./getOriginalAsset";
export * from "./redeem";
export * from "./transfer";
export * from "./updateWrapped";

View File

@ -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),
},
});
}

View File

@ -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;
}
};