diff --git a/bridge_ui/src/components/Recovery.tsx b/bridge_ui/src/components/Recovery.tsx index 0efc0ab2f..fab7f9e8a 100644 --- a/bridge_ui/src/components/Recovery.tsx +++ b/bridge_ui/src/components/Recovery.tsx @@ -38,16 +38,8 @@ import { useDispatch } from "react-redux"; import { useHistory } from "react-router"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { COLORS } from "../muiTheme"; -import { - setSignedVAAHex as setNFTSignedVAAHex, - setStep as setNFTStep, - setTargetChain as setNFTTargetChain, -} from "../store/nftSlice"; -import { - setSignedVAAHex, - setStep, - setTargetChain, -} from "../store/transferSlice"; +import { setRecoveryVaa as setRecoveryNFTVaa } from "../store/nftSlice"; +import { setRecoveryVaa } from "../store/transferSlice"; import { CHAINS, CHAINS_WITH_NFT_SUPPORT, @@ -311,14 +303,30 @@ export default function Recovery() { if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) { // TODO: make recovery reducer if (isNFT) { - dispatch(setNFTSignedVAAHex(recoverySignedVAA)); - dispatch(setNFTTargetChain(parsedPayloadTargetChain)); - dispatch(setNFTStep(3)); + dispatch( + setRecoveryNFTVaa({ + vaa: recoverySignedVAA, + parsedPayload: { + targetChain: parsedPayload.targetChain, + targetAddress: parsedPayload.targetAddress, + originChain: parsedPayload.originChain, + originAddress: parsedPayload.originAddress, + }, + }) + ); push("/nft"); } else { - dispatch(setSignedVAAHex(recoverySignedVAA)); - dispatch(setTargetChain(parsedPayloadTargetChain)); - dispatch(setStep(3)); + dispatch( + setRecoveryVaa({ + vaa: recoverySignedVAA, + parsedPayload: { + targetChain: parsedPayload.targetChain, + targetAddress: parsedPayload.targetAddress, + originChain: parsedPayload.originChain, + originAddress: parsedPayload.originAddress, + }, + }) + ); push("/transfer"); } } @@ -327,6 +335,7 @@ export default function Recovery() { enableRecovery, recoverySignedVAA, parsedPayloadTargetChain, + parsedPayload, isNFT, push, ]); diff --git a/bridge_ui/src/components/SolanaCreateAssociatedAddress.tsx b/bridge_ui/src/components/SolanaCreateAssociatedAddress.tsx index eead85112..455b2fe73 100644 --- a/bridge_ui/src/components/SolanaCreateAssociatedAddress.tsx +++ b/bridge_ui/src/components/SolanaCreateAssociatedAddress.tsx @@ -1,4 +1,10 @@ -import { ChainId, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; +import { + ChainId, + CHAIN_ID_SOLANA, + getForeignAssetSolana, + hexToNativeString, + hexToUint8Array, +} from "@certusone/wormhole-sdk"; import { Typography } from "@material-ui/core"; import { ASSOCIATED_TOKEN_PROGRAM_ID, @@ -7,10 +13,17 @@ import { } from "@solana/spl-token"; import { Connection, PublicKey, Transaction } from "@solana/web3.js"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useSelector } from "react-redux"; import { useSolanaWallet } from "../contexts/SolanaWalletContext"; -import { SOLANA_HOST } from "../utils/consts"; +import { + selectTransferOriginAsset, + selectTransferOriginChain, + selectTransferTargetAddressHex, +} from "../store/selectors"; +import { SOLANA_HOST, SOL_TOKEN_BRIDGE_ADDRESS } from "../utils/consts"; import { signSendAndConfirm } from "../utils/solana"; import ButtonWithLoader from "./ButtonWithLoader"; +import SmartAddress from "./SmartAddress"; export function useAssociatedAccountExistsState( targetChain: ChainId, @@ -117,6 +130,8 @@ export default function SolanaCreateAssociatedAddress({ await signSendAndConfirm(solanaWallet, connection, transaction); setIsCreating(false); setAssociatedAccountExists(true); + } else { + console.log("Account already exists."); } } })(); @@ -146,3 +161,77 @@ export default function SolanaCreateAssociatedAddress({ ); } + +export function SolanaCreateAssociatedAddressAlternate() { + const originChain = useSelector(selectTransferOriginChain); + const originAsset = useSelector(selectTransferOriginAsset); + const addressHex = useSelector(selectTransferTargetAddressHex); + const base58TargetAddress = useMemo( + () => hexToNativeString(addressHex, CHAIN_ID_SOLANA) || "", + [addressHex] + ); + const base58OriginAddress = useMemo( + () => hexToNativeString(originAsset, CHAIN_ID_SOLANA) || "", + [originAsset] + ); + const connection = useMemo(() => new Connection(SOLANA_HOST), []); + const [targetAsset, setTargetAsset] = useState(null); + + useEffect(() => { + let cancelled = false; + if (!(originChain && originAsset && addressHex && base58TargetAddress)) { + setTargetAsset(null); + } else if (originChain === CHAIN_ID_SOLANA && base58OriginAddress) { + setTargetAsset(base58OriginAddress); + } else { + getForeignAssetSolana( + connection, + SOL_TOKEN_BRIDGE_ADDRESS, + originChain, + hexToUint8Array(originAsset) + ).then((result) => { + if (!cancelled) { + setTargetAsset(result); + } + }); + } + + return () => { + cancelled = true; + }; + }, [ + originChain, + originAsset, + addressHex, + base58TargetAddress, + connection, + base58OriginAddress, + ]); + + const { associatedAccountExists, setAssociatedAccountExists } = + useAssociatedAccountExistsState( + CHAIN_ID_SOLANA, + targetAsset, + base58TargetAddress + ); + + return targetAsset && !associatedAccountExists ? ( +
+ Recipient Address: + + + + + +
+ ) : null; +} diff --git a/bridge_ui/src/components/Transfer/Redeem.tsx b/bridge_ui/src/components/Transfer/Redeem.tsx index 4395d6eaf..d4d4ac90c 100644 --- a/bridge_ui/src/components/Transfer/Redeem.tsx +++ b/bridge_ui/src/components/Transfer/Redeem.tsx @@ -16,6 +16,7 @@ import { import { WBNB_ADDRESS, WETH_ADDRESS } from "../../utils/consts"; import ButtonWithLoader from "../ButtonWithLoader"; import KeyAndBalance from "../KeyAndBalance"; +import { SolanaCreateAssociatedAddressAlternate } from "../SolanaCreateAssociatedAddress"; import StepDescription from "../StepDescription"; import WaitingForWalletMessage from "./WaitingForWalletMessage"; @@ -60,8 +61,12 @@ function Redeem() { label="Automatically unwrap to native currency" /> )} + {targetChain === CHAIN_ID_SOLANA ? ( + + ) : null} { + if (isRecovery) { + return; + } // TODO: loading state, error state dispatch(setSourceWormholeWrappedInfo(undefined)); let cancelled = false; @@ -133,6 +141,7 @@ function useCheckIfWormholeWrapped(nft?: boolean) { }; }, [ dispatch, + isRecovery, sourceChain, sourceAsset, provider, diff --git a/bridge_ui/src/hooks/useFetchTargetAsset.ts b/bridge_ui/src/hooks/useFetchTargetAsset.ts index fb0a92349..db1d06887 100644 --- a/bridge_ui/src/hooks/useFetchTargetAsset.ts +++ b/bridge_ui/src/hooks/useFetchTargetAsset.ts @@ -20,11 +20,13 @@ import { useDispatch, useSelector } from "react-redux"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice"; import { + selectNFTIsRecovery, selectNFTIsSourceAssetWormholeWrapped, selectNFTOriginAsset, selectNFTOriginChain, selectNFTOriginTokenId, selectNFTTargetChain, + selectTransferIsRecovery, selectTransferIsSourceAssetWormholeWrapped, selectTransferOriginAsset, selectTransferOriginChain, @@ -65,7 +67,13 @@ function useFetchTargetAsset(nft?: boolean) { const { provider, chainId: evmChainId } = useEthereumProvider(); const correctEvmNetwork = getEvmChainId(targetChain); const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork; + const isRecovery = useSelector( + nft ? selectNFTIsRecovery : selectTransferIsRecovery + ); useEffect(() => { + if (isRecovery) { + return; + } if (isSourceAssetWormholeWrapped && originChain === targetChain) { dispatch(setTargetAsset(hexToNativeString(originAsset, originChain))); return; @@ -158,6 +166,7 @@ function useFetchTargetAsset(nft?: boolean) { }; }, [ dispatch, + isRecovery, isSourceAssetWormholeWrapped, originChain, originAsset, diff --git a/bridge_ui/src/store/nftSlice.ts b/bridge_ui/src/store/nftSlice.ts index 451958b89..7012266e1 100644 --- a/bridge_ui/src/store/nftSlice.ts +++ b/bridge_ui/src/store/nftSlice.ts @@ -49,6 +49,7 @@ export interface NFTState { isSending: boolean; isRedeeming: boolean; redeemTx: Transaction | undefined; + isRecovery: boolean; } const initialState: NFTState = { @@ -70,6 +71,7 @@ const initialState: NFTState = { isSending: false, isRedeeming: false, redeemTx: undefined, + isRecovery: false, }; export const nftSlice = createSlice({ @@ -203,6 +205,26 @@ export const nftSlice = createSlice({ sourceChain: state.sourceChain, targetChain: state.targetChain, }), + setRecoveryVaa: ( + state, + action: PayloadAction<{ + vaa: any; + parsedPayload: { + targetChain: ChainId; + targetAddress: string; + originChain: ChainId; + originAddress: string; //TODO maximum amount of fields + }; + }> + ) => { + state.signedVAAHex = action.payload.vaa; + state.targetChain = action.payload.parsedPayload.targetChain; + state.targetAddressHex = action.payload.parsedPayload.targetAddress; + state.originChain = action.payload.parsedPayload.originChain; + state.originAsset = action.payload.parsedPayload.originAddress; + state.activeStep = 3; + state.isRecovery = true; + }, }, }); @@ -228,6 +250,7 @@ export const { setIsRedeeming, setRedeemTx, reset, + setRecoveryVaa, } = nftSlice.actions; export default nftSlice.reducer; diff --git a/bridge_ui/src/store/selectors.ts b/bridge_ui/src/store/selectors.ts index 048af0f0d..987d44d2b 100644 --- a/bridge_ui/src/store/selectors.ts +++ b/bridge_ui/src/store/selectors.ts @@ -139,7 +139,7 @@ export const selectNFTIsRedeemComplete = (state: RootState) => !!selectNFTRedeemTx(state); export const selectNFTShouldLockFields = (state: RootState) => selectNFTIsSending(state) || selectNFTIsSendComplete(state); - +export const selectNFTIsRecovery = (state: RootState) => state.nft.isRecovery; /* * Transfer */ @@ -277,6 +277,8 @@ export const selectTransferIsRedeemComplete = (state: RootState) => !!selectTransferRedeemTx(state); export const selectTransferShouldLockFields = (state: RootState) => selectTransferIsSending(state) || selectTransferIsSendComplete(state); +export const selectTransferIsRecovery = (state: RootState) => + state.transfer.isRecovery; export const selectSolanaTokenMap = (state: RootState) => { return state.tokens.solanaTokenMap; diff --git a/bridge_ui/src/store/transferSlice.ts b/bridge_ui/src/store/transferSlice.ts index 9797eebdf..bbc459792 100644 --- a/bridge_ui/src/store/transferSlice.ts +++ b/bridge_ui/src/store/transferSlice.ts @@ -55,6 +55,7 @@ export interface TransferState { isRedeeming: boolean; redeemTx: Transaction | undefined; isApproving: boolean; + isRecovery: boolean; } const initialState: TransferState = { @@ -77,6 +78,7 @@ const initialState: TransferState = { isRedeeming: false, redeemTx: undefined, isApproving: false, + isRecovery: false, }; export const transferSlice = createSlice({ @@ -214,6 +216,26 @@ export const transferSlice = createSlice({ sourceChain: state.sourceChain, targetChain: state.targetChain, }), + setRecoveryVaa: ( + state, + action: PayloadAction<{ + vaa: any; + parsedPayload: { + targetChain: ChainId; + targetAddress: string; + originChain: ChainId; + originAddress: string; + }; + }> + ) => { + state.signedVAAHex = action.payload.vaa; + state.targetChain = action.payload.parsedPayload.targetChain; + state.targetAddressHex = action.payload.parsedPayload.targetAddress; + state.originChain = action.payload.parsedPayload.originChain; + state.originAsset = action.payload.parsedPayload.originAddress; + state.activeStep = 3; + state.isRecovery = true; + }, }, }); @@ -241,6 +263,7 @@ export const { setRedeemTx, setIsApproving, reset, + setRecoveryVaa, } = transferSlice.actions; export default transferSlice.reducer;