diff --git a/src/renderers/js/getRenderMapVisitor.ts b/src/renderers/js/getRenderMapVisitor.ts index 526c53c4..d0b3599a 100644 --- a/src/renderers/js/getRenderMapVisitor.ts +++ b/src/renderers/js/getRenderMapVisitor.ts @@ -2,6 +2,7 @@ import { format as formatCodeUsingPrettier } from '@prettier/sync'; import { ConfigureOptions } from 'nunjucks'; import { Options as PrettierOptions } from 'prettier'; import { + AccountNode, FieldDiscriminatorNode, getAllAccounts, getAllDefinedTypes, @@ -214,8 +215,26 @@ export function getRenderMapVisitor( .add('programs/index.ts', render('programsIndex.njk', ctx)) .add('errors/index.ts', render('errorsIndex.njk', ctx)); } + const programsWithAccountDiscriminators = programsToExport + .filter((p) => + p.accounts.some( + (a) => (a.discriminators ?? []).length > 0 + ) + ) + .map((p) => camelCase(p.name)); if (accountsToExport.length > 0) { - map.add('accounts/index.ts', render('accountsIndex.njk', ctx)); + map + .add( + 'accounts/index.ts', + render('accountsIndex.njk', { + ...ctx, + programsWithAccountDiscriminators, + }) + ) + .add( + 'accounts/fetchHelpers.ts', + render('accountsFetchHelpers.njk') + ); } if (instructionsToExport.length > 0) { map.add( @@ -283,6 +302,95 @@ export function getRenderMapVisitor( program: node, }) ); + // Generate per-program account helpers with discriminator-based identification. + const accountsWithDisc = node.accounts.filter( + (a) => (a.discriminators ?? []).length > 0 + ); + if (accountsWithDisc.length > 0) { + const helperImports = new JavaScriptImportMap() + .add('umi', [ + 'assertAccountExists', + 'Context', + 'Pda', + 'PublicKey', + 'RpcAccount', + 'RpcGetAccountsOptions', + 'publicKey', + ]) + .addAlias('umi', 'publicKey', 'toPublicKey'); + const resolvedAccounts = accountsWithDisc.map( + (account: AccountNode) => { + const conditions: string[] = []; + for (const disc of account.discriminators ?? []) { + if (isNode(disc, 'byteDiscriminatorNode')) { + conditions.push( + `accountDataMatches(data, new Uint8Array([${disc.bytes.join(', ')}]), ${disc.offset})` + ); + } else if (isNode(disc, 'fieldDiscriminatorNode')) { + const field = account.data.fields.find( + (f) => f.name === disc.name + ); + if (field && field.defaultValue) { + // Anchor-style u8 array discriminator — use raw bytes. + if ( + isNode(field.type, 'arrayTypeNode') && + isNode(field.type.item, 'numberTypeNode') && + field.type.item.format === 'u8' && + isNode(field.type.size, 'fixedSizeNode') && + isNode(field.defaultValue, 'arrayValueNode') && + field.defaultValue.items.every( + isNodeFilter('numberValueNode') + ) + ) { + const bytes = field.defaultValue.items.map( + (n) => (n as { number: number }).number + ); + conditions.push( + `accountDataMatches(data, new Uint8Array([${bytes.join(', ')}]), ${disc.offset})` + ); + } else { + const fieldManifest = visit( + field.type, + typeManifestVisitor + ); + const fieldValue = visit( + field.defaultValue, + valueNodeVisitor + ); + helperImports.mergeWith( + fieldManifest.serializerImports, + fieldValue.imports + ); + conditions.push( + `accountDataMatches(data, ${fieldManifest.serializer}.serialize(${fieldValue.render}), ${disc.offset})` + ); + } + } + } else if (isNode(disc, 'sizeDiscriminatorNode')) { + conditions.push(`data.length === ${disc.size}`); + } + } + return { + name: account.name, + pascalName: pascalCase(account.name), + camelName: camelCase(account.name), + condition: conditions.join(' && '), + }; + } + ); + + renderMap.add( + `accounts/${camelCase(node.name)}Helpers.ts`, + render('accountsProgramHelpers.njk', { + imports: helperImports.toString(dependencyMap), + program: node, + programPascalName: pascalCaseName, + programCamelName: camelCase(node.name), + accounts: resolvedAccounts, + }) + ); + } + program = null; return renderMap; }, diff --git a/src/renderers/js/templates/accountsFetchHelpers.njk b/src/renderers/js/templates/accountsFetchHelpers.njk new file mode 100644 index 00000000..0c457bfe --- /dev/null +++ b/src/renderers/js/templates/accountsFetchHelpers.njk @@ -0,0 +1,107 @@ +{% extends "layout.njk" %} + +{% block main %} +import { + Account, + assertAccountExists, + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountsOptions, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; + +/** + * Defines the input for a single account to be fetched and deserialized + * as part of a mixed-account batch fetch. + * + * @typeParam T - The deserialized account type. + */ +export type FetchAccountInput> = { + publicKey: PublicKey | Pda; + deserialize: (rawAccount: RpcAccount) => T; +}; + +/** @internal */ +type DeserializedAccounts[]> = { + [K in keyof T]: T[K] extends FetchAccountInput ? U : never; +}; + +/** @internal */ +type MaybeDeserializedAccounts[]> = { + [K in keyof T]: T[K] extends FetchAccountInput ? U | null : never; +}; + +/** + * Fetches multiple accounts of potentially different types in a single RPC + * call and deserializes each one using its provided deserializer. + * + * This is useful when you need to fetch accounts of different types (possibly + * from different programs) in a single batch. The underlying RPC + * `getMultipleAccounts` call has a limit of 100 accounts per request; this + * helper does not perform client-side chunking, so callers must ensure the + * input array does not exceed that limit. + * + * All accounts must exist, otherwise an error is thrown. Use + * {@link safeFetchAllMixedAccounts} if some accounts may not exist. + * + * @example + * ```ts + * const [metadata, edition] = await fetchAllMixedAccounts(context, [ + * { publicKey: metadataAddr, deserialize: deserializeMetadata }, + * { publicKey: editionAddr, deserialize: deserializeEdition }, + * ]); + * // metadata: Metadata, edition: Edition — fully typed! + * ``` + */ +export async function fetchAllMixedAccounts< + T extends FetchAccountInput[] +>( + context: Pick, + inputs: [...T], + options?: RpcGetAccountsOptions +): Promise> { + const publicKeys = inputs.map((input) => + toPublicKey(input.publicKey, false) + ); + const maybeAccounts = await context.rpc.getAccounts(publicKeys, options); + return maybeAccounts.map((maybeAccount, index) => { + assertAccountExists(maybeAccount); + return inputs[index].deserialize(maybeAccount); + }) as DeserializedAccounts; +} + +/** + * Fetches multiple accounts of potentially different types in a single RPC + * call and deserializes each one using its provided deserializer. + * + * Accounts that do not exist are returned as `null` at the corresponding + * position in the output tuple. + * + * @example + * ```ts + * const [metadata, edition] = await safeFetchAllMixedAccounts(context, [ + * { publicKey: metadataAddr, deserialize: deserializeMetadata }, + * { publicKey: editionAddr, deserialize: deserializeEdition }, + * ]); + * // metadata: Metadata | null, edition: Edition | null + * ``` + */ +export async function safeFetchAllMixedAccounts< + T extends FetchAccountInput[] +>( + context: Pick, + inputs: [...T], + options?: RpcGetAccountsOptions +): Promise> { + const publicKeys = inputs.map((input) => + toPublicKey(input.publicKey, false) + ); + const maybeAccounts = await context.rpc.getAccounts(publicKeys, options); + return maybeAccounts.map((maybeAccount, index) => { + if (!maybeAccount.exists) return null; + return inputs[index].deserialize(maybeAccount); + }) as MaybeDeserializedAccounts; +} +{% endblock %} diff --git a/src/renderers/js/templates/accountsIndex.njk b/src/renderers/js/templates/accountsIndex.njk index 06e34514..f8e31de9 100644 --- a/src/renderers/js/templates/accountsIndex.njk +++ b/src/renderers/js/templates/accountsIndex.njk @@ -1,9 +1,13 @@ {% extends "layout.njk" %} {% block main %} +export * from './fetchHelpers'; {% for account in accountsToExport | sort(false, false, 'name') %} export * from './{{ account.name | camelCase }}'; {% else %} export default {}; {% endfor %} +{% for programName in programsWithAccountDiscriminators %} +export * from './{{ programName }}Helpers'; +{% endfor %} {% endblock %} diff --git a/src/renderers/js/templates/accountsProgramHelpers.njk b/src/renderers/js/templates/accountsProgramHelpers.njk new file mode 100644 index 00000000..ec7a55b6 --- /dev/null +++ b/src/renderers/js/templates/accountsProgramHelpers.njk @@ -0,0 +1,69 @@ +{% extends "layout.njk" %} + +{% block main %} +{{ imports }} +{% for account in accounts %} +import { {{ account.pascalName }}, deserialize{{ account.pascalName }} } from './{{ account.camelName }}'; +{% endfor %} + +export type {{ programPascalName }}Account = +{% for account in accounts %} + | {{ account.pascalName }} +{% endfor %}; + +function accountDataMatches( + data: Uint8Array, + expected: Uint8Array, + offset: number +): boolean { + if (data.length < offset + expected.length) return false; + for (let i = 0; i < expected.length; i++) { + if (data[offset + i] !== expected[i]) return false; + } + return true; +} + +export function deserialize{{ programPascalName }}Account( + rawAccount: RpcAccount +): {{ programPascalName }}Account { + const data = rawAccount.data; +{% for account in accounts %} + if ({{ account.condition }}) { + return deserialize{{ account.pascalName }}(rawAccount); + } +{% endfor %} + throw new Error( + 'The provided account could not be identified as a {{ program.name }} account.' + ); +} + +export async function fetchAll{{ programPascalName }}Accounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions, +): Promise<{{ programPascalName }}Account[]> { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options, + ); + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount); + return deserialize{{ programPascalName }}Account(maybeAccount); + }); +} + +export async function safeFetchAll{{ programPascalName }}Accounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions, +): Promise<({{ programPascalName }}Account | null)[]> { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options, + ); + return maybeAccounts.map((maybeAccount) => { + if (!maybeAccount.exists) return null; + return deserialize{{ programPascalName }}Account(maybeAccount); + }); +} +{% endblock %} diff --git a/src/visitors/getUniqueHashStringVisitor.ts b/src/visitors/getUniqueHashStringVisitor.ts index 4f876d90..c7b607e9 100644 --- a/src/visitors/getUniqueHashStringVisitor.ts +++ b/src/visitors/getUniqueHashStringVisitor.ts @@ -9,7 +9,7 @@ export function getUniqueHashStringVisitor( ): Visitor { const removeDocs = options.removeDocs ?? false; if (!removeDocs) { - return staticVisitor((node) => stringify(node)); + return staticVisitor((node) => stringify(node) ?? ''); } - return mapVisitor(removeDocsVisitor(), (node) => stringify(node)); + return mapVisitor(removeDocsVisitor(), (node) => stringify(node) ?? ''); } diff --git a/test/packages/js/src/generated/accounts/fetchHelpers.ts b/test/packages/js/src/generated/accounts/fetchHelpers.ts new file mode 100644 index 00000000..12877a60 --- /dev/null +++ b/test/packages/js/src/generated/accounts/fetchHelpers.ts @@ -0,0 +1,105 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Account, + assertAccountExists, + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountsOptions, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; + +/** + * Defines the input for a single account to be fetched and deserialized + * as part of a mixed-account batch fetch. + * + * @typeParam T - The deserialized account type. + */ +export type FetchAccountInput> = { + publicKey: PublicKey | Pda; + deserialize: (rawAccount: RpcAccount) => T; +}; + +/** @internal */ +type DeserializedAccounts[]> = { + [K in keyof T]: T[K] extends FetchAccountInput ? U : never; +}; + +/** @internal */ +type MaybeDeserializedAccounts[]> = { + [K in keyof T]: T[K] extends FetchAccountInput ? U | null : never; +}; + +/** + * Fetches multiple accounts of potentially different types in a single RPC + * call and deserializes each one using its provided deserializer. + * + * This is useful when you need to fetch accounts of different types (possibly + * from different programs) in a single batch. The underlying RPC + * `getMultipleAccounts` call has a limit of 100 accounts per request; this + * helper does not perform client-side chunking, so callers must ensure the + * input array does not exceed that limit. + * + * All accounts must exist, otherwise an error is thrown. Use + * {@link safeFetchAllMixedAccounts} if some accounts may not exist. + * + * @example + * ```ts + * const [metadata, edition] = await fetchAllMixedAccounts(context, [ + * { publicKey: metadataAddr, deserialize: deserializeMetadata }, + * { publicKey: editionAddr, deserialize: deserializeEdition }, + * ]); + * // metadata: Metadata, edition: Edition — fully typed! + * ``` + */ +export async function fetchAllMixedAccounts[]>( + context: Pick, + inputs: [...T], + options?: RpcGetAccountsOptions +): Promise> { + const publicKeys = inputs.map((input) => toPublicKey(input.publicKey, false)); + const maybeAccounts = await context.rpc.getAccounts(publicKeys, options); + return maybeAccounts.map((maybeAccount, index) => { + assertAccountExists(maybeAccount); + return inputs[index].deserialize(maybeAccount); + }) as DeserializedAccounts; +} + +/** + * Fetches multiple accounts of potentially different types in a single RPC + * call and deserializes each one using its provided deserializer. + * + * Accounts that do not exist are returned as `null` at the corresponding + * position in the output tuple. + * + * @example + * ```ts + * const [metadata, edition] = await safeFetchAllMixedAccounts(context, [ + * { publicKey: metadataAddr, deserialize: deserializeMetadata }, + * { publicKey: editionAddr, deserialize: deserializeEdition }, + * ]); + * // metadata: Metadata | null, edition: Edition | null + * ``` + */ +export async function safeFetchAllMixedAccounts< + T extends FetchAccountInput[], +>( + context: Pick, + inputs: [...T], + options?: RpcGetAccountsOptions +): Promise> { + const publicKeys = inputs.map((input) => toPublicKey(input.publicKey, false)); + const maybeAccounts = await context.rpc.getAccounts(publicKeys, options); + return maybeAccounts.map((maybeAccount, index) => { + if (!maybeAccount.exists) return null; + return inputs[index].deserialize(maybeAccount); + }) as MaybeDeserializedAccounts; +} diff --git a/test/packages/js/src/generated/accounts/index.ts b/test/packages/js/src/generated/accounts/index.ts index 2fdbfaeb..9fede506 100644 --- a/test/packages/js/src/generated/accounts/index.ts +++ b/test/packages/js/src/generated/accounts/index.ts @@ -6,6 +6,7 @@ * @see https://github.com/metaplex-foundation/kinobi */ +export * from './fetchHelpers'; export * from './accountWithPadding'; export * from './accountWithPoddedTypes'; export * from './candyMachine'; @@ -21,3 +22,6 @@ export * from './reservationListV1'; export * from './reservationListV2'; export * from './tokenOwnedEscrow'; export * from './useAuthorityRecord'; +export * from './mplCandyMachineCoreHelpers'; +export * from './mplTokenAuthRulesHelpers'; +export * from './mplTokenMetadataHelpers'; diff --git a/test/packages/js/src/generated/accounts/mplCandyMachineCoreHelpers.ts b/test/packages/js/src/generated/accounts/mplCandyMachineCoreHelpers.ts new file mode 100644 index 00000000..2a5c700f --- /dev/null +++ b/test/packages/js/src/generated/accounts/mplCandyMachineCoreHelpers.ts @@ -0,0 +1,80 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountsOptions, + assertAccountExists, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; +import { CandyMachine, deserializeCandyMachine } from './candyMachine'; + +export type MplCandyMachineCoreAccount = CandyMachine; + +function accountDataMatches( + data: Uint8Array, + expected: Uint8Array, + offset: number +): boolean { + if (data.length < offset + expected.length) return false; + for (let i = 0; i < expected.length; i++) { + if (data[offset + i] !== expected[i]) return false; + } + return true; +} + +export function deserializeMplCandyMachineCoreAccount( + rawAccount: RpcAccount +): MplCandyMachineCoreAccount { + const data = rawAccount.data; + if ( + accountDataMatches( + data, + new Uint8Array([51, 173, 177, 113, 25, 241, 109, 189]), + 0 + ) + ) { + return deserializeCandyMachine(rawAccount); + } + throw new Error( + 'The provided account could not be identified as a mplCandyMachineCore account.' + ); +} + +export async function fetchAllMplCandyMachineCoreAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount); + return deserializeMplCandyMachineCoreAccount(maybeAccount); + }); +} + +export async function safeFetchAllMplCandyMachineCoreAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise<(MplCandyMachineCoreAccount | null)[]> { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + if (!maybeAccount.exists) return null; + return deserializeMplCandyMachineCoreAccount(maybeAccount); + }); +} diff --git a/test/packages/js/src/generated/accounts/mplTokenAuthRulesHelpers.ts b/test/packages/js/src/generated/accounts/mplTokenAuthRulesHelpers.ts new file mode 100644 index 00000000..c28f3a69 --- /dev/null +++ b/test/packages/js/src/generated/accounts/mplTokenAuthRulesHelpers.ts @@ -0,0 +1,79 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountsOptions, + assertAccountExists, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; +import { u64 } from '@metaplex-foundation/umi/serializers'; +import { TaKey } from '../types'; +import { + FrequencyAccount, + deserializeFrequencyAccount, +} from './frequencyAccount'; + +export type MplTokenAuthRulesAccount = FrequencyAccount; + +function accountDataMatches( + data: Uint8Array, + expected: Uint8Array, + offset: number +): boolean { + if (data.length < offset + expected.length) return false; + for (let i = 0; i < expected.length; i++) { + if (data[offset + i] !== expected[i]) return false; + } + return true; +} + +export function deserializeMplTokenAuthRulesAccount( + rawAccount: RpcAccount +): MplTokenAuthRulesAccount { + const data = rawAccount.data; + if (accountDataMatches(data, u64().serialize(TaKey.Frequency), 0)) { + return deserializeFrequencyAccount(rawAccount); + } + throw new Error( + 'The provided account could not be identified as a mplTokenAuthRules account.' + ); +} + +export async function fetchAllMplTokenAuthRulesAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount); + return deserializeMplTokenAuthRulesAccount(maybeAccount); + }); +} + +export async function safeFetchAllMplTokenAuthRulesAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise<(MplTokenAuthRulesAccount | null)[]> { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + if (!maybeAccount.exists) return null; + return deserializeMplTokenAuthRulesAccount(maybeAccount); + }); +} diff --git a/test/packages/js/src/generated/accounts/mplTokenMetadataHelpers.ts b/test/packages/js/src/generated/accounts/mplTokenMetadataHelpers.ts new file mode 100644 index 00000000..21c7ebf0 --- /dev/null +++ b/test/packages/js/src/generated/accounts/mplTokenMetadataHelpers.ts @@ -0,0 +1,199 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountsOptions, + assertAccountExists, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; +import { TmKey, getTmKeySerializer } from '../types'; +import { + CollectionAuthorityRecord, + deserializeCollectionAuthorityRecord, +} from './collectionAuthorityRecord'; +import { DelegateRecord, deserializeDelegateRecord } from './delegateRecord'; +import { Edition, deserializeEdition } from './edition'; +import { EditionMarker, deserializeEditionMarker } from './editionMarker'; +import { + TokenOwnedEscrow, + deserializeTokenOwnedEscrow, +} from './tokenOwnedEscrow'; +import { MasterEditionV2, deserializeMasterEditionV2 } from './masterEditionV2'; +import { MasterEditionV1, deserializeMasterEditionV1 } from './masterEditionV1'; +import { Metadata, deserializeMetadata } from './metadata'; +import { + ReservationListV2, + deserializeReservationListV2, +} from './reservationListV2'; +import { + ReservationListV1, + deserializeReservationListV1, +} from './reservationListV1'; +import { + UseAuthorityRecord, + deserializeUseAuthorityRecord, +} from './useAuthorityRecord'; + +export type MplTokenMetadataAccount = + | CollectionAuthorityRecord + | DelegateRecord + | Edition + | EditionMarker + | TokenOwnedEscrow + | MasterEditionV2 + | MasterEditionV1 + | Metadata + | ReservationListV2 + | ReservationListV1 + | UseAuthorityRecord; + +function accountDataMatches( + data: Uint8Array, + expected: Uint8Array, + offset: number +): boolean { + if (data.length < offset + expected.length) return false; + for (let i = 0; i < expected.length; i++) { + if (data[offset + i] !== expected[i]) return false; + } + return true; +} + +export function deserializeMplTokenMetadataAccount( + rawAccount: RpcAccount +): MplTokenMetadataAccount { + const data = rawAccount.data; + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.CollectionAuthorityRecord), + 0 + ) + ) { + return deserializeCollectionAuthorityRecord(rawAccount); + } + if ( + accountDataMatches(data, getTmKeySerializer().serialize(TmKey.Delegate), 0) + ) { + return deserializeDelegateRecord(rawAccount); + } + if ( + accountDataMatches(data, getTmKeySerializer().serialize(TmKey.EditionV1), 0) + ) { + return deserializeEdition(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.EditionMarker), + 0 + ) + ) { + return deserializeEditionMarker(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.TokenOwnedEscrow), + 0 + ) + ) { + return deserializeTokenOwnedEscrow(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.MasterEditionV2), + 0 + ) + ) { + return deserializeMasterEditionV2(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.MasterEditionV1), + 0 + ) + ) { + return deserializeMasterEditionV1(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.MetadataV1), + 0 + ) + ) { + return deserializeMetadata(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.ReservationListV2), + 0 + ) + ) { + return deserializeReservationListV2(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.ReservationListV1), + 0 + ) + ) { + return deserializeReservationListV1(rawAccount); + } + if ( + accountDataMatches( + data, + getTmKeySerializer().serialize(TmKey.UseAuthorityRecord), + 0 + ) + ) { + return deserializeUseAuthorityRecord(rawAccount); + } + throw new Error( + 'The provided account could not be identified as a mplTokenMetadata account.' + ); +} + +export async function fetchAllMplTokenMetadataAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount); + return deserializeMplTokenMetadataAccount(maybeAccount); + }); +} + +export async function safeFetchAllMplTokenMetadataAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise<(MplTokenMetadataAccount | null)[]> { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + if (!maybeAccount.exists) return null; + return deserializeMplTokenMetadataAccount(maybeAccount); + }); +}