Skip to content

Commit 1dc3100

Browse files
committed
Add fetchAllMixedAccounts helper for heterogeneous multi-account fetching
Generate a new `fetchHelpers.ts` in the JS accounts output that provides `fetchAllMixedAccounts` and `safeFetchAllMixedAccounts`. These helpers take an array of { publicKey, deserialize } inputs of different account types, batch them into a single `rpc.getAccounts()` call, and return a fully-typed tuple where each position matches the corresponding deserializer's output type. https://claude.ai/code/session_01Dddxti9yQJBCaoBqKPjye1
1 parent 41437a7 commit 1dc3100

5 files changed

Lines changed: 224 additions & 1 deletion

File tree

src/renderers/js/getRenderMapVisitor.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,12 @@ export function getRenderMapVisitor(
215215
.add('errors/index.ts', render('errorsIndex.njk', ctx));
216216
}
217217
if (accountsToExport.length > 0) {
218-
map.add('accounts/index.ts', render('accountsIndex.njk', ctx));
218+
map
219+
.add('accounts/index.ts', render('accountsIndex.njk', ctx))
220+
.add(
221+
'accounts/fetchHelpers.ts',
222+
render('accountsFetchHelpers.njk')
223+
);
219224
}
220225
if (instructionsToExport.length > 0) {
221226
map.add(
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
{% extends "layout.njk" %}
2+
3+
{% block main %}
4+
import {
5+
Account,
6+
assertAccountExists,
7+
Context,
8+
Pda,
9+
PublicKey,
10+
RpcAccount,
11+
RpcGetAccountsOptions,
12+
publicKey as toPublicKey,
13+
} from '@metaplex-foundation/umi';
14+
15+
/**
16+
* Defines the input for a single account to be fetched and deserialized
17+
* as part of a mixed-account batch fetch.
18+
*
19+
* @typeParam T - The deserialized account type.
20+
*/
21+
export type FetchAccountInput<T extends Account<any>> = {
22+
publicKey: PublicKey | Pda;
23+
deserialize: (rawAccount: RpcAccount) => T;
24+
};
25+
26+
/** @internal */
27+
type DeserializedAccounts<T extends FetchAccountInput<any>[]> = {
28+
[K in keyof T]: T[K] extends FetchAccountInput<infer U> ? U : never;
29+
};
30+
31+
/** @internal */
32+
type MaybeDeserializedAccounts<T extends FetchAccountInput<any>[]> = {
33+
[K in keyof T]: T[K] extends FetchAccountInput<infer U> ? U | null : never;
34+
};
35+
36+
/**
37+
* Fetches multiple accounts of potentially different types in a single RPC
38+
* call and deserializes each one using its provided deserializer.
39+
*
40+
* This is useful in frontends and other scenarios where you want to minimize
41+
* the number of RPC round-trips by batching up to 100 accounts into a single
42+
* `getMultipleAccounts` call while still getting fully typed results.
43+
*
44+
* All accounts must exist, otherwise an error is thrown. Use
45+
* {@link safeFetchAllMixedAccounts} if some accounts may not exist.
46+
*
47+
* @example
48+
* ```ts
49+
* const [metadata, edition] = await fetchAllMixedAccounts(context, [
50+
* { publicKey: metadataAddr, deserialize: deserializeMetadata },
51+
* { publicKey: editionAddr, deserialize: deserializeEdition },
52+
* ]);
53+
* // metadata: Metadata, edition: Edition — fully typed!
54+
* ```
55+
*/
56+
export async function fetchAllMixedAccounts<
57+
T extends FetchAccountInput<any>[]
58+
>(
59+
context: Pick<Context, 'rpc'>,
60+
inputs: [...T],
61+
options?: RpcGetAccountsOptions
62+
): Promise<DeserializedAccounts<T>> {
63+
const publicKeys = inputs.map((input) =>
64+
toPublicKey(input.publicKey, false)
65+
);
66+
const maybeAccounts = await context.rpc.getAccounts(publicKeys, options);
67+
return maybeAccounts.map((maybeAccount, index) => {
68+
assertAccountExists(maybeAccount);
69+
return inputs[index].deserialize(maybeAccount);
70+
}) as DeserializedAccounts<T>;
71+
}
72+
73+
/**
74+
* Fetches multiple accounts of potentially different types in a single RPC
75+
* call and deserializes each one using its provided deserializer.
76+
*
77+
* Accounts that do not exist are returned as `null` at the corresponding
78+
* position in the output tuple.
79+
*
80+
* @example
81+
* ```ts
82+
* const [metadata, edition] = await safeFetchAllMixedAccounts(context, [
83+
* { publicKey: metadataAddr, deserialize: deserializeMetadata },
84+
* { publicKey: editionAddr, deserialize: deserializeEdition },
85+
* ]);
86+
* // metadata: Metadata | null, edition: Edition | null
87+
* ```
88+
*/
89+
export async function safeFetchAllMixedAccounts<
90+
T extends FetchAccountInput<any>[]
91+
>(
92+
context: Pick<Context, 'rpc'>,
93+
inputs: [...T],
94+
options?: RpcGetAccountsOptions
95+
): Promise<MaybeDeserializedAccounts<T>> {
96+
const publicKeys = inputs.map((input) =>
97+
toPublicKey(input.publicKey, false)
98+
);
99+
const maybeAccounts = await context.rpc.getAccounts(publicKeys, options);
100+
return maybeAccounts.map((maybeAccount, index) => {
101+
return maybeAccount.exists
102+
? inputs[index].deserialize(maybeAccount as RpcAccount)
103+
: null;
104+
}) as MaybeDeserializedAccounts<T>;
105+
}
106+
{% endblock %}

src/renderers/js/templates/accountsIndex.njk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{% extends "layout.njk" %}
22

33
{% block main %}
4+
export * from './fetchHelpers';
45
{% for account in accountsToExport | sort(false, false, 'name') %}
56
export * from './{{ account.name | camelCase }}';
67
{% else %}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* This code was AUTOGENERATED using the kinobi library.
3+
* Please DO NOT EDIT THIS FILE, instead use visitors
4+
* to add features, then rerun kinobi to update it.
5+
*
6+
* @see https://github.com/metaplex-foundation/kinobi
7+
*/
8+
9+
import {
10+
Account,
11+
assertAccountExists,
12+
Context,
13+
Pda,
14+
PublicKey,
15+
RpcAccount,
16+
RpcGetAccountsOptions,
17+
publicKey as toPublicKey,
18+
} from '@metaplex-foundation/umi';
19+
20+
/**
21+
* Defines the input for a single account to be fetched and deserialized
22+
* as part of a mixed-account batch fetch.
23+
*
24+
* @typeParam T - The deserialized account type.
25+
*/
26+
export type FetchAccountInput<T extends Account<any>> = {
27+
publicKey: PublicKey | Pda;
28+
deserialize: (rawAccount: RpcAccount) => T;
29+
};
30+
31+
/** @internal */
32+
type DeserializedAccounts<T extends FetchAccountInput<any>[]> = {
33+
[K in keyof T]: T[K] extends FetchAccountInput<infer U> ? U : never;
34+
};
35+
36+
/** @internal */
37+
type MaybeDeserializedAccounts<T extends FetchAccountInput<any>[]> = {
38+
[K in keyof T]: T[K] extends FetchAccountInput<infer U> ? U | null : never;
39+
};
40+
41+
/**
42+
* Fetches multiple accounts of potentially different types in a single RPC
43+
* call and deserializes each one using its provided deserializer.
44+
*
45+
* This is useful in frontends and other scenarios where you want to minimize
46+
* the number of RPC round-trips by batching up to 100 accounts into a single
47+
* `getMultipleAccounts` call while still getting fully typed results.
48+
*
49+
* All accounts must exist, otherwise an error is thrown. Use
50+
* {@link safeFetchAllMixedAccounts} if some accounts may not exist.
51+
*
52+
* @example
53+
* ```ts
54+
* const [metadata, edition] = await fetchAllMixedAccounts(context, [
55+
* { publicKey: metadataAddr, deserialize: deserializeMetadata },
56+
* { publicKey: editionAddr, deserialize: deserializeEdition },
57+
* ]);
58+
* // metadata: Metadata, edition: Edition — fully typed!
59+
* ```
60+
*/
61+
export async function fetchAllMixedAccounts<
62+
T extends FetchAccountInput<any>[],
63+
>(
64+
context: Pick<Context, 'rpc'>,
65+
inputs: [...T],
66+
options?: RpcGetAccountsOptions
67+
): Promise<DeserializedAccounts<T>> {
68+
const publicKeys = inputs.map((input) =>
69+
toPublicKey(input.publicKey, false)
70+
);
71+
const maybeAccounts = await context.rpc.getAccounts(publicKeys, options);
72+
return maybeAccounts.map((maybeAccount, index) => {
73+
assertAccountExists(maybeAccount);
74+
return inputs[index].deserialize(maybeAccount);
75+
}) as DeserializedAccounts<T>;
76+
}
77+
78+
/**
79+
* Fetches multiple accounts of potentially different types in a single RPC
80+
* call and deserializes each one using its provided deserializer.
81+
*
82+
* Accounts that do not exist are returned as `null` at the corresponding
83+
* position in the output tuple.
84+
*
85+
* @example
86+
* ```ts
87+
* const [metadata, edition] = await safeFetchAllMixedAccounts(context, [
88+
* { publicKey: metadataAddr, deserialize: deserializeMetadata },
89+
* { publicKey: editionAddr, deserialize: deserializeEdition },
90+
* ]);
91+
* // metadata: Metadata | null, edition: Edition | null
92+
* ```
93+
*/
94+
export async function safeFetchAllMixedAccounts<
95+
T extends FetchAccountInput<any>[],
96+
>(
97+
context: Pick<Context, 'rpc'>,
98+
inputs: [...T],
99+
options?: RpcGetAccountsOptions
100+
): Promise<MaybeDeserializedAccounts<T>> {
101+
const publicKeys = inputs.map((input) =>
102+
toPublicKey(input.publicKey, false)
103+
);
104+
const maybeAccounts = await context.rpc.getAccounts(publicKeys, options);
105+
return maybeAccounts.map((maybeAccount, index) => {
106+
return maybeAccount.exists
107+
? inputs[index].deserialize(maybeAccount as RpcAccount)
108+
: null;
109+
}) as MaybeDeserializedAccounts<T>;
110+
}

test/packages/js/src/generated/accounts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* @see https://github.com/metaplex-foundation/kinobi
77
*/
88

9+
export * from './fetchHelpers';
910
export * from './accountWithPadding';
1011
export * from './accountWithPoddedTypes';
1112
export * from './candyMachine';

0 commit comments

Comments
 (0)