bridge_ui: add createAssociatedTokenAccount to redeem step in transfer

Change-Id: I3ccb1895613e7b2bb6fa8c1ddb08c138c14c0d0d
This commit is contained in:
Chase Moran 2021-10-19 15:26:57 -04:00
parent 0f8eb3b933
commit 51cbec55b8
8 changed files with 188 additions and 19 deletions

View File

@ -38,16 +38,8 @@ import { useDispatch } from "react-redux";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { COLORS } from "../muiTheme"; import { COLORS } from "../muiTheme";
import { import { setRecoveryVaa as setRecoveryNFTVaa } from "../store/nftSlice";
setSignedVAAHex as setNFTSignedVAAHex, import { setRecoveryVaa } from "../store/transferSlice";
setStep as setNFTStep,
setTargetChain as setNFTTargetChain,
} from "../store/nftSlice";
import {
setSignedVAAHex,
setStep,
setTargetChain,
} from "../store/transferSlice";
import { import {
CHAINS, CHAINS,
CHAINS_WITH_NFT_SUPPORT, CHAINS_WITH_NFT_SUPPORT,
@ -311,14 +303,30 @@ export default function Recovery() {
if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) { if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
// TODO: make recovery reducer // TODO: make recovery reducer
if (isNFT) { if (isNFT) {
dispatch(setNFTSignedVAAHex(recoverySignedVAA)); dispatch(
dispatch(setNFTTargetChain(parsedPayloadTargetChain)); setRecoveryNFTVaa({
dispatch(setNFTStep(3)); vaa: recoverySignedVAA,
parsedPayload: {
targetChain: parsedPayload.targetChain,
targetAddress: parsedPayload.targetAddress,
originChain: parsedPayload.originChain,
originAddress: parsedPayload.originAddress,
},
})
);
push("/nft"); push("/nft");
} else { } else {
dispatch(setSignedVAAHex(recoverySignedVAA)); dispatch(
dispatch(setTargetChain(parsedPayloadTargetChain)); setRecoveryVaa({
dispatch(setStep(3)); vaa: recoverySignedVAA,
parsedPayload: {
targetChain: parsedPayload.targetChain,
targetAddress: parsedPayload.targetAddress,
originChain: parsedPayload.originChain,
originAddress: parsedPayload.originAddress,
},
})
);
push("/transfer"); push("/transfer");
} }
} }
@ -327,6 +335,7 @@ export default function Recovery() {
enableRecovery, enableRecovery,
recoverySignedVAA, recoverySignedVAA,
parsedPayloadTargetChain, parsedPayloadTargetChain,
parsedPayload,
isNFT, isNFT,
push, push,
]); ]);

View File

@ -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 { Typography } from "@material-ui/core";
import { import {
ASSOCIATED_TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID,
@ -7,10 +13,17 @@ import {
} from "@solana/spl-token"; } from "@solana/spl-token";
import { Connection, PublicKey, Transaction } from "@solana/web3.js"; import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useSolanaWallet } from "../contexts/SolanaWalletContext"; 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 { signSendAndConfirm } from "../utils/solana";
import ButtonWithLoader from "./ButtonWithLoader"; import ButtonWithLoader from "./ButtonWithLoader";
import SmartAddress from "./SmartAddress";
export function useAssociatedAccountExistsState( export function useAssociatedAccountExistsState(
targetChain: ChainId, targetChain: ChainId,
@ -117,6 +130,8 @@ export default function SolanaCreateAssociatedAddress({
await signSendAndConfirm(solanaWallet, connection, transaction); await signSendAndConfirm(solanaWallet, connection, transaction);
setIsCreating(false); setIsCreating(false);
setAssociatedAccountExists(true); 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<string | null>(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 ? (
<div style={{ textAlign: "center" }}>
<Typography variant="subtitle2">Recipient Address:</Typography>
<Typography component="div">
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={base58TargetAddress}
variant="h6"
/>
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={targetAsset}
readableTargetAddress={base58TargetAddress}
associatedAccountExists={associatedAccountExists}
setAssociatedAccountExists={setAssociatedAccountExists}
/>
</div>
) : null;
}

View File

@ -16,6 +16,7 @@ import {
import { WBNB_ADDRESS, WETH_ADDRESS } from "../../utils/consts"; import { WBNB_ADDRESS, WETH_ADDRESS } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
import { SolanaCreateAssociatedAddressAlternate } from "../SolanaCreateAssociatedAddress";
import StepDescription from "../StepDescription"; import StepDescription from "../StepDescription";
import WaitingForWalletMessage from "./WaitingForWalletMessage"; import WaitingForWalletMessage from "./WaitingForWalletMessage";
@ -60,8 +61,12 @@ function Redeem() {
label="Automatically unwrap to native currency" label="Automatically unwrap to native currency"
/> />
)} )}
{targetChain === CHAIN_ID_SOLANA ? (
<SolanaCreateAssociatedAddressAlternate />
) : null}
<ButtonWithLoader <ButtonWithLoader
//TODO disable when the associated token account is confirmed to not exist
disabled={!isReady || disabled} disabled={!isReady || disabled}
onClick={ onClick={
isNativeEligible && useNativeRedeem ? handleNativeClick : handleClick isNativeEligible && useNativeRedeem ? handleNativeClick : handleClick

View File

@ -19,9 +19,11 @@ import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice"; import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice";
import { import {
selectNFTIsRecovery,
selectNFTSourceAsset, selectNFTSourceAsset,
selectNFTSourceChain, selectNFTSourceChain,
selectNFTSourceParsedTokenAccount, selectNFTSourceParsedTokenAccount,
selectTransferIsRecovery,
selectTransferSourceAsset, selectTransferSourceAsset,
selectTransferSourceChain, selectTransferSourceChain,
} from "../store/selectors"; } from "../store/selectors";
@ -69,7 +71,13 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
? setNFTSourceWormholeWrappedInfo ? setNFTSourceWormholeWrappedInfo
: setTransferSourceWormholeWrappedInfo; : setTransferSourceWormholeWrappedInfo;
const { provider } = useEthereumProvider(); const { provider } = useEthereumProvider();
const isRecovery = useSelector(
nft ? selectNFTIsRecovery : selectTransferIsRecovery
);
useEffect(() => { useEffect(() => {
if (isRecovery) {
return;
}
// TODO: loading state, error state // TODO: loading state, error state
dispatch(setSourceWormholeWrappedInfo(undefined)); dispatch(setSourceWormholeWrappedInfo(undefined));
let cancelled = false; let cancelled = false;
@ -133,6 +141,7 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
}; };
}, [ }, [
dispatch, dispatch,
isRecovery,
sourceChain, sourceChain,
sourceAsset, sourceAsset,
provider, provider,

View File

@ -20,11 +20,13 @@ import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice"; import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice";
import { import {
selectNFTIsRecovery,
selectNFTIsSourceAssetWormholeWrapped, selectNFTIsSourceAssetWormholeWrapped,
selectNFTOriginAsset, selectNFTOriginAsset,
selectNFTOriginChain, selectNFTOriginChain,
selectNFTOriginTokenId, selectNFTOriginTokenId,
selectNFTTargetChain, selectNFTTargetChain,
selectTransferIsRecovery,
selectTransferIsSourceAssetWormholeWrapped, selectTransferIsSourceAssetWormholeWrapped,
selectTransferOriginAsset, selectTransferOriginAsset,
selectTransferOriginChain, selectTransferOriginChain,
@ -65,7 +67,13 @@ function useFetchTargetAsset(nft?: boolean) {
const { provider, chainId: evmChainId } = useEthereumProvider(); const { provider, chainId: evmChainId } = useEthereumProvider();
const correctEvmNetwork = getEvmChainId(targetChain); const correctEvmNetwork = getEvmChainId(targetChain);
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork; const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
const isRecovery = useSelector(
nft ? selectNFTIsRecovery : selectTransferIsRecovery
);
useEffect(() => { useEffect(() => {
if (isRecovery) {
return;
}
if (isSourceAssetWormholeWrapped && originChain === targetChain) { if (isSourceAssetWormholeWrapped && originChain === targetChain) {
dispatch(setTargetAsset(hexToNativeString(originAsset, originChain))); dispatch(setTargetAsset(hexToNativeString(originAsset, originChain)));
return; return;
@ -158,6 +166,7 @@ function useFetchTargetAsset(nft?: boolean) {
}; };
}, [ }, [
dispatch, dispatch,
isRecovery,
isSourceAssetWormholeWrapped, isSourceAssetWormholeWrapped,
originChain, originChain,
originAsset, originAsset,

View File

@ -49,6 +49,7 @@ export interface NFTState {
isSending: boolean; isSending: boolean;
isRedeeming: boolean; isRedeeming: boolean;
redeemTx: Transaction | undefined; redeemTx: Transaction | undefined;
isRecovery: boolean;
} }
const initialState: NFTState = { const initialState: NFTState = {
@ -70,6 +71,7 @@ const initialState: NFTState = {
isSending: false, isSending: false,
isRedeeming: false, isRedeeming: false,
redeemTx: undefined, redeemTx: undefined,
isRecovery: false,
}; };
export const nftSlice = createSlice({ export const nftSlice = createSlice({
@ -203,6 +205,26 @@ export const nftSlice = createSlice({
sourceChain: state.sourceChain, sourceChain: state.sourceChain,
targetChain: state.targetChain, 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, setIsRedeeming,
setRedeemTx, setRedeemTx,
reset, reset,
setRecoveryVaa,
} = nftSlice.actions; } = nftSlice.actions;
export default nftSlice.reducer; export default nftSlice.reducer;

View File

@ -139,7 +139,7 @@ export const selectNFTIsRedeemComplete = (state: RootState) =>
!!selectNFTRedeemTx(state); !!selectNFTRedeemTx(state);
export const selectNFTShouldLockFields = (state: RootState) => export const selectNFTShouldLockFields = (state: RootState) =>
selectNFTIsSending(state) || selectNFTIsSendComplete(state); selectNFTIsSending(state) || selectNFTIsSendComplete(state);
export const selectNFTIsRecovery = (state: RootState) => state.nft.isRecovery;
/* /*
* Transfer * Transfer
*/ */
@ -277,6 +277,8 @@ export const selectTransferIsRedeemComplete = (state: RootState) =>
!!selectTransferRedeemTx(state); !!selectTransferRedeemTx(state);
export const selectTransferShouldLockFields = (state: RootState) => export const selectTransferShouldLockFields = (state: RootState) =>
selectTransferIsSending(state) || selectTransferIsSendComplete(state); selectTransferIsSending(state) || selectTransferIsSendComplete(state);
export const selectTransferIsRecovery = (state: RootState) =>
state.transfer.isRecovery;
export const selectSolanaTokenMap = (state: RootState) => { export const selectSolanaTokenMap = (state: RootState) => {
return state.tokens.solanaTokenMap; return state.tokens.solanaTokenMap;

View File

@ -55,6 +55,7 @@ export interface TransferState {
isRedeeming: boolean; isRedeeming: boolean;
redeemTx: Transaction | undefined; redeemTx: Transaction | undefined;
isApproving: boolean; isApproving: boolean;
isRecovery: boolean;
} }
const initialState: TransferState = { const initialState: TransferState = {
@ -77,6 +78,7 @@ const initialState: TransferState = {
isRedeeming: false, isRedeeming: false,
redeemTx: undefined, redeemTx: undefined,
isApproving: false, isApproving: false,
isRecovery: false,
}; };
export const transferSlice = createSlice({ export const transferSlice = createSlice({
@ -214,6 +216,26 @@ export const transferSlice = createSlice({
sourceChain: state.sourceChain, sourceChain: state.sourceChain,
targetChain: state.targetChain, 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, setRedeemTx,
setIsApproving, setIsApproving,
reset, reset,
setRecoveryVaa,
} = transferSlice.actions; } = transferSlice.actions;
export default transferSlice.reducer; export default transferSlice.reducer;