bridge_ui: fix solana nft filter
Change-Id: Ie1fab3839cf7cf91f65dcbb9e1b0a3ad8ca261f5
This commit is contained in:
parent
7fede406a4
commit
0fc7b06b8d
|
@ -1,3 +1,4 @@
|
|||
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { Button, makeStyles } from "@material-ui/core";
|
||||
import { VerifiedUser } from "@material-ui/icons";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
|
@ -79,6 +80,11 @@ function Source() {
|
|||
Only NFTs which implement ERC-721 are supported.
|
||||
</Alert>
|
||||
) : null}
|
||||
{sourceChain === CHAIN_ID_SOLANA ? (
|
||||
<Alert severity="info" variant="outlined">
|
||||
Only NFTs with a supply of 1 are supported.
|
||||
</Alert>
|
||||
) : null}
|
||||
<KeyAndBalance chainId={sourceChain} />
|
||||
{isReady || uiAmountString ? (
|
||||
<div className={classes.transferField}>
|
||||
|
|
|
@ -1,643 +0,0 @@
|
|||
import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi";
|
||||
import {
|
||||
CircularProgress,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Alert, Autocomplete, createFilterOptions } from "@material-ui/lab";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import { CovalentData } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import { DataWrapper } from "../../store/helpers";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import {
|
||||
getMigrationAssetMap,
|
||||
WORMHOLE_V1_ETH_ADDRESS,
|
||||
} from "../../utils/consts";
|
||||
import {
|
||||
ethNFTToNFTParsedTokenAccount,
|
||||
ethTokenToParsedTokenAccount,
|
||||
getEthereumNFT,
|
||||
getEthereumToken,
|
||||
isNFT,
|
||||
isValidEthereumAddress,
|
||||
} from "../../utils/ethereum";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
import OffsetButton from "./OffsetButton";
|
||||
import { NFTParsedTokenAccount } from "../../store/nftSlice";
|
||||
import NFTViewer from "./NFTViewer";
|
||||
import { useDebounce } from "use-debounce/lib";
|
||||
import RefreshButtonWrapper from "./RefreshButtonWrapper";
|
||||
import { ChainId, CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
|
||||
import { sortParsedTokenAccounts } from "../../utils/sort";
|
||||
import { getAddress } from "@ethersproject/address";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
selectInput: { minWidth: "10rem" },
|
||||
tokenOverviewContainer: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
"& div": {
|
||||
margin: theme.spacing(1),
|
||||
flexBasis: "33%",
|
||||
"&$tokenImageContainer": {
|
||||
maxWidth: 40,
|
||||
},
|
||||
"&:last-child": {
|
||||
textAlign: "right",
|
||||
},
|
||||
},
|
||||
},
|
||||
tokenImageContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 40,
|
||||
},
|
||||
tokenImage: {
|
||||
maxHeight: "2.5rem", //Eyeballing this based off the text size
|
||||
},
|
||||
migrationAlert: {
|
||||
width: "100%",
|
||||
"& .MuiAlert-message": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const getSymbol = (account: ParsedTokenAccount | null) => {
|
||||
if (!account) {
|
||||
return undefined;
|
||||
}
|
||||
return account.symbol;
|
||||
};
|
||||
|
||||
const getLogo = (account: ParsedTokenAccount | null) => {
|
||||
if (!account) {
|
||||
return undefined;
|
||||
}
|
||||
return account.logo;
|
||||
};
|
||||
|
||||
const isWormholev1 = (provider: any, address: string, chainId: ChainId) => {
|
||||
if (chainId !== CHAIN_ID_ETH) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const connection = WormholeAbi__factory.connect(
|
||||
WORMHOLE_V1_ETH_ADDRESS,
|
||||
provider
|
||||
);
|
||||
return connection.isWrappedAsset(address);
|
||||
};
|
||||
|
||||
const isMigrationEligible = (chainId: ChainId, address: string) => {
|
||||
const assetMap = getMigrationAssetMap(chainId);
|
||||
return !!assetMap.get(getAddress(address));
|
||||
};
|
||||
|
||||
type EthereumSourceTokenSelectorProps = {
|
||||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||
covalent: DataWrapper<CovalentData[]> | undefined;
|
||||
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
|
||||
disabled: boolean;
|
||||
resetAccounts: (() => void) | undefined;
|
||||
chainId: ChainId;
|
||||
nft?: boolean;
|
||||
};
|
||||
|
||||
const renderAccount = (
|
||||
chainId: ChainId,
|
||||
account: ParsedTokenAccount,
|
||||
covalentData: CovalentData | undefined,
|
||||
classes: any
|
||||
) => {
|
||||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
const uri = getLogo(account);
|
||||
const symbol = getSymbol(account) || "Unknown";
|
||||
const content = (
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div className={classes.tokenImageContainer}>
|
||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="subtitle1">{symbol}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
{
|
||||
<Typography variant="body1">
|
||||
{account.isNativeAsset ? "Native" : mintPrettyString}
|
||||
</Typography>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body2">{"Balance"}</Typography>
|
||||
<Typography variant="h6">{account.uiAmountString}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const migrationRender = (
|
||||
<div className={classes.migrationAlert}>
|
||||
<Alert severity="warning">
|
||||
<Typography variant="body2">
|
||||
This is a legacy asset eligible for migration.
|
||||
</Typography>
|
||||
<div>{content}</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
|
||||
return isMigrationEligible(chainId, account.mintKey)
|
||||
? migrationRender
|
||||
: content;
|
||||
};
|
||||
|
||||
const renderNFTAccount = (
|
||||
account: NFTParsedTokenAccount,
|
||||
covalentData: CovalentData | undefined,
|
||||
classes: any
|
||||
) => {
|
||||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
const tokenId = account.tokenId;
|
||||
const uri = account.image_256;
|
||||
const symbol = covalentData?.contract_ticker_symbol || "Unknown";
|
||||
const name = account.name || "Unknown";
|
||||
return (
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div className={classes.tokenImageContainer}>
|
||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||
</div>
|
||||
<div>
|
||||
<Typography>{symbol}</Typography>
|
||||
<Typography>{name}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography>{mintPrettyString}</Typography>
|
||||
<Typography>{tokenId}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function EthereumSourceTokenSelector(
|
||||
props: EthereumSourceTokenSelectorProps
|
||||
) {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
covalent,
|
||||
tokenAccounts,
|
||||
disabled,
|
||||
resetAccounts,
|
||||
chainId,
|
||||
nft,
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const [advancedMode, setAdvancedMode] = useState(false);
|
||||
const [advancedModeLoading, setAdvancedModeLoading] = useState(false);
|
||||
const [advancedModeSymbol, setAdvancedModeSymbol] = useState("");
|
||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||
const [advancedModeHolderTokenIdRaw, setAdvancedModeHolderTokenId] =
|
||||
useState("");
|
||||
const [advancedModeHolderTokenId] = useDebounce(
|
||||
advancedModeHolderTokenIdRaw,
|
||||
500
|
||||
);
|
||||
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";
|
||||
|
||||
const resetAccountWrapper = useCallback(() => {
|
||||
setAdvancedModeHolderString("");
|
||||
setAutocompleteHolder(null);
|
||||
setAdvancedModeError("");
|
||||
setAutocompleteError("");
|
||||
resetAccounts && resetAccounts();
|
||||
}, [resetAccounts]);
|
||||
|
||||
useEffect(() => {
|
||||
//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);
|
||||
// @ts-ignore // TODO: could be NFTParsedTokenAccount which has a tokenId, nicer way to represent this?
|
||||
if (nft && advancedModeHolderTokenId !== value.tokenId) {
|
||||
// @ts-ignore
|
||||
setAdvancedModeHolderTokenId(value.tokenId || "");
|
||||
}
|
||||
}
|
||||
if (!advancedMode && value && !autocompleteHolder) {
|
||||
setAutocompleteHolder(value);
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
advancedMode,
|
||||
advancedModeHolderString,
|
||||
autocompleteHolder,
|
||||
nft,
|
||||
advancedModeHolderTokenId,
|
||||
]);
|
||||
|
||||
//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("");
|
||||
if (nft) {
|
||||
onChange(autocompleteHolder);
|
||||
return;
|
||||
}
|
||||
if (autocompleteHolder.isNativeAsset) {
|
||||
onChange(autocompleteHolder);
|
||||
return;
|
||||
}
|
||||
isWormholev1(provider, autocompleteHolder.mintKey, chainId).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, nft, chainId]);
|
||||
|
||||
//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)) {
|
||||
return;
|
||||
} else {
|
||||
//TODO get a bit smarter about setting & clearing errors
|
||||
if (provider === undefined || signerAddress === undefined) {
|
||||
!cancelled &&
|
||||
setAdvancedModeError("Your Ethereum wallet is no longer connected.");
|
||||
return;
|
||||
}
|
||||
!cancelled && setAdvancedModeLoading(true);
|
||||
!cancelled && setAdvancedModeError("");
|
||||
!cancelled && setAdvancedModeSymbol("");
|
||||
try {
|
||||
if (nft) {
|
||||
getEthereumNFT(advancedModeHolderString, provider)
|
||||
.then((token) => {
|
||||
isNFT(token)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
ethNFTToNFTParsedTokenAccount(
|
||||
token,
|
||||
advancedModeHolderTokenId,
|
||||
signerAddress
|
||||
)
|
||||
.then((parsedTokenAccount) => {
|
||||
!cancelled && onChange(parsedTokenAccount);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified tokenId"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
});
|
||||
} else {
|
||||
console.error("no NFT result");
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"This token does not support ERC-165, ERC-721, and ERC-721 metadata"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("isNFT", error);
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"This token does not support ERC-165, ERC-721, and ERC-721 metadata"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("getEthereumNFT", error);
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"This token does not support ERC-165, ERC-721, and ERC-721 metadata"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
});
|
||||
} else {
|
||||
//Validate that the token is not a wormhole v1 asset
|
||||
const isWormholePromise = isWormholev1(
|
||||
provider,
|
||||
advancedModeHolderString,
|
||||
chainId
|
||||
).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);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
},
|
||||
(error) => {
|
||||
//These errors can maybe be consolidated
|
||||
!cancelled &&
|
||||
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"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {}
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
!cancelled &&
|
||||
setAdvancedModeError("Failed to find the specified address");
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
advancedModeHolderString,
|
||||
advancedMode,
|
||||
provider,
|
||||
signerAddress,
|
||||
onChange,
|
||||
nft,
|
||||
advancedModeHolderTokenId,
|
||||
chainId,
|
||||
]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(null);
|
||||
setAdvancedModeHolderString("");
|
||||
setAdvancedModeHolderTokenId("");
|
||||
}, [onChange]);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(event) => setAdvancedModeHolderString(event.target.value),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleTokenIdOnChange = useCallback(
|
||||
(event) => setAdvancedModeHolderTokenId(event.target.value),
|
||||
[]
|
||||
);
|
||||
|
||||
const filterConfig = createFilterOptions({
|
||||
matchFrom: "any",
|
||||
stringify: (option: ParsedTokenAccount) => {
|
||||
const symbol = getSymbol(option) + " " || "";
|
||||
const mint = option.mintKey + " ";
|
||||
|
||||
return symbol + mint;
|
||||
},
|
||||
});
|
||||
|
||||
const filterConfigNFT = createFilterOptions({
|
||||
matchFrom: "any",
|
||||
stringify: (option: NFTParsedTokenAccount) => {
|
||||
const symbol = getSymbol(option) + " " || "";
|
||||
const mint = option.mintKey + " ";
|
||||
const name = option.name ? option.name + " " : "";
|
||||
const id = option.tokenId ? option.tokenId + " " : "";
|
||||
|
||||
return symbol + mint + name + id;
|
||||
},
|
||||
});
|
||||
|
||||
const toggleAdvancedMode = () => {
|
||||
setAdvancedModeHolderString("");
|
||||
setAdvancedModeError("");
|
||||
setAdvancedModeSymbol("");
|
||||
setAutocompleteHolder(null);
|
||||
setAutocompleteError("");
|
||||
setAdvancedMode(!advancedMode);
|
||||
};
|
||||
|
||||
const handleAutocompleteChange = useCallback(
|
||||
(event, newValue: ParsedTokenAccount | null) => {
|
||||
setAutocompleteHolder(newValue);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const tokenAccountsData = tokenAccounts?.data;
|
||||
const sortedOptions = useMemo(() => {
|
||||
const options = tokenAccountsData || [];
|
||||
options.sort(sortParsedTokenAccounts);
|
||||
return options;
|
||||
}, [tokenAccountsData]);
|
||||
|
||||
const isLoading =
|
||||
props.covalent?.isFetching || props.tokenAccounts?.isFetching;
|
||||
|
||||
const autoComplete = (
|
||||
<>
|
||||
<Autocomplete
|
||||
autoComplete
|
||||
autoHighlight
|
||||
blurOnSelect
|
||||
clearOnBlur
|
||||
fullWidth={true}
|
||||
filterOptions={nft ? filterConfigNFT : filterConfig}
|
||||
value={autocompleteHolder}
|
||||
onChange={handleAutocompleteChange}
|
||||
disabled={disabled}
|
||||
noOptionsText={
|
||||
nft
|
||||
? "No ERC-721 tokens found at the moment."
|
||||
: "No ERC-20 tokens found at the moment."
|
||||
}
|
||||
options={sortedOptions}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Token Account" variant="outlined" />
|
||||
)}
|
||||
renderOption={(option) => {
|
||||
return nft
|
||||
? renderNFTAccount(
|
||||
option,
|
||||
covalent?.data?.find(
|
||||
(x) => x.contract_address === option.mintKey
|
||||
),
|
||||
classes
|
||||
)
|
||||
: renderAccount(
|
||||
chainId,
|
||||
option,
|
||||
covalent?.data?.find(
|
||||
(x) => x.contract_address === option.mintKey
|
||||
),
|
||||
classes
|
||||
);
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
const symbol = getSymbol(option);
|
||||
return `${symbol ? symbol : "Unknown"} ${
|
||||
nft && option.name ? option.name : ""
|
||||
} (Address: ${shortenAddress(option.mintKey)}${
|
||||
nft ? `, ID: ${option.tokenId}` : ""
|
||||
})`;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const advancedModeToggleButton = (
|
||||
<OffsetButton onClick={toggleAdvancedMode} disabled={disabled}>
|
||||
{advancedMode ? "Toggle Token Picker" : "Toggle Manual Entry"}
|
||||
</OffsetButton>
|
||||
);
|
||||
|
||||
const clearButton = (
|
||||
<OffsetButton onClick={handleClick} disabled={disabled}>
|
||||
Clear
|
||||
</OffsetButton>
|
||||
);
|
||||
|
||||
const symbol = getSymbol(value) || advancedModeSymbol;
|
||||
|
||||
const content = value ? (
|
||||
<>
|
||||
{nft ? (
|
||||
<NFTViewer value={value} chainId={chainId} />
|
||||
) : (
|
||||
<RefreshButtonWrapper callback={resetAccountWrapper}>
|
||||
<Typography>
|
||||
{value.isNativeAsset
|
||||
? value.symbol
|
||||
: (symbol ? symbol + " " : "") + value.mintKey}
|
||||
</Typography>
|
||||
</RefreshButtonWrapper>
|
||||
)}
|
||||
</>
|
||||
) : advancedMode ? (
|
||||
<>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
label="Enter an asset address"
|
||||
value={advancedModeHolderString}
|
||||
onChange={handleOnChange}
|
||||
error={
|
||||
(advancedModeHolderString !== "" &&
|
||||
!isValidEthereumAddress(advancedModeHolderString)) ||
|
||||
!!advancedModeError
|
||||
}
|
||||
helperText={
|
||||
advancedModeHolderString &&
|
||||
!isValidEthereumAddress(advancedModeHolderString) &&
|
||||
"Invalid Ethereum address"
|
||||
}
|
||||
disabled={disabled || advancedModeLoading}
|
||||
/>
|
||||
{nft ? (
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
label="Enter a tokenId"
|
||||
value={advancedModeHolderTokenIdRaw}
|
||||
onChange={handleTokenIdOnChange}
|
||||
disabled={disabled || advancedModeLoading}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : isLoading ? (
|
||||
<Typography component="div">
|
||||
<CircularProgress size={"1em"} />{" "}
|
||||
{nft ? "Loading (this may take a while)..." : "Loading..."}
|
||||
</Typography>
|
||||
) : (
|
||||
<RefreshButtonWrapper callback={resetAccountWrapper}>
|
||||
{autoComplete}
|
||||
</RefreshButtonWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{content}
|
||||
{!advancedMode && autocompleteError ? (
|
||||
<Typography color="error">{autocompleteError}</Typography>
|
||||
) : advancedMode && advancedModeError ? (
|
||||
<Typography color="error">{advancedModeError}</Typography>
|
||||
) : null}
|
||||
{value ? clearButton : advancedModeToggleButton}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,393 +0,0 @@
|
|||
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
CircularProgress,
|
||||
TextField,
|
||||
Typography,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
} from "@material-ui/core";
|
||||
import { Alert, Autocomplete } from "@material-ui/lab";
|
||||
import { createFilterOptions } from "@material-ui/lab/Autocomplete";
|
||||
import { TokenInfo } from "@solana/spl-token-registry";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import useMetaplexData from "../../hooks/useMetaplexData";
|
||||
import useSolanaTokenMap from "../../hooks/useSolanaTokenMap";
|
||||
import { DataWrapper } from "../../store/helpers";
|
||||
import { NFTParsedTokenAccount } from "../../store/nftSlice";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import {
|
||||
MIGRATION_ASSET_MAP,
|
||||
WORMHOLE_V1_MINT_AUTHORITY,
|
||||
} from "../../utils/consts";
|
||||
import { ExtractedMintInfo, shortenAddress } from "../../utils/solana";
|
||||
import { sortParsedTokenAccounts } from "../../utils/sort";
|
||||
import NFTViewer from "./NFTViewer";
|
||||
import RefreshButtonWrapper from "./RefreshButtonWrapper";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
selectInput: { minWidth: "10rem" },
|
||||
tokenOverviewContainer: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
"& div": {
|
||||
margin: theme.spacing(1),
|
||||
flexBasis: "33%",
|
||||
"&$tokenImageContainer": {
|
||||
maxWidth: 40,
|
||||
},
|
||||
"&:last-child": {
|
||||
textAlign: "right",
|
||||
},
|
||||
},
|
||||
},
|
||||
tokenImageContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 40,
|
||||
},
|
||||
tokenImage: {
|
||||
maxHeight: "2.5rem", //Eyeballing this based off the text size
|
||||
},
|
||||
v1Warning: {
|
||||
width: "100%",
|
||||
},
|
||||
migrationAlert: {
|
||||
width: "100%",
|
||||
"& .MuiAlert-message": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
type SolanaSourceTokenSelectorProps = {
|
||||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: NFTParsedTokenAccount | null) => void;
|
||||
accounts: ParsedTokenAccount[];
|
||||
disabled: boolean;
|
||||
mintAccounts:
|
||||
| DataWrapper<Map<string, ExtractedMintInfo | null> | undefined>
|
||||
| undefined;
|
||||
resetAccounts: (() => void) | undefined;
|
||||
nft?: boolean;
|
||||
};
|
||||
|
||||
const getOptionSelected = (
|
||||
option: ParsedTokenAccount,
|
||||
value: ParsedTokenAccount
|
||||
) => option.mintKey === value.mintKey && option.publicKey === value.publicKey;
|
||||
|
||||
export default function SolanaSourceTokenSelector(
|
||||
props: SolanaSourceTokenSelectorProps
|
||||
) {
|
||||
const { value, onChange, disabled, resetAccounts, nft, mintAccounts } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const resetAccountWrapper = resetAccounts || (() => {}); //This should never happen.
|
||||
const solanaTokenMap = useSolanaTokenMap();
|
||||
|
||||
const mintAddresses = useMemo(() => {
|
||||
const output: string[] = [];
|
||||
mintAccounts?.data?.forEach(
|
||||
(mintAuth, mintAddress) => mintAddress && output.push(mintAddress)
|
||||
);
|
||||
return output;
|
||||
}, [mintAccounts?.data]);
|
||||
|
||||
const metaplex = useMetaplexData(mintAddresses);
|
||||
|
||||
const memoizedTokenMap: Map<String, TokenInfo> = useMemo(() => {
|
||||
const output = new Map<String, TokenInfo>();
|
||||
|
||||
if (solanaTokenMap.data) {
|
||||
for (const data of solanaTokenMap.data) {
|
||||
if (data && data.address) {
|
||||
output.set(data.address, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [solanaTokenMap]);
|
||||
|
||||
const getLogo = useCallback(
|
||||
(account: ParsedTokenAccount) => {
|
||||
return (
|
||||
(account.isNativeAsset && account.logo) ||
|
||||
memoizedTokenMap.get(account.mintKey)?.logoURI ||
|
||||
metaplex.data?.get(account.mintKey)?.data?.uri ||
|
||||
undefined
|
||||
);
|
||||
},
|
||||
[memoizedTokenMap, metaplex]
|
||||
);
|
||||
|
||||
const getSymbol = useCallback(
|
||||
(account: ParsedTokenAccount) => {
|
||||
return (
|
||||
(account.isNativeAsset && account.symbol) ||
|
||||
memoizedTokenMap.get(account.mintKey)?.symbol ||
|
||||
metaplex.data?.get(account.mintKey)?.data?.symbol ||
|
||||
undefined
|
||||
);
|
||||
},
|
||||
[memoizedTokenMap, metaplex]
|
||||
);
|
||||
|
||||
const getName = useCallback(
|
||||
(account: ParsedTokenAccount) => {
|
||||
return (
|
||||
(account.isNativeAsset && account.name) ||
|
||||
memoizedTokenMap.get(account.mintKey)?.name ||
|
||||
metaplex.data?.get(account.mintKey)?.data?.name ||
|
||||
undefined
|
||||
);
|
||||
},
|
||||
[memoizedTokenMap, metaplex]
|
||||
);
|
||||
|
||||
//I wish there was a way to make this more intelligent,
|
||||
//but the autocomplete filterConfig options seem pretty limiting.
|
||||
const filterConfig = createFilterOptions({
|
||||
matchFrom: "any",
|
||||
stringify: (option: ParsedTokenAccount) => {
|
||||
const symbol = getSymbol(option) + " " || "";
|
||||
const name = getName(option) + " " || "";
|
||||
const mint = option.mintKey + " ";
|
||||
const pubkey = option.publicKey + " ";
|
||||
|
||||
return symbol + name + mint + pubkey;
|
||||
},
|
||||
});
|
||||
|
||||
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 mintAuthority = props.mintAccounts.data.get(address)?.mintAuthority;
|
||||
|
||||
if (!mintAuthority) {
|
||||
return true; //We should never fail to pull the mint of an account.
|
||||
}
|
||||
|
||||
if (mintAuthority === 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 isMigrationEligible = useCallback((address: string) => {
|
||||
return !!MIGRATION_ASSET_MAP.get(address);
|
||||
}, []);
|
||||
|
||||
const renderAccount = useCallback(
|
||||
(account: ParsedTokenAccount) => {
|
||||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
const accountAddressPrettyString = shortenAddress(account.publicKey);
|
||||
const uri = getLogo(account);
|
||||
const symbol = getSymbol(account) || "Unknown";
|
||||
const name = getName(account) || "--";
|
||||
|
||||
const content = (
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div className={classes.tokenImageContainer}>
|
||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="subtitle1">{symbol}</Typography>
|
||||
<Typography variant="subtitle2">{name}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
{account.isNativeAsset ? (
|
||||
<Typography>{"Native"}</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
{"Mint : " + mintPrettyString}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{"Account :" + accountAddressPrettyString}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{nft ? null : (
|
||||
<div>
|
||||
<Typography variant="body2">{"Balance"}</Typography>
|
||||
<Typography variant="h6">{account.uiAmountString}</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const v1Warning = (
|
||||
<div className={classes.v1Warning}>
|
||||
<Typography variant="body2">
|
||||
Wormhole v1 tokens are not eligible for transfer.
|
||||
</Typography>
|
||||
<div>{content}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const migrationRender = (
|
||||
<div className={classes.migrationAlert}>
|
||||
<Alert severity="warning" variant="outlined">
|
||||
<Typography variant="body2">
|
||||
This is a legacy asset eligible for migration.
|
||||
</Typography>
|
||||
<div>{content}</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
|
||||
return isMigrationEligible(account.mintKey)
|
||||
? migrationRender
|
||||
: isWormholev1(account.mintKey)
|
||||
? v1Warning
|
||||
: content;
|
||||
},
|
||||
[
|
||||
getLogo,
|
||||
getSymbol,
|
||||
getName,
|
||||
classes,
|
||||
isWormholev1,
|
||||
isMigrationEligible,
|
||||
nft,
|
||||
]
|
||||
);
|
||||
|
||||
//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 =
|
||||
metaplex.isFetching ||
|
||||
solanaTokenMap.isFetching ||
|
||||
props.mintAccounts?.isFetching;
|
||||
|
||||
const accountLoadError =
|
||||
props.mintAccounts?.error && "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.
|
||||
const filteredOptions = useMemo(() => {
|
||||
const tokenList = props.accounts
|
||||
.filter((x) => {
|
||||
const zeroBalance = x.amount === "0";
|
||||
if (zeroBalance) {
|
||||
return false;
|
||||
}
|
||||
const isNFT =
|
||||
x.decimals === 0 &&
|
||||
metaplex.data?.get(x.mintKey)?.data?.uri &&
|
||||
mintAccounts?.data?.get(x.mintKey)?.supply === "1";
|
||||
return nft ? isNFT : !isNFT;
|
||||
})
|
||||
.map((account) => ({
|
||||
...account,
|
||||
symbol: account.symbol || getSymbol(account) || undefined,
|
||||
}));
|
||||
tokenList.sort(sortParsedTokenAccounts);
|
||||
return tokenList;
|
||||
}, [mintAccounts?.data, metaplex.data, nft, props.accounts, getSymbol]);
|
||||
|
||||
const isOptionDisabled = useMemo(() => {
|
||||
return (value: ParsedTokenAccount) => {
|
||||
return isWormholev1(value.mintKey) && !isMigrationEligible(value.mintKey);
|
||||
};
|
||||
}, [isWormholev1, isMigrationEligible]);
|
||||
|
||||
const onAutocompleteChange = useCallback(
|
||||
(event, newValue: NFTParsedTokenAccount | null) => {
|
||||
if (!newValue) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
const symbol = getSymbol(newValue);
|
||||
const name = getName(newValue);
|
||||
const logo = getLogo(newValue);
|
||||
// TODO: more nft data
|
||||
|
||||
onChange({
|
||||
...newValue,
|
||||
symbol,
|
||||
name,
|
||||
logo: nft ? undefined : logo,
|
||||
uri: nft ? logo : undefined,
|
||||
});
|
||||
},
|
||||
[getSymbol, getName, getLogo, onChange, nft]
|
||||
);
|
||||
|
||||
const renderInput = useCallback(
|
||||
(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={nft ? "NFT Account" : "Token Account"}
|
||||
variant="outlined"
|
||||
/>
|
||||
),
|
||||
[nft]
|
||||
);
|
||||
|
||||
const getOptionLabel = useCallback(
|
||||
(option: ParsedTokenAccount) => {
|
||||
const symbol = getSymbol(option);
|
||||
const label = option.isNativeAsset
|
||||
? symbol || "" //default for non-null guarantee
|
||||
: `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress(
|
||||
option.publicKey
|
||||
)}, Mint: ${shortenAddress(option.mintKey)})`;
|
||||
|
||||
return label;
|
||||
},
|
||||
[getSymbol]
|
||||
);
|
||||
|
||||
const autoComplete = (
|
||||
<Autocomplete
|
||||
autoComplete
|
||||
autoHighlight
|
||||
blurOnSelect
|
||||
clearOnBlur
|
||||
fullWidth={false}
|
||||
filterOptions={filterConfig}
|
||||
value={value}
|
||||
onChange={onAutocompleteChange}
|
||||
disabled={disabled}
|
||||
options={filteredOptions}
|
||||
renderInput={renderInput}
|
||||
renderOption={renderAccount}
|
||||
getOptionDisabled={isOptionDisabled}
|
||||
getOptionLabel={getOptionLabel}
|
||||
getOptionSelected={getOptionSelected}
|
||||
/>
|
||||
);
|
||||
|
||||
const wrappedContent = (
|
||||
<RefreshButtonWrapper callback={resetAccountWrapper}>
|
||||
{autoComplete}
|
||||
</RefreshButtonWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isLoading ? <CircularProgress /> : wrappedContent}
|
||||
{error && <Typography color="error">{error}</Typography>}
|
||||
{nft && value ? (
|
||||
<NFTViewer value={value} chainId={CHAIN_ID_SOLANA} />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -112,10 +112,10 @@ export default function SolanaSourceTokenSelector(
|
|||
return false;
|
||||
}
|
||||
const isNFT =
|
||||
x.decimals === 0 &&
|
||||
metaplex.data?.get(x.mintKey)?.data?.uri &&
|
||||
mintAccounts?.data?.get(x.mintKey)?.supply === "1";
|
||||
return nft ? isNFT : !isNFT;
|
||||
x.decimals === 0 && metaplex.data?.get(x.mintKey)?.data?.uri;
|
||||
const is721CompatibleNFT =
|
||||
isNFT && mintAccounts?.data?.get(x.mintKey)?.supply === "1";
|
||||
return nft ? is721CompatibleNFT : !isNFT;
|
||||
});
|
||||
tokenList.sort(sortParsedTokenAccounts);
|
||||
return tokenList;
|
||||
|
|
|
@ -1,351 +0,0 @@
|
|||
import { isNativeDenom } from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
CircularProgress,
|
||||
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, useMemo, useRef, useState } from "react";
|
||||
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import useTerraNativeBalances from "../../hooks/useTerraNativeBalances";
|
||||
import { TerraTokenMetadata } from "../../hooks/useTerraTokenMap";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import { SUPPORTED_TERRA_TOKENS, TERRA_HOST } from "../../utils/consts";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
import {
|
||||
formatNativeDenom,
|
||||
formatTerraNativeBalance,
|
||||
getNativeTerraIcon,
|
||||
NATIVE_TERRA_DECIMALS,
|
||||
} from "../../utils/terra";
|
||||
import OffsetButton from "./OffsetButton";
|
||||
import RefreshButtonWrapper from "./RefreshButtonWrapper";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
selectInput: { minWidth: "10rem" },
|
||||
tokenOverviewContainer: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
"& div": {
|
||||
margin: theme.spacing(1),
|
||||
"&$tokenImageContainer": {
|
||||
maxWidth: 40,
|
||||
},
|
||||
},
|
||||
},
|
||||
tokenImageContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 40,
|
||||
},
|
||||
tokenImage: {
|
||||
maxHeight: "2.5rem", //Eyeballing this based off the text size
|
||||
},
|
||||
tokenSymbolContainer: {
|
||||
flexBasis: 112,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
type TerraSourceTokenSelectorProps = {
|
||||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||
disabled: boolean;
|
||||
resetAccounts: (() => void) | undefined;
|
||||
};
|
||||
|
||||
//TODO move elsewhere
|
||||
//TODO async
|
||||
const lookupTerraAddress = (
|
||||
lookupAsset: string,
|
||||
terraWallet: ConnectedWallet
|
||||
) => {
|
||||
const lcd = new LCDClient(TERRA_HOST);
|
||||
return lcd.wasm
|
||||
.contractQuery(lookupAsset, {
|
||||
token_info: {},
|
||||
})
|
||||
.then((info: any) =>
|
||||
lcd.wasm
|
||||
.contractQuery(lookupAsset, {
|
||||
balance: {
|
||||
address: terraWallet.walletAddress,
|
||||
},
|
||||
})
|
||||
.then((balance: any) => {
|
||||
if (balance && info) {
|
||||
return createParsedTokenAccount(
|
||||
terraWallet.walletAddress,
|
||||
lookupAsset,
|
||||
balance.balance.toString(),
|
||||
info.decimals,
|
||||
Number(formatUnits(balance.balance, info.decimals)),
|
||||
formatUnits(balance.balance, info.decimals)
|
||||
);
|
||||
} else {
|
||||
throw new Error("Failed to retrieve Terra account.");
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch(() => {
|
||||
return Promise.reject();
|
||||
});
|
||||
};
|
||||
|
||||
export default function TerraSourceTokenSelector(
|
||||
props: TerraSourceTokenSelectorProps
|
||||
) {
|
||||
const classes = useStyles();
|
||||
const { onChange, value, disabled, resetAccounts } = props;
|
||||
// const tokenMap = useTerraTokenMap();
|
||||
const [advancedMode, setAdvancedMode] = useState(false);
|
||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||
const terraWallet = useConnectedWallet();
|
||||
|
||||
const [autocompleteString, setAutocompleteString] = useState("");
|
||||
|
||||
const handleAutocompleteChange = useCallback(
|
||||
(event) => {
|
||||
setAutocompleteString(event?.target?.value);
|
||||
},
|
||||
[setAutocompleteString]
|
||||
);
|
||||
|
||||
const nativeRefresh = useRef<() => void>(() => {});
|
||||
|
||||
const { balances, isLoading: nativeIsLoading } = useTerraNativeBalances(
|
||||
terraWallet?.walletAddress,
|
||||
nativeRefresh
|
||||
);
|
||||
|
||||
const resetAccountWrapper = useCallback(() => {
|
||||
setAdvancedModeHolderString("");
|
||||
setAdvancedModeError("");
|
||||
setAutocompleteString("");
|
||||
resetAccounts && resetAccounts();
|
||||
nativeRefresh.current();
|
||||
}, [resetAccounts]);
|
||||
|
||||
const isLoading = nativeIsLoading; // || (tokenMap?.isFetching || false);
|
||||
|
||||
const terraTokenArray = useMemo(() => {
|
||||
const balancesItems = balances
|
||||
? Object.keys(balances).map(
|
||||
(denom) =>
|
||||
({
|
||||
protocol: "native",
|
||||
symbol: formatNativeDenom(denom),
|
||||
token: denom,
|
||||
icon: getNativeTerraIcon(formatNativeDenom(denom)),
|
||||
balance: balances[denom],
|
||||
} as TerraTokenMetadata)
|
||||
)
|
||||
: [];
|
||||
return balancesItems.filter((metadata) =>
|
||||
SUPPORTED_TERRA_TOKENS.includes(metadata.token)
|
||||
);
|
||||
// const values = tokenMap.data?.mainnet;
|
||||
// const tokenMapItems = Object.values(values || {}) || [];
|
||||
// return [...balancesItems, ...tokenMapItems];
|
||||
}, [
|
||||
balances,
|
||||
// tokenMap
|
||||
]);
|
||||
|
||||
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
||||
if (!fromProps) return null;
|
||||
else {
|
||||
return terraTokenArray.find((x) => x.token === fromProps.mintKey);
|
||||
}
|
||||
};
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(null);
|
||||
setAdvancedModeHolderString("");
|
||||
}, [onChange]);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(event) => setAdvancedModeHolderString(event?.target?.value),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleConfirm = (address: string | undefined) => {
|
||||
if (terraWallet === undefined || address === undefined) {
|
||||
setAdvancedModeError("Terra wallet not connected.");
|
||||
return;
|
||||
}
|
||||
setAdvancedModeError("");
|
||||
if (isNativeDenom(address)) {
|
||||
if (balances && balances[address]) {
|
||||
onChange(
|
||||
createParsedTokenAccount(
|
||||
terraWallet.walletAddress,
|
||||
address,
|
||||
balances[address],
|
||||
NATIVE_TERRA_DECIMALS,
|
||||
Number(formatUnits(balances[address], NATIVE_TERRA_DECIMALS)),
|
||||
formatUnits(balances[address], NATIVE_TERRA_DECIMALS),
|
||||
formatNativeDenom(address)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setAdvancedModeError("Unable to retrieve that address.");
|
||||
}
|
||||
} else {
|
||||
lookupTerraAddress(address, terraWallet).then(
|
||||
(result) => {
|
||||
onChange(result);
|
||||
},
|
||||
(error) => {
|
||||
setAdvancedModeError("Unable to retrieve that address.");
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 className={classes.tokenImageContainer}>
|
||||
<img alt="" className={classes.tokenImage} src={option.icon} />
|
||||
</div>
|
||||
<div className={classes.tokenSymbolContainer}>
|
||||
<Typography variant="h6">{option.symbol}</Typography>
|
||||
<Typography variant="body2">{option.protocol}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body1">{option.token}</Typography>
|
||||
{option.balance ? (
|
||||
<Typography variant="h6">
|
||||
{formatTerraNativeBalance(option.balance)}
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAdvancedMode = () => {
|
||||
setAdvancedMode(!advancedMode);
|
||||
setAdvancedModeError("");
|
||||
};
|
||||
|
||||
const advancedModeToggleButton = (
|
||||
<OffsetButton onClick={toggleAdvancedMode} disabled={disabled}>
|
||||
{advancedMode ? "Toggle Token Picker" : "Toggle Manual Entry"}
|
||||
</OffsetButton>
|
||||
);
|
||||
|
||||
const selectedValue = valueToOption(value);
|
||||
|
||||
const autoComplete = (
|
||||
<>
|
||||
<Autocomplete
|
||||
autoComplete
|
||||
autoHighlight
|
||||
blurOnSelect
|
||||
clearOnBlur
|
||||
fullWidth={false}
|
||||
filterOptions={filterConfig}
|
||||
value={selectedValue}
|
||||
onChange={(event, newValue) => {
|
||||
handleConfirm(newValue?.token);
|
||||
}}
|
||||
inputValue={autocompleteString}
|
||||
onInputChange={handleAutocompleteChange}
|
||||
disabled={disabled}
|
||||
noOptionsText={"No CW20 tokens found at the moment."}
|
||||
options={terraTokenArray}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Token" variant="outlined" />
|
||||
)}
|
||||
renderOption={renderOption}
|
||||
getOptionLabel={renderOptionLabel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const clearButton = (
|
||||
<OffsetButton onClick={handleClick} disabled={disabled}>
|
||||
Clear
|
||||
</OffsetButton>
|
||||
);
|
||||
|
||||
const content = value ? (
|
||||
<>
|
||||
<Typography>{value.mintKey}</Typography>
|
||||
</>
|
||||
) : !advancedMode ? (
|
||||
autoComplete
|
||||
) : (
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
label="Enter an asset address"
|
||||
value={advancedModeHolderString}
|
||||
onChange={handleOnChange}
|
||||
disabled={disabled}
|
||||
error={advancedModeHolderString !== "" && !!advancedModeError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const wrappedContent = (
|
||||
<RefreshButtonWrapper callback={resetAccountWrapper}>
|
||||
{content}
|
||||
</RefreshButtonWrapper>
|
||||
);
|
||||
|
||||
const confirmButton = (
|
||||
<OffsetButton
|
||||
onClick={() => handleConfirm(advancedModeHolderString)}
|
||||
disabled={disabled}
|
||||
>
|
||||
Confirm
|
||||
</OffsetButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isLoading && !value && !advancedMode ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
wrappedContent
|
||||
)}
|
||||
{advancedModeError && (
|
||||
<Typography color="error">{advancedModeError}</Typography>
|
||||
)}
|
||||
<div>
|
||||
{advancedMode && !value && confirmButton}
|
||||
{!value && !isLoading && advancedModeToggleButton}
|
||||
{value && clearButton}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue