Skip to content

Commit

Permalink
Add InstructionByteDeltaNode (#135)
Browse files Browse the repository at this point in the history
* Add InstructionByteDeltaNode

* Update templates

* Refactor instructionRemainingAccounts fragment to match the byteDelta fragment

* Add changeset
  • Loading branch information
lorisleiva authored Jan 3, 2024
1 parent 9439055 commit cd243ea
Show file tree
Hide file tree
Showing 23 changed files with 284 additions and 208 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-socks-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/kinobi': minor
---

Add InstructionByteDeltaNode
34 changes: 34 additions & 0 deletions src/nodes/InstructionByteDeltaNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { isNode } from './Node';
import { ArgumentValueNode, ResolverValueNode } from './contextualValueNodes';
import { AccountLinkNode } from './linkNodes';
import { NumberValueNode } from './valueNodes';

export type InstructionByteDeltaNode = {
readonly kind: 'instructionByteDeltaNode';

// Children.
readonly value:
| NumberValueNode
| AccountLinkNode
| ArgumentValueNode
| ResolverValueNode;

// Data.
readonly withHeader: boolean;
readonly subtract?: boolean;
};

export function instructionByteDeltaNode(
value: InstructionByteDeltaNode['value'],
options: {
withHeader?: boolean;
subtract?: boolean;
} = {}
): InstructionByteDeltaNode {
return {
kind: 'instructionByteDeltaNode',
value,
withHeader: options.withHeader ?? !isNode(value, 'resolverValueNode'),
subtract: options.subtract,
};
}
14 changes: 4 additions & 10 deletions src/nodes/InstructionNode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import type { IdlInstruction } from '../idl';
import {
BytesCreatedOnChain,
InvalidKinobiTreeError,
MainCaseString,
mainCase,
} from '../shared';
import { InvalidKinobiTreeError, MainCaseString, mainCase } from '../shared';
import {
InstructionAccountNode,
instructionAccountNodeFromIdl,
Expand All @@ -14,6 +9,7 @@ import {
instructionArgumentNode,
instructionArgumentNodeFromIdl,
} from './InstructionArgumentNode';
import { InstructionByteDeltaNode } from './InstructionByteDeltaNode';
import { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode';
import { isNode } from './Node';
import { ProgramNode } from './ProgramNode';
Expand All @@ -30,9 +26,7 @@ export type InstructionNode = {
readonly extraArguments?: InstructionArgumentNode[];
readonly subInstructions?: InstructionNode[];
readonly remainingAccounts?: InstructionRemainingAccountsNode[];

// Children to-be.
readonly bytesCreatedOnChain?: BytesCreatedOnChain;
readonly byteDeltas?: InstructionByteDeltaNode[];

// Data.
readonly name: MainCaseString;
Expand Down Expand Up @@ -62,8 +56,8 @@ export function instructionNode(input: InstructionNodeInput): InstructionNode {
subInstructions: input.subInstructions,
idlName: input.idlName ?? input.name,
docs: input.docs ?? [],
bytesCreatedOnChain: input.bytesCreatedOnChain,
remainingAccounts: input.remainingAccounts,
byteDeltas: input.byteDeltas,
optionalAccountStrategy: input.optionalAccountStrategy ?? 'programId',
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/nodes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { DefinedTypeNode } from './DefinedTypeNode';
import type { ErrorNode } from './ErrorNode';
import type { InstructionAccountNode } from './InstructionAccountNode';
import type { InstructionArgumentNode } from './InstructionArgumentNode';
import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode';
import type { InstructionNode } from './InstructionNode';
import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode';
import type { PdaNode } from './PdaNode';
Expand All @@ -25,6 +26,7 @@ const REGISTERED_NODES = {
accountNode: {} as AccountNode,
instructionAccountNode: {} as InstructionAccountNode,
instructionArgumentNode: {} as InstructionArgumentNode,
instructionByteDeltaNode: {} as InstructionByteDeltaNode,
instructionNode: {} as InstructionNode,
instructionRemainingAccountsNode: {} as InstructionRemainingAccountsNode,
errorNode: {} as ErrorNode,
Expand Down
1 change: 1 addition & 0 deletions src/nodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './DefinedTypeNode';
export * from './ErrorNode';
export * from './InstructionAccountNode';
export * from './InstructionArgumentNode';
export * from './InstructionByteDeltaNode';
export * from './InstructionNode';
export * from './InstructionRemainingAccountsNode';
export * from './Node';
Expand Down
9 changes: 5 additions & 4 deletions src/renderers/js-experimental/asyncHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ export function hasAsyncFunction(
resolvedInputs: ResolvedInstructionInput[],
asyncResolvers: string[]
): boolean {
const hasBytesCreatedOnChainAsync =
instructionNode.bytesCreatedOnChain?.kind === 'resolver' &&
asyncResolvers.includes(instructionNode.bytesCreatedOnChain.name);
const hasByteDeltasAsync = (instructionNode.byteDeltas ?? []).some(
({ value }) =>
isNode(value, 'resolverValueNode') && asyncResolvers.includes(value.name)
);
const hasRemainingAccountsAsync = (
instructionNode.remainingAccounts ?? []
).some(
Expand All @@ -22,7 +23,7 @@ export function hasAsyncFunction(

return (
hasAsyncDefaultValues(resolvedInputs, asyncResolvers) ||
hasBytesCreatedOnChainAsync ||
hasByteDeltasAsync ||
hasRemainingAccountsAsync
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/js-experimental/fragments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export * from './accountType';
export * from './common';
export * from './instructionAccountMeta';
export * from './instructionAccountTypeParam';
export * from './instructionBytesCreatedOnChain';
export * from './instructionByteDelta';
export * from './instructionData';
export * from './instructionExtraArgs';
export * from './instructionFunctionHighLevel';
Expand Down
109 changes: 109 additions & 0 deletions src/renderers/js-experimental/fragments/instructionByteDelta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
InstructionByteDeltaNode,
InstructionNode,
assertIsNode,
isNode,
} from '../../../nodes';
import { camelCase } from '../../../shared';
import type { GlobalFragmentScope } from '../getRenderMapVisitor';
import { Fragment, fragment, mergeFragments } from './common';

export function getInstructionByteDeltaFragment(
scope: Pick<GlobalFragmentScope, 'nameApi' | 'asyncResolvers'> & {
instructionNode: InstructionNode;
useAsync: boolean;
}
): Fragment {
const { byteDeltas } = scope.instructionNode;
const fragments = (byteDeltas ?? []).flatMap((r) =>
getByteDeltaFragment(r, scope)
);
if (fragments.length === 0) return fragment('');
return mergeFragments(
fragments,
(r) =>
`// Bytes created or reallocated by the instruction.\n` +
`const byteDelta: number = [${r.join(',')}].reduce((a, b) => a + b, 0);`
);
}

function getByteDeltaFragment(
byteDelta: InstructionByteDeltaNode,
scope: Pick<GlobalFragmentScope, 'nameApi' | 'asyncResolvers'> & {
useAsync: boolean;
}
): Fragment[] {
const bytesFragment = ((): Fragment | null => {
if (isNode(byteDelta.value, 'numberValueNode')) {
return getNumberValueNodeFragment(byteDelta);
}
if (isNode(byteDelta.value, 'argumentValueNode')) {
return getArgumentValueNodeFragment(byteDelta);
}
if (isNode(byteDelta.value, 'accountLinkNode')) {
return getAccountLinkNodeFragment(byteDelta, scope);
}
if (isNode(byteDelta.value, 'resolverValueNode')) {
return getResolverValueNodeFragment(byteDelta, scope);
}
return null;
})();

if (bytesFragment === null) return [];

if (byteDelta.withHeader) {
bytesFragment
.mapRender((r) => `${r} + BASE_ACCOUNT_SIZE`)
.addImports('solanaAccounts', 'BASE_ACCOUNT_SIZE');
}

if (byteDelta.subtract) {
bytesFragment.mapRender((r) => `- (${r})`);
}

return [bytesFragment];
}

function getNumberValueNodeFragment(
byteDelta: InstructionByteDeltaNode
): Fragment {
assertIsNode(byteDelta.value, 'numberValueNode');
return fragment(byteDelta.value.number.toString());
}

function getArgumentValueNodeFragment(
byteDelta: InstructionByteDeltaNode
): Fragment {
assertIsNode(byteDelta.value, 'argumentValueNode');
const argumentName = camelCase(byteDelta.value.name);
return fragment(`Number(args.${argumentName})`);
}

function getAccountLinkNodeFragment(
byteDelta: InstructionByteDeltaNode,
scope: Pick<GlobalFragmentScope, 'nameApi'>
): Fragment {
assertIsNode(byteDelta.value, 'accountLinkNode');
const functionName = scope.nameApi.accountGetSizeFunction(
byteDelta.value.name
);
const importFrom = byteDelta.value.importFrom ?? 'generatedAccounts';
return fragment(`${functionName}()`).addImports(importFrom, functionName);
}

function getResolverValueNodeFragment(
byteDelta: InstructionByteDeltaNode,
scope: Pick<GlobalFragmentScope, 'nameApi' | 'asyncResolvers'> & {
useAsync: boolean;
}
): Fragment | null {
assertIsNode(byteDelta.value, 'resolverValueNode');
const isAsync = scope.asyncResolvers.includes(byteDelta.value.name);
if (!scope.useAsync && isAsync) return null;

const awaitKeyword = scope.useAsync && isAsync ? 'await ' : '';
const functionName = scope.nameApi.resolverFunction(byteDelta.value.name);
return fragment(`${awaitKeyword}${functionName}(resolverScope)`)
.addImports(byteDelta.value.importFrom ?? 'hooked', functionName)
.addFeatures(['instruction:resolverScopeVariable']);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export {{ 'async' if useAsync }} function {{ functionName }}<{{ typeParamsFragme
{% endif %}
);

{% if hasBytesCreatedOnChain %}
return Object.freeze({ ...instruction, bytesCreatedOnChain });
{% if hasByteDeltas %}
return Object.freeze({ ...instruction, byteDelta });
{% else %}
return instruction;
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
fragmentFromTemplate,
mergeFragments,
} from './common';
import { getInstructionBytesCreatedOnChainFragment } from './instructionBytesCreatedOnChain';
import { getInstructionByteDeltaFragment } from './instructionByteDelta';
import { getInstructionInputResolvedFragment } from './instructionInputResolved';
import { getInstructionInputTypeFragment } from './instructionInputType';
import { getInstructionRemainingAccountsFragment } from './instructionRemainingAccounts';
Expand Down Expand Up @@ -111,25 +111,20 @@ export function getInstructionFunctionHighLevelFragment(
const resolvedInputsFragment = getInstructionInputResolvedFragment(scope);
const remainingAccountsFragment =
getInstructionRemainingAccountsFragment(scope);
const bytesCreatedOnChainFragment =
getInstructionBytesCreatedOnChainFragment(scope);
const byteDeltaFragment = getInstructionByteDeltaFragment(scope);
const resolvedFragment = mergeFragments(
[
resolvedInputsFragment,
remainingAccountsFragment,
bytesCreatedOnChainFragment,
],
[resolvedInputsFragment, remainingAccountsFragment, byteDeltaFragment],
(renders) => renders.join('\n\n')
);
const hasRemainingAccounts = remainingAccountsFragment.render !== '';
const hasBytesCreatedOnChain = bytesCreatedOnChainFragment.render !== '';
const hasByteDeltas = byteDeltaFragment.render !== '';
const hasResolver = resolvedFragment.hasFeatures(
'instruction:resolverScopeVariable'
);
const getReturnType = (instructionType: string) => {
let returnType = instructionType;
if (hasBytesCreatedOnChain) {
returnType = `${returnType} & IInstructionWithBytesCreatedOnChain`;
if (hasByteDeltas) {
returnType = `${returnType} & IInstructionWithByteDelta`;
}
return useAsync ? `Promise<${returnType}>` : returnType;
};
Expand All @@ -156,7 +151,7 @@ export function getInstructionFunctionHighLevelFragment(
renamedArgs: renamedArgsText,
resolvedFragment,
hasRemainingAccounts,
hasBytesCreatedOnChain,
hasByteDeltas,
hasResolver,
useAsync,
getReturnType,
Expand All @@ -181,10 +176,8 @@ export function getInstructionFunctionHighLevelFragment(
.addImports('shared', ['getAccountMetasWithSigners', 'ResolvedAccount']);
}

if (hasBytesCreatedOnChain) {
functionFragment.addImports('shared', [
'IInstructionWithBytesCreatedOnChain',
]);
if (hasByteDeltas) {
functionFragment.addImports('shared', ['IInstructionWithByteDelta']);
}

return functionFragment;
Expand Down

This file was deleted.

Loading

0 comments on commit cd243ea

Please sign in to comment.