Skip to content

Commit

Permalink
Remove non-signer function overloads in JS experimental (#182)
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva authored Mar 21, 2024
1 parent dc8dea7 commit 7af4ce6
Show file tree
Hide file tree
Showing 87 changed files with 2,869 additions and 17,332 deletions.
5 changes: 5 additions & 0 deletions .changeset/six-gorillas-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@metaplex-foundation/kinobi": minor
---

Remove non-signer function overloads in JS experimental renderer
5 changes: 5 additions & 0 deletions .changeset/tough-bags-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@metaplex-foundation/kinobi": patch
---

Use generated address constant in instruction pages of JS experimental renderer
3 changes: 1 addition & 2 deletions src/renderers/js-experimental/fragments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export * from './instructionAccountTypeParam';
export * from './instructionByteDelta';
export * from './instructionData';
export * from './instructionExtraArgs';
export * from './instructionFunctionHighLevel';
export * from './instructionFunctionLowLevel';
export * from './instructionFunction';
export * from './instructionInputDefault';
export * from './instructionInputResolved';
export * from './instructionInputType';
Expand Down
15 changes: 5 additions & 10 deletions src/renderers/js-experimental/fragments/instructionAccountMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { pascalCase } from '../../../shared';
import { Fragment, fragment } from './common';

export function getInstructionAccountMetaFragment(
instructionAccountNode: InstructionAccountNode,
withSigners: boolean
instructionAccountNode: InstructionAccountNode
): Fragment {
const typeParam = `TAccount${pascalCase(instructionAccountNode.name)}`;

Expand All @@ -14,23 +13,19 @@ export function getInstructionAccountMetaFragment(
instructionAccountNode.isWritable
) {
return fragment(
withSigners
? `WritableSignerAccount<${typeParam}> & IAccountSignerMeta<${typeParam}>`
: `WritableSignerAccount<${typeParam}>`
`WritableSignerAccount<${typeParam}> & IAccountSignerMeta<${typeParam}>`
)
.addImports('solanaInstructions', ['WritableSignerAccount'])
.addImports('solanaSigners', withSigners ? ['IAccountSignerMeta'] : []);
.addImports('solanaSigners', ['IAccountSignerMeta']);
}

// Readonly, signer.
if (instructionAccountNode.isSigner === true) {
return fragment(
withSigners
? `ReadonlySignerAccount<${typeParam}> & IAccountSignerMeta<${typeParam}>`
: `ReadonlySignerAccount<${typeParam}>`
`ReadonlySignerAccount<${typeParam}> & IAccountSignerMeta<${typeParam}>`
)
.addImports('solanaInstructions', ['ReadonlySignerAccount'])
.addImports('solanaSigners', withSigners ? ['IAccountSignerMeta'] : []);
.addImports('solanaSigners', ['IAccountSignerMeta']);
}

// Writable, non-signer or optional signer.
Expand Down
62 changes: 62 additions & 0 deletions src/renderers/js-experimental/fragments/instructionFunction.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{{ inputTypeFragment }}

export {{ 'async' if useAsync }} function {{ functionName }}<{{ typeParamsFragment }}>(input: {{ inputTypeCallFragment }}): {{ getReturnType(instructionTypeFragment) }} {
// Program address.
const programAddress = {{ programAddressConstant }};

{% if hasAccounts %}
// Original accounts.
const originalAccounts = {
{% for account in instruction.accounts %}
{{ account.name | camelCase }}: { value: input.{{ account.name | camelCase }} ?? null, isWritable: {{ "true" if account.isWritable else "false" }} },
{% endfor %}
};
const accounts = originalAccounts as Record<keyof typeof originalAccounts, ResolvedAccount>;
{% endif %}

{% if hasAnyArgs %}
// Original args.
const args = { ...input, {{ renamedArgs }} };
{% endif %}

{% if hasResolver %}
// Resolver scope.
const resolverScope = { programAddress{{ ', accounts' if hasAccounts }}{{ ', args' if hasAnyArgs }} };
{% endif %}

{{ resolvedFragment }}

{% if hasAccounts %}
const getAccountMeta = getAccountMetaFactory(programAddress, '{{ 'omitted' if instruction.optionalAccountStrategy === 'omitted' else 'programId' }}');
{% endif %}
const instruction = {
{%- if hasAccounts -%}
accounts: [
{% for account in instruction.accounts %}
getAccountMeta(accounts.{{ account.name | camelCase }}),
{% endfor %}
{% if hasRemainingAccounts %}
...remainingAccounts,
{% endif %}
]
{%- if hasLegacyOptionalAccounts -%}
.filter(<T>(x: T | undefined): x is T => x !== undefined)
{% endif %}
,
{%- elif hasRemainingAccounts -%}
accounts: remainingAccounts,
{% endif %}
programAddress,
{% if hasDataArgs %}
data: {{ encoderFunction }}.encode(args as {{ argsTypeFragment }}),
{% elif hasData %}
data: {{ encoderFunction }}.encode({}),
{% endif %}
} as {{ instructionTypeFragment }};

{% if hasByteDeltas %}
return Object.freeze({ ...instruction, byteDelta });
{% else %}
return instruction;
{% endif %}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getInstructionInputResolvedFragment } from './instructionInputResolved'
import { getInstructionInputTypeFragment } from './instructionInputType';
import { getInstructionRemainingAccountsFragment } from './instructionRemainingAccounts';

export function getInstructionFunctionHighLevelFragment(
export function getInstructionFunctionFragment(
scope: Pick<
GlobalFragmentScope,
'nameApi' | 'asyncResolvers' | 'valueNodeVisitor' | 'customInstructionData'
Expand Down Expand Up @@ -50,6 +50,10 @@ export function getInstructionFunctionHighLevelFragment(

const customData = customInstructionData.get(instructionNode.name);
const hasAccounts = instructionNode.accounts.length > 0;
const hasLegacyOptionalAccounts =
instructionNode.optionalAccountStrategy === 'omitted' &&
instructionNode.accounts.some((account) => account.isOptional);
const hasData = !!customData || instructionNode.arguments.length > 0;
const hasDataArgs =
!!customData ||
instructionNode.arguments.filter(
Expand All @@ -61,49 +65,34 @@ export function getInstructionFunctionHighLevelFragment(
).length > 0;
const hasAnyArgs = hasDataArgs || hasExtraArgs;
const instructionDataName = nameApi.instructionDataType(instructionNode.name);
const programAddressConstant = nameApi.programAddressConstant(
programNode.name
);
const encoderFunction = customData
? dataArgsManifest.encoder.render
: `${nameApi.encoderFunction(instructionDataName)}()`;
const argsTypeFragment = fragment(
customData
? dataArgsManifest.looseType.render
: nameApi.dataArgsType(instructionDataName)
);
if (customData) {
argsTypeFragment.mergeImportsWith(dataArgsManifest.looseType);
argsTypeFragment.mergeImportsWith(
dataArgsManifest.looseType,
dataArgsManifest.encoder
);
}

const functionName = useAsync
? nameApi.instructionAsyncFunction(instructionNode.name)
: nameApi.instructionSyncFunction(instructionNode.name);
const lowLevelFunctionName = nameApi.instructionRawFunction(
instructionNode.name
);

const typeParamsFragment = getTypeParams(instructionNode, programNode);
const instructionTypeFragment = getInstructionType({
...scope,
withSigners: false,
});
const instructionTypeWithSignersFragment = getInstructionType({
...scope,
withSigners: true,
});
const typeParamsFragment = getTypeParams(instructionNode);
const instructionTypeFragment = getInstructionType(scope);

// Input.
const inputTypeFragment = getInstructionInputTypeFragment({
...scope,
withSigners: false,
});
const inputTypeWithSignersFragment = getInstructionInputTypeFragment({
...scope,
withSigners: true,
});
const inputTypeCallFragment = getInputTypeCall({
...scope,
withSigners: false,
});
const inputTypeCallWithSignersFragment = getInputTypeCall({
...scope,
withSigners: true,
});
const inputTypeFragment = getInstructionInputTypeFragment(scope);
const inputTypeCallFragment = getInputTypeCall(scope);
const renamedArgsText = [...renamedArgs.entries()]
.map(([k, v]) => `${k}: input.${v}`)
.join(', ');
Expand All @@ -129,51 +118,45 @@ export function getInstructionFunctionHighLevelFragment(
return useAsync ? `Promise<${returnType}>` : returnType;
};

const functionFragment = fragmentFromTemplate(
'instructionFunctionHighLevel.njk',
{
instruction: instructionNode,
program: programNode,
hasAccounts,
hasDataArgs,
hasExtraArgs,
hasAnyArgs,
argsTypeFragment,
functionName,
lowLevelFunctionName,
typeParamsFragment,
instructionTypeFragment,
instructionTypeWithSignersFragment,
inputTypeFragment,
inputTypeWithSignersFragment,
inputTypeCallFragment,
inputTypeCallWithSignersFragment,
renamedArgs: renamedArgsText,
resolvedFragment,
hasRemainingAccounts,
hasByteDeltas,
hasResolver,
useAsync,
getReturnType,
}
)
const functionFragment = fragmentFromTemplate('instructionFunction.njk', {
instruction: instructionNode,
programAddressConstant,
hasAccounts,
hasLegacyOptionalAccounts,
hasData,
hasDataArgs,
hasExtraArgs,
hasAnyArgs,
encoderFunction,
argsTypeFragment,
functionName,
typeParamsFragment,
instructionTypeFragment,
inputTypeFragment,
inputTypeCallFragment,
renamedArgs: renamedArgsText,
resolvedFragment,
hasRemainingAccounts,
hasByteDeltas,
hasResolver,
useAsync,
getReturnType,
})
.mergeImportsWith(
typeParamsFragment,
instructionTypeFragment,
instructionTypeWithSignersFragment,
inputTypeFragment,
inputTypeWithSignersFragment,
inputTypeCallFragment,
inputTypeCallWithSignersFragment,
resolvedFragment,
argsTypeFragment
)
.addImports('generatedPrograms', [programAddressConstant])
.addImports('solanaAddresses', ['Address']);

if (hasAccounts) {
functionFragment
.addImports('solanaInstructions', ['IAccountMeta'])
.addImports('shared', ['getAccountMetasWithSigners', 'ResolvedAccount']);
.addImports('shared', ['getAccountMetaFactory', 'ResolvedAccount']);
}

if (hasByteDeltas) {
Expand All @@ -183,33 +166,31 @@ export function getInstructionFunctionHighLevelFragment(
return functionFragment;
}

function getTypeParams(
instructionNode: InstructionNode,
programNode: ProgramNode
): Fragment {
const typeParams = [
...instructionNode.accounts.map(
(account) => `TAccount${pascalCase(account.name)} extends string`
),
`TProgram extends string = "${programNode.publicKey}"`,
];
function getTypeParams(instructionNode: InstructionNode): Fragment {
const typeParams = instructionNode.accounts.map(
(account) => `TAccount${pascalCase(account.name)} extends string`
);
return fragment(typeParams.filter((x) => !!x).join(', '));
}

function getInstructionType(scope: {
instructionNode: InstructionNode;
withSigners: boolean;
programNode: ProgramNode;
nameApi: NameApi;
}): Fragment {
const { instructionNode, withSigners, nameApi } = scope;
const instructionTypeName = withSigners
? nameApi.instructionWithSignersType(instructionNode.name)
: nameApi.instructionType(instructionNode.name);
const { instructionNode, programNode, nameApi } = scope;
const instructionTypeName = nameApi.instructionType(instructionNode.name);
const programAddressConstant = nameApi.programAddressConstant(
programNode.name
);
const programAddressFragment = fragment(
`typeof ${programAddressConstant}`
).addImports('generatedPrograms', [programAddressConstant]);
const accountTypeParamsFragments = instructionNode.accounts.map((account) => {
const typeParam = `TAccount${pascalCase(account.name)}`;
const camelName = camelCase(account.name);

if (account.isSigner === 'either' && withSigners) {
if (account.isSigner === 'either') {
const signerRole = account.isWritable
? 'WritableSignerAccount'
: 'ReadonlySignerAccount';
Expand All @@ -226,25 +207,20 @@ function getInstructionType(scope: {
});

return mergeFragments(
[fragment('TProgram'), ...accountTypeParamsFragments],
[programAddressFragment, ...accountTypeParamsFragments],
(renders) => renders.join(', ')
).mapRender((r) => `${instructionTypeName}<${r}>`);
}

function getInputTypeCall(scope: {
instructionNode: InstructionNode;
withSigners: boolean;
useAsync: boolean;
nameApi: NameApi;
}): Fragment {
const { instructionNode, withSigners, useAsync, nameApi } = scope;
const syncInputTypeName = withSigners
? nameApi.instructionSyncInputWithSignersType(instructionNode.name)
const { instructionNode, useAsync, nameApi } = scope;
const inputTypeName = useAsync
? nameApi.instructionAsyncInputType(instructionNode.name)
: nameApi.instructionSyncInputType(instructionNode.name);
const asyncInputTypeName = withSigners
? nameApi.instructionAsyncInputWithSignersType(instructionNode.name)
: nameApi.instructionAsyncInputType(instructionNode.name);
const inputTypeName = useAsync ? asyncInputTypeName : syncInputTypeName;
if (instructionNode.accounts.length === 0) return fragment(inputTypeName);
const accountTypeParams = instructionNode.accounts
.map((account) => `TAccount${pascalCase(account.name)}`)
Expand Down
Loading

0 comments on commit 7af4ce6

Please sign in to comment.