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 { 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 { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import { WORMHOLE_V1_MINT_AUTHORITY } from "../../utils/consts";
|
||||
|
@ -30,10 +32,8 @@ type SolanaSourceTokenSelectorProps = {
|
|||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||
accounts: ParsedTokenAccount[];
|
||||
solanaTokenMap: DataWrapper<TokenInfo[]> | undefined;
|
||||
metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null;
|
||||
disabled: boolean;
|
||||
mintAccounts: DataWrapper<Map<String, string | null>> | undefined;
|
||||
mintAccounts: DataWrapper<Map<string, string | null>> | undefined;
|
||||
resetAccounts: (() => void) | undefined;
|
||||
nft?: boolean;
|
||||
};
|
||||
|
@ -41,16 +41,27 @@ type SolanaSourceTokenSelectorProps = {
|
|||
export default function SolanaSourceTokenSelector(
|
||||
props: SolanaSourceTokenSelectorProps
|
||||
) {
|
||||
const { value, onChange, disabled, resetAccounts, nft } = props;
|
||||
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 (props.solanaTokenMap?.data) {
|
||||
for (const data of props.solanaTokenMap.data) {
|
||||
if (solanaTokenMap.data) {
|
||||
for (const data of solanaTokenMap.data) {
|
||||
if (data && data.address) {
|
||||
output.set(data.address, data);
|
||||
}
|
||||
|
@ -58,26 +69,12 @@ export default function SolanaSourceTokenSelector(
|
|||
}
|
||||
|
||||
return output;
|
||||
}, [props.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]);
|
||||
}, [solanaTokenMap]);
|
||||
|
||||
const getSymbol = (account: ParsedTokenAccount) => {
|
||||
return (
|
||||
memoizedTokenMap.get(account.mintKey)?.symbol ||
|
||||
memoizedMetaplex.get(account.mintKey)?.data?.symbol ||
|
||||
metaplex.data?.get(account.mintKey)?.data?.symbol ||
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
@ -85,7 +82,7 @@ export default function SolanaSourceTokenSelector(
|
|||
const getName = (account: ParsedTokenAccount) => {
|
||||
return (
|
||||
memoizedTokenMap.get(account.mintKey)?.name ||
|
||||
memoizedMetaplex.get(account.mintKey)?.data?.name ||
|
||||
metaplex.data?.get(account.mintKey)?.data?.name ||
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
@ -130,11 +127,11 @@ export default function SolanaSourceTokenSelector(
|
|||
const renderAccount = (
|
||||
account: ParsedTokenAccount,
|
||||
solanaTokenMap: Map<String, TokenInfo>,
|
||||
metaplexData: Map<String, Metadata>,
|
||||
metaplexData: Map<String, Metadata | undefined> | null | undefined,
|
||||
classes: any
|
||||
) => {
|
||||
const tokenMapData = solanaTokenMap.get(account.mintKey);
|
||||
const metaplexValue = metaplexData.get(account.mintKey);
|
||||
const metaplexValue = metaplexData?.get(account.mintKey);
|
||||
|
||||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
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.
|
||||
//TODO This can flicker dependent on how fast the useEffects in the getSourceAccounts hook complete.
|
||||
const isLoading =
|
||||
props.metaplexData.isFetching ||
|
||||
props.solanaTokenMap?.isFetching ||
|
||||
metaplex.isFetching ||
|
||||
solanaTokenMap.isFetching ||
|
||||
props.mintAccounts?.isFetching;
|
||||
|
||||
const accountLoadError =
|
||||
!(props.mintAccounts?.isFetching || props.mintAccounts?.data) &&
|
||||
"Unable to retrieve your token accounts";
|
||||
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
|
||||
|
@ -200,10 +196,10 @@ export default function SolanaSourceTokenSelector(
|
|||
return props.accounts.filter((x) => {
|
||||
//TODO, do a better check which likely involves supply or checking masterEdition.
|
||||
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;
|
||||
});
|
||||
}, [memoizedMetaplex, nft, props.accounts]);
|
||||
}, [metaplex.data, nft, props.accounts]);
|
||||
|
||||
const isOptionDisabled = useMemo(() => {
|
||||
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
||||
|
@ -232,12 +228,7 @@ export default function SolanaSourceTokenSelector(
|
|||
/>
|
||||
)}
|
||||
renderOption={(option) => {
|
||||
return renderAccount(
|
||||
option,
|
||||
memoizedTokenMap,
|
||||
memoizedMetaplex,
|
||||
classes
|
||||
);
|
||||
return renderAccount(option, memoizedTokenMap, metaplex.data, classes);
|
||||
}}
|
||||
getOptionDisabled={isOptionDisabled}
|
||||
getOptionLabel={(option) => {
|
||||
|
|
|
@ -88,8 +88,6 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
onChange={handleOnChange}
|
||||
disabled={disabled}
|
||||
accounts={maps?.tokenAccounts?.data || []}
|
||||
solanaTokenMap={maps?.tokenMap}
|
||||
metaplexData={maps?.metaplex}
|
||||
mintAccounts={maps?.mintAccounts}
|
||||
resetAccounts={maps?.resetAccounts}
|
||||
nft={nft}
|
||||
|
@ -109,7 +107,6 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
value={sourceParsedTokenAccount || null}
|
||||
disabled={disabled}
|
||||
onChange={handleOnChange}
|
||||
tokenMap={maps?.terraTokenMap}
|
||||
resetAccounts={maps?.resetAccounts}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -13,12 +13,10 @@ import {
|
|||
} from "@terra-money/wallet-provider";
|
||||
import { formatUnits } from "ethers/lib/utils";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
createParsedTokenAccount,
|
||||
TerraTokenMap,
|
||||
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import useTerraTokenMap, {
|
||||
TerraTokenMetadata,
|
||||
} from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import { DataWrapper } from "../../store/helpers";
|
||||
} from "../../hooks/useTerraTokenMap";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import { TERRA_HOST } from "../../utils/consts";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
|
@ -44,7 +42,6 @@ type TerraSourceTokenSelectorProps = {
|
|||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||
disabled: boolean;
|
||||
tokenMap: DataWrapper<TerraTokenMap> | undefined; //TODO better type
|
||||
resetAccounts: (() => void) | undefined;
|
||||
};
|
||||
|
||||
|
@ -90,7 +87,8 @@ export default function TerraSourceTokenSelector(
|
|||
props: TerraSourceTokenSelectorProps
|
||||
) {
|
||||
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 [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||
|
@ -115,10 +113,10 @@ export default function TerraSourceTokenSelector(
|
|||
const isLoading = tokenMap?.isFetching || false;
|
||||
|
||||
const terraTokenArray = useMemo(() => {
|
||||
const values = props.tokenMap?.data?.mainnet;
|
||||
const values = tokenMap.data?.mainnet;
|
||||
const items = Object.values(values || {});
|
||||
return items || [];
|
||||
}, [props.tokenMap]);
|
||||
}, [tokenMap]);
|
||||
|
||||
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
||||
if (!fromProps) return null;
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
} 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";
|
||||
import {
|
||||
AccountInfo,
|
||||
Connection,
|
||||
|
@ -18,7 +17,6 @@ import { useCallback, useEffect, useState } from "react";
|
|||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { DataWrapper } from "../store/helpers";
|
||||
import {
|
||||
errorSourceParsedTokenAccounts as errorSourceParsedTokenAccountsNFT,
|
||||
fetchSourceParsedTokenAccounts as fetchSourceParsedTokenAccountsNFT,
|
||||
|
@ -32,20 +30,10 @@ import {
|
|||
selectNFTSourceChain,
|
||||
selectNFTSourceParsedTokenAccounts,
|
||||
selectNFTSourceWalletAddress,
|
||||
selectSolanaTokenMap,
|
||||
selectSourceWalletAddress,
|
||||
selectTerraTokenMap,
|
||||
selectTransferSourceChain,
|
||||
selectTransferSourceParsedTokenAccounts,
|
||||
} from "../store/selectors";
|
||||
import {
|
||||
errorSolanaTokenMap,
|
||||
errorTerraTokenMap,
|
||||
fetchSolanaTokenMap,
|
||||
fetchTerraTokenMap,
|
||||
receiveSolanaTokenMap,
|
||||
receiveTerraTokenMap,
|
||||
} from "../store/tokenSlice";
|
||||
import {
|
||||
errorSourceParsedTokenAccounts,
|
||||
fetchSourceParsedTokenAccounts,
|
||||
|
@ -56,17 +44,7 @@ import {
|
|||
setSourceParsedTokenAccounts,
|
||||
setSourceWalletAddress,
|
||||
} from "../store/transferSlice";
|
||||
import {
|
||||
CLUSTER,
|
||||
COVALENT_GET_TOKENS_URL,
|
||||
SOLANA_HOST,
|
||||
TERRA_TOKEN_METADATA_URL,
|
||||
} from "../utils/consts";
|
||||
import {
|
||||
decodeMetadata,
|
||||
getMetadataAddress,
|
||||
Metadata,
|
||||
} from "../utils/metaplex";
|
||||
import { COVALENT_GET_TOKENS_URL, SOLANA_HOST } from "../utils/consts";
|
||||
import {
|
||||
extractMintAuthorityInfo,
|
||||
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 = (
|
||||
pubkey: PublicKey,
|
||||
item: AccountInfo<ParsedAccountData>
|
||||
|
@ -158,6 +123,9 @@ const createParsedTokenAccountFromCovalent = (
|
|||
decimals: covalent.contract_decimals,
|
||||
uiAmount: Number(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;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
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 = (
|
||||
walletAddress: string,
|
||||
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
|
||||
* 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
|
||||
: selectTransferSourceParsedTokenAccounts
|
||||
);
|
||||
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
||||
const terraTokenMap = useSelector(selectTerraTokenMap);
|
||||
|
||||
const lookupChain = useSelector(
|
||||
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||
);
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
//const terraWallet = useConnectedWallet(); //TODO
|
||||
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 [covalentLoading, setCovalentLoading] = useState(false);
|
||||
const [covalentError, setCovalentError] = useState<string | undefined>(
|
||||
|
@ -422,39 +333,7 @@ function useGetAvailableTokens(nft: boolean = false) {
|
|||
resetSourceAccounts,
|
||||
]);
|
||||
|
||||
// Solana metaplex 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
|
||||
//Solana accountinfos load
|
||||
useEffect(() => {
|
||||
if (lookupChain === CHAIN_ID_SOLANA && solPK) {
|
||||
if (
|
||||
|
@ -462,27 +341,10 @@ function useGetAvailableTokens(nft: boolean = false) {
|
|||
) {
|
||||
getSolanaParsedTokenAccounts(solPK.toString(), dispatch, nft);
|
||||
}
|
||||
if (
|
||||
!(
|
||||
solanaTokenMap.data ||
|
||||
solanaTokenMap.isFetching ||
|
||||
solanaTokenMap.error
|
||||
)
|
||||
) {
|
||||
getSolanaTokenMap(dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [
|
||||
dispatch,
|
||||
solanaWallet,
|
||||
lookupChain,
|
||||
solPK,
|
||||
tokenAccounts,
|
||||
solanaTokenMap,
|
||||
nft,
|
||||
]);
|
||||
}, [dispatch, solanaWallet, lookupChain, solPK, tokenAccounts, nft]);
|
||||
|
||||
//Solana Mint Accounts lookup
|
||||
useEffect(() => {
|
||||
|
@ -605,46 +467,9 @@ function useGetAvailableTokens(nft: boolean = false) {
|
|||
//At present, we don't have any mechanism for doing this.
|
||||
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
|
||||
? {
|
||||
tokenMap: solanaTokenMap,
|
||||
tokenAccounts: tokenAccounts,
|
||||
metaplex: {
|
||||
data: metaplex,
|
||||
isFetching: metaplexLoading,
|
||||
error: metaplexError,
|
||||
receivedAt: null, //TODO
|
||||
} as DataWrapper<Metadata[]>,
|
||||
mintAccounts: {
|
||||
data: solanaMintAccounts,
|
||||
isFetching: solanaMintAccountsLoading,
|
||||
|
@ -666,7 +491,6 @@ function useGetAvailableTokens(nft: boolean = false) {
|
|||
}
|
||||
: lookupChain === CHAIN_ID_TERRA
|
||||
? {
|
||||
terraTokenMap: terraTokenMap,
|
||||
resetAccounts: resetSourceAccounts,
|
||||
}
|
||||
: 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 { TokenInfo } from "@solana/spl-token-registry";
|
||||
import { TerraTokenMap } from "../hooks/useGetSourceParsedTokenAccounts";
|
||||
import { TerraTokenMap } from "../hooks/useTerraTokenMap";
|
||||
import {
|
||||
DataWrapper,
|
||||
errorDataWrapper,
|
||||
|
|
|
@ -24,6 +24,9 @@ export interface ParsedTokenAccount {
|
|||
decimals: number;
|
||||
uiAmount: number;
|
||||
uiAmountString: string;
|
||||
symbol?: string;
|
||||
name?: string;
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
|
|
Loading…
Reference in New Issue