import type { AccountParsers } from "@saberhq/anchor-contrib"; import type { KeyedAccountInfo, PublicKey } from "@solana/web3.js"; import mapValues from "lodash.mapvalues"; import zip from "lodash.zip"; import { startTransition, useEffect, useMemo, useState } from "react"; import type { ParserHooks } from ".."; import { getCacheKeyOfPublicKey, SailAccountParseError, useSail } from ".."; import { useAccountsData } from "../hooks/useAccountsData"; import type { ParsedAccountDatum } from "../types"; export type AccountParser<T> = (info: KeyedAccountInfo) => T; /** * Makes account parsers from a coder. * @param parsers * @returns */ export const makeParsersFromCoder = <M>(parsers: AccountParsers<M>) => { return mapValues( parsers, (p) => (info: KeyedAccountInfo) => p(info.accountInfo.data) ); }; /** * Makes hooks for parsers. * @param parsers * @returns */ export const makeParserHooks = <M>( parsers: AccountParsers<M> ): { [K in keyof M]: ParserHooks<M[K]>; } => { const sailParsers = makeParsersFromCoder(parsers); return mapValues(sailParsers, (parser) => ({ useSingleData: (key: PublicKey | null | undefined) => useParsedAccountData(key, parser), useData: (keys: (PublicKey | null | undefined)[]) => useParsedAccountsData(keys, parser), })) as { [K in keyof M]: ParserHooks<M[K]>; }; }; /** * Parses accounts with the given parser. * * @deprecated use {@link useBatchedParsedAccounts} instead * @param keys * @param parser * @returns */ export const useParsedAccountsData = <T>( keys: (PublicKey | null | undefined)[], parser: AccountParser<T> ): ParsedAccountDatum<T>[] => { const { onError } = useSail(); const data = useAccountsData(keys); const [parsed, setParsed] = useState<Record<string, ParsedAccountDatum<T>>>( keys.reduce<Record<string, ParsedAccountDatum<T>>>((acc, k) => { if (k) { acc[getCacheKeyOfPublicKey(k)] = undefined; } return acc; }, {}) ); useEffect(() => { startTransition(() => { setParsed((prevParsed) => { const nextParsed = { ...prevParsed }; zip(keys, data).forEach(([key, datum]) => { if (datum) { const key = getCacheKeyOfPublicKey(datum.accountId); const prevValue = prevParsed[key]; if ( prevValue && prevValue.raw.length === datum.accountInfo.data.length && prevValue.raw.equals(datum.accountInfo.data) ) { // preserve referential equality if buffers are equal return; } try { const parsed = parser(datum); nextParsed[key] = { ...datum, accountInfo: { ...datum.accountInfo, data: parsed, }, raw: datum.accountInfo.data, }; } catch (e) { onError(new SailAccountParseError(e, datum)); nextParsed[key] = null; return; } } if (key && datum === null) { nextParsed[getCacheKeyOfPublicKey(key)] = null; } }); return nextParsed; }); }); }, [data, keys, onError, parser]); return useMemo(() => { return keys.map((k) => { if (!k) { return k; } return parsed[getCacheKeyOfPublicKey(k)]; }); }, [keys, parsed]); }; /** * Loads the parsed data of a single account. * @returns */ export const useParsedAccountData = <T>( key: PublicKey | null | undefined, parser: AccountParser<T> ): { loading: boolean; data: ParsedAccountDatum<T> } => { const theKey = useMemo(() => [key], [key]); const [data] = useParsedAccountsData(theKey, parser); return { loading: key !== undefined && data === undefined, data, }; };