From 9439055406574408820c70f7e72e2460e7aa7c43 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 3 Jan 2024 18:27:36 +0000 Subject: [PATCH] Add InstructionRemainingAccountsNode (#134) * Add InstructionRemainingAccountsNode * Update js renderer templates * Add changeset --- .changeset/strong-countries-raise.md | 5 ++ src/nodes/InstructionNode.ts | 4 +- src/nodes/InstructionRemainingAccountsNode.ts | 32 ++++++++++++ src/nodes/Node.ts | 4 +- src/nodes/index.ts | 1 + src/renderers/js-experimental/asyncHelpers.ts | 21 +++++--- .../instructionRemainingAccounts.njk | 9 ++-- .../fragments/instructionRemainingAccounts.ts | 50 +++++++++++++++---- src/renderers/js/getRenderMapVisitor.ts | 12 +++-- .../instructionsPageRemainingAccounts.njk | 8 +-- src/shared/RemainingAccounts.ts | 20 -------- src/shared/index.ts | 1 - src/visitors/identityVisitor.ts | 20 ++++++++ src/visitors/mergeVisitor.ts | 8 +++ .../src/generated/instructions/dummy.ts | 14 +++--- test/testFile.cjs | 4 +- 16 files changed, 148 insertions(+), 65 deletions(-) create mode 100644 .changeset/strong-countries-raise.md create mode 100644 src/nodes/InstructionRemainingAccountsNode.ts delete mode 100644 src/shared/RemainingAccounts.ts diff --git a/.changeset/strong-countries-raise.md b/.changeset/strong-countries-raise.md new file mode 100644 index 000000000..6ef81916b --- /dev/null +++ b/.changeset/strong-countries-raise.md @@ -0,0 +1,5 @@ +--- +'@metaplex-foundation/kinobi': minor +--- + +Add InstructionRemainingAccountsNode diff --git a/src/nodes/InstructionNode.ts b/src/nodes/InstructionNode.ts index 063c86787..d3399a895 100644 --- a/src/nodes/InstructionNode.ts +++ b/src/nodes/InstructionNode.ts @@ -3,7 +3,6 @@ import { BytesCreatedOnChain, InvalidKinobiTreeError, MainCaseString, - RemainingAccounts, mainCase, } from '../shared'; import { @@ -15,6 +14,7 @@ import { instructionArgumentNode, instructionArgumentNodeFromIdl, } from './InstructionArgumentNode'; +import { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode'; import { isNode } from './Node'; import { ProgramNode } from './ProgramNode'; import { RootNode } from './RootNode'; @@ -29,10 +29,10 @@ export type InstructionNode = { readonly arguments: InstructionArgumentNode[]; readonly extraArguments?: InstructionArgumentNode[]; readonly subInstructions?: InstructionNode[]; + readonly remainingAccounts?: InstructionRemainingAccountsNode[]; // Children to-be. readonly bytesCreatedOnChain?: BytesCreatedOnChain; - readonly remainingAccounts?: RemainingAccounts; // Data. readonly name: MainCaseString; diff --git a/src/nodes/InstructionRemainingAccountsNode.ts b/src/nodes/InstructionRemainingAccountsNode.ts new file mode 100644 index 000000000..bfcaec07b --- /dev/null +++ b/src/nodes/InstructionRemainingAccountsNode.ts @@ -0,0 +1,32 @@ +import { ArgumentValueNode, ResolverValueNode } from './contextualValueNodes'; + +export type InstructionRemainingAccountsNode = { + readonly kind: 'instructionRemainingAccountsNode'; + + // Children. + readonly value: ArgumentValueNode | ResolverValueNode; + + // Data. + readonly isWritable?: boolean; + readonly isSigner?: boolean | 'either'; +}; + +export type InstructionRemainingAccountsNodeInput = Omit< + InstructionRemainingAccountsNode, + 'kind' +>; + +export function instructionRemainingAccountsNode( + value: ArgumentValueNode | ResolverValueNode, + options: { + isWritable?: boolean; + isSigner?: boolean | 'either'; + } = {} +): InstructionRemainingAccountsNode { + return { + kind: 'instructionRemainingAccountsNode', + value, + isWritable: options.isWritable, + isSigner: options.isSigner, + }; +} diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index dec6cac8c..c12ec306c 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -5,6 +5,7 @@ import type { ErrorNode } from './ErrorNode'; import type { InstructionAccountNode } from './InstructionAccountNode'; import type { InstructionArgumentNode } from './InstructionArgumentNode'; import type { InstructionNode } from './InstructionNode'; +import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode'; import type { PdaNode } from './PdaNode'; import type { ProgramNode } from './ProgramNode'; import type { RootNode } from './RootNode'; @@ -22,9 +23,10 @@ const REGISTERED_NODES = { programNode: {} as ProgramNode, pdaNode: {} as PdaNode, accountNode: {} as AccountNode, - instructionNode: {} as InstructionNode, instructionAccountNode: {} as InstructionAccountNode, instructionArgumentNode: {} as InstructionArgumentNode, + instructionNode: {} as InstructionNode, + instructionRemainingAccountsNode: {} as InstructionRemainingAccountsNode, errorNode: {} as ErrorNode, definedTypeNode: {} as DefinedTypeNode, diff --git a/src/nodes/index.ts b/src/nodes/index.ts index beb1e1e17..74b02a338 100644 --- a/src/nodes/index.ts +++ b/src/nodes/index.ts @@ -4,6 +4,7 @@ export * from './ErrorNode'; export * from './InstructionAccountNode'; export * from './InstructionArgumentNode'; export * from './InstructionNode'; +export * from './InstructionRemainingAccountsNode'; export * from './Node'; export * from './PdaNode'; export * from './ProgramNode'; diff --git a/src/renderers/js-experimental/asyncHelpers.ts b/src/renderers/js-experimental/asyncHelpers.ts index 04f5f8a4a..eabde9dbd 100644 --- a/src/renderers/js-experimental/asyncHelpers.ts +++ b/src/renderers/js-experimental/asyncHelpers.ts @@ -1,4 +1,8 @@ -import { InstructionInputValueNode, InstructionNode } from '../../nodes'; +import { + InstructionInputValueNode, + InstructionNode, + isNode, +} from '../../nodes'; import { ResolvedInstructionInput } from '../../visitors'; export function hasAsyncFunction( @@ -6,17 +10,20 @@ export function hasAsyncFunction( resolvedInputs: ResolvedInstructionInput[], asyncResolvers: string[] ): boolean { - const isBytesCreatedOnChainAsync = + const hasBytesCreatedOnChainAsync = instructionNode.bytesCreatedOnChain?.kind === 'resolver' && asyncResolvers.includes(instructionNode.bytesCreatedOnChain.name); - const isRemainingAccountAsync = - instructionNode.remainingAccounts?.kind === 'resolver' && - asyncResolvers.includes(instructionNode.remainingAccounts.name); + const hasRemainingAccountsAsync = ( + instructionNode.remainingAccounts ?? [] + ).some( + ({ value }) => + isNode(value, 'resolverValueNode') && asyncResolvers.includes(value.name) + ); return ( hasAsyncDefaultValues(resolvedInputs, asyncResolvers) || - isBytesCreatedOnChainAsync || - isRemainingAccountAsync + hasBytesCreatedOnChainAsync || + hasRemainingAccountsAsync ); } diff --git a/src/renderers/js-experimental/fragments/instructionRemainingAccounts.njk b/src/renderers/js-experimental/fragments/instructionRemainingAccounts.njk index acf297b30..face392ed 100644 --- a/src/renderers/js-experimental/fragments/instructionRemainingAccounts.njk +++ b/src/renderers/js-experimental/fragments/instructionRemainingAccounts.njk @@ -1,6 +1,5 @@ -// Remaining accounts. -{% if remainingAccounts.kind === 'arg' %} - const remainingAccounts: IAccountMeta[] = args.{{ remainingAccounts.name | camelCase }}.map((address) => ({ address, role: {{ "AccountRole.WRITABLE" if remainingAccounts.isWritable else "AccountRole.READONLY" }} })); -{% elif remainingAccounts.kind === 'resolver' %} - const remainingAccounts: IAccountMeta[] = {{ awaitKeyword }}{{ nameApi.resolverFunction(remainingAccounts.name) }}(resolverScope); +{% if remainingAccounts.value.kind === 'argumentValueNode' %} + args.{{ remainingAccounts.value.name | camelCase }}.map((address) => ({ address, role: {{ "AccountRole.WRITABLE" if remainingAccounts.isWritable else "AccountRole.READONLY" }} })) +{% elif remainingAccounts.value.kind === 'resolverValueNode' %} + {{ awaitKeyword }}{{ nameApi.resolverFunction(remainingAccounts.value.name) }}(resolverScope) {% endif %} diff --git a/src/renderers/js-experimental/fragments/instructionRemainingAccounts.ts b/src/renderers/js-experimental/fragments/instructionRemainingAccounts.ts index 141bfb203..0fb1377fe 100644 --- a/src/renderers/js-experimental/fragments/instructionRemainingAccounts.ts +++ b/src/renderers/js-experimental/fragments/instructionRemainingAccounts.ts @@ -1,6 +1,15 @@ -import { InstructionNode } from '../../../nodes'; +import { + InstructionNode, + InstructionRemainingAccountsNode, + isNode, +} from '../../../nodes'; import type { GlobalFragmentScope } from '../getRenderMapVisitor'; -import { Fragment, fragment, fragmentFromTemplate } from './common'; +import { + Fragment, + fragment, + fragmentFromTemplate, + mergeFragments, +} from './common'; export function getInstructionRemainingAccountsFragment( scope: Pick & { @@ -9,12 +18,29 @@ export function getInstructionRemainingAccountsFragment( } ): Fragment { const { remainingAccounts } = scope.instructionNode; - if (!remainingAccounts) return fragment(''); + const fragments = (remainingAccounts ?? []).flatMap((r) => + getSingleFragment(r, scope) + ); + if (fragments.length === 0) return fragment(''); + return mergeFragments( + fragments, + (r) => + `// Remaining accounts.\n` + + `const remainingAccounts: IAccountMeta[] = [...${r.join(', ...')}]` + ); +} +function getSingleFragment( + remainingAccounts: InstructionRemainingAccountsNode, + scope: Pick & { + instructionNode: InstructionNode; + useAsync: boolean; + } +): Fragment[] { const isAsync = - remainingAccounts?.kind === 'resolver' && - scope.asyncResolvers.includes(remainingAccounts.name); - if (!scope.useAsync && isAsync) return fragment(''); + isNode(remainingAccounts.value, 'resolverValueNode') && + scope.asyncResolvers.includes(remainingAccounts.value.name); + if (!scope.useAsync && isAsync) return []; const remainingAccountsFragment = fragmentFromTemplate( 'instructionRemainingAccounts.njk', @@ -25,14 +51,16 @@ export function getInstructionRemainingAccountsFragment( } ).addImports('solanaInstructions', ['IAccountMeta']); - if (remainingAccounts?.kind === 'arg') { + if (isNode(remainingAccounts.value, 'argumentValueNode')) { remainingAccountsFragment.addImports('solanaInstructions', ['AccountRole']); - } else if (remainingAccounts?.kind === 'resolver') { - const functionName = scope.nameApi.resolverFunction(remainingAccounts.name); + } else { + const functionName = scope.nameApi.resolverFunction( + remainingAccounts.value.name + ); remainingAccountsFragment - .addImports(remainingAccounts.importFrom, functionName) + .addImports(remainingAccounts.value.importFrom ?? 'hooked', functionName) .addFeatures(['instruction:resolverScopeVariable']); } - return remainingAccountsFragment; + return [remainingAccountsFragment]; } diff --git a/src/renderers/js/getRenderMapVisitor.ts b/src/renderers/js/getRenderMapVisitor.ts index 758f2b348..306adecba 100644 --- a/src/renderers/js/getRenderMapVisitor.ts +++ b/src/renderers/js/getRenderMapVisitor.ts @@ -461,8 +461,10 @@ export function getRenderMapVisitor( isNode(a.defaultValue, 'resolverValueNode') ); const hasByteResolver = node.bytesCreatedOnChain?.kind === 'resolver'; + const remainingAccounts = node.remainingAccounts?.[0] ?? undefined; const hasRemainingAccountsResolver = - node.remainingAccounts?.kind === 'resolver'; + remainingAccounts && + isNode(remainingAccounts.value, 'resolverValueNode'); const hasResolvers = hasArgResolvers || hasAccountResolvers || @@ -585,11 +587,10 @@ export function getRenderMapVisitor( } // Remaining accounts. - const { remainingAccounts } = node; - if (remainingAccounts?.kind === 'resolver') { + if (hasRemainingAccountsResolver) { imports.add( - remainingAccounts.importFrom, - camelCase(remainingAccounts.name) + remainingAccounts.value.importFrom ?? 'hooked', + camelCase(remainingAccounts.value.name) ); } @@ -620,6 +621,7 @@ export function getRenderMapVisitor( hasResolvers, hasResolvedArgs, customData, + remainingAccounts, }) ); }, diff --git a/src/renderers/js/templates/instructionsPageRemainingAccounts.njk b/src/renderers/js/templates/instructionsPageRemainingAccounts.njk index d54839e9b..1c84f1acb 100644 --- a/src/renderers/js/templates/instructionsPageRemainingAccounts.njk +++ b/src/renderers/js/templates/instructionsPageRemainingAccounts.njk @@ -1,9 +1,9 @@ -{% if instruction.remainingAccounts.kind === 'arg' %} +{% if remainingAccounts.value.kind === 'argumentValueNode' %} // Remaining Accounts. - const remainingAccounts = resolvedArgs.{{ instruction.remainingAccounts.name | camelCase }}.map((value, index) => ({ index, value, isWritable: {{ "true" if instruction.remainingAccounts.isWritable else "false" }} })); + const remainingAccounts = resolvedArgs.{{ remainingAccounts.value.name | camelCase }}.map((value, index) => ({ index, value, isWritable: {{ "true" if remainingAccounts.isWritable else "false" }} })); orderedAccounts.push(...remainingAccounts); -{% elif instruction.remainingAccounts.kind === 'resolver' %} +{% elif remainingAccounts.value.kind === 'resolverValueNode' %} // Remaining Accounts. - const remainingAccounts = {{ instruction.remainingAccounts.name | camelCase }}(context, resolvedAccounts, resolvedArgs, programId); + const remainingAccounts = {{ remainingAccounts.value.name | camelCase }}(context, resolvedAccounts, resolvedArgs, programId); orderedAccounts.push(...remainingAccounts); {% endif %} diff --git a/src/shared/RemainingAccounts.ts b/src/shared/RemainingAccounts.ts deleted file mode 100644 index f88756dbf..000000000 --- a/src/shared/RemainingAccounts.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ImportFrom } from './ImportFrom'; -import { MainCaseString, mainCase } from './utils'; - -export type RemainingAccounts = - | { kind: 'arg'; name: MainCaseString; isWritable: boolean } - | { kind: 'resolver'; name: MainCaseString; importFrom: ImportFrom }; - -export const remainingAccountsFromArg = ( - arg: string, - isWritable: boolean = false -): RemainingAccounts => ({ kind: 'arg', name: mainCase(arg), isWritable }); - -export const remainingAccountsFromResolver = ( - name: string, - importFrom: ImportFrom = 'hooked' -): RemainingAccounts => ({ - kind: 'resolver', - name: mainCase(name), - importFrom, -}); diff --git a/src/shared/index.ts b/src/shared/index.ts index 00441a44b..86c354441 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -6,7 +6,6 @@ export * from './ImportFrom'; export * from './LinkableDictionary'; export * from './NodeSelector'; export * from './NodeStack'; -export * from './RemainingAccounts'; export * from './RenderMap'; export * from './ValidatorBag'; export * from './errors'; diff --git a/src/visitors/identityVisitor.ts b/src/visitors/identityVisitor.ts index 30d5a3b3a..33e653cfd 100644 --- a/src/visitors/identityVisitor.ts +++ b/src/visitors/identityVisitor.ts @@ -27,6 +27,7 @@ import { instructionAccountNode, instructionArgumentNode, instructionNode, + instructionRemainingAccountsNode, mapEntryValueNode, mapTypeNode, mapValueNode, @@ -145,6 +146,15 @@ export function identityVisitor( .map(visit(this)) .filter(removeNullAndAssertIsNodeFilter('instructionNode')) : undefined, + remainingAccounts: node.remainingAccounts + ? node.remainingAccounts + .map(visit(this)) + .filter( + removeNullAndAssertIsNodeFilter( + 'instructionRemainingAccountsNode' + ) + ) + : undefined, }); }; } @@ -174,6 +184,16 @@ export function identityVisitor( }; } + if (castedNodeKeys.includes('instructionRemainingAccountsNode')) { + visitor.visitInstructionRemainingAccounts = + function visitInstructionRemainingAccounts(node) { + const value = visit(this)(node.value); + if (value === null) return null; + assertIsNode(value, ['argumentValueNode', 'resolverValueNode']); + return instructionRemainingAccountsNode(value, { ...node }); + }; + } + if (castedNodeKeys.includes('definedTypeNode')) { visitor.visitDefinedType = function visitDefinedType(node) { const type = visit(this)(node.type); diff --git a/src/visitors/mergeVisitor.ts b/src/visitors/mergeVisitor.ts index 8872f7dac..d2094e96e 100644 --- a/src/visitors/mergeVisitor.ts +++ b/src/visitors/mergeVisitor.ts @@ -54,6 +54,7 @@ export function mergeVisitor( ...node.arguments.flatMap(visit(this)), ...(node.extraArguments ?? []).flatMap(visit(this)), ...(node.subInstructions ?? []).flatMap(visit(this)), + ...(node.remainingAccounts ?? []).flatMap(visit(this)), ]); }; } @@ -75,6 +76,13 @@ export function mergeVisitor( }; } + if (castedNodeKeys.includes('instructionRemainingAccountsNode')) { + visitor.visitInstructionRemainingAccounts = + function visitInstructionRemainingAccounts(node) { + return merge(node, visit(this)(node.value)); + }; + } + if (castedNodeKeys.includes('definedTypeNode')) { visitor.visitDefinedType = function visitDefinedType(node) { return merge(node, visit(this)(node.type)); diff --git a/test/packages/js-experimental/src/generated/instructions/dummy.ts b/test/packages/js-experimental/src/generated/instructions/dummy.ts index 08b45e852..55a92481f 100644 --- a/test/packages/js-experimental/src/generated/instructions/dummy.ts +++ b/test/packages/js-experimental/src/generated/instructions/dummy.ts @@ -422,10 +422,9 @@ export async function getDummyInstructionAsync< } // Remaining accounts. - const remainingAccounts: IAccountMeta[] = args.proof.map((address) => ({ - address, - role: AccountRole.READONLY, - })); + const remainingAccounts: IAccountMeta[] = [ + ...args.proof.map((address) => ({ address, role: AccountRole.READONLY })), + ]; // Get account metas and signers. const accountMetas = getAccountMetasWithSigners( @@ -666,10 +665,9 @@ export function getDummyInstruction< } // Remaining accounts. - const remainingAccounts: IAccountMeta[] = args.proof.map((address) => ({ - address, - role: AccountRole.READONLY, - })); + const remainingAccounts: IAccountMeta[] = [ + ...args.proof.map((address) => ({ address, role: AccountRole.READONLY })), + ]; // Get account metas and signers. const accountMetas = getAccountMetasWithSigners( diff --git a/test/testFile.cjs b/test/testFile.cjs index da3a90d29..d621d19cf 100644 --- a/test/testFile.cjs +++ b/test/testFile.cjs @@ -152,7 +152,9 @@ kinobi.update( defaultValue: k.arrayValueNode([]), }, }, - remainingAccounts: k.remainingAccountsFromArg('proof'), + remainingAccounts: [ + k.instructionRemainingAccountsNode(k.argumentValueNode('proof')), + ], }, DeprecatedCreateReservationList: { name: 'CreateReservationList' }, Transfer: {