Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion src/renderers/js/getRenderMapVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -214,8 +215,21 @@ 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,
})
);
}
if (instructionsToExport.length > 0) {
map.add(
Expand Down Expand Up @@ -283,6 +297,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;
},
Expand Down
3 changes: 3 additions & 0 deletions src/renderers/js/templates/accountsIndex.njk
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export * from './{{ account.name | camelCase }}';
{% else %}
export default {};
{% endfor %}
{% for programName in programsWithAccountDiscriminators %}
export * from './{{ programName }}Helpers';
{% endfor %}
{% endblock %}
70 changes: 70 additions & 0 deletions src/renderers/js/templates/accountsProgramHelpers.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{% 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<Context, 'rpc'>,
publicKeys: Array<PublicKey | Pda>,
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<Context, 'rpc'>,
publicKeys: Array<PublicKey | Pda>,
options?: RpcGetAccountsOptions,
): Promise<({{ programPascalName }}Account | null)[]> {
const maybeAccounts = await context.rpc.getAccounts(
publicKeys.map((key) => toPublicKey(key, false)),
options,
);
return maybeAccounts.map((maybeAccount) => {
return maybeAccount.exists
? deserialize{{ programPascalName }}Account(maybeAccount as RpcAccount)
: null;
});
}
{% endblock %}
4 changes: 2 additions & 2 deletions src/visitors/getUniqueHashStringVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function getUniqueHashStringVisitor(
): Visitor<string> {
const removeDocs = options.removeDocs ?? false;
if (!removeDocs) {
return staticVisitor((node) => stringify(node));
return staticVisitor((node) => stringify(node)) as Visitor<string>;
}
return mapVisitor(removeDocsVisitor(), (node) => stringify(node));
return mapVisitor(removeDocsVisitor(), (node) => stringify(node)) as Visitor<string>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't ever need to cast if the types are correct. (we should make the types correct)

}
3 changes: 3 additions & 0 deletions test/packages/js/src/generated/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ export * from './reservationListV1';
export * from './reservationListV2';
export * from './tokenOwnedEscrow';
export * from './useAuthorityRecord';
export * from './mplCandyMachineCoreHelpers';
export * from './mplTokenAuthRulesHelpers';
export * from './mplTokenMetadataHelpers';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* 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<Context, 'rpc'>,
publicKeys: Array<PublicKey | Pda>,
options?: RpcGetAccountsOptions
): Promise<MplCandyMachineCoreAccount[]> {
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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

context: Pick<Context, 'rpc'>,
publicKeys: Array<PublicKey | Pda>,
options?: RpcGetAccountsOptions
): Promise<(MplCandyMachineCoreAccount | null)[]> {
const maybeAccounts = await context.rpc.getAccounts(
publicKeys.map((key) => toPublicKey(key, false)),
options
);
return maybeAccounts.map((maybeAccount) => {
return maybeAccount.exists
? deserializeMplCandyMachineCoreAccount(maybeAccount as RpcAccount)
: null;
});
}
Original file line number Diff line number Diff line change
@@ -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 { 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<Context, 'rpc'>,
publicKeys: Array<PublicKey | Pda>,
options?: RpcGetAccountsOptions
): Promise<MplTokenAuthRulesAccount[]> {
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<Context, 'rpc'>,
publicKeys: Array<PublicKey | Pda>,
options?: RpcGetAccountsOptions
): Promise<(MplTokenAuthRulesAccount | null)[]> {
const maybeAccounts = await context.rpc.getAccounts(
publicKeys.map((key) => toPublicKey(key, false)),
options
);
return maybeAccounts.map((maybeAccount) => {
return maybeAccount.exists
? deserializeMplTokenAuthRulesAccount(maybeAccount as RpcAccount)
: null;
});
}
Loading