bridge_ui: added metadata hooks
Change-Id: Ia256ea5b84b6dc21238b4808d607c253b4e9dc87
This commit is contained in:
parent
d6640e2b3d
commit
b550a7c47b
|
@ -4,6 +4,8 @@ import { Autocomplete } from "@material-ui/lab";
|
||||||
import { createFilterOptions } from "@material-ui/lab/Autocomplete";
|
import { createFilterOptions } from "@material-ui/lab/Autocomplete";
|
||||||
import { TokenInfo } from "@solana/spl-token-registry";
|
import { TokenInfo } from "@solana/spl-token-registry";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
|
import useMetaplexData from "../../hooks/useMetaplexData";
|
||||||
|
import useSolanaTokenMap from "../../hooks/useSolanaTokenMap";
|
||||||
import { DataWrapper } from "../../store/helpers";
|
import { DataWrapper } from "../../store/helpers";
|
||||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||||
import { WORMHOLE_V1_MINT_AUTHORITY } from "../../utils/consts";
|
import { WORMHOLE_V1_MINT_AUTHORITY } from "../../utils/consts";
|
||||||
|
@ -30,10 +32,8 @@ type SolanaSourceTokenSelectorProps = {
|
||||||
value: ParsedTokenAccount | null;
|
value: ParsedTokenAccount | null;
|
||||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||||
accounts: ParsedTokenAccount[];
|
accounts: ParsedTokenAccount[];
|
||||||
solanaTokenMap: DataWrapper<TokenInfo[]> | undefined;
|
|
||||||
metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null;
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
mintAccounts: DataWrapper<Map<String, string | null>> | undefined;
|
mintAccounts: DataWrapper<Map<string, string | null>> | undefined;
|
||||||
resetAccounts: (() => void) | undefined;
|
resetAccounts: (() => void) | undefined;
|
||||||
nft?: boolean;
|
nft?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -41,16 +41,27 @@ type SolanaSourceTokenSelectorProps = {
|
||||||
export default function SolanaSourceTokenSelector(
|
export default function SolanaSourceTokenSelector(
|
||||||
props: SolanaSourceTokenSelectorProps
|
props: SolanaSourceTokenSelectorProps
|
||||||
) {
|
) {
|
||||||
const { value, onChange, disabled, resetAccounts, nft } = props;
|
const { value, onChange, disabled, resetAccounts, nft, mintAccounts } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
const resetAccountWrapper = resetAccounts || (() => {}); //This should never happen.
|
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 memoizedTokenMap: Map<String, TokenInfo> = useMemo(() => {
|
||||||
const output = new Map<String, TokenInfo>();
|
const output = new Map<String, TokenInfo>();
|
||||||
|
|
||||||
if (props.solanaTokenMap?.data) {
|
if (solanaTokenMap.data) {
|
||||||
for (const data of props.solanaTokenMap.data) {
|
for (const data of solanaTokenMap.data) {
|
||||||
if (data && data.address) {
|
if (data && data.address) {
|
||||||
output.set(data.address, data);
|
output.set(data.address, data);
|
||||||
}
|
}
|
||||||
|
@ -58,26 +69,12 @@ export default function SolanaSourceTokenSelector(
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}, [props.solanaTokenMap]);
|
}, [solanaTokenMap]);
|
||||||
|
|
||||||
const memoizedMetaplex: Map<String, Metadata> = useMemo(() => {
|
|
||||||
const output = new Map<String, Metadata>();
|
|
||||||
|
|
||||||
if (props.metaplexData.data) {
|
|
||||||
for (const data of props.metaplexData.data) {
|
|
||||||
if (data && data.mint) {
|
|
||||||
output.set(data.mint, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}, [props.metaplexData]);
|
|
||||||
|
|
||||||
const getSymbol = (account: ParsedTokenAccount) => {
|
const getSymbol = (account: ParsedTokenAccount) => {
|
||||||
return (
|
return (
|
||||||
memoizedTokenMap.get(account.mintKey)?.symbol ||
|
memoizedTokenMap.get(account.mintKey)?.symbol ||
|
||||||
memoizedMetaplex.get(account.mintKey)?.data?.symbol ||
|
metaplex.data?.get(account.mintKey)?.data?.symbol ||
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -85,7 +82,7 @@ export default function SolanaSourceTokenSelector(
|
||||||
const getName = (account: ParsedTokenAccount) => {
|
const getName = (account: ParsedTokenAccount) => {
|
||||||
return (
|
return (
|
||||||
memoizedTokenMap.get(account.mintKey)?.name ||
|
memoizedTokenMap.get(account.mintKey)?.name ||
|
||||||
memoizedMetaplex.get(account.mintKey)?.data?.name ||
|
metaplex.data?.get(account.mintKey)?.data?.name ||
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -130,11 +127,11 @@ export default function SolanaSourceTokenSelector(
|
||||||
const renderAccount = (
|
const renderAccount = (
|
||||||
account: ParsedTokenAccount,
|
account: ParsedTokenAccount,
|
||||||
solanaTokenMap: Map<String, TokenInfo>,
|
solanaTokenMap: Map<String, TokenInfo>,
|
||||||
metaplexData: Map<String, Metadata>,
|
metaplexData: Map<String, Metadata | undefined> | null | undefined,
|
||||||
classes: any
|
classes: any
|
||||||
) => {
|
) => {
|
||||||
const tokenMapData = solanaTokenMap.get(account.mintKey);
|
const tokenMapData = solanaTokenMap.get(account.mintKey);
|
||||||
const metaplexValue = metaplexData.get(account.mintKey);
|
const metaplexValue = metaplexData?.get(account.mintKey);
|
||||||
|
|
||||||
const mintPrettyString = shortenAddress(account.mintKey);
|
const mintPrettyString = shortenAddress(account.mintKey);
|
||||||
const accountAddressPrettyString = shortenAddress(account.publicKey);
|
const accountAddressPrettyString = shortenAddress(account.publicKey);
|
||||||
|
@ -185,13 +182,12 @@ export default function SolanaSourceTokenSelector(
|
||||||
//Thus we should wait for the metadata to arrive before rendering it.
|
//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.
|
//TODO This can flicker dependent on how fast the useEffects in the getSourceAccounts hook complete.
|
||||||
const isLoading =
|
const isLoading =
|
||||||
props.metaplexData.isFetching ||
|
metaplex.isFetching ||
|
||||||
props.solanaTokenMap?.isFetching ||
|
solanaTokenMap.isFetching ||
|
||||||
props.mintAccounts?.isFetching;
|
props.mintAccounts?.isFetching;
|
||||||
|
|
||||||
const accountLoadError =
|
const accountLoadError =
|
||||||
!(props.mintAccounts?.isFetching || props.mintAccounts?.data) &&
|
props.mintAccounts?.error && "Unable to retrieve your token accounts";
|
||||||
"Unable to retrieve your token accounts";
|
|
||||||
const error = accountLoadError;
|
const error = accountLoadError;
|
||||||
|
|
||||||
//This exists to remove NFTs from the list of potential options. It requires reading the metaplex data, so it would be
|
//This exists to remove NFTs from the list of potential options. It requires reading the metaplex data, so it would be
|
||||||
|
@ -200,10 +196,10 @@ export default function SolanaSourceTokenSelector(
|
||||||
return props.accounts.filter((x) => {
|
return props.accounts.filter((x) => {
|
||||||
//TODO, do a better check which likely involves supply or checking masterEdition.
|
//TODO, do a better check which likely involves supply or checking masterEdition.
|
||||||
const isNFT =
|
const isNFT =
|
||||||
x.decimals === 0 && memoizedMetaplex.get(x.mintKey)?.data?.uri;
|
x.decimals === 0 && metaplex.data?.get(x.mintKey)?.data?.uri;
|
||||||
return nft ? isNFT : !isNFT;
|
return nft ? isNFT : !isNFT;
|
||||||
});
|
});
|
||||||
}, [memoizedMetaplex, nft, props.accounts]);
|
}, [metaplex.data, nft, props.accounts]);
|
||||||
|
|
||||||
const isOptionDisabled = useMemo(() => {
|
const isOptionDisabled = useMemo(() => {
|
||||||
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
||||||
|
@ -232,12 +228,7 @@ export default function SolanaSourceTokenSelector(
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderOption={(option) => {
|
renderOption={(option) => {
|
||||||
return renderAccount(
|
return renderAccount(option, memoizedTokenMap, metaplex.data, classes);
|
||||||
option,
|
|
||||||
memoizedTokenMap,
|
|
||||||
memoizedMetaplex,
|
|
||||||
classes
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
getOptionDisabled={isOptionDisabled}
|
getOptionDisabled={isOptionDisabled}
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
|
|
|
@ -88,8 +88,6 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
accounts={maps?.tokenAccounts?.data || []}
|
accounts={maps?.tokenAccounts?.data || []}
|
||||||
solanaTokenMap={maps?.tokenMap}
|
|
||||||
metaplexData={maps?.metaplex}
|
|
||||||
mintAccounts={maps?.mintAccounts}
|
mintAccounts={maps?.mintAccounts}
|
||||||
resetAccounts={maps?.resetAccounts}
|
resetAccounts={maps?.resetAccounts}
|
||||||
nft={nft}
|
nft={nft}
|
||||||
|
@ -109,7 +107,6 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
value={sourceParsedTokenAccount || null}
|
value={sourceParsedTokenAccount || null}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
tokenMap={maps?.terraTokenMap}
|
|
||||||
resetAccounts={maps?.resetAccounts}
|
resetAccounts={maps?.resetAccounts}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -13,12 +13,10 @@ import {
|
||||||
} from "@terra-money/wallet-provider";
|
} from "@terra-money/wallet-provider";
|
||||||
import { formatUnits } from "ethers/lib/utils";
|
import { formatUnits } from "ethers/lib/utils";
|
||||||
import React, { useCallback, useMemo, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||||
createParsedTokenAccount,
|
import useTerraTokenMap, {
|
||||||
TerraTokenMap,
|
|
||||||
TerraTokenMetadata,
|
TerraTokenMetadata,
|
||||||
} from "../../hooks/useGetSourceParsedTokenAccounts";
|
} from "../../hooks/useTerraTokenMap";
|
||||||
import { DataWrapper } from "../../store/helpers";
|
|
||||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||||
import { TERRA_HOST } from "../../utils/consts";
|
import { TERRA_HOST } from "../../utils/consts";
|
||||||
import { shortenAddress } from "../../utils/solana";
|
import { shortenAddress } from "../../utils/solana";
|
||||||
|
@ -44,7 +42,6 @@ type TerraSourceTokenSelectorProps = {
|
||||||
value: ParsedTokenAccount | null;
|
value: ParsedTokenAccount | null;
|
||||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
tokenMap: DataWrapper<TerraTokenMap> | undefined; //TODO better type
|
|
||||||
resetAccounts: (() => void) | undefined;
|
resetAccounts: (() => void) | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,7 +87,8 @@ export default function TerraSourceTokenSelector(
|
||||||
props: TerraSourceTokenSelectorProps
|
props: TerraSourceTokenSelectorProps
|
||||||
) {
|
) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { onChange, value, disabled, tokenMap, resetAccounts } = props;
|
const { onChange, value, disabled, resetAccounts } = props;
|
||||||
|
const tokenMap = useTerraTokenMap();
|
||||||
const [advancedMode, setAdvancedMode] = useState(false);
|
const [advancedMode, setAdvancedMode] = useState(false);
|
||||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||||
|
@ -115,10 +113,10 @@ export default function TerraSourceTokenSelector(
|
||||||
const isLoading = tokenMap?.isFetching || false;
|
const isLoading = tokenMap?.isFetching || false;
|
||||||
|
|
||||||
const terraTokenArray = useMemo(() => {
|
const terraTokenArray = useMemo(() => {
|
||||||
const values = props.tokenMap?.data?.mainnet;
|
const values = tokenMap.data?.mainnet;
|
||||||
const items = Object.values(values || {});
|
const items = Object.values(values || {});
|
||||||
return items || [];
|
return items || [];
|
||||||
}, [props.tokenMap]);
|
}, [tokenMap]);
|
||||||
|
|
||||||
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
||||||
if (!fromProps) return null;
|
if (!fromProps) return null;
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import { Dispatch } from "@reduxjs/toolkit";
|
import { Dispatch } from "@reduxjs/toolkit";
|
||||||
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||||
import { ENV, TokenListProvider } from "@solana/spl-token-registry";
|
|
||||||
import {
|
import {
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
Connection,
|
Connection,
|
||||||
|
@ -18,7 +17,6 @@ import { useCallback, useEffect, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import { DataWrapper } from "../store/helpers";
|
|
||||||
import {
|
import {
|
||||||
errorSourceParsedTokenAccounts as errorSourceParsedTokenAccountsNFT,
|
errorSourceParsedTokenAccounts as errorSourceParsedTokenAccountsNFT,
|
||||||
fetchSourceParsedTokenAccounts as fetchSourceParsedTokenAccountsNFT,
|
fetchSourceParsedTokenAccounts as fetchSourceParsedTokenAccountsNFT,
|
||||||
|
@ -32,20 +30,10 @@ import {
|
||||||
selectNFTSourceChain,
|
selectNFTSourceChain,
|
||||||
selectNFTSourceParsedTokenAccounts,
|
selectNFTSourceParsedTokenAccounts,
|
||||||
selectNFTSourceWalletAddress,
|
selectNFTSourceWalletAddress,
|
||||||
selectSolanaTokenMap,
|
|
||||||
selectSourceWalletAddress,
|
selectSourceWalletAddress,
|
||||||
selectTerraTokenMap,
|
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferSourceParsedTokenAccounts,
|
selectTransferSourceParsedTokenAccounts,
|
||||||
} from "../store/selectors";
|
} from "../store/selectors";
|
||||||
import {
|
|
||||||
errorSolanaTokenMap,
|
|
||||||
errorTerraTokenMap,
|
|
||||||
fetchSolanaTokenMap,
|
|
||||||
fetchTerraTokenMap,
|
|
||||||
receiveSolanaTokenMap,
|
|
||||||
receiveTerraTokenMap,
|
|
||||||
} from "../store/tokenSlice";
|
|
||||||
import {
|
import {
|
||||||
errorSourceParsedTokenAccounts,
|
errorSourceParsedTokenAccounts,
|
||||||
fetchSourceParsedTokenAccounts,
|
fetchSourceParsedTokenAccounts,
|
||||||
|
@ -56,17 +44,7 @@ import {
|
||||||
setSourceParsedTokenAccounts,
|
setSourceParsedTokenAccounts,
|
||||||
setSourceWalletAddress,
|
setSourceWalletAddress,
|
||||||
} from "../store/transferSlice";
|
} from "../store/transferSlice";
|
||||||
import {
|
import { COVALENT_GET_TOKENS_URL, SOLANA_HOST } from "../utils/consts";
|
||||||
CLUSTER,
|
|
||||||
COVALENT_GET_TOKENS_URL,
|
|
||||||
SOLANA_HOST,
|
|
||||||
TERRA_TOKEN_METADATA_URL,
|
|
||||||
} from "../utils/consts";
|
|
||||||
import {
|
|
||||||
decodeMetadata,
|
|
||||||
getMetadataAddress,
|
|
||||||
Metadata,
|
|
||||||
} from "../utils/metaplex";
|
|
||||||
import {
|
import {
|
||||||
extractMintAuthorityInfo,
|
extractMintAuthorityInfo,
|
||||||
getMultipleAccountsRPC,
|
getMultipleAccountsRPC,
|
||||||
|
@ -120,19 +98,6 @@ export function createNFTParsedTokenAccount(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TerraTokenMetadata = {
|
|
||||||
protocol: string;
|
|
||||||
symbol: string;
|
|
||||||
token: string;
|
|
||||||
icon: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TerraTokenMap = {
|
|
||||||
mainnet: {
|
|
||||||
[address: string]: TerraTokenMetadata;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createParsedTokenAccountFromInfo = (
|
const createParsedTokenAccountFromInfo = (
|
||||||
pubkey: PublicKey,
|
pubkey: PublicKey,
|
||||||
item: AccountInfo<ParsedAccountData>
|
item: AccountInfo<ParsedAccountData>
|
||||||
|
@ -158,6 +123,9 @@ const createParsedTokenAccountFromCovalent = (
|
||||||
decimals: covalent.contract_decimals,
|
decimals: covalent.contract_decimals,
|
||||||
uiAmount: Number(formatUnits(covalent.balance, covalent.contract_decimals)),
|
uiAmount: Number(formatUnits(covalent.balance, covalent.contract_decimals)),
|
||||||
uiAmountString: formatUnits(covalent.balance, covalent.contract_decimals),
|
uiAmountString: formatUnits(covalent.balance, covalent.contract_decimals),
|
||||||
|
symbol: covalent.contract_ticker_symbol,
|
||||||
|
name: covalent.contract_name,
|
||||||
|
logo: covalent.logo_url,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -243,46 +211,10 @@ const getEthereumAccountsCovalent = async (
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
|
||||||
return Promise.reject("Unable to retrieve your Ethereum Tokens.");
|
return Promise.reject("Unable to retrieve your Ethereum Tokens.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const environment = CLUSTER === "testnet" ? ENV.Testnet : ENV.MainnetBeta;
|
|
||||||
|
|
||||||
const getMetaplexData = async (mintAddresses: string[]) => {
|
|
||||||
const promises = [];
|
|
||||||
for (const address of mintAddresses) {
|
|
||||||
promises.push(getMetadataAddress(address));
|
|
||||||
}
|
|
||||||
const metaAddresses = await Promise.all(promises);
|
|
||||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
|
||||||
const results = await getMultipleAccountsRPC(
|
|
||||||
connection,
|
|
||||||
metaAddresses.map((pair) => pair && pair[0])
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = results.map((account) => {
|
|
||||||
if (account === null) {
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
if (account.data) {
|
|
||||||
try {
|
|
||||||
const MetadataParsed = decodeMetadata(account.data);
|
|
||||||
return MetadataParsed;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSolanaParsedTokenAccounts = (
|
const getSolanaParsedTokenAccounts = (
|
||||||
walletAddress: string,
|
walletAddress: string,
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
|
@ -317,20 +249,6 @@ const getSolanaParsedTokenAccounts = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSolanaTokenMap = (dispatch: Dispatch) => {
|
|
||||||
dispatch(fetchSolanaTokenMap());
|
|
||||||
|
|
||||||
new TokenListProvider().resolve().then(
|
|
||||||
(tokens) => {
|
|
||||||
const tokenList = tokens.filterByChainId(environment).getList();
|
|
||||||
dispatch(receiveSolanaTokenMap(tokenList));
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
console.error(error);
|
|
||||||
dispatch(errorSolanaTokenMap("Failed to retrieve the Solana token map."));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Fetches the balance of an asset for the connected wallet
|
* Fetches the balance of an asset for the connected wallet
|
||||||
* This should handle every type of chain in the future, but only reads the Transfer state.
|
* This should handle every type of chain in the future, but only reads the Transfer state.
|
||||||
|
@ -343,21 +261,14 @@ function useGetAvailableTokens(nft: boolean = false) {
|
||||||
? selectNFTSourceParsedTokenAccounts
|
? selectNFTSourceParsedTokenAccounts
|
||||||
: selectTransferSourceParsedTokenAccounts
|
: selectTransferSourceParsedTokenAccounts
|
||||||
);
|
);
|
||||||
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
|
||||||
const terraTokenMap = useSelector(selectTerraTokenMap);
|
|
||||||
|
|
||||||
const lookupChain = useSelector(
|
const lookupChain = useSelector(
|
||||||
nft ? selectNFTSourceChain : selectTransferSourceChain
|
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||||
);
|
);
|
||||||
const solanaWallet = useSolanaWallet();
|
const solanaWallet = useSolanaWallet();
|
||||||
const solPK = solanaWallet?.publicKey;
|
const solPK = solanaWallet?.publicKey;
|
||||||
//const terraWallet = useConnectedWallet(); //TODO
|
|
||||||
const { provider, signerAddress } = useEthereumProvider();
|
const { provider, signerAddress } = useEthereumProvider();
|
||||||
|
|
||||||
const [metaplex, setMetaplex] = useState<any>(undefined);
|
|
||||||
const [metaplexLoading, setMetaplexLoading] = useState(false);
|
|
||||||
const [metaplexError, setMetaplexError] = useState(null);
|
|
||||||
|
|
||||||
const [covalent, setCovalent] = useState<any>(undefined);
|
const [covalent, setCovalent] = useState<any>(undefined);
|
||||||
const [covalentLoading, setCovalentLoading] = useState(false);
|
const [covalentLoading, setCovalentLoading] = useState(false);
|
||||||
const [covalentError, setCovalentError] = useState<string | undefined>(
|
const [covalentError, setCovalentError] = useState<string | undefined>(
|
||||||
|
@ -422,39 +333,7 @@ function useGetAvailableTokens(nft: boolean = false) {
|
||||||
resetSourceAccounts,
|
resetSourceAccounts,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Solana metaplex load
|
//Solana accountinfos load
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
if (tokenAccounts.data && lookupChain === CHAIN_ID_SOLANA) {
|
|
||||||
setMetaplexLoading(true);
|
|
||||||
const accounts = tokenAccounts.data.map((account) => account.mintKey);
|
|
||||||
accounts.filter((x) => !!x);
|
|
||||||
getMetaplexData(accounts as string[]).then(
|
|
||||||
(results) => {
|
|
||||||
if (!cancelled) {
|
|
||||||
setMetaplex(results);
|
|
||||||
setMetaplexLoading(false);
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
if (!cancelled) {
|
|
||||||
console.error(error);
|
|
||||||
setMetaplexLoading(false);
|
|
||||||
setMetaplexError(error);
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [tokenAccounts, lookupChain]);
|
|
||||||
|
|
||||||
//Solana token map & accountinfos load
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lookupChain === CHAIN_ID_SOLANA && solPK) {
|
if (lookupChain === CHAIN_ID_SOLANA && solPK) {
|
||||||
if (
|
if (
|
||||||
|
@ -462,27 +341,10 @@ function useGetAvailableTokens(nft: boolean = false) {
|
||||||
) {
|
) {
|
||||||
getSolanaParsedTokenAccounts(solPK.toString(), dispatch, nft);
|
getSolanaParsedTokenAccounts(solPK.toString(), dispatch, nft);
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
!(
|
|
||||||
solanaTokenMap.data ||
|
|
||||||
solanaTokenMap.isFetching ||
|
|
||||||
solanaTokenMap.error
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
getSolanaTokenMap(dispatch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [
|
}, [dispatch, solanaWallet, lookupChain, solPK, tokenAccounts, nft]);
|
||||||
dispatch,
|
|
||||||
solanaWallet,
|
|
||||||
lookupChain,
|
|
||||||
solPK,
|
|
||||||
tokenAccounts,
|
|
||||||
solanaTokenMap,
|
|
||||||
nft,
|
|
||||||
]);
|
|
||||||
|
|
||||||
//Solana Mint Accounts lookup
|
//Solana Mint Accounts lookup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -605,46 +467,9 @@ function useGetAvailableTokens(nft: boolean = false) {
|
||||||
//At present, we don't have any mechanism for doing this.
|
//At present, we don't have any mechanism for doing this.
|
||||||
useEffect(() => {}, []);
|
useEffect(() => {}, []);
|
||||||
|
|
||||||
//Terra metadata load
|
|
||||||
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
|
return lookupChain === CHAIN_ID_SOLANA
|
||||||
? {
|
? {
|
||||||
tokenMap: solanaTokenMap,
|
|
||||||
tokenAccounts: tokenAccounts,
|
tokenAccounts: tokenAccounts,
|
||||||
metaplex: {
|
|
||||||
data: metaplex,
|
|
||||||
isFetching: metaplexLoading,
|
|
||||||
error: metaplexError,
|
|
||||||
receivedAt: null, //TODO
|
|
||||||
} as DataWrapper<Metadata[]>,
|
|
||||||
mintAccounts: {
|
mintAccounts: {
|
||||||
data: solanaMintAccounts,
|
data: solanaMintAccounts,
|
||||||
isFetching: solanaMintAccountsLoading,
|
isFetching: solanaMintAccountsLoading,
|
||||||
|
@ -666,7 +491,6 @@ function useGetAvailableTokens(nft: boolean = false) {
|
||||||
}
|
}
|
||||||
: lookupChain === CHAIN_ID_TERRA
|
: lookupChain === CHAIN_ID_TERRA
|
||||||
? {
|
? {
|
||||||
terraTokenMap: terraTokenMap,
|
|
||||||
resetAccounts: resetSourceAccounts,
|
resetAccounts: resetSourceAccounts,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { Connection } from "@solana/web3.js";
|
||||||
|
import { useLayoutEffect, useState } from "react";
|
||||||
|
import { DataWrapper } from "../store/helpers";
|
||||||
|
import { SOLANA_HOST } from "../utils/consts";
|
||||||
|
import {
|
||||||
|
decodeMetadata,
|
||||||
|
getMetadataAddress,
|
||||||
|
Metadata,
|
||||||
|
} from "../utils/metaplex";
|
||||||
|
import { getMultipleAccountsRPC } from "../utils/solana";
|
||||||
|
|
||||||
|
const getMetaplexData = async (mintAddresses: string[]) => {
|
||||||
|
const promises = [];
|
||||||
|
for (const address of mintAddresses) {
|
||||||
|
promises.push(getMetadataAddress(address));
|
||||||
|
}
|
||||||
|
const metaAddresses = await Promise.all(promises);
|
||||||
|
const connection = new Connection(SOLANA_HOST, "finalized");
|
||||||
|
const results = await getMultipleAccountsRPC(
|
||||||
|
connection,
|
||||||
|
metaAddresses.map((pair) => pair && pair[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = results.map((account) => {
|
||||||
|
if (account === null) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
if (account.data) {
|
||||||
|
try {
|
||||||
|
const MetadataParsed = decodeMetadata(account.data);
|
||||||
|
return MetadataParsed;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResultMap = (
|
||||||
|
addresses: string[],
|
||||||
|
metadatas: (Metadata | undefined)[]
|
||||||
|
) => {
|
||||||
|
const output = new Map<string, Metadata | undefined>();
|
||||||
|
|
||||||
|
addresses.forEach((address) => {
|
||||||
|
const metadata = metadatas.find((x) => x?.mint === address);
|
||||||
|
if (metadata) {
|
||||||
|
output.set(address, metadata);
|
||||||
|
} else {
|
||||||
|
output.set(address, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMetaplexData = (
|
||||||
|
addresses: string[]
|
||||||
|
): DataWrapper<Map<string, Metadata | undefined> | undefined> => {
|
||||||
|
const [results, setResults] = useState<
|
||||||
|
Map<string, Metadata | undefined> | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [receivedAt, setReceivedAt] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setIsLoading(true);
|
||||||
|
getMetaplexData(addresses).then(
|
||||||
|
(results) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setResults(createResultMap(addresses, results));
|
||||||
|
setIsLoading(false);
|
||||||
|
setError("");
|
||||||
|
setReceivedAt(new Date().toISOString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setResults(undefined);
|
||||||
|
setIsLoading(false);
|
||||||
|
setError("Failed to fetch Metaplex data.");
|
||||||
|
setReceivedAt(new Date().toISOString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [addresses, setResults, setIsLoading, setError]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: results,
|
||||||
|
isFetching: isLoading,
|
||||||
|
error,
|
||||||
|
receivedAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMetaplexData;
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Dispatch } from "@reduxjs/toolkit";
|
||||||
|
import { ENV, TokenInfo, TokenListProvider } from "@solana/spl-token-registry";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { DataWrapper } from "../store/helpers";
|
||||||
|
import { selectSolanaTokenMap } from "../store/selectors";
|
||||||
|
import {
|
||||||
|
errorSolanaTokenMap,
|
||||||
|
fetchSolanaTokenMap,
|
||||||
|
receiveSolanaTokenMap,
|
||||||
|
} from "../store/tokenSlice";
|
||||||
|
import { CLUSTER } from "../utils/consts";
|
||||||
|
|
||||||
|
const environment = CLUSTER === "testnet" ? ENV.Testnet : ENV.MainnetBeta;
|
||||||
|
|
||||||
|
const useSolanaTokenMap = (): DataWrapper<TokenInfo[]> => {
|
||||||
|
const tokenMap = useSelector(selectSolanaTokenMap);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const shouldFire =
|
||||||
|
tokenMap.data === undefined ||
|
||||||
|
(tokenMap.data === null && !tokenMap.isFetching);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldFire) {
|
||||||
|
getSolanaTokenMap(dispatch);
|
||||||
|
}
|
||||||
|
}, [dispatch, shouldFire]);
|
||||||
|
|
||||||
|
return tokenMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSolanaTokenMap = (dispatch: Dispatch) => {
|
||||||
|
dispatch(fetchSolanaTokenMap());
|
||||||
|
|
||||||
|
new TokenListProvider().resolve().then(
|
||||||
|
(tokens) => {
|
||||||
|
const tokenList = tokens.filterByChainId(environment).getList();
|
||||||
|
dispatch(receiveSolanaTokenMap(tokenList));
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error(error);
|
||||||
|
dispatch(errorSolanaTokenMap("Failed to retrieve the Solana token map."));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSolanaTokenMap;
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Dispatch } from "@reduxjs/toolkit";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { DataWrapper } from "../store/helpers";
|
||||||
|
import { selectTerraTokenMap } from "../store/selectors";
|
||||||
|
import {
|
||||||
|
errorTerraTokenMap,
|
||||||
|
fetchTerraTokenMap,
|
||||||
|
receiveTerraTokenMap,
|
||||||
|
} from "../store/tokenSlice";
|
||||||
|
import { TERRA_TOKEN_METADATA_URL } from "../utils/consts";
|
||||||
|
|
||||||
|
export type TerraTokenMetadata = {
|
||||||
|
protocol: string;
|
||||||
|
symbol: string;
|
||||||
|
token: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TerraTokenMap = {
|
||||||
|
mainnet: {
|
||||||
|
[address: string]: TerraTokenMetadata;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTerraTokenMap = (): DataWrapper<TerraTokenMap> => {
|
||||||
|
const terraTokenMap = useSelector(selectTerraTokenMap);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const shouldFire =
|
||||||
|
terraTokenMap.data === undefined ||
|
||||||
|
(terraTokenMap.data === null && !terraTokenMap.isFetching);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldFire) {
|
||||||
|
getTerraTokenMap(dispatch);
|
||||||
|
}
|
||||||
|
}, [shouldFire, dispatch]);
|
||||||
|
|
||||||
|
return terraTokenMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTerraTokenMap = (dispatch: Dispatch) => {
|
||||||
|
dispatch(fetchTerraTokenMap());
|
||||||
|
axios.get(TERRA_TOKEN_METADATA_URL).then(
|
||||||
|
(response) => {
|
||||||
|
dispatch(receiveTerraTokenMap(response.data as TerraTokenMap));
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
dispatch(errorTerraTokenMap("Failed to retrieve the Terra Token List."));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTerraTokenMap;
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { TokenInfo } from "@solana/spl-token-registry";
|
import { TokenInfo } from "@solana/spl-token-registry";
|
||||||
import { TerraTokenMap } from "../hooks/useGetSourceParsedTokenAccounts";
|
import { TerraTokenMap } from "../hooks/useTerraTokenMap";
|
||||||
import {
|
import {
|
||||||
DataWrapper,
|
DataWrapper,
|
||||||
errorDataWrapper,
|
errorDataWrapper,
|
||||||
|
|
|
@ -24,6 +24,9 @@ export interface ParsedTokenAccount {
|
||||||
decimals: number;
|
decimals: number;
|
||||||
uiAmount: number;
|
uiAmount: number;
|
||||||
uiAmountString: string;
|
uiAmountString: string;
|
||||||
|
symbol?: string;
|
||||||
|
name?: string;
|
||||||
|
logo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
|
|
Loading…
Reference in New Issue