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 { 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,
]);

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 {
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<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 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 ? (
<SolanaCreateAssociatedAddressAlternate />
) : null}
<ButtonWithLoader
//TODO disable when the associated token account is confirmed to not exist
disabled={!isReady || disabled}
onClick={
isNativeEligible && useNativeRedeem ? handleNativeClick : handleClick

View File

@ -19,9 +19,11 @@ import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice";
import {
selectNFTIsRecovery,
selectNFTSourceAsset,
selectNFTSourceChain,
selectNFTSourceParsedTokenAccount,
selectTransferIsRecovery,
selectTransferSourceAsset,
selectTransferSourceChain,
} from "../store/selectors";
@ -69,7 +71,13 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
? setNFTSourceWormholeWrappedInfo
: setTransferSourceWormholeWrappedInfo;
const { provider } = useEthereumProvider();
const isRecovery = useSelector(
nft ? selectNFTIsRecovery : selectTransferIsRecovery
);
useEffect(() => {
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,

View File

@ -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,

View File

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

View File

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

View File

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