bridge_ui: add createAssociatedTokenAccount to redeem step in transfer
Change-Id: I3ccb1895613e7b2bb6fa8c1ddb08c138c14c0d0d
This commit is contained in:
parent
0f8eb3b933
commit
51cbec55b8
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue