Skip to content

Commit

Permalink
Add InstructionRemainingAccountsNode (#134)
Browse files Browse the repository at this point in the history
* Add InstructionRemainingAccountsNode

* Update js renderer templates

* Add changeset
  • Loading branch information
lorisleiva authored Jan 3, 2024
1 parent 9432c41 commit 9439055
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 65 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-countries-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/kinobi': minor
---

Add InstructionRemainingAccountsNode
4 changes: 2 additions & 2 deletions src/nodes/InstructionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
BytesCreatedOnChain,
InvalidKinobiTreeError,
MainCaseString,
RemainingAccounts,
mainCase,
} from '../shared';
import {
Expand All @@ -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';
Expand All @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions src/nodes/InstructionRemainingAccountsNode.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
4 changes: 3 additions & 1 deletion src/nodes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,

Expand Down
1 change: 1 addition & 0 deletions src/nodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
21 changes: 14 additions & 7 deletions src/renderers/js-experimental/asyncHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { InstructionInputValueNode, InstructionNode } from '../../nodes';
import {
InstructionInputValueNode,
InstructionNode,
isNode,
} from '../../nodes';
import { ResolvedInstructionInput } from '../../visitors';

export function hasAsyncFunction(
instructionNode: InstructionNode,
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
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
@@ -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<GlobalFragmentScope, 'nameApi' | 'asyncResolvers'> & {
Expand All @@ -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<GlobalFragmentScope, 'nameApi' | 'asyncResolvers'> & {
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',
Expand All @@ -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];
}
12 changes: 7 additions & 5 deletions src/renderers/js/getRenderMapVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down Expand Up @@ -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)
);
}

Expand Down Expand Up @@ -620,6 +621,7 @@ export function getRenderMapVisitor(
hasResolvers,
hasResolvedArgs,
customData,
remainingAccounts,
})
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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 %}
20 changes: 0 additions & 20 deletions src/shared/RemainingAccounts.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
20 changes: 20 additions & 0 deletions src/visitors/identityVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
instructionAccountNode,
instructionArgumentNode,
instructionNode,
instructionRemainingAccountsNode,
mapEntryValueNode,
mapTypeNode,
mapValueNode,
Expand Down Expand Up @@ -145,6 +146,15 @@ export function identityVisitor<TNodeKind extends NodeKind = NodeKind>(
.map(visit(this))
.filter(removeNullAndAssertIsNodeFilter('instructionNode'))
: undefined,
remainingAccounts: node.remainingAccounts
? node.remainingAccounts
.map(visit(this))
.filter(
removeNullAndAssertIsNodeFilter(
'instructionRemainingAccountsNode'
)
)
: undefined,
});
};
}
Expand Down Expand Up @@ -174,6 +184,16 @@ export function identityVisitor<TNodeKind extends NodeKind = NodeKind>(
};
}

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);
Expand Down
8 changes: 8 additions & 0 deletions src/visitors/mergeVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function mergeVisitor<TReturn, TNodeKind extends NodeKind = NodeKind>(
...node.arguments.flatMap(visit(this)),
...(node.extraArguments ?? []).flatMap(visit(this)),
...(node.subInstructions ?? []).flatMap(visit(this)),
...(node.remainingAccounts ?? []).flatMap(visit(this)),
]);
};
}
Expand All @@ -75,6 +76,13 @@ export function mergeVisitor<TReturn, TNodeKind extends NodeKind = NodeKind>(
};
}

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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion test/testFile.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ kinobi.update(
defaultValue: k.arrayValueNode([]),
},
},
remainingAccounts: k.remainingAccountsFromArg('proof'),
remainingAccounts: [
k.instructionRemainingAccountsNode(k.argumentValueNode('proof')),
],
},
DeprecatedCreateReservationList: { name: 'CreateReservationList' },
Transfer: {
Expand Down

0 comments on commit 9439055

Please sign in to comment.