Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 21 additions & 9 deletions src/fragments/instructionFunction.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { InstructionNode, isNode, PdaSeedValueNode, RootNode } from '@codama/nodes';
import { InstructionNode, isNode, pascalCase, PdaSeedValueNode, RootNode } from '@codama/nodes';
import { findProgramNodeFromPath, getLastNodeFromPath, NodePath } from '@codama/visitors-core';

import { createFragment, Fragment, RenderScope } from '../utils';
import { getBuiltinProgramAddress } from '../utils/builtinPrograms';
import { generatePdaSeeds } from '../utils/pda';

export function getInstructionFunctionFragment(
scope: Pick<RenderScope, 'nameApi' | 'packageName' | 'programName'> & {
Expand All @@ -29,6 +28,7 @@ export function getInstructionFunctionFragment(
const hasArguments = instructionNode.arguments.length > 0;

const params: string[] = [];
const pdaImports = new Set<string>();

if (hasAccounts) {
instructionNode.accounts.forEach(account => {
Expand All @@ -50,7 +50,7 @@ export function getInstructionFunctionFragment(
)?.value;

if (valueSeed && isNode(valueSeed, 'argumentValueNode')) {
canAutoderivePda = false;
canAutoderivePda = true;
break;
}
}
Expand Down Expand Up @@ -99,7 +99,7 @@ export function getInstructionFunctionFragment(
)?.value;

if (valueSeed && isNode(valueSeed, 'argumentValueNode')) {
canAutoderivePda = false;
canAutoderivePda = true;
break;
}
}
Expand All @@ -110,11 +110,23 @@ export function getInstructionFunctionFragment(
if (hasDefaultPda && canAutoderivePda) {
const pdaValue = account.defaultValue;
if (isNode(pdaValue.pda, 'pdaNode')) {
const seeds = generatePdaSeeds(pdaValue.pda, pdaValue.seeds, nameApi);
const resolvePdaFunctionName = `derive${pascalCase(pdaValue.pda.name)}Pda`;

const params: string[] = [];

pdaValue.seeds?.forEach(seed => {
if (isNode(seed.value, 'accountValueNode')) {
params.push(`${seed.value.name}`);
} else if (isNode(seed.value, 'argumentValueNode')) {
params.push(`data.${seed.value.name}`);
}
});

pdaImports.add(`package:${scope.packageName}/${scope.programName}/pdas/${pdaValue.pda.name}.dart`);

pdaResolutions.push(
` final resolved${accountName.charAt(0).toUpperCase() + accountName.slice(1)} = ${accountName} ?? ` +
`await Ed25519HDPublicKey.findProgramAddress(\n seeds: [${seeds.join(', ')}],\n programId: programId ?? Ed25519HDPublicKey.fromBase58(${rootProgramClassName}),\n );`,
`await ${resolvePdaFunctionName}(${params.join(', ')});`,
);
}
} else if (builtinAddress) {
Expand Down Expand Up @@ -146,7 +158,7 @@ export function getInstructionFunctionFragment(
)?.value;

if (valueSeed && isNode(valueSeed, 'argumentValueNode')) {
canAutoderivePda = false;
canAutoderivePda = true;
break;
}
}
Expand Down Expand Up @@ -190,7 +202,7 @@ export function getInstructionFunctionFragment(
const valueSeed = pdaValue.seeds?.find((s: PdaSeedValueNode) => s.name === seed.name)?.value;

if (valueSeed && isNode(valueSeed, 'argumentValueNode')) {
canAutoderive = false;
canAutoderive = true;
break;
}
}
Expand All @@ -216,7 +228,7 @@ ${asyncModifier}Instruction${asyncSuffix} ${functionName}(${parameterList}) ${as
${functionBody}
}`;

const imports: Set<string> = new Set([]);
const imports = new Set(['package:borsh_annotation_extended/borsh_annotation_extended.dart', ...pdaImports]);
if (rootNode) {
imports.add(`package:${scope.packageName}/${scope.programName}/programs/${rootNode.program.name}.dart`);
}
Expand Down
31 changes: 16 additions & 15 deletions src/utils/pda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { CamelCaseString, isNode, PdaNode, PdaSeedValueNode, StandaloneValueNode

import { Fragment } from './fragment';
import { RenderScope } from './options';
import { generateDartSeedSerializationCode, getTypeInfo } from './types';

/**
* Generates Dart code for PDA seeds based on a PDA node and its seed values
*/
export function generatePdaSeeds(
pdaNode: PdaNode,
pdaSeedValues: PdaSeedValueNode[] | undefined,
nameApi: RenderScope['nameApi'],
scope: RenderScope,
): string[] {
return pdaNode.seeds.map(seed => {
if (isNode(seed, 'constantPdaSeedNode')) {
Expand All @@ -20,12 +21,10 @@ export function generatePdaSeeds(
return `programId.bytes`;
}
if (isNode(seed.value, 'bytesValueNode')) {
// Convert byte array to Uint8List
const byteArray = Array.from(Buffer.from(seed.value.data, 'hex'));
return `Uint8List.fromList([${byteArray.join(', ')}])`;
}
if (seed.value.kind === 'arrayValueNode' && seed.value.items) {
// Handle array of bytes like [108, 101, 32, 115, 101, 101, 100]
const bytes = seed.value.items.map((item: StandaloneValueNode) =>
isNode(item, 'numberValueNode') ? item.number : 0,
);
Expand All @@ -35,12 +34,16 @@ export function generatePdaSeeds(
if (isNode(seed, 'variablePdaSeedNode')) {
const valueSeed = pdaSeedValues?.find((s: PdaSeedValueNode) => s.name === seed.name)?.value;

if (valueSeed && (isNode(valueSeed, 'accountValueNode') || isNode(valueSeed, 'argumentValueNode'))) {
if (valueSeed && isNode(valueSeed, 'accountValueNode')) {
const paramName = valueSeed.name;
return `${paramName}.toByteArray()`;
}
if (valueSeed && isNode(valueSeed, 'argumentValueNode')) {
const fieldSerializationCode = generateDartSeedSerializationCode(seed, scope.nameApi);
return fieldSerializationCode;
}

const seedName = nameApi.instructionField(seed.name);
const seedName = scope.nameApi.instructionField(seed.name);
return `${seedName}.toByteArray()`;
}
return 'utf8.encode("fallback")';
Expand All @@ -54,28 +57,26 @@ export function createInlinePdaFile(
accountName: string,
pdaNode: PdaNode,
pdaSeedValues: PdaSeedValueNode[] | undefined,
nameApi: RenderScope['nameApi'],
programPublicKey: string | undefined,
programName: string | undefined,
packageName: string,
scope: Pick<RenderScope, 'definedTypes' | 'nameApi' | 'packageName' | 'programName'>,
asPage: <TFragment extends Fragment | undefined>(fragment: TFragment) => TFragment,
): Fragment | undefined {
const { nameApi, programName, packageName } = scope;
const functionName = `derive${accountName.charAt(0).toUpperCase() + accountName.slice(1)}Pda`;
const seeds = generatePdaSeeds(pdaNode, pdaSeedValues, nameApi);
const seeds = generatePdaSeeds(pdaNode, pdaSeedValues, scope);

const parameters: string[] = [];
let programClassName: string = '';
if (programName) {
programClassName = nameApi.programType(programName as CamelCaseString);
}

programClassName = nameApi.programType(programName as CamelCaseString);

pdaNode.seeds.forEach(seed => {
if (isNode(seed, 'variablePdaSeedNode')) {
const valueSeed = pdaSeedValues?.find((s: PdaSeedValueNode) => s.name === seed.name)?.value;
if (valueSeed && (isNode(valueSeed, 'accountValueNode') || isNode(valueSeed, 'argumentValueNode'))) {
// This is either an account or argument parameter
const paramName = valueSeed.name;
parameters.push(`Ed25519HDPublicKey ${paramName}`);
const dartType = getTypeInfo(seed.type, scope).dartType;
parameters.push(`${dartType} ${paramName}`);
}
}
});
Expand All @@ -96,7 +97,7 @@ Future<Ed25519HDPublicKey> ${functionName}(${parameterList}) async {
);
}`;

const imports = new Set(['dart:typed_data', 'package:borsh_annotation_extended/borsh_annotation_extended.dart']);
const imports = new Set(['dart:convert', 'package:borsh_annotation_extended/borsh_annotation_extended.dart']);
if (programClassName) {
imports.add(`package:${packageName}/${programName}/programs/${programName}.dart`);
}
Expand Down
6 changes: 1 addition & 5 deletions src/utils/pubspec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// Constants for Git-based dependencies
const REPO_URL = 'https://github.com/vlady-kotsev/borsh_annotation_extended.git';
const REPO_REF = 'b6b2c80d3b198fc2af9fc74d78fcdc86c23ed7cd';

export function generatePubspec(
packageName: string,
options: {
Expand Down Expand Up @@ -48,7 +44,7 @@ export function generatePubspec(
sections.push(' borsh: 0.3.2');

// Borsh annotation dependency
sections.push(' borsh_annotation_extended:', ' git:', ` url: ${REPO_URL}`, ` ref: ${REPO_REF}`);
sections.push(' borsh_annotation_extended: ^0.0.1');

// Solana dependency
sections.push(' solana: 0.31.2');
Expand Down
37 changes: 37 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
resolveNestedTypeNode,
SizePrefixTypeNode,
TypeNode,
VariablePdaSeedNode,
} from '@codama/nodes';

import { getBEnumAnnotation } from '../fragments/enumType';
Expand Down Expand Up @@ -346,3 +347,39 @@ function getSizePrefixTypeInfo(node: SizePrefixTypeNode): TypeInfo {
imports: [],
};
}

export function generateDartSeedSerializationCode(seed: VariablePdaSeedNode, nameApi: NameApi): string {
const argType = seed.type;
const fieldName = nameApi.instructionField(seed.name);

return generateSerializationForType(argType, fieldName);
}

function generateSerializationForType(typeNode: TypeNode, fieldName: string): string {
switch (typeNode.kind) {
case 'sizePrefixTypeNode':
case 'fixedSizeTypeNode':
return generateSerializationForType(resolveNestedTypeNode(typeNode.type), fieldName);

case 'stringTypeNode':
return `Uint8List.fromList(utf8.encode(${fieldName}))`;

case 'bytesTypeNode':
return fieldName;

case 'numberTypeNode': {
const numberType = typeNode as NumberTypeNode;
return `(() {
final extWriter = ExtendedBinaryWriter.fromBinaryWriter(BinaryWriter());
extWriter.write${numberType.format.toUpperCase()}(${fieldName});
return extWriter.toArray();
})()`;
}

case 'publicKeyTypeNode':
return `${fieldName}.bytes`;

default:
return `${fieldName}.toBorsh()`;
}
}
4 changes: 1 addition & 3 deletions src/visitors/getRenderMapVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,8 @@ export function getRenderMapVisitor(
account.name,
account.defaultValue.pda,
account.defaultValue.seeds,
renderScope.nameApi,
programPublicKey,
programName,
packageName,
renderScope,
asPage,
);
if (pdaFile) {
Expand Down