diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json index b326bd57a..69f264e2e 100644 --- a/bridge_ui/package-lock.json +++ b/bridge_ui/package-lock.json @@ -29,6 +29,7 @@ "@solana/wallet-base": "^0.0.1", "@solana/web3.js": "^1.22.0", "@terra-money/wallet-provider": "^1.4.0-alpha.1", + "bech32": "^1.1.4", "ethers": "^5.4.1", "js-base64": "^3.6.1", "notistack": "^1.0.10", @@ -57,6 +58,9 @@ "@improbable-eng/grpc-web": "^0.14.0", "@solana/spl-token": "^0.1.8", "@solana/web3.js": "^1.24.0", + "@terra-money/terra.js": "^1.8.10", + "@terra-money/wallet-provider": "^1.2.4", + "js-base64": "^3.6.1", "protobufjs": "^6.11.2", "rxjs": "^7.3.0" }, @@ -65,6 +69,7 @@ "@typechain/ethers-v5": "^7.0.1", "@types/long": "^4.0.1", "@types/node": "^16.6.1", + "@types/react": "^17.0.19", "copy-dir": "^1.3.0", "ethers": "^5.4.4", "prettier": "^2.3.2", @@ -36739,6 +36744,7 @@ "resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz", "integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==", "dev": true, + "peer": true, "dependencies": { "@babel/core": "^7.0.0-beta.39", "@babel/traverse": "^7.0.0-beta.39", @@ -36752,6 +36758,7 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz", "integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==", "dev": true, + "peer": true, "bin": { "babylon": "bin/babylon.js" }, @@ -37587,6 +37594,7 @@ "resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz", "integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==", "dev": true, + "peer": true, "engines": { "node": "*" } @@ -37596,6 +37604,7 @@ "resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz", "integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0-beta.36", "long": "^3.2.0", @@ -37615,6 +37624,7 @@ "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", "dev": true, + "peer": true, "engines": { "node": ">=0.6" } @@ -41022,11 +41032,15 @@ "@openzeppelin/contracts": "^4.2.0", "@solana/spl-token": "^0.1.8", "@solana/web3.js": "^1.24.0", + "@terra-money/terra.js": "^1.8.10", + "@terra-money/wallet-provider": "^1.2.4", "@typechain/ethers-v5": "^7.0.1", "@types/long": "^4.0.1", "@types/node": "^16.6.1", + "@types/react": "^17.0.19", "copy-dir": "^1.3.0", "ethers": "^5.4.4", + "js-base64": "^3.6.1", "prettier": "^2.3.2", "protobufjs": "^6.11.2", "rxjs": "^7.3.0", @@ -48382,7 +48396,6 @@ "dev": true, "optional": true, "requires": { - "bitcore-lib": "^8.25.10", "unorm": "^1.4.1" } }, @@ -69246,6 +69259,7 @@ "resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz", "integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==", "dev": true, + "peer": true, "requires": { "@babel/core": "^7.0.0-beta.39", "@babel/traverse": "^7.0.0-beta.39", @@ -69258,7 +69272,8 @@ "version": "7.0.0-beta.47", "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz", "integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==", - "dev": true + "dev": true, + "peer": true } } }, @@ -69268,8 +69283,7 @@ "integrity": "sha512-R4s75XH+o8qM+WaRrAU9S2rbAMDzob18/S3V8R9ZoFpZkPWLAohWWlzWAp1ybeTkOuuku/X1zJtxiV0pBYxZww==", "dev": true, "requires": { - "loader-utils": "^1.1.0", - "wasm-dce": "^1.0.0" + "loader-utils": "^1.1.0" }, "dependencies": { "json5": { @@ -69991,13 +70005,15 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz", "integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==", - "dev": true + "dev": true, + "peer": true }, "webassembly-interpreter": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz", "integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "^7.0.0-beta.36", "long": "^3.2.0", @@ -70008,7 +70024,8 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", - "dev": true + "dev": true, + "peer": true } } }, diff --git a/bridge_ui/package.json b/bridge_ui/package.json index ea33caa5c..006cd88b2 100644 --- a/bridge_ui/package.json +++ b/bridge_ui/package.json @@ -23,6 +23,7 @@ "@solana/wallet-base": "^0.0.1", "@solana/web3.js": "^1.22.0", "@terra-money/wallet-provider": "^1.4.0-alpha.1", + "bech32": "^1.1.4", "ethers": "^5.4.1", "js-base64": "^3.6.1", "notistack": "^1.0.10", diff --git a/bridge_ui/src/components/Attest/Source.tsx b/bridge_ui/src/components/Attest/Source.tsx index 4630ac835..1d496b7b1 100644 --- a/bridge_ui/src/components/Attest/Source.tsx +++ b/bridge_ui/src/components/Attest/Source.tsx @@ -63,7 +63,7 @@ function Source() { {/* TODO: token list for eth, check own */} { dispatch(setTargetChain(event.target.value)); @@ -76,7 +80,14 @@ function Target() { + (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 @@ -272,7 +235,7 @@ export function useHandleTransfer() { sourceChain === CHAIN_ID_ETH && !!signer && decimals !== undefined && - !!tpkRef.current + !!targetAddress ) { eth( dispatch, @@ -282,14 +245,14 @@ export function useHandleTransfer() { decimals, amount, targetChain, - tpkRef.current + targetAddress ); } else if ( sourceChain === CHAIN_ID_SOLANA && !!solanaWallet && !!solPK && !!sourceTokenPublicKey && - !!signerAddress && + !!targetAddress && decimals !== undefined ) { solana( @@ -299,10 +262,10 @@ export function useHandleTransfer() { solPK.toString(), sourceTokenPublicKey, sourceAsset, - amount, //TODO: avoid decimals, pass in parsed amount + amount, decimals, - signerAddress, targetChain, + targetAddress, originAsset, originChain ); @@ -310,7 +273,7 @@ export function useHandleTransfer() { sourceChain === CHAIN_ID_TERRA && !!terraWallet && decimals !== undefined && - !!signerAddress + !!targetAddress ) { terra( dispatch, @@ -318,8 +281,8 @@ export function useHandleTransfer() { terraWallet, sourceAsset, amount, - signerAddress, // TODO: only works for Eth - targetChain + targetChain, + targetAddress ); } else { // enqueueSnackbar("Transfers from this chain are not yet supported", { @@ -331,7 +294,6 @@ export function useHandleTransfer() { enqueueSnackbar, sourceChain, signer, - signerAddress, solanaWallet, solPK, terraWallet, @@ -340,6 +302,7 @@ export function useHandleTransfer() { amount, decimals, targetChain, + targetAddress, originAsset, originChain, ]); diff --git a/bridge_ui/src/hooks/useSyncTargetAddress.ts b/bridge_ui/src/hooks/useSyncTargetAddress.ts new file mode 100644 index 000000000..0e05ff10a --- /dev/null +++ b/bridge_ui/src/hooks/useSyncTargetAddress.ts @@ -0,0 +1,110 @@ +import { + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + CHAIN_ID_TERRA, +} from "@certusone/wormhole-sdk"; +import { arrayify, zeroPad } from "@ethersproject/bytes"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; +import { useConnectedWallet } from "@terra-money/wallet-provider"; +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import { useSolanaWallet } from "../contexts/SolanaWalletContext"; +import { + selectTransferTargetAsset, + selectTransferTargetChain, + selectTransferTargetParsedTokenAccount, +} from "../store/selectors"; +import { setTargetAddressHex } from "../store/transferSlice"; +import { uint8ArrayToHex } from "../utils/array"; +import bech32 from "bech32"; + +function useSyncTargetAddress(shouldFire: boolean) { + const dispatch = useDispatch(); + const targetChain = useSelector(selectTransferTargetChain); + const { signerAddress } = useEthereumProvider(); + const solanaWallet = useSolanaWallet(); + const solPK = solanaWallet?.publicKey; + const targetAsset = useSelector(selectTransferTargetAsset); + const targetParsedTokenAccount = useSelector( + selectTransferTargetParsedTokenAccount + ); + const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey; + const terraWallet = useConnectedWallet(); + useEffect(() => { + if (shouldFire) { + let cancelled = false; + if (targetChain === CHAIN_ID_ETH && signerAddress) { + dispatch( + setTargetAddressHex( + uint8ArrayToHex(zeroPad(arrayify(signerAddress), 32)) + ) + ); + } + // TODO: have the user explicitly select an account on solana + else if (targetChain === CHAIN_ID_SOLANA && targetTokenAccountPublicKey) { + // use the target's TokenAccount if it exists + dispatch( + setTargetAddressHex( + uint8ArrayToHex( + zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32) + ) + ) + ); + } else if (targetChain === CHAIN_ID_SOLANA && solPK && targetAsset) { + // otherwise, use the associated token account (which we create in the case it doesn't exist) + (async () => { + const associatedTokenAccount = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + new PublicKey(targetAsset), + solPK + ); + if (!cancelled) { + dispatch( + setTargetAddressHex( + uint8ArrayToHex(zeroPad(associatedTokenAccount.toBytes(), 32)) + ) + ); + } + })(); + } else if ( + targetChain === CHAIN_ID_TERRA && + terraWallet && + terraWallet.walletAddress + ) { + dispatch( + setTargetAddressHex( + uint8ArrayToHex( + zeroPad( + new Uint8Array(bech32.decode(terraWallet.walletAddress).words), + 32 + ) + ) + ) + ); + } else { + dispatch(setTargetAddressHex(undefined)); + } + return () => { + cancelled = true; + }; + } + }, [ + dispatch, + shouldFire, + targetChain, + signerAddress, + solPK, + targetAsset, + targetTokenAccountPublicKey, + terraWallet, + ]); +} + +export default useSyncTargetAddress; diff --git a/bridge_ui/src/hooks/useTransferTargetAddress.ts b/bridge_ui/src/hooks/useTransferTargetAddress.ts new file mode 100644 index 000000000..ef3bdd0a6 --- /dev/null +++ b/bridge_ui/src/hooks/useTransferTargetAddress.ts @@ -0,0 +1,13 @@ +import { useMemo } from "react"; +import { useSelector } from "react-redux"; +import { selectTransferTargetAddressHex } from "../store/selectors"; +import { hexToUint8Array } from "../utils/array"; + +export default function useTransferTargetAddressHex() { + const targetAddressHex = useSelector(selectTransferTargetAddressHex); + const targetAddress = useMemo( + () => (targetAddressHex ? hexToUint8Array(targetAddressHex) : undefined), + [targetAddressHex] + ); + return targetAddress; +} diff --git a/bridge_ui/src/store/selectors.ts b/bridge_ui/src/store/selectors.ts index f643ae5d3..2792676d9 100644 --- a/bridge_ui/src/store/selectors.ts +++ b/bridge_ui/src/store/selectors.ts @@ -57,6 +57,8 @@ export const selectTransferSourceBalanceString = (state: RootState) => export const selectTransferAmount = (state: RootState) => state.transfer.amount; export const selectTransferTargetChain = (state: RootState) => state.transfer.targetChain; +export const selectTransferTargetAddressHex = (state: RootState) => + state.transfer.targetAddressHex; export const selectTransferTargetAsset = (state: RootState) => state.transfer.targetAsset; export const selectTransferTargetParsedTokenAccount = (state: RootState) => @@ -96,11 +98,8 @@ export const selectTransferIsTargetComplete = (state: RootState) => !!state.transfer.targetChain && !!state.transfer.targetAsset && (state.transfer.targetChain !== CHAIN_ID_ETH || - state.transfer.targetAsset !== ethers.constants.AddressZero); //&& -// Associated Token Account exists -// (state.transfer.targetChain !== CHAIN_ID_SOLANA || -// (!!state.transfer.targetParsedTokenAccount && -// !!state.transfer.targetParsedTokenAccount.publicKey)); + state.transfer.targetAsset !== ethers.constants.AddressZero) && + !!state.transfer.targetAddressHex; export const selectTransferIsSendComplete = (state: RootState) => !!selectTransferSignedVAAHex(state); export const selectTransferShouldLockFields = (state: RootState) => diff --git a/bridge_ui/src/store/transferSlice.ts b/bridge_ui/src/store/transferSlice.ts index 92d7e3b89..570aa9001 100644 --- a/bridge_ui/src/store/transferSlice.ts +++ b/bridge_ui/src/store/transferSlice.ts @@ -34,6 +34,7 @@ export interface TransferState { sourceParsedTokenAccount: ParsedTokenAccount | undefined; amount: string; targetChain: ChainId; + targetAddressHex: string | undefined; targetAsset: string | null | undefined; targetParsedTokenAccount: ParsedTokenAccount | undefined; signedVAAHex: string | undefined; @@ -51,6 +52,7 @@ const initialState: TransferState = { originAsset: undefined, amount: "", targetChain: CHAIN_ID_ETH, + targetAddressHex: undefined, targetAsset: undefined, targetParsedTokenAccount: undefined, signedVAAHex: undefined, @@ -86,6 +88,7 @@ export const transferSlice = createSlice({ } if (state.targetChain === action.payload) { state.targetChain = prevSourceChain; + state.targetAddressHex = undefined; } }, setSourceAsset: (state, action: PayloadAction) => { @@ -117,6 +120,8 @@ export const transferSlice = createSlice({ setTargetChain: (state, action: PayloadAction) => { const prevTargetChain = state.targetChain; state.targetChain = action.payload; + state.targetAddressHex = undefined; + // targetAsset is handled by useFetchTargetAsset if (state.sourceChain === action.payload) { state.sourceChain = prevTargetChain; state.activeStep = 0; @@ -132,6 +137,9 @@ export const transferSlice = createSlice({ } } }, + setTargetAddressHex: (state, action: PayloadAction) => { + state.targetAddressHex = action.payload; + }, setTargetAsset: ( state, action: PayloadAction @@ -169,6 +177,7 @@ export const { setSourceParsedTokenAccount, setAmount, setTargetChain, + setTargetAddressHex, setTargetAsset, setTargetParsedTokenAccount, setSignedVAAHex,