bridge_ui: v1 safety checks, wallet desync fix, basic terra token picker
Change-Id: I9e45ce77c573e6940e6280b52ab2a319e6c4472f
This commit is contained in:
parent
c47d32ba9c
commit
fc300f47e6
|
@ -18,6 +18,8 @@ import {
|
|||
} from "../../utils/ethereum";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
import OffsetButton from "./OffsetButton";
|
||||
import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi";
|
||||
import { WORMHOLE_V1_ETH_ADDRESS } from "../../utils/consts";
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
|
@ -34,6 +36,14 @@ const useStyles = makeStyles(() =>
|
|||
})
|
||||
);
|
||||
|
||||
const isWormholev1 = (provider: any, address: string) => {
|
||||
const connection = WormholeAbi__factory.connect(
|
||||
WORMHOLE_V1_ETH_ADDRESS,
|
||||
provider
|
||||
);
|
||||
return connection.isWrappedAsset(address);
|
||||
};
|
||||
|
||||
type EthereumSourceTokenSelectorProps = {
|
||||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||
|
@ -79,18 +89,64 @@ export default function EthereumSourceTokenSelector(
|
|||
const [advancedModeSymbol, setAdvancedModeSymbol] = useState("");
|
||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||
|
||||
const [autocompleteHolder, setAutocompleteHolder] =
|
||||
useState<ParsedTokenAccount | null>(null);
|
||||
const [autocompleteError, setAutocompleteError] = useState("");
|
||||
|
||||
const { provider, signerAddress } = useEthereumProvider();
|
||||
|
||||
// const wrappedTestToken = "0x8bf3c393b588bb6ad021e154654493496139f06d";
|
||||
// const notWrappedTestToken = "0xaaaebe6fe48e54f431b0c390cfaf0b017d09d42d";
|
||||
|
||||
useEffect(() => {
|
||||
//If we receive a push from our parent, usually on component mount, we set the advancedModeString to synchronize.
|
||||
//If we receive a push from our parent, usually on component mount, we set our internal value to synchronize
|
||||
//This also kicks off the metadata load.
|
||||
if (advancedMode && value && advancedModeHolderString !== value.mintKey) {
|
||||
setAdvancedModeHolderString(value.mintKey);
|
||||
}
|
||||
}, [value, advancedMode, advancedModeHolderString]);
|
||||
if (!advancedMode && value && !autocompleteHolder) {
|
||||
setAutocompleteHolder(value);
|
||||
}
|
||||
}, [value, advancedMode, advancedModeHolderString, autocompleteHolder]);
|
||||
|
||||
//This loads the parsedTokenAccount & symbol from the advancedModeString
|
||||
//TODO move to util or hook
|
||||
//This effect is watching the autocomplete selection.
|
||||
//It checks to make sure the token is a valid choice before putting it on the state.
|
||||
//At present, that just means it can't be wormholev1
|
||||
useEffect(() => {
|
||||
if (advancedMode || !autocompleteHolder || !provider) {
|
||||
return;
|
||||
} else {
|
||||
let cancelled = false;
|
||||
setAutocompleteError("");
|
||||
isWormholev1(provider, autocompleteHolder.mintKey).then(
|
||||
(result) => {
|
||||
if (!cancelled) {
|
||||
result
|
||||
? setAutocompleteError(
|
||||
"Wormhole v1 tokens cannot be transferred with this bridge."
|
||||
)
|
||||
: onChange(autocompleteHolder);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
if (!cancelled) {
|
||||
setAutocompleteError(
|
||||
"Warning: please verify if this is a Wormhole v1 token address. V1 tokens should not be transferred with this bridge"
|
||||
);
|
||||
onChange(autocompleteHolder);
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
}, [autocompleteHolder, provider, advancedMode, onChange]);
|
||||
|
||||
//This effect watches the advancedModeString, and checks that the selected asset is valid before putting
|
||||
// it on the state.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!advancedMode || !isValidEthereumAddress(advancedModeHolderString)) {
|
||||
|
@ -106,7 +162,36 @@ export default function EthereumSourceTokenSelector(
|
|||
!cancelled && setAdvancedModeError("");
|
||||
!cancelled && setAdvancedModeSymbol("");
|
||||
try {
|
||||
getEthereumToken(advancedModeHolderString, provider).then((token) => {
|
||||
//Validate that the token is not a wormhole v1 asset
|
||||
const isWormholePromise = isWormholev1(
|
||||
provider,
|
||||
advancedModeHolderString
|
||||
).then(
|
||||
(result) => {
|
||||
if (result && !cancelled) {
|
||||
setAdvancedModeError(
|
||||
"Wormhole v1 assets are not eligible for transfer."
|
||||
);
|
||||
setAdvancedModeLoading(false);
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Warning: please verify if this is a Wormhole v1 token address. V1 tokens should not be transferred with this bridge"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
return Promise.resolve(); //Don't allow an error here to tank the workflow
|
||||
}
|
||||
);
|
||||
|
||||
//Then fetch the asset's information & transform to a parsed token account
|
||||
isWormholePromise.then(() =>
|
||||
getEthereumToken(advancedModeHolderString, provider).then(
|
||||
(token) => {
|
||||
ethTokenToParsedTokenAccount(token, signerAddress).then(
|
||||
(parsedTokenAccount) => {
|
||||
!cancelled && onChange(parsedTokenAccount);
|
||||
|
@ -115,23 +200,31 @@ export default function EthereumSourceTokenSelector(
|
|||
(error) => {
|
||||
//These errors can maybe be consolidated
|
||||
!cancelled &&
|
||||
setAdvancedModeError("Failed to find the specified address");
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified address"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
);
|
||||
|
||||
//Also attempt to store off the symbol
|
||||
token.symbol().then(
|
||||
(result) => {
|
||||
!cancelled && setAdvancedModeSymbol(result);
|
||||
},
|
||||
(error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError("Failed to find the specified address");
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified address"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
},
|
||||
(error) => {}
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
!cancelled &&
|
||||
setAdvancedModeError("Failed to find the specified address");
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
|
@ -162,7 +255,10 @@ export default function EthereumSourceTokenSelector(
|
|||
if (!account) {
|
||||
return undefined;
|
||||
}
|
||||
return covalent?.data?.find((x) => x.contract_address === account.mintKey);
|
||||
const item = covalent?.data?.find(
|
||||
(x) => x.contract_address === account.mintKey
|
||||
);
|
||||
return item ? item.contract_ticker_symbol : undefined;
|
||||
};
|
||||
|
||||
const filterConfig = createFilterOptions({
|
||||
|
@ -176,19 +272,21 @@ export default function EthereumSourceTokenSelector(
|
|||
});
|
||||
|
||||
const toggleAdvancedMode = () => {
|
||||
setAdvancedModeHolderString("");
|
||||
setAdvancedModeError("");
|
||||
setAdvancedModeSymbol("");
|
||||
setAdvancedMode(!advancedMode);
|
||||
};
|
||||
|
||||
const handleAutocompleteChange = (newValue: ParsedTokenAccount | null) => {
|
||||
setAutocompleteHolder(newValue);
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
props.covalent?.isFetching || props.tokenAccounts?.isFetching;
|
||||
|
||||
const symbolString = advancedModeSymbol
|
||||
? advancedModeSymbol + " "
|
||||
: getSymbol(value)
|
||||
? getSymbol(value)?.contract_ticker_symbol + " "
|
||||
: "";
|
||||
|
||||
const autoComplete = (
|
||||
<>
|
||||
<Autocomplete
|
||||
autoComplete
|
||||
autoHighlight
|
||||
|
@ -197,9 +295,9 @@ export default function EthereumSourceTokenSelector(
|
|||
clearOnBlur
|
||||
fullWidth={false}
|
||||
filterOptions={filterConfig}
|
||||
value={value}
|
||||
value={autocompleteHolder}
|
||||
onChange={(event, newValue) => {
|
||||
onChange(newValue);
|
||||
handleAutocompleteChange(newValue);
|
||||
}}
|
||||
disabled={disabled}
|
||||
noOptionsText={"No ERC20 tokens found at the moment."}
|
||||
|
@ -216,11 +314,15 @@ export default function EthereumSourceTokenSelector(
|
|||
}}
|
||||
getOptionLabel={(option) => {
|
||||
const symbol = getSymbol(option);
|
||||
return `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress(
|
||||
option.publicKey
|
||||
)}, Address: ${shortenAddress(option.mintKey)})`;
|
||||
return `${symbol ? symbol : "Unknown"} (Address: ${shortenAddress(
|
||||
option.mintKey
|
||||
)})`;
|
||||
}}
|
||||
/>
|
||||
{autocompleteError && (
|
||||
<Typography color="error">{autocompleteError}</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const advancedModeToggleButton = (
|
||||
|
@ -229,12 +331,19 @@ export default function EthereumSourceTokenSelector(
|
|||
</OffsetButton>
|
||||
);
|
||||
|
||||
const symbol = getSymbol(value) || advancedModeSymbol;
|
||||
|
||||
const content = value ? (
|
||||
<>
|
||||
<Typography>{symbolString + value.mintKey}</Typography>
|
||||
<Typography>{(symbol ? symbol + " " : "") + value.mintKey}</Typography>
|
||||
<OffsetButton onClick={handleClick} disabled={disabled}>
|
||||
Clear
|
||||
</OffsetButton>
|
||||
{!advancedMode && autocompleteError ? (
|
||||
<Typography color="error">{autocompleteError}</Typography>
|
||||
) : advancedMode && advancedModeError ? (
|
||||
<Typography color="error">{advancedModeError}</Typography>
|
||||
) : null}
|
||||
</>
|
||||
) : isLoading ? (
|
||||
<CircularProgress />
|
||||
|
|
|
@ -3,9 +3,10 @@ import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
|||
import { Autocomplete } from "@material-ui/lab";
|
||||
import { createFilterOptions } from "@material-ui/lab/Autocomplete";
|
||||
import { TokenInfo } from "@solana/spl-token-registry";
|
||||
import React, { useMemo } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { DataWrapper } from "../../store/helpers";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import { WORMHOLE_V1_MINT_AUTHORITY } from "../../utils/consts";
|
||||
import { Metadata } from "../../utils/metaplex";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
|
||||
|
@ -31,44 +32,7 @@ type SolanaSourceTokenSelectorProps = {
|
|||
solanaTokenMap: DataWrapper<TokenInfo[]> | undefined;
|
||||
metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
const renderAccount = (
|
||||
account: ParsedTokenAccount,
|
||||
solanaTokenMap: Map<String, TokenInfo>,
|
||||
metaplexData: Map<String, Metadata>,
|
||||
classes: any
|
||||
) => {
|
||||
const tokenMapData = solanaTokenMap.get(account.mintKey);
|
||||
const metaplexValue = metaplexData.get(account.mintKey);
|
||||
|
||||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
const accountAddressPrettyString = shortenAddress(account.publicKey);
|
||||
const uri = tokenMapData?.logoURI || metaplexValue?.data?.uri || undefined;
|
||||
const symbol =
|
||||
tokenMapData?.symbol || metaplexValue?.data.symbol || "Unknown";
|
||||
const name = tokenMapData?.name || metaplexValue?.data?.name || "--";
|
||||
return (
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div>
|
||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="subtitle1">{symbol}</Typography>
|
||||
<Typography variant="subtitle2">{name}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body1">{"Mint : " + mintPrettyString}</Typography>
|
||||
<Typography variant="body1">
|
||||
{"Account :" + accountAddressPrettyString}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body2">{"Balance"}</Typography>
|
||||
<Typography variant="h6">{account.uiAmountString}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
mintAccounts: DataWrapper<Map<String, string | null>> | undefined;
|
||||
};
|
||||
|
||||
export default function SolanaSourceTokenSelector(
|
||||
|
@ -135,11 +99,95 @@ export default function SolanaSourceTokenSelector(
|
|||
},
|
||||
});
|
||||
|
||||
const isWormholev1 = useCallback(
|
||||
(address: string) => {
|
||||
//This is a v1 wormhole token on testnet
|
||||
//const testAddress = "4QixXecTZ4zdZGa39KH8gVND5NZ2xcaB12wiBhE4S7rn";
|
||||
|
||||
if (!props.mintAccounts?.data) {
|
||||
return true; //These should never be null by this point
|
||||
}
|
||||
const mintInfo = props.mintAccounts.data.get(address);
|
||||
|
||||
if (!mintInfo) {
|
||||
return true; //We should never fail to pull the mint of an account.
|
||||
}
|
||||
|
||||
if (mintInfo === WORMHOLE_V1_MINT_AUTHORITY) {
|
||||
return true; //This means the mint was created by the wormhole v1 contract, and we want to disallow its transfer.
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[props.mintAccounts]
|
||||
);
|
||||
|
||||
const renderAccount = (
|
||||
account: ParsedTokenAccount,
|
||||
solanaTokenMap: Map<String, TokenInfo>,
|
||||
metaplexData: Map<String, Metadata>,
|
||||
classes: any
|
||||
) => {
|
||||
const tokenMapData = solanaTokenMap.get(account.mintKey);
|
||||
const metaplexValue = metaplexData.get(account.mintKey);
|
||||
|
||||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
const accountAddressPrettyString = shortenAddress(account.publicKey);
|
||||
const uri = tokenMapData?.logoURI || metaplexValue?.data?.uri || undefined;
|
||||
const symbol =
|
||||
tokenMapData?.symbol || metaplexValue?.data.symbol || "Unknown";
|
||||
const name = tokenMapData?.name || metaplexValue?.data?.name || "--";
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div>
|
||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="subtitle1">{symbol}</Typography>
|
||||
<Typography variant="subtitle2">{name}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body1">
|
||||
{"Mint : " + mintPrettyString}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{"Account :" + accountAddressPrettyString}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body2">{"Balance"}</Typography>
|
||||
<Typography variant="h6">{account.uiAmountString}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const v1Warning = (
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
Wormhole v1 tokens are not eligible for transfer.
|
||||
</Typography>
|
||||
<div>{content}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return isWormholev1(account.mintKey) ? v1Warning : content;
|
||||
};
|
||||
|
||||
//The autocomplete doesn't rerender the option label unless the value changes.
|
||||
//Thus we should wait for the metadata to arrive before rendering it.
|
||||
//TODO This can flicker dependent on how fast the useEffects in the getSourceAccounts hook complete.
|
||||
const isLoading =
|
||||
props.metaplexData.isFetching || props.solanaTokenMap?.isFetching;
|
||||
props.metaplexData.isFetching ||
|
||||
props.solanaTokenMap?.isFetching ||
|
||||
props.mintAccounts?.isFetching;
|
||||
|
||||
const accountLoadError =
|
||||
!(props.mintAccounts?.isFetching || props.mintAccounts?.data) &&
|
||||
"Unable to retrieve your token accounts";
|
||||
const error = accountLoadError;
|
||||
|
||||
//This exists to remove NFTs from the list of potential options. It requires reading the metaplex data, so it would be
|
||||
//difficult to do before this point.
|
||||
|
@ -152,6 +200,10 @@ export default function SolanaSourceTokenSelector(
|
|||
});
|
||||
}, [memoizedMetaplex, props.accounts]);
|
||||
|
||||
const isOptionDisabled = useMemo(() => {
|
||||
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
||||
}, [isWormholev1]);
|
||||
|
||||
const autoComplete = (
|
||||
<Autocomplete
|
||||
autoComplete
|
||||
|
@ -178,6 +230,7 @@ export default function SolanaSourceTokenSelector(
|
|||
classes
|
||||
);
|
||||
}}
|
||||
getOptionDisabled={isOptionDisabled}
|
||||
getOptionLabel={(option) => {
|
||||
const symbol = getSymbol(option);
|
||||
return `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress(
|
||||
|
@ -190,6 +243,7 @@ export default function SolanaSourceTokenSelector(
|
|||
return (
|
||||
<React.Fragment>
|
||||
{isLoading ? <CircularProgress /> : autoComplete}
|
||||
{error && <Typography color="error">{error}</Typography>}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { TextField, Typography } from "@material-ui/core";
|
|||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useGetSourceParsedTokens from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import {
|
||||
selectTransferSourceChain,
|
||||
selectTransferSourceParsedTokenAccount,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
import {
|
||||
ParsedTokenAccount,
|
||||
setSourceParsedTokenAccount,
|
||||
setSourceWalletAddress,
|
||||
} from "../../store/transferSlice";
|
||||
import EthereumSourceTokenSelector from "./EthereumSourceTokenSelector";
|
||||
import SolanaSourceTokenSelector from "./SolanaSourceTokenSelector";
|
||||
|
@ -32,13 +34,19 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
const sourceParsedTokenAccount = useSelector(
|
||||
selectTransferSourceParsedTokenAccount
|
||||
);
|
||||
const handleSolanaOnChange = useCallback(
|
||||
const walletIsReady = useIsWalletReady(lookupChain);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(newTokenAccount: ParsedTokenAccount | null) => {
|
||||
if (newTokenAccount !== undefined) {
|
||||
dispatch(setSourceParsedTokenAccount(newTokenAccount || undefined));
|
||||
if (!newTokenAccount) {
|
||||
dispatch(setSourceParsedTokenAccount(undefined));
|
||||
dispatch(setSourceWalletAddress(undefined));
|
||||
} else if (newTokenAccount !== undefined && walletIsReady.walletAddress) {
|
||||
dispatch(setSourceParsedTokenAccount(newTokenAccount));
|
||||
dispatch(setSourceWalletAddress(walletIsReady.walletAddress));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, walletIsReady]
|
||||
);
|
||||
|
||||
const maps = useGetSourceParsedTokens();
|
||||
|
@ -54,17 +62,18 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
) : lookupChain === CHAIN_ID_SOLANA ? (
|
||||
<SolanaSourceTokenSelector
|
||||
value={sourceParsedTokenAccount || null}
|
||||
onChange={handleSolanaOnChange}
|
||||
onChange={handleOnChange}
|
||||
disabled={disabled}
|
||||
accounts={maps?.tokenAccounts?.data || []}
|
||||
solanaTokenMap={maps?.tokenMap}
|
||||
metaplexData={maps?.metaplex}
|
||||
mintAccounts={maps?.mintAccounts}
|
||||
/>
|
||||
) : lookupChain === CHAIN_ID_ETH ? (
|
||||
<EthereumSourceTokenSelector
|
||||
value={sourceParsedTokenAccount || null}
|
||||
disabled={disabled}
|
||||
onChange={handleSolanaOnChange}
|
||||
onChange={handleOnChange}
|
||||
covalent={maps?.covalent || undefined}
|
||||
tokenAccounts={maps?.tokenAccounts} //TODO standardize
|
||||
/>
|
||||
|
@ -72,7 +81,8 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
<TerraSourceTokenSelector
|
||||
value={sourceParsedTokenAccount || null}
|
||||
disabled={disabled}
|
||||
onChange={handleSolanaOnChange}
|
||||
onChange={handleOnChange}
|
||||
tokenMap={maps?.terraTokenMap}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
|
|
|
@ -1,20 +1,48 @@
|
|||
import { TextField, Typography } from "@material-ui/core";
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Autocomplete, createFilterOptions } from "@material-ui/lab";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import {
|
||||
ConnectedWallet,
|
||||
useConnectedWallet,
|
||||
} from "@terra-money/wallet-provider";
|
||||
import { formatUnits } from "ethers/lib/utils";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
createParsedTokenAccount,
|
||||
TerraTokenMap,
|
||||
TerraTokenMetadata,
|
||||
} from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import { DataWrapper } from "../../store/helpers";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import { TERRA_HOST } from "../../utils/consts";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
import OffsetButton from "./OffsetButton";
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
selectInput: { minWidth: "10rem" },
|
||||
tokenOverviewContainer: {
|
||||
display: "flex",
|
||||
"& div": {
|
||||
margin: ".5rem",
|
||||
},
|
||||
},
|
||||
tokenImage: {
|
||||
maxHeight: "2.5rem",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
type TerraSourceTokenSelectorProps = {
|
||||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||
disabled: boolean;
|
||||
tokenMap: DataWrapper<TerraTokenMap> | undefined; //TODO better type
|
||||
};
|
||||
|
||||
//TODO move elsewhere
|
||||
|
@ -58,12 +86,27 @@ const lookupTerraAddress = (
|
|||
export default function TerraSourceTokenSelector(
|
||||
props: TerraSourceTokenSelectorProps
|
||||
) {
|
||||
const { onChange, value, disabled } = props;
|
||||
//const advancedMode = true; //const [advancedMode, setAdvancedMode] = useState(true);
|
||||
const classes = useStyles();
|
||||
const { onChange, value, disabled, tokenMap } = props;
|
||||
const [advancedMode, setAdvancedMode] = useState(false);
|
||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||
const terraWallet = useConnectedWallet();
|
||||
|
||||
const isLoading = tokenMap?.isFetching || false;
|
||||
|
||||
const terraTokenArray = useMemo(() => {
|
||||
const values = props.tokenMap?.data?.mainnet;
|
||||
const items = Object.values(values || {});
|
||||
return items || [];
|
||||
}, [props.tokenMap]);
|
||||
|
||||
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
||||
if (!fromProps) return undefined;
|
||||
else {
|
||||
return terraTokenArray.find((x) => x.token === fromProps.mintKey);
|
||||
}
|
||||
};
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(null);
|
||||
setAdvancedModeHolderString("");
|
||||
|
@ -74,22 +117,93 @@ export default function TerraSourceTokenSelector(
|
|||
[]
|
||||
);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (terraWallet === undefined) {
|
||||
const handleConfirm = (address: string | undefined) => {
|
||||
if (terraWallet === undefined || address === undefined) {
|
||||
setAdvancedModeError("Terra wallet not connected.");
|
||||
return;
|
||||
}
|
||||
lookupTerraAddress(advancedModeHolderString, terraWallet).then(
|
||||
setAdvancedModeError("");
|
||||
lookupTerraAddress(address, terraWallet).then(
|
||||
(result) => {
|
||||
onChange(result);
|
||||
},
|
||||
(error) => {
|
||||
setAdvancedModeError("Unable to retrieve address.");
|
||||
setAdvancedModeError("Unable to retrieve this address.");
|
||||
}
|
||||
);
|
||||
setAdvancedModeError("");
|
||||
};
|
||||
|
||||
const filterConfig = createFilterOptions({
|
||||
matchFrom: "any",
|
||||
stringify: (option: TerraTokenMetadata) => {
|
||||
const symbol = option.symbol + " " || "";
|
||||
const mint = option.token + " " || "";
|
||||
const name = option.protocol + " " || "";
|
||||
|
||||
return symbol + mint + name;
|
||||
},
|
||||
});
|
||||
|
||||
const renderOptionLabel = (option: TerraTokenMetadata) => {
|
||||
return option.symbol + " (" + shortenAddress(option.token) + ")";
|
||||
};
|
||||
const renderOption = (option: TerraTokenMetadata) => {
|
||||
return (
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div>
|
||||
<img alt="" className={classes.tokenImage} src={option.icon} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h6">{option.symbol}</Typography>
|
||||
<Typography variant="body2">{option.protocol}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body1">{option.token}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAdvancedMode = () => {
|
||||
setAdvancedMode(!advancedMode);
|
||||
};
|
||||
|
||||
const advancedModeToggleButton = (
|
||||
<OffsetButton onClick={toggleAdvancedMode} disabled={disabled}>
|
||||
{advancedMode ? "Toggle Token Picker" : "Toggle Override"}
|
||||
</OffsetButton>
|
||||
);
|
||||
|
||||
const autoComplete = (
|
||||
<>
|
||||
<Autocomplete
|
||||
autoComplete
|
||||
autoHighlight
|
||||
autoSelect
|
||||
blurOnSelect
|
||||
clearOnBlur
|
||||
fullWidth={false}
|
||||
filterOptions={filterConfig}
|
||||
value={valueToOption(value)}
|
||||
onChange={(event, newValue) => {
|
||||
handleConfirm(newValue?.token);
|
||||
}}
|
||||
disabled={disabled}
|
||||
noOptionsText={"No CW20 tokens found at the moment."}
|
||||
options={terraTokenArray}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Token" variant="outlined" />
|
||||
)}
|
||||
renderOption={renderOption}
|
||||
getOptionLabel={renderOptionLabel}
|
||||
/>
|
||||
{advancedModeError && (
|
||||
<Typography color="error">{advancedModeError}</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const content = value ? (
|
||||
<>
|
||||
<Typography>{value.mintKey}</Typography>
|
||||
|
@ -97,22 +211,32 @@ export default function TerraSourceTokenSelector(
|
|||
Clear
|
||||
</OffsetButton>
|
||||
</>
|
||||
) : !advancedMode ? (
|
||||
autoComplete
|
||||
) : (
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Asset Address"
|
||||
label="Enter an asset address"
|
||||
value={advancedModeHolderString}
|
||||
onChange={handleOnChange}
|
||||
disabled={disabled}
|
||||
error={advancedModeHolderString !== "" && !!advancedModeError}
|
||||
helperText={advancedModeError === "" ? undefined : advancedModeError}
|
||||
/>
|
||||
<OffsetButton onClick={handleConfirm} disabled={disabled}>
|
||||
<OffsetButton
|
||||
onClick={() => handleConfirm(advancedModeHolderString)}
|
||||
disabled={disabled}
|
||||
>
|
||||
Confirm
|
||||
</OffsetButton>
|
||||
</>
|
||||
);
|
||||
|
||||
return <React.Fragment>{content}</React.Fragment>;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{content}
|
||||
{!value && !isLoading && advancedModeToggleButton}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useSelector } from "react-redux";
|
|||
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import {
|
||||
selectSourceWalletAddress,
|
||||
selectTransferSourceChain,
|
||||
selectTransferTargetError,
|
||||
} from "../../store/selectors";
|
||||
|
@ -16,7 +17,18 @@ function Send() {
|
|||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const error = useSelector(selectTransferTargetError);
|
||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
||||
const { isReady, statusMessage, walletAddress } =
|
||||
useIsWalletReady(sourceChain);
|
||||
const sourceWalletAddress = useSelector(selectSourceWalletAddress);
|
||||
//The chain ID compare is handled implicitly, as the isWalletReady hook should report !isReady if the wallet is on the wrong chain.
|
||||
const isWrongWallet =
|
||||
sourceWalletAddress &&
|
||||
walletAddress &&
|
||||
sourceWalletAddress !== walletAddress;
|
||||
const isDisabled = !isReady || isWrongWallet || disabled;
|
||||
const errorMessage = isWrongWallet
|
||||
? "A different wallet is connected than in Step 1."
|
||||
: statusMessage || error || undefined;
|
||||
return (
|
||||
<>
|
||||
<StepDescription>
|
||||
|
@ -30,10 +42,10 @@ function Send() {
|
|||
complete the transfer.
|
||||
</Alert>
|
||||
<ButtonWithLoader
|
||||
disabled={!isReady || disabled}
|
||||
disabled={isDisabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
error={statusMessage || error}
|
||||
error={errorMessage}
|
||||
>
|
||||
Transfer
|
||||
</ButtonWithLoader>
|
||||
|
|
|
@ -8,8 +8,8 @@ import React, {
|
|||
useState,
|
||||
} from "react";
|
||||
|
||||
type Provider = ethers.providers.Web3Provider | undefined;
|
||||
type Signer = ethers.Signer | undefined;
|
||||
export type Provider = ethers.providers.Web3Provider | undefined;
|
||||
export type Signer = ethers.Signer | undefined;
|
||||
|
||||
interface IEthereumProviderContext {
|
||||
connect(): void;
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Dispatch } from "@reduxjs/toolkit";
|
||||
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import { ENV, TokenListProvider } from "@solana/spl-token-registry";
|
||||
|
@ -17,27 +21,44 @@ import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
|||
import { DataWrapper } from "../store/helpers";
|
||||
import {
|
||||
selectSolanaTokenMap,
|
||||
selectSourceWalletAddress,
|
||||
selectTerraTokenMap,
|
||||
selectTransferSourceChain,
|
||||
selectTransferSourceParsedTokenAccounts,
|
||||
} from "../store/selectors";
|
||||
import {
|
||||
errorSolanaTokenMap,
|
||||
errorTerraTokenMap,
|
||||
fetchSolanaTokenMap,
|
||||
fetchTerraTokenMap,
|
||||
receiveSolanaTokenMap,
|
||||
receiveTerraTokenMap,
|
||||
} from "../store/tokenSlice";
|
||||
import {
|
||||
errorSourceParsedTokenAccounts,
|
||||
fetchSourceParsedTokenAccounts,
|
||||
ParsedTokenAccount,
|
||||
receiveSourceParsedTokenAccounts,
|
||||
setAmount,
|
||||
setSourceParsedTokenAccount,
|
||||
setSourceParsedTokenAccounts,
|
||||
setSourceWalletAddress,
|
||||
} from "../store/transferSlice";
|
||||
import { CLUSTER, COVALENT_GET_TOKENS_URL, SOLANA_HOST } from "../utils/consts";
|
||||
import {
|
||||
CLUSTER,
|
||||
COVALENT_GET_TOKENS_URL,
|
||||
SOLANA_HOST,
|
||||
TERRA_TOKEN_METADATA_URL,
|
||||
} from "../utils/consts";
|
||||
import {
|
||||
decodeMetadata,
|
||||
getMetadataAddress,
|
||||
Metadata,
|
||||
} from "../utils/metaplex";
|
||||
import { getMultipleAccountsRPC } from "../utils/solana";
|
||||
import {
|
||||
extractMintAuthorityInfo,
|
||||
getMultipleAccountsRPC,
|
||||
} from "../utils/solana";
|
||||
|
||||
export function createParsedTokenAccount(
|
||||
publicKey: string,
|
||||
|
@ -57,6 +78,19 @@ export function createParsedTokenAccount(
|
|||
};
|
||||
}
|
||||
|
||||
export type TerraTokenMetadata = {
|
||||
protocol: string;
|
||||
symbol: string;
|
||||
token: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
export type TerraTokenMap = {
|
||||
mainnet: {
|
||||
[address: string]: TerraTokenMetadata;
|
||||
};
|
||||
};
|
||||
|
||||
const createParsedTokenAccountFromInfo = (
|
||||
pubkey: PublicKey,
|
||||
item: AccountInfo<ParsedAccountData>
|
||||
|
@ -211,6 +245,7 @@ function useGetAvailableTokens() {
|
|||
|
||||
const tokenAccounts = useSelector(selectTransferSourceParsedTokenAccounts);
|
||||
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
||||
const terraTokenMap = useSelector(selectTerraTokenMap);
|
||||
|
||||
const lookupChain = useSelector(selectTransferSourceChain);
|
||||
const solanaWallet = useSolanaWallet();
|
||||
|
@ -228,6 +263,38 @@ function useGetAvailableTokens() {
|
|||
undefined
|
||||
);
|
||||
|
||||
const [solanaMintAccounts, setSolanaMintAccounts] = useState<any>(undefined);
|
||||
const [solanaMintAccountsLoading, setSolanaMintAccountsLoading] =
|
||||
useState(false);
|
||||
const [solanaMintAccountsError, setSolanaMintAccountsError] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
const selectedSourceWalletAddress = useSelector(selectSourceWalletAddress);
|
||||
const currentSourceWalletAddress: string | undefined =
|
||||
lookupChain === CHAIN_ID_ETH
|
||||
? signerAddress
|
||||
: lookupChain === CHAIN_ID_SOLANA
|
||||
? solPK?.toString()
|
||||
: undefined;
|
||||
|
||||
//TODO this useEffect could be somewhere else in the codebase
|
||||
//It resets the SourceParsedTokens accounts when the wallet changes
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedSourceWalletAddress !== undefined &&
|
||||
currentSourceWalletAddress !== undefined &&
|
||||
currentSourceWalletAddress !== selectedSourceWalletAddress
|
||||
) {
|
||||
dispatch(setSourceWalletAddress(undefined));
|
||||
dispatch(setSourceParsedTokenAccount(undefined));
|
||||
dispatch(setSourceParsedTokenAccounts(undefined));
|
||||
dispatch(setAmount(""));
|
||||
return;
|
||||
} else {
|
||||
}
|
||||
}, [selectedSourceWalletAddress, currentSourceWalletAddress, dispatch]);
|
||||
|
||||
// Solana metaplex load
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
@ -289,6 +356,53 @@ function useGetAvailableTokens() {
|
|||
solanaTokenMap,
|
||||
]);
|
||||
|
||||
//Solana Mint Accounts lookup
|
||||
useEffect(() => {
|
||||
if (lookupChain !== CHAIN_ID_SOLANA || !tokenAccounts.data?.length) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setSolanaMintAccountsLoading(true);
|
||||
setSolanaMintAccountsError(undefined);
|
||||
const mintAddresses = tokenAccounts.data.map((x) => x.mintKey);
|
||||
//This is a known wormhole v1 token on testnet
|
||||
//mintAddresses.push("4QixXecTZ4zdZGa39KH8gVND5NZ2xcaB12wiBhE4S7rn");
|
||||
|
||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
||||
getMultipleAccountsRPC(
|
||||
connection,
|
||||
mintAddresses.map((x) => new PublicKey(x))
|
||||
).then(
|
||||
(results) => {
|
||||
if (!cancelled) {
|
||||
const output = new Map<String, string | null>();
|
||||
|
||||
results.forEach((result, index) =>
|
||||
output.set(
|
||||
mintAddresses[index],
|
||||
(result && extractMintAuthorityInfo(result)) || null
|
||||
)
|
||||
);
|
||||
|
||||
setSolanaMintAccounts(output);
|
||||
setSolanaMintAccountsLoading(false);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
setSolanaMintAccounts(undefined);
|
||||
setSolanaMintAccountsLoading(false);
|
||||
setSolanaMintAccountsError(
|
||||
"Could not retrieve Solana mint accounts."
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => (cancelled = true);
|
||||
}, [tokenAccounts.data, lookupChain]);
|
||||
|
||||
//Ethereum accounts load
|
||||
useEffect(() => {
|
||||
//const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
|
||||
|
@ -333,10 +447,38 @@ function useGetAvailableTokens() {
|
|||
}, [lookupChain, provider, signerAddress, dispatch]);
|
||||
|
||||
//Terra accounts load
|
||||
//At present, we don't have any mechanism for doing this.
|
||||
useEffect(() => {}, []);
|
||||
|
||||
//Terra metadata load
|
||||
useEffect(() => {}, []);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
if (terraTokenMap.data || lookupChain !== CHAIN_ID_TERRA) {
|
||||
return; //So we don't fetch the whole list on every mount.
|
||||
}
|
||||
|
||||
dispatch(fetchTerraTokenMap());
|
||||
axios.get(TERRA_TOKEN_METADATA_URL).then(
|
||||
(response) => {
|
||||
if (!cancelled) {
|
||||
//TODO parse this in a safer manner
|
||||
dispatch(receiveTerraTokenMap(response.data as TerraTokenMap));
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
dispatch(
|
||||
errorTerraTokenMap("Failed to retrieve the Terra Token List.")
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [lookupChain, terraTokenMap.data, dispatch]);
|
||||
|
||||
return lookupChain === CHAIN_ID_SOLANA
|
||||
? {
|
||||
|
@ -348,6 +490,12 @@ function useGetAvailableTokens() {
|
|||
error: metaplexError,
|
||||
receivedAt: null, //TODO
|
||||
} as DataWrapper<Metadata[]>,
|
||||
mintAccounts: {
|
||||
data: solanaMintAccounts,
|
||||
isFetching: solanaMintAccountsLoading,
|
||||
error: solanaMintAccountsError,
|
||||
receivedAt: null, //TODO
|
||||
},
|
||||
}
|
||||
: lookupChain === CHAIN_ID_ETH
|
||||
? {
|
||||
|
@ -359,6 +507,10 @@ function useGetAvailableTokens() {
|
|||
receivedAt: null, //TODO
|
||||
},
|
||||
}
|
||||
: lookupChain === CHAIN_ID_TERRA
|
||||
? {
|
||||
terraTokenMap: terraTokenMap,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,14 +11,20 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
|||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { CLUSTER, ETH_NETWORK_CHAIN_ID } from "../utils/consts";
|
||||
|
||||
const createWalletStatus = (isReady: boolean, statusMessage: string = "") => ({
|
||||
const createWalletStatus = (
|
||||
isReady: boolean,
|
||||
statusMessage: string = "",
|
||||
walletAddress?: string
|
||||
) => ({
|
||||
isReady,
|
||||
statusMessage,
|
||||
walletAddress,
|
||||
});
|
||||
|
||||
function useIsWalletReady(chainId: ChainId): {
|
||||
isReady: boolean;
|
||||
statusMessage: string;
|
||||
walletAddress?: string;
|
||||
} {
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
|
@ -33,16 +39,20 @@ function useIsWalletReady(chainId: ChainId): {
|
|||
const hasCorrectEthNetwork = ethChainId === ETH_NETWORK_CHAIN_ID;
|
||||
|
||||
return useMemo(() => {
|
||||
if (chainId === CHAIN_ID_TERRA && hasTerraWallet) {
|
||||
if (
|
||||
chainId === CHAIN_ID_TERRA &&
|
||||
hasTerraWallet &&
|
||||
terraWallet?.walletAddress
|
||||
) {
|
||||
// TODO: terraWallet does not update on wallet changes
|
||||
return createWalletStatus(true);
|
||||
return createWalletStatus(true, undefined, terraWallet.walletAddress);
|
||||
}
|
||||
if (chainId === CHAIN_ID_SOLANA && solPK) {
|
||||
return createWalletStatus(true);
|
||||
return createWalletStatus(true, undefined, solPK.toString());
|
||||
}
|
||||
if (chainId === CHAIN_ID_ETH && hasEthInfo) {
|
||||
if (chainId === CHAIN_ID_ETH && hasEthInfo && signerAddress) {
|
||||
if (hasCorrectEthNetwork) {
|
||||
return createWalletStatus(true);
|
||||
return createWalletStatus(true, undefined, signerAddress);
|
||||
} else {
|
||||
if (provider) {
|
||||
try {
|
||||
|
@ -53,7 +63,8 @@ function useIsWalletReady(chainId: ChainId): {
|
|||
}
|
||||
return createWalletStatus(
|
||||
false,
|
||||
`Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}`
|
||||
`Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}`,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +77,8 @@ function useIsWalletReady(chainId: ChainId): {
|
|||
hasEthInfo,
|
||||
hasCorrectEthNetwork,
|
||||
provider,
|
||||
signerAddress,
|
||||
terraWallet,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ export const selectTransferOriginChain = (state: RootState) =>
|
|||
state.transfer.originChain;
|
||||
export const selectTransferOriginAsset = (state: RootState) =>
|
||||
state.transfer.originAsset;
|
||||
export const selectSourceWalletAddress = (state: RootState) =>
|
||||
state.transfer.sourceWalletAddress;
|
||||
export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
|
||||
state.transfer.sourceParsedTokenAccount;
|
||||
export const selectTransferSourceParsedTokenAccounts = (state: RootState) =>
|
||||
|
@ -168,3 +170,7 @@ export const selectTransferShouldLockFields = (state: RootState) =>
|
|||
export const selectSolanaTokenMap = (state: RootState) => {
|
||||
return state.tokens.solanaTokenMap;
|
||||
};
|
||||
|
||||
export const selectTerraTokenMap = (state: RootState) => {
|
||||
return state.tokens.terraTokenMap;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { TokenInfo } from "@solana/spl-token-registry";
|
||||
import { TerraTokenMap } from "../hooks/useGetSourceParsedTokenAccounts";
|
||||
import {
|
||||
DataWrapper,
|
||||
errorDataWrapper,
|
||||
|
@ -10,10 +11,12 @@ import {
|
|||
|
||||
export interface TokenMetadataState {
|
||||
solanaTokenMap: DataWrapper<TokenInfo[]>;
|
||||
terraTokenMap: DataWrapper<TerraTokenMap>; //TODO make a decent type for this.
|
||||
}
|
||||
|
||||
const initialState: TokenMetadataState = {
|
||||
solanaTokenMap: getEmptyDataWrapper(),
|
||||
terraTokenMap: getEmptyDataWrapper(),
|
||||
};
|
||||
|
||||
export const tokenSlice = createSlice({
|
||||
|
@ -29,6 +32,17 @@ export const tokenSlice = createSlice({
|
|||
errorSolanaTokenMap: (state, action: PayloadAction<string>) => {
|
||||
state.solanaTokenMap = errorDataWrapper(action.payload);
|
||||
},
|
||||
|
||||
receiveTerraTokenMap: (state, action: PayloadAction<TerraTokenMap>) => {
|
||||
state.terraTokenMap = receiveDataWrapper(action.payload);
|
||||
},
|
||||
fetchTerraTokenMap: (state) => {
|
||||
state.terraTokenMap = fetchDataWrapper();
|
||||
},
|
||||
errorTerraTokenMap: (state, action: PayloadAction<string>) => {
|
||||
state.terraTokenMap = errorDataWrapper(action.payload);
|
||||
},
|
||||
|
||||
reset: () => initialState,
|
||||
},
|
||||
});
|
||||
|
@ -37,6 +51,9 @@ export const {
|
|||
receiveSolanaTokenMap,
|
||||
fetchSolanaTokenMap,
|
||||
errorSolanaTokenMap,
|
||||
receiveTerraTokenMap,
|
||||
fetchTerraTokenMap,
|
||||
errorTerraTokenMap,
|
||||
reset,
|
||||
} = tokenSlice.actions;
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ export interface TransferState {
|
|||
isSourceAssetWormholeWrapped: boolean | undefined;
|
||||
originChain: ChainId | undefined;
|
||||
originAsset: string | undefined;
|
||||
sourceWalletAddress: string | undefined;
|
||||
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
|
||||
sourceParsedTokenAccounts: DataWrapper<ParsedTokenAccount[]>;
|
||||
amount: string;
|
||||
|
@ -55,6 +56,7 @@ const initialState: TransferState = {
|
|||
activeStep: 0,
|
||||
sourceChain: CHAIN_ID_SOLANA,
|
||||
isSourceAssetWormholeWrapped: false,
|
||||
sourceWalletAddress: undefined,
|
||||
sourceParsedTokenAccount: undefined,
|
||||
sourceParsedTokenAccounts: getEmptyDataWrapper(),
|
||||
originChain: undefined,
|
||||
|
@ -110,12 +112,26 @@ export const transferSlice = createSlice({
|
|||
state.originAsset = undefined;
|
||||
}
|
||||
},
|
||||
setSourceWalletAddress: (
|
||||
state,
|
||||
action: PayloadAction<string | undefined>
|
||||
) => {
|
||||
state.sourceWalletAddress = action.payload;
|
||||
},
|
||||
setSourceParsedTokenAccount: (
|
||||
state,
|
||||
action: PayloadAction<ParsedTokenAccount | undefined>
|
||||
) => {
|
||||
state.sourceParsedTokenAccount = action.payload;
|
||||
},
|
||||
setSourceParsedTokenAccounts: (
|
||||
state,
|
||||
action: PayloadAction<ParsedTokenAccount[] | undefined>
|
||||
) => {
|
||||
state.sourceParsedTokenAccounts = action.payload
|
||||
? receiveDataWrapper(action.payload)
|
||||
: getEmptyDataWrapper();
|
||||
},
|
||||
fetchSourceParsedTokenAccounts: (state) => {
|
||||
state.sourceParsedTokenAccounts = fetchDataWrapper();
|
||||
},
|
||||
|
@ -195,7 +211,9 @@ export const {
|
|||
setStep,
|
||||
setSourceChain,
|
||||
setSourceWormholeWrappedInfo,
|
||||
setSourceWalletAddress,
|
||||
setSourceParsedTokenAccount,
|
||||
setSourceParsedTokenAccounts,
|
||||
receiveSourceParsedTokenAccounts,
|
||||
errorSourceParsedTokenAccounts,
|
||||
fetchSourceParsedTokenAccounts,
|
||||
|
|
|
@ -30,6 +30,10 @@ export const CHAINS =
|
|||
id: CHAIN_ID_SOLANA,
|
||||
name: "Solana",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_TERRA,
|
||||
name: "Terra",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
|
@ -62,7 +66,15 @@ export const ETH_NETWORK_CHAIN_ID =
|
|||
CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337;
|
||||
export const SOLANA_HOST =
|
||||
CLUSTER === "testnet" ? clusterApiUrl("testnet") : "http://localhost:8899";
|
||||
export const TERRA_HOST = {
|
||||
|
||||
export const TERRA_HOST =
|
||||
CLUSTER === "testnet"
|
||||
? {
|
||||
URL: "https://tequila-lcd.terra.dev",
|
||||
chainID: "tequila-0004",
|
||||
name: "testnet",
|
||||
}
|
||||
: {
|
||||
URL: "http://localhost:1317",
|
||||
chainID: "columbus-4",
|
||||
name: "localterra",
|
||||
|
@ -119,3 +131,24 @@ export const COVALENT_GET_TOKENS_URL = (
|
|||
};
|
||||
|
||||
export const COVALENT_ETHEREUM_MAINNET = "1";
|
||||
|
||||
export const WORMHOLE_V1_ETH_ADDRESS =
|
||||
CLUSTER === "testnet"
|
||||
? "0xdae0Cba01eFc4bfEc1F7Fece73Fe8b8d2Eda65B0"
|
||||
: CLUSTER === "mainnet"
|
||||
? "0xf92cD566Ea4864356C5491c177A430C222d7e678"
|
||||
: "0xf92cD566Ea4864356C5491c177A430C222d7e678"; //TODO something that doesn't explode in localhost
|
||||
export const WORMHOLE_V1_SOLANA_ADDRESS =
|
||||
CLUSTER === "testnet"
|
||||
? "BrdgiFmZN3BKkcY3danbPYyxPKwb8RhQzpM2VY5L97ED"
|
||||
: "WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC";
|
||||
|
||||
export const TERRA_TOKEN_METADATA_URL =
|
||||
"https://assets.terra.money/cw20/tokens.json";
|
||||
|
||||
export const WORMHOLE_V1_MINT_AUTHORITY =
|
||||
CLUSTER === "mainnet"
|
||||
? "9zyPU1mjgzaVyQsYwKJJ7AhVz5bgx5uc1NPABvAcUXsT"
|
||||
: CLUSTER === "testnet"
|
||||
? "BJa7dq3bRP216zaTdw4cdcV71WkPc1HXvmnGeFVDi5DC"
|
||||
: "";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { MintLayout } from "@solana/spl-token";
|
||||
import { WalletContextState } from "@solana/wallet-adapter-react";
|
||||
import {
|
||||
AccountInfo,
|
||||
|
@ -17,6 +18,19 @@ export async function signSendAndConfirm(
|
|||
return txid;
|
||||
}
|
||||
|
||||
export function extractMintAuthorityInfo(
|
||||
account: AccountInfo<Buffer>
|
||||
): string | null {
|
||||
const data = Buffer.from(account.data);
|
||||
const mintInfo = MintLayout.decode(data);
|
||||
|
||||
const uintArray = mintInfo?.mintAuthority;
|
||||
const pubkey = new PublicKey(uintArray);
|
||||
const output = pubkey?.toString();
|
||||
|
||||
return output || null;
|
||||
}
|
||||
|
||||
export async function getMultipleAccountsRPC(
|
||||
connection: Connection,
|
||||
pubkeys: PublicKey[]
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
"lib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "npm run build-contracts",
|
||||
"postinstall": "npm run build-abis && npm run build-contracts",
|
||||
"build-contracts": "npm run build --prefix ../../ethereum && node scripts/copyContracts.js && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
|
||||
"build-abis": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts/abi src/abi/Wormhole.abi.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc && node scripts/copyEthersTypes.js && node scripts/copyWasm.js",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
|
|
|
@ -7,3 +7,12 @@ fs.readdirSync("src/ethers-contracts").forEach((file) => {
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
fs.readdirSync("src/ethers-contracts/abi").forEach((file) => {
|
||||
if (file.endsWith(".d.ts")) {
|
||||
fs.copyFileSync(
|
||||
`src/ethers-contracts/abi/${file}`,
|
||||
`lib/ethers-contracts/abi/${file}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,388 @@
|
|||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address[]",
|
||||
"name": "keys",
|
||||
"type": "address[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "expiration_time",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"internalType": "struct Wormhole.GuardianSet",
|
||||
"name": "initial_guardian_set",
|
||||
"type": "tuple"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "wrapped_asset_master",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "_guardian_set_expirity",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint32",
|
||||
"name": "oldGuardianIndex",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint32",
|
||||
"name": "newGuardianIndex",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"name": "LogGuardianSetChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint8",
|
||||
"name": "target_chain",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint8",
|
||||
"name": "token_chain",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint8",
|
||||
"name": "token_decimals",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "token",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "sender",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes32",
|
||||
"name": "recipient",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint32",
|
||||
"name": "nonce",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"name": "LogTokensLocked",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"stateMutability": "payable",
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "consumedVAAs",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "idx",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"name": "getGuardianSet",
|
||||
"outputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address[]",
|
||||
"name": "keys",
|
||||
"type": "address[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "expiration_time",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"internalType": "struct Wormhole.GuardianSet",
|
||||
"name": "gs",
|
||||
"type": "tuple"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "guardian_set_expirity",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "guardian_set_index",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"name": "guardian_sets",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "expiration_time",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isWrappedAsset",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "asset",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "recipient",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "target_chain",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "nonce",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "refund_dust",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "lockAssets",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "recipient",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "target_chain",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "nonce",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"name": "lockETH",
|
||||
"outputs": [],
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "vaa",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "parseAndVerifyVAA",
|
||||
"outputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "version",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "hash",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "guardian_set_index",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "timestamp",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "action",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "payload",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"internalType": "struct Wormhole.ParsedVAA",
|
||||
"name": "parsed_vaa",
|
||||
"type": "tuple"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "vaa",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "submitVAA",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "wrappedAssetMaster",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "wrappedAssets",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"stateMutability": "payable",
|
||||
"type": "receive"
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue