diff --git a/packages/renderers-rust-cpi/.gitignore b/packages/renderers-rust-cpi/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/packages/renderers-rust-cpi/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/renderers-rust-cpi/.prettierignore b/packages/renderers-rust-cpi/.prettierignore new file mode 100644 index 000000000..ebf37de71 --- /dev/null +++ b/packages/renderers-rust-cpi/.prettierignore @@ -0,0 +1,5 @@ +dist/ +test/e2e/ +test-ledger/ +target/ +CHANGELOG.md diff --git a/packages/renderers-rust-cpi/LICENSE b/packages/renderers-rust-cpi/LICENSE new file mode 100644 index 000000000..2db0266e0 --- /dev/null +++ b/packages/renderers-rust-cpi/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Codama + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/renderers-rust-cpi/README.md b/packages/renderers-rust-cpi/README.md new file mode 100644 index 000000000..5d2479c5c --- /dev/null +++ b/packages/renderers-rust-cpi/README.md @@ -0,0 +1,134 @@ +# Codama ➤ Renderers ➤ Pinocchio + +[![npm][npm-image]][npm-url] +[![npm-downloads][npm-downloads-image]][npm-url] + +[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/renderers-rust.svg?style=flat +[npm-image]: https://img.shields.io/npm/v/@codama/renderers-rust.svg?style=flat&label=%40codama%2Frenderers-pinocchio +[npm-url]: https://www.npmjs.com/package/@codama/renderers-pinocchio + +This package generates Pinocchio-based Rust clients from your Codama IDLs. + +## Installation + +```sh +pnpm install @codama/renderers-pinocchio +``` + +> [!NOTE] +> This package is **not** included in the main [`codama`](../library) package. +> +> However, note that the [`renderers`](../renderers) package re-exports the `renderVisitor` function of this package as `renderRustVisitor`. + +## Usage + +Once you have a Codama IDL, you can use the `renderVisitor` of this package to generate Rust clients. You will need to provide the base directory where the generated files will be saved and an optional set of options to customize the output. + +```ts +// node ./codama.mjs +import { renderVisitor } from '@codama/renderers-pinocchio'; + +const pathToGeneratedFolder = path.join(__dirname, 'clients', 'pinocchio', 'src', 'generated'); +const options = {}; // See below. +codama.accept(renderVisitor(pathToGeneratedFolder, options)); +``` + +## Options + +The `renderVisitor` accepts the following options. + +| Name | Type | Default | Description | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `deleteFolderBeforeRendering` | `boolean` | `true` | Whether the base directory should be cleaned before generating new files. | +| `formatCode` | `boolean` | `false` | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided. | +| `toolchain` | `string` | `"+stable"` | The toolchain to use when formatting the generated code. | +| `crateFolder` | `string` | none | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`. | +| `linkOverrides` | `Record<'accounts' \| 'definedTypes' \| 'instructions' \| 'pdas' \| 'programs' \| 'resolvers', Record>` | `{}` | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. | +| `dependencyMap` | `Record` | `{}` | A mapping between import aliases and their actual crate name or path in Rust. | +| `renderParentInstructions` | `boolean` | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered. | +| `traitOptions` | [`TraitOptions`](#trait-options) | `DEFAULT_TRAIT_OPTIONS` | A set of options that can be used to configure how traits are rendered for every Rust types. See [documentation below](#trait-options) for more information. | +| `anchorTraits` | `boolean` | `true` | Whether to generate Anchor traits `impl` for account types. | + +## Trait Options + +The Rust renderer provides sensible default traits when generating the various Rust types you client will use. However, you may wish to configure these traits to better suit your needs. The `traitOptions` attribute is here to help you with that. Let's see the various settings it provides. + +### Default traits + +Using the `traitOptions` attribute, you may configure the default traits that will be applied to every Rust type. These default traits can be configured using 4 different attributes: + +- `baseDefaults`: The default traits to implement for all types. +- `dataEnumDefaults`: The default traits to implement for all data enum types, in addition to the `baseDefaults` traits. Data enums are enums with at least one non-unit variant — e.g. `pub enum Command { Write(String), Quit }`. +- `scalarEnumDefaults`: The default traits to implement for all scalar enum types, in addition to the `baseDefaults` traits. Scalar enums are enums with unit variants only — e.g. `pub enum Feedback { Good, Bad }`. +- `structDefaults`: The default traits to implement for all struct types, in addition to the `baseDefaults` traits. + +Note that you must provide the fully qualified name of the traits you provide (e.g. `serde::Serialize`). Here are the default values for these attributes: + +```ts +const traitOptions = { + baseDefaults: ['Clone', 'Debug', 'Eq', 'PartialEq'], + dataEnumDefaults: [], + scalarEnumDefaults: ['Copy', 'PartialOrd', 'Hash', 'num_derive::FromPrimitive'], + structDefaults: [], +}; +``` + +### Overridden traits + +In addition to configure the default traits, you may also override the traits for specific types. This will completely replace the default traits for the given type. To do so, you may use the `overrides` attribute of the `traitOptions` object. + +This attribute is a map where the keys are the names of the types you want to override, and the values are the traits you want to apply to these types. Here is an example: + +```ts +const traitOptions = { + overrides: { + myCustomType: ['Clone', 'my::custom::Trait', 'my::custom::OtherTrait'], + myTypeWithNoTraits: [], + }, +}; +``` + +### Feature Flags + +You may also configure which traits should be rendered under a feature flag by using the `featureFlags` attribute. This attribute is a map where the keys are feature flag names and the values are the traits that should be rendered under that feature flag. Here is an example: + +```ts +const traitOptions = { + featureFlags: { fruits: ['fruits::Apple', 'fruits::Banana'] }, +}; +``` + +Now, if at any point, we encounter a `fruits::Apple` or `fruits::Banana` trait to be rendered (either as default traits or as overridden traits), they will be rendered under the `fruits` feature flag. For instance: + +```rust +#[cfg_attr(feature = "fruits", derive(fruits::Apple, fruits::Banana))] +``` + +Note that for feature flags to be effective, they must be added to the `Cargo.toml` file of the generated Rust client. + +### Using the Fully Qualified Name + +By default, all traits are imported using the provided Fully Qualified Name which means their short name will be used within the `derive` attributes. + +However, you may want to avoid importing these traits and use the Fully Qualified Name directly in the generated code. To do so, you may use the `useFullyQualifiedName` attribute of the `traitOptions` object by setting it to `true`: + +```ts +const traitOptions = { + useFullyQualifiedName: true, +}; +``` + +Here is an example of rendered traits with this option set to `true` and `false` (which is the default): + +```rust +// With `useFullyQualifiedName` set to `false` (default). +use serde::Serialize; +use serde::Deserialize; +// ... +#[derive(Serialize, Deserialize)] + +// With `useFullyQualifiedName` set to `true`. +#[derive(serde::Serialize, serde::Deserialize)] +``` + +Note that any trait rendered under a feature flag will always use the Fully Qualified Name in order to ensure we only reference the trait when the feature is enabled. diff --git a/packages/renderers-rust-cpi/package.json b/packages/renderers-rust-cpi/package.json new file mode 100644 index 000000000..83af61a50 --- /dev/null +++ b/packages/renderers-rust-cpi/package.json @@ -0,0 +1,70 @@ +{ + "name": "@codama/renderers-rust-cpi", + "version": "0.1.0", + "description": "Renders CPI clients for your programs", + "exports": { + "types": "./dist/types/index.d.ts", + "react-native": "./dist/index.react-native.mjs", + "browser": { + "import": "./dist/index.browser.mjs", + "require": "./dist/index.browser.cjs" + }, + "node": { + "import": "./dist/index.node.mjs", + "require": "./dist/index.node.cjs" + } + }, + "browser": { + "./dist/index.node.cjs": "./dist/index.browser.cjs", + "./dist/index.node.mjs": "./dist/index.browser.mjs" + }, + "main": "./dist/index.node.cjs", + "module": "./dist/index.node.mjs", + "react-native": "./dist/index.react-native.mjs", + "types": "./dist/types/index.d.ts", + "type": "commonjs", + "files": [ + "./dist/types", + "./dist/index.*" + ], + "sideEffects": false, + "keywords": [ + "solana", + "framework", + "standard", + "renderers", + "rust", + "client" + ], + "scripts": { + "build": "rimraf dist && tsup && tsc -p ./tsconfig.declarations.json", + "dev": "vitest --project node", + "lint": "eslint . && prettier --check .", + "lint:fix": "eslint --fix . && prettier --write .", + "test": "pnpm test:types && pnpm test:treeshakability && pnpm test:unit && pnpm test:e2e && pnpm test:exports", + "test:e2e": "./test/e2e/test.sh", + "test:exports": "node ./test/exports/module.mjs && node ./test/exports/commonjs.cjs", + "test:treeshakability": "for file in dist/index.*.mjs; do agadoo $file; done", + "test:types": "tsc --noEmit", + "test:unit": "vitest run" + }, + "dependencies": { + "@codama/errors": "workspace:*", + "@codama/nodes": "workspace:*", + "@codama/renderers-core": "workspace:*", + "@codama/visitors-core": "workspace:*", + "@solana/codecs-strings": "^3.0.2" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/codama-idl/codama" + }, + "bugs": { + "url": "http://github.com/codama-idl/codama/issues" + }, + "browserslist": [ + "supports bigint and not dead", + "maintained node versions" + ] +} diff --git a/packages/renderers-rust-cpi/src/fragments/index.ts b/packages/renderers-rust-cpi/src/fragments/index.ts new file mode 100644 index 000000000..a0e106d1e --- /dev/null +++ b/packages/renderers-rust-cpi/src/fragments/index.ts @@ -0,0 +1,4 @@ +export * from './instructionPage'; +export * from './modPage'; +export * from './programModPage'; +export * from './rootModPage'; diff --git a/packages/renderers-rust-cpi/src/fragments/instructionModPage.ts b/packages/renderers-rust-cpi/src/fragments/instructionModPage.ts new file mode 100644 index 000000000..be933ef55 --- /dev/null +++ b/packages/renderers-rust-cpi/src/fragments/instructionModPage.ts @@ -0,0 +1,38 @@ +import { InstructionNode } from '@codama/nodes'; + +import { addFragmentImports, Fragment, fragment, getPageFragment, mergeFragments, RenderScope } from '../utils'; +import { getModImportsFragment } from './modPage'; + +/** + * Get the mod page fragment for instructions. + */ +export function getInstructionModPageFragment( + scope: Pick & { instructions: InstructionNode[] }, +): Fragment | undefined { + const imports = getModImportsFragment(scope.instructions); + if (!imports) return; + + return getPageFragment( + mergeFragments([imports, getInstructionHelpersFragment()], cs => cs.join('\n\n')), + scope, + ); +} + +/** + * Helpers for handling `MaybeUninit` buffers. + */ +function getInstructionHelpersFragment(): Fragment { + return addFragmentImports( + fragment` + const UNINIT_BYTE: MaybeUninit = MaybeUninit::::uninit(); + + /// Write bytes from a source slice to a destination slice of \`MaybeUninit\`. + #[inline(always)] + fn write_bytes(destination: &mut [MaybeUninit], source: &[u8]) { + for (d, s) in destination.iter_mut().zip(source.iter()) { + d.write(*s); + } + }`, + ['core::mem::MaybeUninit'], + ); +} diff --git a/packages/renderers-rust-cpi/src/fragments/instructionPage.ts b/packages/renderers-rust-cpi/src/fragments/instructionPage.ts new file mode 100644 index 000000000..ccfd36465 --- /dev/null +++ b/packages/renderers-rust-cpi/src/fragments/instructionPage.ts @@ -0,0 +1,342 @@ +import { logWarn } from '@codama/errors'; +import { + InstructionArgumentNode, + InstructionNode, + isNode, + pascalCase, + snakeCase, + SnakeCaseString, + VALUE_NODES, +} from '@codama/nodes'; +import { getLastNodeFromPath, NodePath, visit } from '@codama/visitors-core'; + +import { + addFragmentImports, + Fragment, + fragment, + getDocblockFragment, + getPageFragment, + mergeFragments, + RenderScope, +} from '../utils'; +import { getInstructionArgumentAssignmentVisitor } from '../visitors'; +import { getTypeManifestVisitor, TypeManifest } from '../visitors/getTypeManifestVisitor'; +import { renderValueNode } from '../visitors/renderValueNodeVisitor'; + +/** + * Get the instruction page fragment. + */ +export function getInstructionPageFragment( + scope: Pick & { + instructionPath: NodePath; + }, +): Fragment { + const instructionNode = getLastNodeFromPath(scope.instructionPath); + + // canMergeAccountsAndArgs + const accountsAndArgsConflicts = getConflictsBetweenAccountsAndArguments(instructionNode); + if (accountsAndArgsConflicts.length > 0) { + logWarn( + `[Rust] Accounts and args of instruction [${instructionNode.name}] have the following ` + + `conflicting attributes [${accountsAndArgsConflicts.join(', ')}]. ` + + `Thus, the conflicting arguments will be suffixed with "_arg". ` + + 'You may want to rename the conflicting attributes.', + ); + } + + // Instruction arguments. + const instructionArguments = getParsedInstructionArguments(instructionNode, accountsAndArgsConflicts, scope); + + /* + const hasArgs = instructionArguments.some(arg => arg.defaultValueStrategy !== 'omitted'); + const hasOptional = instructionArguments.some( + arg => !arg.resolvedDefaultValue && arg.defaultValueStrategy !== 'omitted', + ); + */ + + // Determines the size of the instruction data. The size is `null` if any of the arguments is + // variable-sized. + const instructionFixedSize = visit(instructionNode, scope.byteSizeVisitor); + + return getPageFragment( + mergeFragments( + [ + getInstructionStructFragment(instructionNode, instructionArguments), + getInstructionImplFragment(instructionNode, instructionArguments, instructionFixedSize), + getInstructionNestedStructsFragment(instructionArguments), + ], + cs => cs.join('\n\n'), + ), + scope, + ); +} + +/** + * Get the instruction `struct` fragment. The fragment includes the accounts and arguments + * as fields. + */ +function getInstructionStructFragment( + instructionNode: InstructionNode, + instructionArguments: ParsedInstructionArgument[], +) { + const accountsFragment = mergeFragments( + instructionNode.accounts.map(account => { + const docs = getDocblockFragment(account.docs ?? [], true); + const name = snakeCase(account.name); + const type = addFragmentImports( + account.isSigner === 'either' ? fragment`(&'a AccountInfo, bool)` : fragment`&'a AccountInfo`, + ['pinocchio::account_info::AccountInfo'], + ); + return account.isOptional + ? fragment`${docs}pub ${name}: Option<${type}>,` + : fragment`${docs}pub ${name}: ${type},`; + }), + cs => cs.join('\n'), + ); + + const structLifetimes = getLifetimeDeclarations(instructionNode, instructionArguments); + const argumentsFragment = mergeFragments( + instructionArguments + .filter(arg => !arg.resolvedDefaultValue) + .map(arg => { + const docs = getDocblockFragment(arg.docs ?? [], true); + const lifetime = arg.lifetime ? `&'${arg.lifetime} ` : ''; + return fragment`${docs}pub ${arg.displayName}: ${lifetime}${arg.manifest.type},`; + }), + cs => cs.join('\n'), + ); + + return fragment`/// Helper for cross-program invocations of \`${snakeCase(instructionNode.name)}\` instruction. +pub struct ${pascalCase(instructionNode.name)}${structLifetimes} { + ${accountsFragment} + ${argumentsFragment} +}`; +} + +/** + * Get the instruction `impl` fragment. The fragment includes the `invoke` and `invoke_signed` methods. + */ +function getInstructionImplFragment( + instructionNode: InstructionNode, + instructionArguments: ParsedInstructionArgument[], + instructionFixedSize: number | null, +) { + const accountMetasFragment = mergeFragments( + instructionNode.accounts.map(account => { + const name = snakeCase(account.name); + const isWritable = account.isWritable ? 'true' : 'false'; + const accountMetaArguments = + account.isSigner === 'either' + ? fragment`self.${name}.0.key(), ${isWritable}, self.${name}.1` + : fragment`self.${name}.key(), ${isWritable}, ${account.isSigner}`; + return fragment`AccountMeta::new(${accountMetaArguments}),`; + }), + cs => cs.join('\n'), + ); + + const accountsFragment = mergeFragments( + instructionNode.accounts.map(account => { + const name = snakeCase(account.name); + return fragment`&self.${name},`; + }), + cs => cs.join('\n'), + ); + + const instructionDataFragment = + instructionArguments.length > 0 + ? getInstructionDataFragment(instructionArguments, instructionFixedSize) + : fragment`let data = &[];`; + + const structLifetimes = getLifetimeDeclarations(instructionNode, instructionArguments, true); + + return addFragmentImports( + fragment`impl ${pascalCase(instructionNode.name)}${structLifetimes} { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + + // account metas + let account_metas: [AccountMeta; ${instructionNode.accounts.length}] = [ + ${accountMetasFragment} + ]; + + ${instructionDataFragment} + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[${accountsFragment}], signers) + } +}`, + [ + 'pinocchio::cpi::invoke_signed', + 'pinocchio::instruction::Instruction', + 'pinocchio::instruction::AccountMeta', + 'pinocchio::ProgramResult', + 'pinocchio::instruction::Signer', + ], + ); +} + +/** + * Get the fragment for any nested `struct`s used in the instruction arguments. + */ +function getInstructionNestedStructsFragment(instructionArguments: ParsedInstructionArgument[]): Fragment { + return mergeFragments( + instructionArguments.flatMap(arg => arg.manifest.nestedStructs), + cs => cs.join('\n\n'), + ); +} + +/** + * Get the fragment that constructs the instruction data. There are several special cases + * to handle single argument instructions. + */ +function getInstructionDataFragment( + instructionArguments: ParsedInstructionArgument[], + instructionFixedSize: number | null, +): Fragment { + const singleArgumentFragment = getInstructionDataFromSingleArgumentFragment(instructionArguments); + if (singleArgumentFragment) return singleArgumentFragment; + + const declareDataFragment = addFragmentImports( + fragment`let mut uninit_data = [UNINIT_BYTE; ${instructionFixedSize !== null ? instructionFixedSize : 0}];`, + ['super::UNINIT_BYTE', 'core::slice::from_raw_parts'], + ); + let offset = 0; + const assignDataContentFragment = mergeFragments( + instructionArguments.map(argument => { + const [fragment, updated] = visit(argument.type, getInstructionArgumentAssignmentVisitor(argument, offset)); + offset = updated; + return fragment; + }), + cs => cs.join('\n'), + ); + const transmuteData = fragment`let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, ${offset}) };`; + + return mergeFragments([declareDataFragment, assignDataContentFragment, transmuteData], cs => cs.join('\n')); +} + +// When there is a single byte array or string (e.g., `&[u8]`, `&str`) argument, there is +// no need to copy the data into a fixed-size array. +function getInstructionDataFromSingleArgumentFragment( + instructionArguments: ParsedInstructionArgument[], +): Fragment | undefined { + if (instructionArguments.length !== 1) return; + const argument = instructionArguments[0]; + + if (isNode(argument.type, 'bytesTypeNode')) { + return argument.resolvedDefaultValue + ? fragment`let data = &${argument.resolvedDefaultValue};` + : fragment`let data = self.${argument.displayName};`; + } + + if (isNode(argument.type, 'stringTypeNode')) { + return argument.resolvedDefaultValue + ? fragment`let data = ${argument.resolvedDefaultValue}.as_bytes();` + : fragment`let data = self.${argument.displayName}.as_bytes();`; + } + + // When there is a single byte argument, the instruction data is a single-element byte array. + if (isNode(argument.type, 'numberTypeNode') && argument.type.format === 'u8') { + return argument.resolvedDefaultValue + ? fragment`let data = &[${argument.resolvedDefaultValue}${argument.type.format}];` + : fragment`let data = &[self.${argument.displayName}];`; + } + + // When there is a single number (e.g., `u16`, `u32`, `u64`) argument, the instruction data is the + // little-endian representation of the number. + if ( + isNode(argument.type, 'numberTypeNode') && + argument.type.format !== 'shortU16' && + argument.type.endian === 'le' + ) { + return argument.resolvedDefaultValue + ? fragment`let data = &${argument.resolvedDefaultValue}${argument.type.format}.to_le_bytes();` + : fragment`let data = &self.${argument.displayName}.to_le_bytes();`; + } +} + +export type ParsedInstructionArgument = InstructionArgumentNode & { + displayName: SnakeCaseString; + fixedSize: number | null; + lifetime: string | null; + manifest: TypeManifest; + resolvedDefaultValue: Fragment | null; + resolvedInnerOptionType: Fragment | null; +}; + +function getParsedInstructionArguments( + instructionNode: InstructionNode, + accountsAndArgsConflicts: string[], + scope: Pick, +): ParsedInstructionArgument[] { + const lifetimeIterator = getLifetimeIterator(instructionNode); + const argumentVisitor = getTypeManifestVisitor({ + ...scope, + nestedStruct: true, + parentName: `${pascalCase(instructionNode.name)}InstructionData`, + }); + + return instructionNode.arguments.map(argument => { + const fixedSize = visit(argument.type, scope.byteSizeVisitor); + const shouldUseLifetime = fixedSize === null || fixedSize > 8; + + return { + ...argument, + displayName: accountsAndArgsConflicts.includes(argument.name) + ? (`${snakeCase(argument.name)}_arg` as SnakeCaseString) + : snakeCase(argument.name), + fixedSize, + lifetime: shouldUseLifetime ? lifetimeIterator.next().value : null, + manifest: visit(argument.type, argumentVisitor), + resolvedDefaultValue: + !!argument.defaultValue && isNode(argument.defaultValue, VALUE_NODES) + ? renderValueNode(argument.defaultValue, scope.getImportFrom) + : null, + resolvedInnerOptionType: isNode(argument.type, 'optionTypeNode') + ? visit(argument.type.item, argumentVisitor).type + : null, + }; + }); +} + +function getLifetimeIterator(instructionNode: InstructionNode): Iterator { + // Start from 'b instead of 'a if we have accounts. + let lifetime = instructionNode.accounts.length > 0 ? 1 : 0; + return { + next: () => { + if (lifetime >= 26) { + throw new Error('Exceeded maximum number of lifetimes (26)'); + } + return { done: false, value: String.fromCharCode(97 + lifetime++) }; + }, + }; +} + +function getLifetimeDeclarations( + instructionNode: InstructionNode, + instructionArguments: ParsedInstructionArgument[], + useUnderscore = false, +): string { + const lifetimes = [ + ...(instructionNode.accounts.length > 0 ? [useUnderscore ? `'_` : `'a`] : []), + ...instructionArguments.flatMap(arg => (arg.lifetime ? [useUnderscore ? `'_` : `'${arg.lifetime}`] : [])), + ]; + return lifetimes.length > 0 ? `<${lifetimes.join(', ')}>` : ''; +} + +function getConflictsBetweenAccountsAndArguments(instructionNode: InstructionNode): string[] { + const allNames = [ + ...instructionNode.accounts.map(account => account.name), + ...instructionNode.arguments.map(argument => argument.name), + ]; + const duplicates = allNames.filter((e, i, a) => a.indexOf(e) !== i); + return [...new Set(duplicates)]; +} diff --git a/packages/renderers-rust-cpi/src/fragments/modPage.ts b/packages/renderers-rust-cpi/src/fragments/modPage.ts new file mode 100644 index 000000000..431fcbe1a --- /dev/null +++ b/packages/renderers-rust-cpi/src/fragments/modPage.ts @@ -0,0 +1,28 @@ +import { CamelCaseString, snakeCase } from '@codama/nodes'; + +import { Fragment, fragment, getPageFragment, mergeFragments, RenderScope } from '../utils'; + +export function getModPageFragment( + scope: Pick & { items: { name: CamelCaseString }[] }, +): Fragment | undefined { + const imports = getModImportsFragment(scope.items); + if (!imports) return; + + return getPageFragment(imports, scope); +} + +export function getModImportsFragment(items: { name: CamelCaseString }[]): Fragment | undefined { + if (items.length === 0) return; + + const sortedItems = items.slice().sort((a, b) => a.name.localeCompare(b.name)); + const modStatements = mergeFragments( + sortedItems.map(item => fragment`pub mod r#${snakeCase(item.name)};`), + cs => cs.join('\n'), + ); + const useStatements = mergeFragments( + sortedItems.map(item => fragment`pub use self::r#${snakeCase(item.name)}::*;`), + cs => cs.join('\n'), + ); + + return mergeFragments([modStatements, useStatements], cs => cs.join('\n\n')); +} diff --git a/packages/renderers-rust-cpi/src/fragments/programModPage.ts b/packages/renderers-rust-cpi/src/fragments/programModPage.ts new file mode 100644 index 000000000..4416d2bdb --- /dev/null +++ b/packages/renderers-rust-cpi/src/fragments/programModPage.ts @@ -0,0 +1,30 @@ +import { ProgramNode, snakeCase } from '@codama/nodes'; +import { pipe } from '@codama/visitors-core'; + +import { addFragmentImports, Fragment, fragment, getPageFragment, mergeFragments, RenderScope } from '../utils'; + +export function getProgramModPageFragment( + scope: Pick & { programsToExport: ProgramNode[] }, +): Fragment | undefined { + const programsToExport = scope.programsToExport; + if (programsToExport.length === 0) return; + + const programAddresses = programsToExport.map(getProgramAddressFragment); + return getPageFragment( + mergeFragments(programAddresses, cs => cs.join('\n')), + scope, + ); +} + +function getProgramAddressFragment(program: ProgramNode): Fragment { + const name = snakeCase(program.name); + return mergeFragments( + [ + fragment`/// \`${name}\` program ID.`, + pipe(fragment`pub const ${name.toUpperCase()}_ID: Pubkey = pubkey!("${program.publicKey}");`, f => + addFragmentImports(f, ['pinocchio::pubkey::Pubkey', 'pinocchio_pubkey::pubkey']), + ), + ], + cs => cs.join('\n'), + ); +} diff --git a/packages/renderers-rust-cpi/src/fragments/rootModPage.ts b/packages/renderers-rust-cpi/src/fragments/rootModPage.ts new file mode 100644 index 000000000..5fc648526 --- /dev/null +++ b/packages/renderers-rust-cpi/src/fragments/rootModPage.ts @@ -0,0 +1,26 @@ +import { InstructionNode, ProgramNode } from '@codama/nodes'; + +import { Fragment, fragment, getPageFragment, mergeFragments, RenderScope } from '../utils'; + +export function getRootModPageFragment( + scope: Pick & { + instructionsToExport: InstructionNode[]; + programsToExport: ProgramNode[]; + }, +): Fragment | undefined { + const hasPrograms = scope.programsToExport.length > 0; + const hasInstructions = scope.instructionsToExport.length > 0; + const hasAnythingToExport = hasPrograms || hasInstructions; + if (!hasAnythingToExport) return; + + return getPageFragment( + mergeFragments( + [ + hasInstructions ? fragment`pub mod instructions;` : undefined, + hasPrograms ? fragment`pub mod programs;` : undefined, + ], + cs => cs.join('\n'), + ), + scope, + ); +} diff --git a/packages/renderers-rust-cpi/src/index.ts b/packages/renderers-rust-cpi/src/index.ts new file mode 100644 index 000000000..97b95cc06 --- /dev/null +++ b/packages/renderers-rust-cpi/src/index.ts @@ -0,0 +1,3 @@ +export * from './visitors'; + +export { renderVisitor as default } from './visitors/renderVisitor'; diff --git a/packages/renderers-rust-cpi/src/types/global.d.ts b/packages/renderers-rust-cpi/src/types/global.d.ts new file mode 100644 index 000000000..13de8a7ce --- /dev/null +++ b/packages/renderers-rust-cpi/src/types/global.d.ts @@ -0,0 +1,6 @@ +declare const __BROWSER__: boolean; +declare const __ESM__: boolean; +declare const __NODEJS__: boolean; +declare const __REACTNATIVE__: boolean; +declare const __TEST__: boolean; +declare const __VERSION__: string; diff --git a/packages/renderers-rust-cpi/src/utils/codecs.ts b/packages/renderers-rust-cpi/src/utils/codecs.ts new file mode 100644 index 000000000..3a524c015 --- /dev/null +++ b/packages/renderers-rust-cpi/src/utils/codecs.ts @@ -0,0 +1,16 @@ +import { BytesValueNode } from '@codama/nodes'; +import { getBase16Encoder, getBase58Encoder, getBase64Encoder, getUtf8Encoder } from '@solana/codecs-strings'; + +export function getBytesFromBytesValueNode(node: BytesValueNode): Uint8Array { + switch (node.encoding) { + case 'utf8': + return getUtf8Encoder().encode(node.data) as Uint8Array; + case 'base16': + return getBase16Encoder().encode(node.data) as Uint8Array; + case 'base58': + return getBase58Encoder().encode(node.data) as Uint8Array; + case 'base64': + default: + return getBase64Encoder().encode(node.data) as Uint8Array; + } +} diff --git a/packages/renderers-rust-cpi/src/utils/fragment.ts b/packages/renderers-rust-cpi/src/utils/fragment.ts new file mode 100644 index 000000000..acc7b0f43 --- /dev/null +++ b/packages/renderers-rust-cpi/src/utils/fragment.ts @@ -0,0 +1,65 @@ +import { Docs } from '@codama/nodes'; +import { BaseFragment, createFragmentTemplate } from '@codama/renderers-core'; + +import { ImportMap } from './importMap'; +import { RenderScope } from './options'; + +export type Fragment = BaseFragment & Readonly<{ imports: ImportMap }>; + +function isFragment(value: unknown): value is Fragment { + return typeof value === 'object' && value !== null && 'content' in value && 'imports' in value; +} + +export function fragment(template: TemplateStringsArray, ...items: unknown[]): Fragment { + return createFragmentTemplate(template, items, isFragment, mergeFragments); +} + +export function mergeFragments(fragments: (Fragment | undefined)[], mergeContent: (contents: string[]) => string) { + const filteredFragments = fragments.filter((f): f is Fragment => typeof f !== 'undefined'); + return Object.freeze({ + content: mergeContent(filteredFragments.map(f => f.content)), + imports: new ImportMap().mergeWith(...filteredFragments.map(f => f.imports)), + }); +} + +export function mergeFragmentImports(fragment: Fragment, importMaps: ImportMap[]): Fragment { + return Object.freeze({ + ...fragment, + imports: new ImportMap().mergeWith(fragment.imports, ...importMaps), + }); +} + +export function addFragmentImports(fragment: Fragment, imports: string[]): Fragment { + return Object.freeze({ + ...fragment, + imports: new ImportMap().mergeWith(fragment.imports).add(imports), + }); +} + +export function addFragmentImportAlias(fragment: Fragment, importName: string, alias: string): Fragment { + return Object.freeze({ + ...fragment, + imports: new ImportMap().mergeWith(fragment.imports).addAlias(importName, alias), + }); +} + +export function getDocblockFragment(lines: Docs, withLineJump = false, internal = false): Fragment { + const prefix = internal ? '//!' : '///'; + const lineJump = withLineJump ? '\n' : ''; + if (lines.length === 0) return fragment``; + const prefixedLines = lines.map(line => (line ? `${prefix} ${line}` : prefix)); + return fragment`${prefixedLines.join('\n')}${lineJump}`; +} + +export function getPageFragment(page: Fragment, scope: Pick): Fragment { + const docs = [ + 'This code was AUTOGENERATED using the Codama library.', + 'Please DO NOT EDIT THIS FILE, instead use visitors', + 'to add features, then rerun Codama to update it.', + '', + '', + ]; + const header = getDocblockFragment(docs, false, true); + const imports = page.imports.isEmpty() ? undefined : fragment`${page.imports.toString(scope.dependencyMap)}`; + return mergeFragments([header, imports, page], cs => cs.join('\n\n')); +} diff --git a/packages/renderers-rust-cpi/src/utils/importMap.ts b/packages/renderers-rust-cpi/src/utils/importMap.ts new file mode 100644 index 000000000..51b2fb84d --- /dev/null +++ b/packages/renderers-rust-cpi/src/utils/importMap.ts @@ -0,0 +1,77 @@ +const DEFAULT_MODULE_MAP: Record = { + generated: 'crate::generated', + generatedAccounts: 'crate::generated::accounts', + generatedErrors: 'crate::generated::errors', + generatedInstructions: 'crate::generated::instructions', + generatedTypes: 'crate::generated::types', + hooked: 'crate::hooked', + mplEssentials: 'mpl_toolbox', + mplToolbox: 'mpl_toolbox', +}; + +export class ImportMap { + protected readonly _imports: Set = new Set(); + + protected readonly _aliases: Map = new Map(); + + get imports(): Set { + return this._imports; + } + + get aliases(): Map { + return this._aliases; + } + + add(imports: Set | string[] | string): ImportMap { + const newImports = typeof imports === 'string' ? [imports] : imports; + newImports.forEach(i => this._imports.add(i)); + return this; + } + + remove(imports: Set | string[] | string): ImportMap { + const importsToRemove = typeof imports === 'string' ? [imports] : imports; + importsToRemove.forEach(i => this._imports.delete(i)); + return this; + } + + mergeWith(...others: ImportMap[]): ImportMap { + others.forEach(other => { + this.add(other._imports); + other._aliases.forEach((alias, importName) => this.addAlias(importName, alias)); + }); + return this; + } + + addAlias(importName: string, alias: string): ImportMap { + this._aliases.set(importName, alias); + return this; + } + + isEmpty(): boolean { + return this._imports.size === 0; + } + + resolveDependencyMap(dependencies: Record): ImportMap { + const dependencyMap = { ...DEFAULT_MODULE_MAP, ...dependencies }; + const newImportMap = new ImportMap(); + const resolveDependency = (i: string): string => { + const dependencyKey = Object.keys(dependencyMap).find(key => i.startsWith(`${key}::`)); + if (!dependencyKey) return i; + const dependencyValue = dependencyMap[dependencyKey]; + return dependencyValue + i.slice(dependencyKey.length); + }; + this._imports.forEach(i => newImportMap.add(resolveDependency(i))); + this._aliases.forEach((alias, i) => newImportMap.addAlias(resolveDependency(i), alias)); + return newImportMap; + } + + toString(dependencies: Record): string { + const resolvedMap = this.resolveDependencyMap(dependencies); + const importStatements = [...resolvedMap.imports].map(i => { + const alias = resolvedMap.aliases.get(i); + if (alias) return `use ${i} as ${alias};`; + return `use ${i};`; + }); + return importStatements.join('\n'); + } +} diff --git a/packages/renderers-rust-cpi/src/utils/index.ts b/packages/renderers-rust-cpi/src/utils/index.ts new file mode 100644 index 000000000..3e7527dad --- /dev/null +++ b/packages/renderers-rust-cpi/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from './codecs'; +export * from './fragment'; +export * from './linkOverrides'; +export * from './options'; +export * from './importMap'; +export * from './traitOptions'; diff --git a/packages/renderers-rust-cpi/src/utils/linkOverrides.ts b/packages/renderers-rust-cpi/src/utils/linkOverrides.ts new file mode 100644 index 000000000..a7a43a3ec --- /dev/null +++ b/packages/renderers-rust-cpi/src/utils/linkOverrides.ts @@ -0,0 +1,70 @@ +import { CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError } from '@codama/errors'; +import { + AccountLinkNode, + DefinedTypeLinkNode, + InstructionLinkNode, + PdaLinkNode, + ProgramLinkNode, + ResolverValueNode, +} from '@codama/nodes'; + +export type LinkOverrides = { + accounts?: Record; + definedTypes?: Record; + instructions?: Record; + pdas?: Record; + programs?: Record; + resolvers?: Record; +}; + +type OverridableNodes = + | AccountLinkNode + | DefinedTypeLinkNode + | InstructionLinkNode + | PdaLinkNode + | ProgramLinkNode + | ResolverValueNode; + +export type GetImportFromFunction = (node: OverridableNodes, fallback?: string) => string; + +export function getImportFromFactory(overrides: LinkOverrides): GetImportFromFunction { + const linkOverrides = { + accounts: overrides.accounts ?? {}, + definedTypes: overrides.definedTypes ?? {}, + instructions: overrides.instructions ?? {}, + pdas: overrides.pdas ?? {}, + programs: overrides.programs ?? {}, + resolvers: overrides.resolvers ?? {}, + }; + + return (node: OverridableNodes) => { + const kind = node.kind; + switch (kind) { + case 'accountLinkNode': + return linkOverrides.accounts[node.name] ?? 'generatedAccounts'; + case 'definedTypeLinkNode': + return linkOverrides.definedTypes[node.name] ?? 'generatedTypes'; + case 'instructionLinkNode': + return linkOverrides.instructions[node.name] ?? 'generatedInstructions'; + case 'pdaLinkNode': + return linkOverrides.pdas[node.name] ?? 'generatedAccounts'; + case 'programLinkNode': + return linkOverrides.programs[node.name] ?? 'generatedPrograms'; + case 'resolverValueNode': + return linkOverrides.resolvers[node.name] ?? 'hooked'; + default: + throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { + expectedKinds: [ + 'AccountLinkNode', + 'DefinedTypeLinkNode', + 'InstructionLinkNode', + 'PdaLinkNode', + 'ProgramLinkNode', + 'resolverValueNode', + ], + kind: kind satisfies never, + node, + }); + } + }; +} diff --git a/packages/renderers-rust-cpi/src/utils/options.ts b/packages/renderers-rust-cpi/src/utils/options.ts new file mode 100644 index 000000000..6cc7582d6 --- /dev/null +++ b/packages/renderers-rust-cpi/src/utils/options.ts @@ -0,0 +1,31 @@ +import { getByteSizeVisitor, LinkableDictionary } from '@codama/visitors-core'; + +import { TypeManifestVisitor } from '../visitors/getTypeManifestVisitor'; +import { GetImportFromFunction, LinkOverrides } from './linkOverrides'; +import { GetTraitsFromNodeFunction, TraitOptions } from './traitOptions'; + +export type RenderOptions = GetRenderMapOptions & { + crateFolder?: string; + deleteFolderBeforeRendering?: boolean; + formatCode?: boolean; + toolchain?: string; +}; + +export type GetRenderMapOptions = { + anchorTraits?: boolean; + defaultTraitOverrides?: string[]; + dependencyMap?: Record; + linkOverrides?: LinkOverrides; + renderParentInstructions?: boolean; + traitOptions?: TraitOptions; +}; + +export type RenderScope = { + byteSizeVisitor: ReturnType; + dependencyMap: Record; + getImportFrom: GetImportFromFunction; + getTraitsFromNode: GetTraitsFromNodeFunction; + linkables: LinkableDictionary; + renderParentInstructions: boolean; + typeManifestVisitor: TypeManifestVisitor; +}; diff --git a/packages/renderers-rust-cpi/src/utils/traitOptions.ts b/packages/renderers-rust-cpi/src/utils/traitOptions.ts new file mode 100644 index 000000000..2714aa47c --- /dev/null +++ b/packages/renderers-rust-cpi/src/utils/traitOptions.ts @@ -0,0 +1,162 @@ +import { AccountNode, assertIsNode, camelCase, DefinedTypeNode, isNode, isScalarEnum } from '@codama/nodes'; + +import { Fragment, fragment, mergeFragmentImports } from './fragment'; +import { ImportMap } from './importMap'; + +export type TraitOptions = { + /** The default traits to implement for all types. */ + baseDefaults?: string[]; + /** + * The default traits to implement for data enums only — on top of the base defaults. + * Data enums are enums with at least one non-unit variant. + */ + dataEnumDefaults?: string[]; + /** + * The mapping of feature flags to traits. + * For each entry, the traits will be rendered within a + * `#[cfg_attr(feature = "feature_name", derive(Traits))]` attribute. + */ + featureFlags?: Record; + /** The complete trait overrides of specific types. */ + overrides?: Record; + /** + * The default traits to implement for scalar enums only — on top of the base defaults. + * Scalar enums are enums with no variants or only unit variants. + */ + scalarEnumDefaults?: string[]; + /** The default traits to implement for structs only — on top of the base defaults. */ + structDefaults?: string[]; + /** Whether or not to use the fully qualified name for traits, instead of importing them. */ + useFullyQualifiedName?: boolean; +}; + +export const DEFAULT_TRAIT_OPTIONS: Required = { + baseDefaults: [ + 'borsh::BorshSerialize', + 'borsh::BorshDeserialize', + 'serde::Serialize', + 'serde::Deserialize', + 'Clone', + 'Debug', + 'Eq', + 'PartialEq', + ], + dataEnumDefaults: [], + featureFlags: { + borsh: ['borsh::BorshSerialize', 'borsh::BorshDeserialize'], + serde: ['serde::Serialize', 'serde::Deserialize'], + }, + overrides: {}, + scalarEnumDefaults: ['Copy', 'PartialOrd', 'Hash', 'num_derive::FromPrimitive'], + structDefaults: [], + useFullyQualifiedName: false, +}; + +export type GetTraitsFromNodeFunction = (node: AccountNode | DefinedTypeNode) => Fragment; + +export function getTraitsFromNodeFactory(options: TraitOptions = {}): GetTraitsFromNodeFunction { + return node => getTraitsFromNode(node, options); +} + +export function getTraitsFromNode(node: AccountNode | DefinedTypeNode, userOptions: TraitOptions = {}): Fragment { + assertIsNode(node, ['accountNode', 'definedTypeNode']); + const options: Required = { ...DEFAULT_TRAIT_OPTIONS, ...userOptions }; + + // Get the node type and return early if it's a type alias. + const nodeType = getNodeType(node); + if (nodeType === 'alias') return fragment``; + + // Find all the FQN traits for the node. + const sanitizedOverrides = Object.fromEntries( + Object.entries(options.overrides).map(([key, value]) => [camelCase(key), value]), + ); + const nodeOverrides: string[] | undefined = sanitizedOverrides[node.name]; + const allTraits = nodeOverrides === undefined ? getDefaultTraits(nodeType, options) : nodeOverrides; + + // Wrap the traits in feature flags if necessary. + const partitionedTraits = partitionTraitsInFeatures(allTraits, options.featureFlags); + let unfeaturedTraits = partitionedTraits[0]; + const featuredTraits = partitionedTraits[1]; + + // Import the traits if necessary. + const imports = new ImportMap(); + if (!options.useFullyQualifiedName) { + unfeaturedTraits = extractFullyQualifiedNames(unfeaturedTraits, imports); + } + + // Render the trait lines. + const traitLines: string[] = [ + ...(unfeaturedTraits.length > 0 ? [`#[derive(${unfeaturedTraits.join(', ')})]\n`] : []), + ...Object.entries(featuredTraits).map(([feature, traits]) => { + return `#[cfg_attr(feature = "${feature}", derive(${traits.join(', ')}))]\n`; + }), + ]; + + return mergeFragmentImports(fragment`${traitLines.join('')}`, [imports]); +} + +function getNodeType(node: AccountNode | DefinedTypeNode): 'alias' | 'dataEnum' | 'scalarEnum' | 'struct' { + if (isNode(node, 'accountNode')) return 'struct'; + if (isNode(node.type, 'structTypeNode')) return 'struct'; + if (isNode(node.type, 'enumTypeNode')) { + return isScalarEnum(node.type) ? 'scalarEnum' : 'dataEnum'; + } + return 'alias'; +} + +function getDefaultTraits( + nodeType: 'dataEnum' | 'scalarEnum' | 'struct', + options: Pick< + Required, + 'baseDefaults' | 'dataEnumDefaults' | 'scalarEnumDefaults' | 'structDefaults' + >, +): string[] { + switch (nodeType) { + case 'dataEnum': + return [...options.baseDefaults, ...options.dataEnumDefaults]; + case 'scalarEnum': + return [...options.baseDefaults, ...options.scalarEnumDefaults]; + case 'struct': + return [...options.baseDefaults, ...options.structDefaults]; + } +} + +function partitionTraitsInFeatures( + traits: string[], + featureFlags: Record, +): [string[], Record] { + // Reverse the feature flags option for quick lookup. + // If there are any duplicate traits, the first one encountered will be used. + const reverseFeatureFlags = Object.entries(featureFlags).reduce( + (acc, [feature, traits]) => { + for (const trait of traits) { + if (!acc[trait]) acc[trait] = feature; + } + return acc; + }, + {} as Record, + ); + + const unfeaturedTraits: string[] = []; + const featuredTraits: Record = {}; + for (const trait of traits) { + const feature: string | undefined = reverseFeatureFlags[trait]; + if (feature === undefined) { + unfeaturedTraits.push(trait); + } else { + if (!featuredTraits[feature]) featuredTraits[feature] = []; + featuredTraits[feature].push(trait); + } + } + + return [unfeaturedTraits, featuredTraits]; +} + +function extractFullyQualifiedNames(traits: string[], imports: ImportMap): string[] { + return traits.map(trait => { + const index = trait.lastIndexOf('::'); + if (index === -1) return trait; + imports.add(trait); + return trait.slice(index + 2); + }); +} diff --git a/packages/renderers-rust-cpi/src/visitors/getInstructionArgumentAssignmentVisitor.ts b/packages/renderers-rust-cpi/src/visitors/getInstructionArgumentAssignmentVisitor.ts new file mode 100644 index 000000000..7de8cfd16 --- /dev/null +++ b/packages/renderers-rust-cpi/src/visitors/getInstructionArgumentAssignmentVisitor.ts @@ -0,0 +1,150 @@ +import { CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, CodamaError } from '@codama/errors'; +import { isNode, REGISTERED_TYPE_NODE_KINDS } from '@codama/nodes'; +import { extendVisitor, pipe, staticVisitor, visit } from '@codama/visitors-core'; + +import type { ParsedInstructionArgument } from '../fragments'; +import { addFragmentImports, Fragment, fragment } from '../utils'; + +export function getInstructionArgumentAssignmentVisitor(argument: ParsedInstructionArgument, offset: number) { + return pipe( + staticVisitor((): [Fragment, number] => [fragment``, 0], { + keys: [...REGISTERED_TYPE_NODE_KINDS, 'definedTypeLinkNode'], + }), + v => + extendVisitor(v, { + visitArrayType() { + return [fragment``, 0]; + }, + + visitBooleanType() { + return [fragment``, 0]; + }, + + visitBytesType() { + return [fragment``, 0]; + }, + + visitDefinedTypeLink() { + return [fragment``, 0]; + }, + + visitEnumEmptyVariantType() { + return [fragment``, 0]; + }, + + visitEnumStructVariantType() { + return [fragment``, 0]; + }, + + visitEnumTupleVariantType() { + return [fragment``, 0]; + }, + + visitEnumType() { + return [fragment``, 0]; + }, + + visitFixedSizeType(fixedSizeType, { self }) { + if (isNode(fixedSizeType.type, 'stringTypeNode')) { + let value: Fragment; + if (argument.defaultValue) { + value = fragment`&${argument.resolvedDefaultValue}`; + } else { + value = fragment`self.${argument.displayName}.as_ref()`; + } + return [ + addFragmentImports( + fragment`write_bytes(&mut uninit_data[${offset}..${offset + argument.fixedSize!}], ${value});`, + ['super::write_bytes'], + ), + offset + argument.fixedSize!, + ]; + } + + return visit(fixedSizeType.type, self); + }, + + visitMapType() { + return [fragment``, 0]; + }, + + visitNumberType(numberType) { + if (numberType.format === 'u8') { + let value: Fragment; + if (argument.defaultValue) { + value = fragment`${argument.resolvedDefaultValue}`; + } else { + value = fragment`self.${argument.displayName}`; + } + return [fragment`uninit_data[${offset}] = ${value};`, offset + 1]; + } else { + let value: Fragment; + if (argument.defaultValue) { + value = fragment`${argument.resolvedDefaultValue}${numberType.format}`; + } else { + value = fragment`self.${argument.displayName}`; + } + return [ + addFragmentImports( + fragment`write_bytes(&mut uninit_data[${offset}..${offset + argument.fixedSize!}], &${value}.to_le_bytes());`, + ['super::write_bytes'], + ), + offset + argument.fixedSize!, + ]; + } + }, + + visitOptionType() { + return [fragment``, 0]; + }, + + visitPublicKeyType() { + let value: Fragment; + if (argument.defaultValue) { + value = fragment`${argument.resolvedDefaultValue}`; + } else { + value = fragment`self.${argument.displayName}`; + } + return [ + addFragmentImports( + fragment`write_bytes(&mut uninit_data[${offset}..${offset + argument.fixedSize!}], ${value}.as_ref());`, + ['super::write_bytes'], + ), + offset + argument.fixedSize!, + ]; + }, + + visitRemainderOptionType(node) { + throw new CodamaError(CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node }); + }, + + visitSetType() { + return [fragment``, 0]; + }, + + visitSizePrefixType() { + return [fragment``, 0]; + }, + + visitStringType() { + return [fragment``, 0]; + }, + + visitStructFieldType() { + return [fragment``, 0]; + }, + + visitStructType() { + return [fragment``, 0]; + }, + + visitTupleType() { + return [fragment``, 0]; + }, + + visitZeroableOptionType(node) { + throw new CodamaError(CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node }); + }, + }), + ); +} diff --git a/packages/renderers-rust-cpi/src/visitors/getRenderMapVisitor.ts b/packages/renderers-rust-cpi/src/visitors/getRenderMapVisitor.ts new file mode 100644 index 000000000..1582c8505 --- /dev/null +++ b/packages/renderers-rust-cpi/src/visitors/getRenderMapVisitor.ts @@ -0,0 +1,86 @@ +import { getAllInstructionsWithSubs, getAllPrograms, snakeCase } from '@codama/nodes'; +import { createRenderMap, mergeRenderMaps } from '@codama/renderers-core'; +import { + extendVisitor, + getByteSizeVisitor, + LinkableDictionary, + NodeStack, + pipe, + recordLinkablesOnFirstVisitVisitor, + recordNodeStackVisitor, + staticVisitor, + visit, +} from '@codama/visitors-core'; + +import { getInstructionPageFragment, getProgramModPageFragment, getRootModPageFragment } from '../fragments'; +import { getInstructionModPageFragment } from '../fragments/instructionModPage'; +import { getImportFromFactory, GetRenderMapOptions, getTraitsFromNodeFactory, RenderScope } from '../utils'; +import { getTypeManifestVisitor } from './getTypeManifestVisitor'; + +export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { + const linkables = new LinkableDictionary(); + const stack = new NodeStack(); + + const renderParentInstructions = options.renderParentInstructions ?? false; + const dependencyMap = options.dependencyMap ?? {}; + const getImportFrom = getImportFromFactory(options.linkOverrides ?? {}); + const getTraitsFromNode = getTraitsFromNodeFactory(options.traitOptions); + const typeManifestVisitor = getTypeManifestVisitor({ getImportFrom, getTraitsFromNode }); + const byteSizeVisitor = getByteSizeVisitor(linkables, { stack }); + + const renderScope: RenderScope = { + byteSizeVisitor, + dependencyMap, + getImportFrom, + getTraitsFromNode, + linkables, + renderParentInstructions, + typeManifestVisitor, + }; + + return pipe( + staticVisitor(() => createRenderMap(), { + keys: ['rootNode', 'programNode', 'instructionNode', 'accountNode', 'definedTypeNode'], + }), + v => + extendVisitor(v, { + visitInstruction(node) { + const instructionPath = stack.getPath('instructionNode'); + return createRenderMap( + `instructions/${snakeCase(node.name)}.rs`, + getInstructionPageFragment({ ...renderScope, instructionPath }), + ); + }, + + visitProgram(node, { self }) { + return mergeRenderMaps([ + ...getAllInstructionsWithSubs(node, { + leavesOnly: !renderParentInstructions, + }).map(ix => visit(ix, self)), + ]); + }, + + visitRoot(node, { self }) { + const programsToExport = getAllPrograms(node); + const instructionsToExport = getAllInstructionsWithSubs(node, { + leavesOnly: !renderParentInstructions, + }); + const scope = { ...renderScope, instructionsToExport, programsToExport }; + + return mergeRenderMaps([ + createRenderMap({ + ['instructions/mod.rs']: getInstructionModPageFragment({ + ...renderScope, + instructions: instructionsToExport, + }), + ['mod.rs']: getRootModPageFragment(scope), + ['programs/mod.rs']: getProgramModPageFragment(scope), + }), + ...programsToExport.map(p => visit(p, self)), + ]); + }, + }), + v => recordNodeStackVisitor(v, stack), + v => recordLinkablesOnFirstVisitVisitor(v, linkables), + ); +} diff --git a/packages/renderers-rust-cpi/src/visitors/getTypeManifestVisitor.ts b/packages/renderers-rust-cpi/src/visitors/getTypeManifestVisitor.ts new file mode 100644 index 000000000..c4f25628f --- /dev/null +++ b/packages/renderers-rust-cpi/src/visitors/getTypeManifestVisitor.ts @@ -0,0 +1,415 @@ +import { CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, CodamaError } from '@codama/errors'; +import { + arrayTypeNode, + CountNode, + definedTypeNode, + fixedCountNode, + isNode, + NumberTypeNode, + numberTypeNode, + pascalCase, + prefixedCountNode, + REGISTERED_TYPE_NODE_KINDS, + remainderCountNode, + resolveNestedTypeNode, + snakeCase, +} from '@codama/nodes'; +import { extendVisitor, mergeVisitor, pipe, visit } from '@codama/visitors-core'; + +import { + addFragmentImports, + Fragment, + fragment, + getDocblockFragment, + GetImportFromFunction, + GetTraitsFromNodeFunction, + mergeFragments, +} from '../utils'; + +export type TypeManifest = { + nestedStructs: Fragment[]; + type: Fragment; +}; + +export type TypeManifestVisitor = ReturnType; + +export function getTypeManifestVisitor(options: { + getImportFrom: GetImportFromFunction; + getTraitsFromNode: GetTraitsFromNodeFunction; + nestedStruct?: boolean; + parentName?: string | null; +}) { + const { getImportFrom, getTraitsFromNode } = options; + let parentName: string | null = options.parentName ?? null; + let nestedStruct: boolean = options.nestedStruct ?? false; + let inlineStruct: boolean = false; + let parentSize: NumberTypeNode | number | null = null; + + return pipe( + mergeVisitor( + (): TypeManifest => ({ nestedStructs: [], type: fragment`` }), + (_, manifests) => mergeManifests(manifests), + { keys: [...REGISTERED_TYPE_NODE_KINDS, 'definedTypeLinkNode', 'definedTypeNode', 'accountNode'] }, + ), + v => + extendVisitor(v, { + visitAccount(account, { self }) { + parentName = pascalCase(account.name); + const manifest = visit(account.data, self); + const traits = getTraitsFromNode(account); + parentName = null; + return { ...manifest, type: fragment`${traits}${manifest.type}` }; + }, + + visitArrayType(arrayType, { self }) { + const childManifest = visit(arrayType.item, self); + + if (isNode(arrayType.count, 'fixedCountNode')) { + return { + ...childManifest, + type: fragment`[${childManifest.type}; ${arrayType.count.value}]`, + }; + } + + // TODO: Add to the Rust validator. + throw new Error('Array size currently not supported.'); + }, + + visitBooleanType(booleanType) { + const resolvedSize = resolveNestedTypeNode(booleanType.size); + if (resolvedSize.format === 'u8' && resolvedSize.endian === 'le') { + return { nestedStructs: [], type: fragment`bool` }; + } + + // TODO: Add to the Rust validator. + throw new Error('Bool size currently not supported.'); + }, + + visitBytesType(_bytesType, { self }) { + let arraySize: CountNode = remainderCountNode(); + if (typeof parentSize === 'number') { + arraySize = fixedCountNode(parentSize); + } else if (parentSize && typeof parentSize === 'object') { + arraySize = prefixedCountNode(parentSize); + } + const arrayType = arrayTypeNode(numberTypeNode('u8'), arraySize); + return visit(arrayType, self); + }, + + visitDefinedType(definedType, { self }) { + parentName = pascalCase(definedType.name); + const manifest = visit(definedType.type, self); + const traits = getTraitsFromNode(definedType); + parentName = null; + + const renderedType = isNode(definedType.type, ['enumTypeNode', 'structTypeNode']) + ? manifest.type + : fragment`pub type ${pascalCase(definedType.name)} = ${manifest.type};`; + + return { ...manifest, type: fragment`${traits}${renderedType}` }; + }, + + visitDefinedTypeLink(node) { + const pascalCaseDefinedType = pascalCase(node.name); + const importFrom = getImportFrom(node); + return { + nestedStructs: [], + type: addFragmentImports(fragment`${pascalCaseDefinedType}`, [ + `${importFrom}::${pascalCaseDefinedType}`, + ]), + }; + }, + + visitEnumEmptyVariantType(enumEmptyVariantType) { + const name = pascalCase(enumEmptyVariantType.name); + return { nestedStructs: [], type: fragment`${name},` }; + }, + + visitEnumStructVariantType(enumStructVariantType, { self }) { + const name = pascalCase(enumStructVariantType.name); + const originalParentName = parentName; + + if (!originalParentName) { + throw new Error('Enum struct variant type must have a parent name.'); + } + + inlineStruct = true; + parentName = pascalCase(originalParentName) + name; + const typeManifest = visit(enumStructVariantType.struct, self); + inlineStruct = false; + parentName = originalParentName; + + return { + ...typeManifest, + type: fragment`${name} ${typeManifest.type},`, + }; + }, + + visitEnumTupleVariantType(enumTupleVariantType, { self }) { + const name = pascalCase(enumTupleVariantType.name); + const originalParentName = parentName; + + if (!originalParentName) { + throw new Error('Enum struct variant type must have a parent name.'); + } + + parentName = pascalCase(originalParentName) + name; + const childManifest = visit(enumTupleVariantType.tuple, self); + parentName = originalParentName; + + let derive = ''; + if (childManifest.type.content === '(Pubkey)') { + derive = + '#[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))]\n'; + } else if (childManifest.type.content === '(Vec)') { + derive = + '#[cfg_attr(feature = "serde", serde(with = "serde_with::As::>"))]\n'; + } + + return { + ...childManifest, + type: fragment`${derive}${name}${childManifest.type},`, + }; + }, + + visitEnumType(enumType, { self }) { + const originalParentName = parentName; + if (!originalParentName) { + // TODO: Add to the Rust validator. + throw new Error('Enum type must have a parent name.'); + } + + const variants = enumType.variants.map(variant => visit(variant, self)); + const mergedManifest = mergeManifests(variants); + + return { + ...mergedManifest, + type: fragment`pub enum ${pascalCase(originalParentName)} {\n${mergedManifest.type}\n}`, + }; + }, + + visitFixedSizeType(fixedSizeType, { self }) { + parentSize = fixedSizeType.size; + const manifest = visit(fixedSizeType.type, self); + parentSize = null; + return manifest; + }, + + visitMapType(mapType, { self }) { + const key = visit(mapType.key, self); + const value = visit(mapType.value, self); + const mergedManifest = mergeManifests([key, value]); + return { + ...mergedManifest, + type: addFragmentImports(fragment`HashMap<${key.type}, ${value.type}>`, [ + 'std::collections::HashMap', + ]), + }; + }, + + visitNumberType(numberType) { + if (numberType.endian !== 'le') { + // TODO: Add to the Rust validator. + throw new Error('Number endianness currently not supported.'); + } + + if (numberType.format === 'shortU16') { + return { + nestedStructs: [], + type: addFragmentImports(fragment`ShortU16`, ['solana_program::short_vec::ShortU16']), + }; + } + + return { nestedStructs: [], type: fragment`${numberType.format}` }; + }, + + visitOptionType(optionType, { self }) { + const childManifest = visit(optionType.item, self); + + const optionPrefix = resolveNestedTypeNode(optionType.prefix); + if (optionPrefix.format === 'u8' && optionPrefix.endian === 'le') { + return { + ...childManifest, + type: fragment`Option<${childManifest.type}>`, + }; + } + + // TODO: Add to the Rust validator. + throw new Error('Option size currently not supported.'); + }, + + visitPublicKeyType() { + return { + nestedStructs: [], + type: addFragmentImports(fragment`Pubkey`, ['pinocchio::pubkey::Pubkey']), + }; + }, + + visitRemainderOptionType(node) { + throw new CodamaError(CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node }); + }, + + visitSetType(setType, { self }) { + const childManifest = visit(setType.item, self); + return { + ...childManifest, + type: addFragmentImports(fragment`HashSet<${childManifest.type}>`, [ + 'std::collections::HashSet', + ]), + }; + }, + + visitSizePrefixType(sizePrefixType, { self }) { + parentSize = resolveNestedTypeNode(sizePrefixType.prefix); + const manifest = visit(sizePrefixType.type, self); + parentSize = null; + return manifest; + }, + + visitStringType() { + if (!parentSize) { + return { nestedStructs: [], type: fragment`str` }; + } + + if (typeof parentSize === 'number') { + return { nestedStructs: [], type: fragment`[u8; ${parentSize}]` }; + } + + if (isNode(parentSize, 'numberTypeNode') && parentSize.endian === 'le') { + switch (parentSize.format) { + case 'u32': + return { nestedStructs: [], type: fragment`String` }; + case 'u8': + case 'u16': + case 'u64': { + const prefix = parentSize.format.toUpperCase(); + return { + nestedStructs: [], + type: addFragmentImports(fragment`${prefix}PrefixString`, [ + `kaigan::types::${prefix}PrefixString`, + ]), + }; + } + default: + throw new Error(`String size not supported: ${parentSize.format}`); + } + } + + // TODO: Add to the Rust validator. + throw new Error(`String size currently not supported: ${parentSize.format}`); + }, + + visitStructFieldType(structFieldType, { self }) { + const originalParentName = parentName; + const originalInlineStruct = inlineStruct; + const originalNestedStruct = nestedStruct; + + if (!originalParentName) { + throw new Error('Struct field type must have a parent name.'); + } + + parentName = pascalCase(originalParentName) + pascalCase(structFieldType.name); + nestedStruct = true; + inlineStruct = false; + + const fieldManifest = visit(structFieldType.type, self); + + parentName = originalParentName; + inlineStruct = originalInlineStruct; + nestedStruct = originalNestedStruct; + + const fieldName = snakeCase(structFieldType.name); + const docs = getDocblockFragment(structFieldType.docs ?? [], true); + const resolvedNestedType = resolveNestedTypeNode(structFieldType.type); + + let derive = ''; + if (fieldManifest.type.content === 'Pubkey') { + derive = + '#[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))]\n'; + } else if (fieldManifest.type.content === 'Vec') { + derive = + '#[cfg_attr(feature = "serde", serde(with = "serde_with::As::>"))]\n'; + } else if ( + (isNode(resolvedNestedType, 'arrayTypeNode') && + isNode(resolvedNestedType.count, 'fixedCountNode') && + resolvedNestedType.count.value > 32) || + (isNode(resolvedNestedType, ['bytesTypeNode', 'stringTypeNode']) && + isNode(structFieldType.type, 'fixedSizeTypeNode') && + structFieldType.type.size > 32) + ) { + derive = + '#[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))]\n'; + } + + return { + ...fieldManifest, + type: inlineStruct + ? fragment`${docs}${derive}${fieldName}: ${fieldManifest.type},` + : fragment`${docs}${derive}pub ${fieldName}: ${fieldManifest.type},`, + }; + }, + + visitStructType(structType, { self }) { + const originalParentName = parentName; + + if (!originalParentName) { + // TODO: Add to the Rust validator. + throw new Error('Struct type must have a parent name.'); + } + + const fields = structType.fields.map(field => visit(field, self)); + const mergedManifest = mergeManifests(fields); + + if (nestedStruct) { + const nestedTraits = getTraitsFromNode( + definedTypeNode({ name: originalParentName, type: structType }), + ); + return { + ...mergedManifest, + nestedStructs: [ + ...mergedManifest.nestedStructs, + fragment`${nestedTraits}pub struct ${pascalCase(originalParentName)} {\n${mergedManifest.type}\n}`, + ], + type: fragment`${pascalCase(originalParentName)}`, + }; + } + + if (inlineStruct) { + return { ...mergedManifest, type: fragment`{\n${mergedManifest.type}\n}` }; + } + + return { + ...mergedManifest, + type: fragment`pub struct ${pascalCase(originalParentName)} {\n${mergedManifest.type}\n}`, + }; + }, + + visitTupleType(tupleType, { self }) { + const items = tupleType.items.map(item => visit(item, self)); + const mergedManifest = mergeManifests(items); + + return { + ...mergedManifest, + type: mergeFragments( + items.map(i => i.type), + cs => `(${cs.join(', ')})`, + ), + }; + }, + + visitZeroableOptionType(node) { + throw new CodamaError(CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node }); + }, + }), + ); +} + +function mergeManifests(manifests: TypeManifest[]): TypeManifest { + return { + nestedStructs: manifests.flatMap(m => m.nestedStructs), + type: mergeFragments( + manifests.map(m => m.type), + cs => cs.join('\n'), + ), + }; +} diff --git a/packages/renderers-rust-cpi/src/visitors/index.ts b/packages/renderers-rust-cpi/src/visitors/index.ts new file mode 100644 index 000000000..dc39bc10a --- /dev/null +++ b/packages/renderers-rust-cpi/src/visitors/index.ts @@ -0,0 +1,5 @@ +export * from './getInstructionArgumentAssignmentVisitor'; +export * from './getRenderMapVisitor'; +export * from './getTypeManifestVisitor'; +export * from './renderValueNodeVisitor'; +export * from './renderVisitor'; diff --git a/packages/renderers-rust-cpi/src/visitors/renderValueNodeVisitor.ts b/packages/renderers-rust-cpi/src/visitors/renderValueNodeVisitor.ts new file mode 100644 index 000000000..f94527eda --- /dev/null +++ b/packages/renderers-rust-cpi/src/visitors/renderValueNodeVisitor.ts @@ -0,0 +1,138 @@ +import { + arrayValueNode, + bytesValueNode, + isNode, + numberValueNode, + pascalCase, + RegisteredValueNode, + ValueNode, +} from '@codama/nodes'; +import { visit, Visitor } from '@codama/visitors-core'; + +import { + addFragmentImports, + Fragment, + fragment, + getBytesFromBytesValueNode, + GetImportFromFunction, + mergeFragments, +} from '../utils'; + +export function renderValueNode( + value: ValueNode, + getImportFrom: GetImportFromFunction, + useStr: boolean = false, +): Fragment { + return visit(value, renderValueNodeVisitor(getImportFrom, useStr)); +} + +export function renderValueNodeVisitor( + getImportFrom: GetImportFromFunction, + useStr: boolean = false, +): Visitor { + return { + visitArrayValue(node) { + return mergeFragments( + node.items.map(v => visit(v, this)), + cs => `[${cs.join(', ')}]`, + ); + }, + + visitBooleanValue(node) { + return fragment`${JSON.stringify(node.boolean)}`; + }, + + visitBytesValue(node) { + const bytes = getBytesFromBytesValueNode(node); + const numbers = Array.from(bytes).map(numberValueNode); + return visit(arrayValueNode(numbers), this); + }, + + visitConstantValue(node) { + if (isNode(node.value, 'bytesValueNode')) { + return visit(node.value, this); + } + if (isNode(node.type, 'stringTypeNode') && isNode(node.value, 'stringValueNode')) { + return visit(bytesValueNode(node.type.encoding, node.value.string), this); + } + if (isNode(node.type, 'numberTypeNode') && isNode(node.value, 'numberValueNode')) { + const child = visit(node.value, this); + const { format, endian } = node.type; + const byteFunction = endian === 'le' ? 'to_le_bytes' : 'to_be_bytes'; + return fragment`${child}${format}.${byteFunction}()`; + } + throw new Error('Unsupported constant value type.'); + }, + + visitEnumValue(node) { + const variantName = pascalCase(node.variant); + const enumName = addFragmentImports(fragment`${pascalCase(node.enum.name)}`, [ + `${getImportFrom(node.enum)}::${pascalCase(node.enum.name)}`, + ]); + if (!node.value) return fragment`${enumName}::${variantName}`; + + const fields = visit(node.value, this); + return fragment`${enumName}::${variantName} ${fields}`; + }, + + visitMapEntryValue(node) { + const mapKey = visit(node.key, this); + const mapValue = visit(node.value, this); + return fragment`[${mapKey}, ${mapValue}]`; + }, + + visitMapValue(node) { + const entries = node.entries.map(entry => visit(entry, this)); + return addFragmentImports( + mergeFragments(entries, cs => `HashMap::from([${cs.join(', ')}])`), + ['std::collection::HashMap'], + ); + }, + + visitNoneValue() { + return fragment`None`; + }, + + visitNumberValue(node) { + return fragment`${node.number}`; + }, + + visitPublicKeyValue(node) { + return addFragmentImports(fragment`pubkey!("${node.publicKey}")`, ['solana_program::pubkey']); + }, + + visitSetValue(node) { + const items = node.items.map(v => visit(v, this)); + return addFragmentImports( + mergeFragments(items, cs => `HashSet::from([${cs.join(', ')}])`), + ['std::collection::HashSet'], + ); + }, + + visitSomeValue(node) { + const child = visit(node.value, this); + return fragment`Some(${child})`; + }, + + visitStringValue(node) { + return useStr + ? fragment`${JSON.stringify(node.string)}` + : fragment`String::from(${JSON.stringify(node.string)})`; + }, + + visitStructFieldValue(node) { + const structValue = visit(node.value, this); + return fragment`${node.name}: ${structValue}`; + }, + + visitStructValue(node) { + const fields = node.fields.map(field => visit(field, this)); + return mergeFragments(fields, cs => `{ ${cs.join(', ')} }`); + }, + + visitTupleValue(node) { + const items = node.items.map(v => visit(v, this)); + return mergeFragments(items, cs => `(${cs.join(', ')})`); + }, + }; +} diff --git a/packages/renderers-rust-cpi/src/visitors/renderVisitor.ts b/packages/renderers-rust-cpi/src/visitors/renderVisitor.ts new file mode 100644 index 000000000..34a29762f --- /dev/null +++ b/packages/renderers-rust-cpi/src/visitors/renderVisitor.ts @@ -0,0 +1,50 @@ +import { spawnSync } from 'node:child_process'; + +import { logError, logWarn } from '@codama/errors'; +import { deleteDirectory, writeRenderMapVisitor } from '@codama/renderers-core'; +import { rootNodeVisitor, visit } from '@codama/visitors-core'; + +import { RenderOptions } from '../utils'; +import { getRenderMapVisitor } from './getRenderMapVisitor'; + +export function renderVisitor(path: string, options: RenderOptions = {}) { + return rootNodeVisitor(root => { + // Delete existing generated folder. + if (options.deleteFolderBeforeRendering ?? true) { + deleteDirectory(path); + } + + // Render the new files. + visit(root, writeRenderMapVisitor(getRenderMapVisitor(options), path)); + + // format the code + if (options.formatCode) { + if (options.crateFolder) { + const toolchain = options.toolchain ?? '+stable'; + runFormatter('cargo', [toolchain, 'fmt', '--manifest-path', `${options.crateFolder}/Cargo.toml`]); + } else { + logWarn('No crate folder specified, skipping formatting.'); + } + } + }); +} + +function runFormatter(cmd: string, args: string[]) { + if (__NODEJS__) { + const { stdout, stderr, error } = spawnSync(cmd, args); + if (error?.message?.includes('ENOENT')) { + logWarn(`Could not find ${cmd}, skipping formatting.`); + return; + } + if (stdout.length > 0) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + logWarn(`(cargo-fmt) ${stdout || error}`); + } + if (stderr.length > 0) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + logError(`(cargo-fmt) ${stderr || error}`); + } + } else { + logWarn('Can only use cargo-fmt in Node environments, skipping formatting.'); + } +} diff --git a/packages/renderers-rust-cpi/test/_setup.ts b/packages/renderers-rust-cpi/test/_setup.ts new file mode 100644 index 000000000..beb4f1d90 --- /dev/null +++ b/packages/renderers-rust-cpi/test/_setup.ts @@ -0,0 +1,23 @@ +import { expect } from 'vitest'; + +export function codeContains(actual: string, expected: (RegExp | string)[] | RegExp | string) { + const expectedArray = Array.isArray(expected) ? expected : [expected]; + expectedArray.forEach(e => { + if (typeof e === 'string') { + expect(actual).toContain(e); + } else { + expect(actual).toMatch(e); + } + }); +} + +export function codeDoesNotContains(actual: string, expected: (RegExp | string)[] | RegExp | string) { + const expectedArray = Array.isArray(expected) ? expected : [expected]; + expectedArray.forEach(e => { + if (typeof e === 'string') { + expect(actual).not.toContain(e); + } else { + expect(actual).not.toMatch(e); + } + }); +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/Cargo.lock b/packages/renderers-rust-cpi/test/e2e/dummy/Cargo.lock new file mode 100644 index 000000000..8aab0eeaf --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/Cargo.lock @@ -0,0 +1,56 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "codama-renderers-rust-e2e-dummy" +version = "0.0.0" +dependencies = [ + "assert_matches", + "pinocchio", + "pinocchio-pubkey", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" + +[[package]] +name = "pinocchio" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3410a5d525f024b0aed7f8747e94e918c91ba64274a190e2365903e2c0b2e9" + +[[package]] +name = "pinocchio-pubkey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" +dependencies = [ + "five8_const", + "pinocchio", + "sha2-const-stable", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/Cargo.toml b/packages/renderers-rust-cpi/test/e2e/dummy/Cargo.toml new file mode 100644 index 000000000..3ec2755c1 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "codama-renderers-rust-e2e-dummy" +version = "0.0.0" +edition = "2021" + +[features] +test-sbf = [] + +[dependencies] +pinocchio = "0.9" +pinocchio-pubkey = "0.3" + +[dev-dependencies] +assert_matches = "1.5.0" diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/idl.json b/packages/renderers-rust-cpi/test/e2e/dummy/idl.json new file mode 100644 index 000000000..b81b84d79 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/idl.json @@ -0,0 +1,149 @@ +{ + "kind": "rootNode", + "program": { + "kind": "programNode", + "pdas": [], + "accounts": [], + "instructions": [ + { + "kind": "instructionNode", + "name": "instruction1", + "optionalAccountStrategy": "programId", + "docs": ["Testing instructions with no accounts or arguments"], + "accounts": [], + "arguments": [], + "remainingAccounts": [] + }, + { + "kind": "instructionNode", + "name": "instruction2", + "optionalAccountStrategy": "programId", + "docs": ["Testing instructions with remaining accounts only"], + "accounts": [], + "arguments": [], + "remainingAccounts": [ + { + "kind": "instructionRemainingAccountsNode", + "value": { + "kind": "argumentValueNode", + "name": "remainingAccounts" + }, + "isOptional": true, + "isSigner": false + } + ] + }, + { + "kind": "instructionNode", + "name": "instruction3", + "optionalAccountStrategy": "programId", + "docs": ["Testing instructions with discriminator only"], + "accounts": [], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 42 }, + "defaultValueStrategy": "omitted" + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ] + }, + { + "kind": "instructionNode", + "name": "instruction4", + "optionalAccountStrategy": "programId", + "docs": ["Testing instructions with arguments only"], + "accounts": [], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "myArgument", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + } + ] + }, + { + "kind": "instructionNode", + "name": "instruction5", + "optionalAccountStrategy": "programId", + "docs": ["Testing instructions with optional arguments only"], + "accounts": [], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "myArgument", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "defaultValue": { "kind": "numberValueNode", "number": 42 }, + "docs": [] + } + ] + }, + { + "kind": "instructionNode", + "name": "instruction6", + "optionalAccountStrategy": "programId", + "docs": ["Testing instructions with accounts only"], + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "myAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + } + ], + "arguments": [] + }, + { + "kind": "instructionNode", + "name": "instruction7", + "optionalAccountStrategy": "programId", + "docs": ["Testing instructions with optional accounts only"], + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "myAccount", + "isWritable": true, + "isSigner": false, + "isOptional": true, + "docs": [] + } + ], + "arguments": [] + } + ], + "definedTypes": [], + "errors": [], + "name": "dummy", + "prefix": "", + "publicKey": "Dummy11111111111111111111111111111111111111", + "version": "3.0.1", + "origin": "shank" + }, + "additionalPrograms": [], + "standard": "codama", + "version": "1.0.0" + } \ No newline at end of file diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction1.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction1.rs new file mode 100644 index 000000000..1afb94408 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction1.rs @@ -0,0 +1,36 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `instruction1` instruction. +pub struct Instruction1 {} + +impl Instruction1 { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 0] = []; + + let data = &[]; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction2.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction2.rs new file mode 100644 index 000000000..3d69c5ab4 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction2.rs @@ -0,0 +1,36 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `instruction2` instruction. +pub struct Instruction2 {} + +impl Instruction2 { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 0] = []; + + let data = &[]; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction3.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction3.rs new file mode 100644 index 000000000..934d0e302 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction3.rs @@ -0,0 +1,36 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `instruction3` instruction. +pub struct Instruction3 {} + +impl Instruction3 { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 0] = []; + + let data = &42u32.to_le_bytes(); + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction4.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction4.rs new file mode 100644 index 000000000..445781343 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction4.rs @@ -0,0 +1,38 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `instruction4` instruction. +pub struct Instruction4 { + pub my_argument: u64, +} + +impl Instruction4 { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 0] = []; + + let data = &self.my_argument.to_le_bytes(); + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction5.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction5.rs new file mode 100644 index 000000000..1bba2e820 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction5.rs @@ -0,0 +1,36 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `instruction5` instruction. +pub struct Instruction5 {} + +impl Instruction5 { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 0] = []; + + let data = &42u64.to_le_bytes(); + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction6.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction6.rs new file mode 100644 index 000000000..5c030f402 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction6.rs @@ -0,0 +1,40 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `instruction6` instruction. +pub struct Instruction6<'a> { + pub my_account: &'a AccountInfo, +} + +impl Instruction6<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 1] = + [AccountMeta::new(self.my_account.key(), true, false)]; + + let data = &[]; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.my_account], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction7.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction7.rs new file mode 100644 index 000000000..244697f7b --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/instruction7.rs @@ -0,0 +1,40 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `instruction7` instruction. +pub struct Instruction7<'a> { + pub my_account: Option<&'a AccountInfo>, +} + +impl Instruction7<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 1] = + [AccountMeta::new(self.my_account.key(), true, false)]; + + let data = &[]; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.my_account], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/mod.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/mod.rs new file mode 100644 index 000000000..d539e79a9 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/instructions/mod.rs @@ -0,0 +1,33 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use core::mem::MaybeUninit; + +pub mod r#instruction1; +pub mod r#instruction2; +pub mod r#instruction3; +pub mod r#instruction4; +pub mod r#instruction5; +pub mod r#instruction6; +pub mod r#instruction7; + +pub use self::r#instruction1::*; +pub use self::r#instruction2::*; +pub use self::r#instruction3::*; +pub use self::r#instruction4::*; +pub use self::r#instruction5::*; +pub use self::r#instruction6::*; +pub use self::r#instruction7::*; + +const UNINIT_BYTE: MaybeUninit = MaybeUninit::::uninit(); + +/// Write bytes from a source slice to a destination slice of `MaybeUninit`. +#[inline(always)] +fn write_bytes(destination: &mut [MaybeUninit], source: &[u8]) { + for (d, s) in destination.iter_mut().zip(source.iter()) { + d.write(*s); + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/mod.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/mod.rs new file mode 100644 index 000000000..c1b581407 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/mod.rs @@ -0,0 +1,8 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +pub mod instructions; +pub mod programs; diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/programs/mod.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/programs/mod.rs new file mode 100644 index 000000000..bada6e398 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/generated/programs/mod.rs @@ -0,0 +1,11 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::pubkey::Pubkey; +use pinocchio_pubkey::pubkey; + +/// `dummy` program ID. +pub const DUMMY_ID: Pubkey = pubkey!("Dummy11111111111111111111111111111111111111"); diff --git a/packages/renderers-rust-cpi/test/e2e/dummy/src/lib.rs b/packages/renderers-rust-cpi/test/e2e/dummy/src/lib.rs new file mode 100644 index 000000000..03cae8387 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/dummy/src/lib.rs @@ -0,0 +1,4 @@ +mod generated; + +pub use generated::programs::DUMMY_ID as ID; +pub use generated::*; diff --git a/packages/renderers-rust-cpi/test/e2e/generate.cjs b/packages/renderers-rust-cpi/test/e2e/generate.cjs new file mode 100755 index 000000000..e18a45786 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/generate.cjs @@ -0,0 +1,35 @@ +#!/usr/bin/env -S node + +const path = require('node:path'); +const process = require('node:process'); + +const { rootNode } = require('@codama/nodes'); +const { readJson } = require('@codama/renderers-core'); +const { visit } = require('@codama/visitors-core'); + +const { renderVisitor } = require('../../dist/index.node.cjs'); + +async function main() { + const project = process.argv.slice(2)[0] ?? undefined; + if (project === undefined) { + throw new Error('Project name is required.'); + } + await generateProject(project); +} + +async function generateProject(project) { + const idl = readJson(path.join(__dirname, project, 'idl.json')); + const node = rootNode(idl.program); + visit( + node, + renderVisitor(path.join(__dirname, project, 'src', 'generated'), { + crateFolder: path.join(__dirname, project), + formatCode: true, + }), + ); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/packages/renderers-rust-cpi/test/e2e/memo/Cargo.lock b/packages/renderers-rust-cpi/test/e2e/memo/Cargo.lock new file mode 100644 index 000000000..5a29d8b74 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/Cargo.lock @@ -0,0 +1,56 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "codama-renderers-rust-e2e-memo" +version = "0.0.0" +dependencies = [ + "assert_matches", + "pinocchio", + "pinocchio-pubkey", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" + +[[package]] +name = "pinocchio" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3410a5d525f024b0aed7f8747e94e918c91ba64274a190e2365903e2c0b2e9" + +[[package]] +name = "pinocchio-pubkey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" +dependencies = [ + "five8_const", + "pinocchio", + "sha2-const-stable", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" diff --git a/packages/renderers-rust-cpi/test/e2e/memo/Cargo.toml b/packages/renderers-rust-cpi/test/e2e/memo/Cargo.toml new file mode 100644 index 000000000..ec2143d9f --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "codama-renderers-rust-e2e-memo" +version = "0.0.0" +edition = "2021" + +[features] +test-sbf = [] + +[dependencies] +pinocchio = "0.9" +pinocchio-pubkey = "0.3" + +[dev-dependencies] +assert_matches = "1.5.0" diff --git a/packages/renderers-rust-cpi/test/e2e/memo/idl.json b/packages/renderers-rust-cpi/test/e2e/memo/idl.json new file mode 100644 index 000000000..f25afa6af --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/idl.json @@ -0,0 +1,44 @@ +{ + "kind": "rootNode", + "program": { + "kind": "programNode", + "pdas": [], + "accounts": [], + "instructions": [ + { + "kind": "instructionNode", + "accounts": [], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "memo", + "type": { "kind": "stringTypeNode", "encoding": "utf8" }, + "docs": [] + } + ], + "remainingAccounts": [ + { + "kind": "instructionRemainingAccountsNode", + "value": { "kind": "argumentValueNode", "name": "signers" }, + "isOptional": true, + "isSigner": true + } + ], + "name": "addMemo", + "idlName": "addMemo", + "docs": [], + "optionalAccountStrategy": "programId" + } + ], + "definedTypes": [], + "errors": [], + "name": "memo", + "prefix": "", + "publicKey": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", + "version": "3.0.1", + "origin": "shank" + }, + "additionalPrograms": [], + "standard": "codama", + "version": "1.0.0" +} diff --git a/packages/renderers-rust-cpi/test/e2e/memo/src/generated/instructions/add_memo.rs b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/instructions/add_memo.rs new file mode 100644 index 000000000..edb7c16dc --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/instructions/add_memo.rs @@ -0,0 +1,38 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `add_memo` instruction. +pub struct AddMemo<'a> { + pub memo: &'a str, +} + +impl AddMemo<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 0] = []; + + let data = self.memo.as_bytes(); + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/memo/src/generated/instructions/mod.rs b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/instructions/mod.rs new file mode 100644 index 000000000..10fd21284 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/instructions/mod.rs @@ -0,0 +1,21 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use core::mem::MaybeUninit; + +pub mod r#add_memo; + +pub use self::r#add_memo::*; + +const UNINIT_BYTE: MaybeUninit = MaybeUninit::::uninit(); + +/// Write bytes from a source slice to a destination slice of `MaybeUninit`. +#[inline(always)] +fn write_bytes(destination: &mut [MaybeUninit], source: &[u8]) { + for (d, s) in destination.iter_mut().zip(source.iter()) { + d.write(*s); + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/memo/src/generated/mod.rs b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/mod.rs new file mode 100644 index 000000000..c1b581407 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/mod.rs @@ -0,0 +1,8 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +pub mod instructions; +pub mod programs; diff --git a/packages/renderers-rust-cpi/test/e2e/memo/src/generated/programs/mod.rs b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/programs/mod.rs new file mode 100644 index 000000000..79694ed81 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/src/generated/programs/mod.rs @@ -0,0 +1,11 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::pubkey::Pubkey; +use pinocchio_pubkey::pubkey; + +/// `memo` program ID. +pub const MEMO_ID: Pubkey = pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); diff --git a/packages/renderers-rust-cpi/test/e2e/memo/src/lib.rs b/packages/renderers-rust-cpi/test/e2e/memo/src/lib.rs new file mode 100644 index 000000000..44c77a499 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/memo/src/lib.rs @@ -0,0 +1,4 @@ +mod generated; + +pub use generated::programs::MEMO_ID as ID; +pub use generated::*; diff --git a/packages/renderers-rust-cpi/test/e2e/system/Cargo.lock b/packages/renderers-rust-cpi/test/e2e/system/Cargo.lock new file mode 100644 index 000000000..9a8b5d1bf --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/Cargo.lock @@ -0,0 +1,720 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + +[[package]] +name = "codama-renderers-rust-e2e-system" +version = "0.0.0" +dependencies = [ + "assert_matches", + "borsh", + "num-derive", + "num-traits", + "pinocchio", + "pinocchio-pubkey", + "serde", + "serde_with", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.85", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "pinocchio" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3410a5d525f024b0aed7f8747e94e918c91ba64274a190e2365903e2c0b2e9" + +[[package]] +name = "pinocchio-pubkey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" +dependencies = [ + "five8_const", + "pinocchio", + "sha2-const-stable", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.85", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] diff --git a/packages/renderers-rust-cpi/test/e2e/system/Cargo.toml b/packages/renderers-rust-cpi/test/e2e/system/Cargo.toml new file mode 100644 index 000000000..71ad5bfbd --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "codama-renderers-rust-e2e-system" +version = "0.0.0" +edition = "2021" + +[features] +borsh = ["dep:borsh"] +serde = ["dep:serde", "dep:serde_with"] +test-sbf = [] + +[dependencies] +borsh = { version = "^0.10", optional = true } +num-derive = "^0.3" +num-traits = "^0.2" +pinocchio = "0.9" +pinocchio-pubkey = "0.3" +serde = { version = "^1.0", features = ["derive"], optional = true } +serde_with = { version = "^3.0", optional = true } + +[dev-dependencies] +assert_matches = "1.5.0" diff --git a/packages/renderers-rust-cpi/test/e2e/system/idl.json b/packages/renderers-rust-cpi/test/e2e/system/idl.json new file mode 100644 index 000000000..d488b69fd --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/idl.json @@ -0,0 +1,1042 @@ +{ + "kind": "rootNode", + "program": { + "kind": "programNode", + "pdas": [], + "accounts": [ + { + "kind": "accountNode", + "data": { + "kind": "structTypeNode", + "fields": [ + { + "kind": "structFieldTypeNode", + "name": "version", + "type": { "kind": "definedTypeLinkNode", "name": "nonceVersion" }, + "docs": [] + }, + { + "kind": "structFieldTypeNode", + "name": "state", + "type": { "kind": "definedTypeLinkNode", "name": "nonceState" }, + "docs": [] + }, + { + "kind": "structFieldTypeNode", + "name": "authority", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + }, + { + "kind": "structFieldTypeNode", + "name": "blockhash", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + }, + { + "kind": "structFieldTypeNode", + "name": "lamportsPerSignature", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + } + ] + }, + "name": "nonce", + "idlName": "Nonce", + "docs": [], + "size": 80 + } + ], + "instructions": [ + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "payer", + "isWritable": true, + "isSigner": true, + "isOptional": false, + "docs": [], + "defaultValue": { "kind": "payerValueNode" } + }, + { + "kind": "instructionAccountNode", + "name": "newAccount", + "isWritable": true, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 0 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "lamports", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "space", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "programAddress", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "byteDeltas": [ + { + "kind": "instructionByteDeltaNode", + "value": { "kind": "argumentValueNode", "name": "space" }, + "withHeader": true + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "createAccount", + "idlName": "CreateAccount", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "account", + "isWritable": true, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 1 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "programAddress", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "assign", + "idlName": "Assign", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "source", + "isWritable": true, + "isSigner": true, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "destination", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 2 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "amount", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "transferSol", + "idlName": "TransferSol", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "payer", + "isWritable": true, + "isSigner": true, + "isOptional": false, + "docs": [], + "defaultValue": { "kind": "payerValueNode" } + }, + { + "kind": "instructionAccountNode", + "name": "newAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "baseAccount", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 3 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "base", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "seed", + "type": { + "kind": "sizePrefixTypeNode", + "type": { "kind": "stringTypeNode", "encoding": "utf8" }, + "prefix": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + } + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "amount", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "space", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "programAddress", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "createAccountWithSeed", + "idlName": "CreateAccountWithSeed", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "nonceAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "recentBlockhashesSysvar", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": [], + "defaultValue": { + "kind": "publicKeyValueNode", + "publicKey": "SysvarRecentB1ockHashes11111111111111111111" + } + }, + { + "kind": "instructionAccountNode", + "name": "nonceAuthority", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 4 }, + "defaultValueStrategy": "omitted" + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "advanceNonceAccount", + "idlName": "AdvanceNonceAccount", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "nonceAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "recipientAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "recentBlockhashesSysvar", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": [], + "defaultValue": { + "kind": "publicKeyValueNode", + "publicKey": "SysvarRecentB1ockHashes11111111111111111111" + } + }, + { + "kind": "instructionAccountNode", + "name": "rentSysvar", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": [], + "defaultValue": { + "kind": "publicKeyValueNode", + "publicKey": "SysvarRent111111111111111111111111111111111" + } + }, + { + "kind": "instructionAccountNode", + "name": "nonceAuthority", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 5 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "withdrawAmount", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "withdrawNonceAccount", + "idlName": "WithdrawNonceAccount", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "nonceAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "recentBlockhashesSysvar", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": [], + "defaultValue": { + "kind": "publicKeyValueNode", + "publicKey": "SysvarRecentB1ockHashes11111111111111111111" + } + }, + { + "kind": "instructionAccountNode", + "name": "rentSysvar", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": [], + "defaultValue": { + "kind": "publicKeyValueNode", + "publicKey": "SysvarRent111111111111111111111111111111111" + } + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 6 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "nonceAuthority", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "initializeNonceAccount", + "idlName": "InitializeNonceAccount", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "nonceAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "nonceAuthority", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 7 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "newNonceAuthority", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "authorizeNonceAccount", + "idlName": "AuthorizeNonceAccount", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "newAccount", + "isWritable": true, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 8 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "space", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "allocate", + "idlName": "Allocate", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "newAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "baseAccount", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 9 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "base", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "seed", + "type": { + "kind": "sizePrefixTypeNode", + "type": { "kind": "stringTypeNode", "encoding": "utf8" }, + "prefix": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + } + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "space", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "programAddress", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "allocateWithSeed", + "idlName": "AllocateWithSeed", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "account", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "baseAccount", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 10 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "base", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "seed", + "type": { + "kind": "sizePrefixTypeNode", + "type": { "kind": "stringTypeNode", "encoding": "utf8" }, + "prefix": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + } + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "programAddress", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "assignWithSeed", + "idlName": "AssignWithSeed", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "source", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "baseAccount", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [] + }, + { + "kind": "instructionAccountNode", + "name": "destination", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 11 }, + "defaultValueStrategy": "omitted" + }, + { + "kind": "instructionArgumentNode", + "name": "amount", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "fromSeed", + "type": { + "kind": "sizePrefixTypeNode", + "type": { "kind": "stringTypeNode", "encoding": "utf8" }, + "prefix": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + } + }, + "docs": [] + }, + { + "kind": "instructionArgumentNode", + "name": "fromOwner", + "type": { "kind": "publicKeyTypeNode" }, + "docs": [] + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "transferSolWithSeed", + "idlName": "TransferSolWithSeed", + "docs": [], + "optionalAccountStrategy": "programId" + }, + { + "kind": "instructionNode", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "nonceAccount", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "type": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + }, + "docs": [], + "defaultValue": { "kind": "numberValueNode", "number": 12 }, + "defaultValueStrategy": "omitted" + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ], + "name": "upgradeNonceAccount", + "idlName": "UpgradeNonceAccount", + "docs": [], + "optionalAccountStrategy": "programId" + } + ], + "definedTypes": [ + { + "kind": "definedTypeNode", + "name": "nonceVersion", + "type": { + "kind": "enumTypeNode", + "variants": [ + { "kind": "enumEmptyVariantTypeNode", "name": "legacy" }, + { "kind": "enumEmptyVariantTypeNode", "name": "current" } + ], + "size": { "kind": "numberTypeNode", "format": "u32", "endian": "le" } + }, + "idlName": "NonceVersion", + "docs": [] + }, + { + "kind": "definedTypeNode", + "name": "nonceState", + "type": { + "kind": "enumTypeNode", + "variants": [ + { "kind": "enumEmptyVariantTypeNode", "name": "uninitialized" }, + { "kind": "enumEmptyVariantTypeNode", "name": "initialized" } + ], + "size": { "kind": "numberTypeNode", "format": "u32", "endian": "le" } + }, + "idlName": "NonceState", + "docs": [] + } + ], + "errors": [ + { + "kind": "errorNode", + "name": "accountAlreadyInUse", + "idlName": "AccountAlreadyInUse", + "code": 0, + "message": "an account with the same address already exists", + "docs": ["AccountAlreadyInUse: an account with the same address already exists"] + }, + { + "kind": "errorNode", + "name": "resultWithNegativeLamports", + "idlName": "ResultWithNegativeLamports", + "code": 1, + "message": "account does not have enough SOL to perform the operation", + "docs": ["ResultWithNegativeLamports: account does not have enough SOL to perform the operation"] + }, + { + "kind": "errorNode", + "name": "invalidProgramId", + "idlName": "InvalidProgramId", + "code": 2, + "message": "cannot assign account to this program id", + "docs": ["InvalidProgramId: cannot assign account to this program id"] + }, + { + "kind": "errorNode", + "name": "invalidAccountDataLength", + "idlName": "InvalidAccountDataLength", + "code": 3, + "message": "cannot allocate account data of this length", + "docs": ["InvalidAccountDataLength: cannot allocate account data of this length"] + }, + { + "kind": "errorNode", + "name": "maxSeedLengthExceeded", + "idlName": "MaxSeedLengthExceeded", + "code": 4, + "message": "length of requested seed is too long", + "docs": ["MaxSeedLengthExceeded: length of requested seed is too long"] + }, + { + "kind": "errorNode", + "name": "addressWithSeedMismatch", + "idlName": "AddressWithSeedMismatch", + "code": 5, + "message": "provided address does not match addressed derived from seed", + "docs": ["AddressWithSeedMismatch: provided address does not match addressed derived from seed"] + }, + { + "kind": "errorNode", + "name": "nonceNoRecentBlockhashes", + "idlName": "NonceNoRecentBlockhashes", + "code": 6, + "message": "advancing stored nonce requires a populated RecentBlockhashes sysvar", + "docs": [ + "NonceNoRecentBlockhashes: advancing stored nonce requires a populated RecentBlockhashes sysvar" + ] + }, + { + "kind": "errorNode", + "name": "nonceBlockhashNotExpired", + "idlName": "NonceBlockhashNotExpired", + "code": 7, + "message": "stored nonce is still in recent_blockhashes", + "docs": ["NonceBlockhashNotExpired: stored nonce is still in recent_blockhashes"] + }, + { + "kind": "errorNode", + "name": "nonceUnexpectedBlockhashValue", + "idlName": "NonceUnexpectedBlockhashValue", + "code": 8, + "message": "specified nonce does not match stored nonce", + "docs": ["NonceUnexpectedBlockhashValue: specified nonce does not match stored nonce"] + } + ], + "name": "system", + "prefix": "", + "publicKey": "11111111111111111111111111111111", + "version": "0.0.1", + "origin": "shank" + }, + "additionalPrograms": [], + "standard": "codama", + "version": "0.20.0" +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/advance_nonce_account.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/advance_nonce_account.rs new file mode 100644 index 000000000..448d2a03d --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/advance_nonce_account.rs @@ -0,0 +1,53 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `advance_nonce_account` instruction. +pub struct AdvanceNonceAccount<'a> { + pub nonce_account: &'a AccountInfo, + pub recent_blockhashes_sysvar: &'a AccountInfo, + pub nonce_authority: &'a AccountInfo, +} + +impl AdvanceNonceAccount<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 3] = [ + AccountMeta::new(self.nonce_account.key(), true, false), + AccountMeta::new(self.recent_blockhashes_sysvar.key(), false, false), + AccountMeta::new(self.nonce_authority.key(), false, true), + ]; + + let data = &4u32.to_le_bytes(); + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed( + &instruction, + &[ + &self.nonce_account, + &self.recent_blockhashes_sysvar, + &self.nonce_authority, + ], + signers, + ) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/allocate.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/allocate.rs new file mode 100644 index 000000000..7b7da8c88 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/allocate.rs @@ -0,0 +1,47 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `allocate` instruction. +pub struct Allocate<'a> { + pub new_account: &'a AccountInfo, + pub space: u64, +} + +impl Allocate<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 1] = + [AccountMeta::new(self.new_account.key(), true, true)]; + + let mut uninit_data = [UNINIT_BYTE; 12]; + write_bytes(&mut uninit_data[0..4], &8u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..12], &self.space.to_le_bytes()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 12) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.new_account], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/allocate_with_seed.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/allocate_with_seed.rs new file mode 100644 index 000000000..bb0e1cd80 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/allocate_with_seed.rs @@ -0,0 +1,61 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `allocate_with_seed` instruction. +pub struct AllocateWithSeed<'a, 'b, 'c, 'd> { + pub new_account: &'a AccountInfo, + pub base_account: &'a AccountInfo, + pub base: &'b Pubkey, + pub seed: &'c String, + pub space: u64, + pub program_address: &'d Pubkey, +} + +impl AllocateWithSeed<'_, '_, '_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 2] = [ + AccountMeta::new(self.new_account.key(), true, false), + AccountMeta::new(self.base_account.key(), false, true), + ]; + + let mut uninit_data = [UNINIT_BYTE; 0]; + write_bytes(&mut uninit_data[0..4], &9u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..36], self.base.as_ref()); + + write_bytes(&mut uninit_data[0..8], &self.space.to_le_bytes()); + write_bytes(&mut uninit_data[8..40], self.program_address.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 40) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed( + &instruction, + &[&self.new_account, &self.base_account], + signers, + ) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/assign.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/assign.rs new file mode 100644 index 000000000..6270e9c15 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/assign.rs @@ -0,0 +1,47 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `assign` instruction. +pub struct Assign<'a, 'b> { + pub account: &'a AccountInfo, + pub program_address: &'b Pubkey, +} + +impl Assign<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 1] = [AccountMeta::new(self.account.key(), true, true)]; + + let mut uninit_data = [UNINIT_BYTE; 36]; + write_bytes(&mut uninit_data[0..4], &1u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..36], self.program_address.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 36) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.account], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/assign_with_seed.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/assign_with_seed.rs new file mode 100644 index 000000000..aff297080 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/assign_with_seed.rs @@ -0,0 +1,55 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `assign_with_seed` instruction. +pub struct AssignWithSeed<'a, 'b, 'c, 'd> { + pub account: &'a AccountInfo, + pub base_account: &'a AccountInfo, + pub base: &'b Pubkey, + pub seed: &'c String, + pub program_address: &'d Pubkey, +} + +impl AssignWithSeed<'_, '_, '_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 2] = [ + AccountMeta::new(self.account.key(), true, false), + AccountMeta::new(self.base_account.key(), false, true), + ]; + + let mut uninit_data = [UNINIT_BYTE; 0]; + write_bytes(&mut uninit_data[0..4], &10u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..36], self.base.as_ref()); + + write_bytes(&mut uninit_data[0..32], self.program_address.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 32) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.account, &self.base_account], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/authorize_nonce_account.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/authorize_nonce_account.rs new file mode 100644 index 000000000..5e3b7a0f7 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/authorize_nonce_account.rs @@ -0,0 +1,55 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `authorize_nonce_account` instruction. +pub struct AuthorizeNonceAccount<'a, 'b> { + pub nonce_account: &'a AccountInfo, + pub nonce_authority: &'a AccountInfo, + pub new_nonce_authority: &'b Pubkey, +} + +impl AuthorizeNonceAccount<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 2] = [ + AccountMeta::new(self.nonce_account.key(), true, false), + AccountMeta::new(self.nonce_authority.key(), false, true), + ]; + + let mut uninit_data = [UNINIT_BYTE; 36]; + write_bytes(&mut uninit_data[0..4], &7u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..36], self.new_nonce_authority.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 36) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed( + &instruction, + &[&self.nonce_account, &self.nonce_authority], + signers, + ) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/create_account.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/create_account.rs new file mode 100644 index 000000000..138aa03de --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/create_account.rs @@ -0,0 +1,55 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `create_account` instruction. +pub struct CreateAccount<'a, 'b> { + pub payer: &'a AccountInfo, + pub new_account: &'a AccountInfo, + pub lamports: u64, + pub space: u64, + pub program_address: &'b Pubkey, +} + +impl CreateAccount<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 2] = [ + AccountMeta::new(self.payer.key(), true, true), + AccountMeta::new(self.new_account.key(), true, true), + ]; + + let mut uninit_data = [UNINIT_BYTE; 52]; + write_bytes(&mut uninit_data[0..4], &0u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..12], &self.lamports.to_le_bytes()); + write_bytes(&mut uninit_data[12..20], &self.space.to_le_bytes()); + write_bytes(&mut uninit_data[20..52], self.program_address.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 52) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.payer, &self.new_account], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/create_account_with_seed.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/create_account_with_seed.rs new file mode 100644 index 000000000..62adc0a3f --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/create_account_with_seed.rs @@ -0,0 +1,65 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `create_account_with_seed` instruction. +pub struct CreateAccountWithSeed<'a, 'b, 'c, 'd> { + pub payer: &'a AccountInfo, + pub new_account: &'a AccountInfo, + pub base_account: &'a AccountInfo, + pub base: &'b Pubkey, + pub seed: &'c String, + pub amount: u64, + pub space: u64, + pub program_address: &'d Pubkey, +} + +impl CreateAccountWithSeed<'_, '_, '_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 3] = [ + AccountMeta::new(self.payer.key(), true, true), + AccountMeta::new(self.new_account.key(), true, false), + AccountMeta::new(self.base_account.key(), false, true), + ]; + + let mut uninit_data = [UNINIT_BYTE; 0]; + write_bytes(&mut uninit_data[0..4], &3u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..36], self.base.as_ref()); + + write_bytes(&mut uninit_data[0..8], &self.amount.to_le_bytes()); + write_bytes(&mut uninit_data[8..16], &self.space.to_le_bytes()); + write_bytes(&mut uninit_data[16..48], self.program_address.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 48) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed( + &instruction, + &[&self.payer, &self.new_account, &self.base_account], + signers, + ) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/initialize_nonce_account.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/initialize_nonce_account.rs new file mode 100644 index 000000000..6dd0fec5f --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/initialize_nonce_account.rs @@ -0,0 +1,61 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `initialize_nonce_account` instruction. +pub struct InitializeNonceAccount<'a, 'b> { + pub nonce_account: &'a AccountInfo, + pub recent_blockhashes_sysvar: &'a AccountInfo, + pub rent_sysvar: &'a AccountInfo, + pub nonce_authority: &'b Pubkey, +} + +impl InitializeNonceAccount<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 3] = [ + AccountMeta::new(self.nonce_account.key(), true, false), + AccountMeta::new(self.recent_blockhashes_sysvar.key(), false, false), + AccountMeta::new(self.rent_sysvar.key(), false, false), + ]; + + let mut uninit_data = [UNINIT_BYTE; 36]; + write_bytes(&mut uninit_data[0..4], &6u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..36], self.nonce_authority.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 36) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed( + &instruction, + &[ + &self.nonce_account, + &self.recent_blockhashes_sysvar, + &self.rent_sysvar, + ], + signers, + ) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/mod.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/mod.rs new file mode 100644 index 000000000..77c98d0b5 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/mod.rs @@ -0,0 +1,45 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use core::mem::MaybeUninit; + +pub mod r#advance_nonce_account; +pub mod r#allocate; +pub mod r#allocate_with_seed; +pub mod r#assign; +pub mod r#assign_with_seed; +pub mod r#authorize_nonce_account; +pub mod r#create_account; +pub mod r#create_account_with_seed; +pub mod r#initialize_nonce_account; +pub mod r#transfer_sol; +pub mod r#transfer_sol_with_seed; +pub mod r#upgrade_nonce_account; +pub mod r#withdraw_nonce_account; + +pub use self::r#advance_nonce_account::*; +pub use self::r#allocate::*; +pub use self::r#allocate_with_seed::*; +pub use self::r#assign::*; +pub use self::r#assign_with_seed::*; +pub use self::r#authorize_nonce_account::*; +pub use self::r#create_account::*; +pub use self::r#create_account_with_seed::*; +pub use self::r#initialize_nonce_account::*; +pub use self::r#transfer_sol::*; +pub use self::r#transfer_sol_with_seed::*; +pub use self::r#upgrade_nonce_account::*; +pub use self::r#withdraw_nonce_account::*; + +const UNINIT_BYTE: MaybeUninit = MaybeUninit::::uninit(); + +/// Write bytes from a source slice to a destination slice of `MaybeUninit`. +#[inline(always)] +fn write_bytes(destination: &mut [MaybeUninit], source: &[u8]) { + for (d, s) in destination.iter_mut().zip(source.iter()) { + d.write(*s); + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/transfer_sol.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/transfer_sol.rs new file mode 100644 index 000000000..644e8430f --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/transfer_sol.rs @@ -0,0 +1,50 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `transfer_sol` instruction. +pub struct TransferSol<'a> { + pub source: &'a AccountInfo, + pub destination: &'a AccountInfo, + pub amount: u64, +} + +impl TransferSol<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 2] = [ + AccountMeta::new(self.source.key(), true, true), + AccountMeta::new(self.destination.key(), true, false), + ]; + + let mut uninit_data = [UNINIT_BYTE; 12]; + write_bytes(&mut uninit_data[0..4], &2u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..12], &self.amount.to_le_bytes()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 12) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.source, &self.destination], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/transfer_sol_with_seed.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/transfer_sol_with_seed.rs new file mode 100644 index 000000000..85b1cf0c2 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/transfer_sol_with_seed.rs @@ -0,0 +1,61 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `transfer_sol_with_seed` instruction. +pub struct TransferSolWithSeed<'a, 'b, 'c> { + pub source: &'a AccountInfo, + pub base_account: &'a AccountInfo, + pub destination: &'a AccountInfo, + pub amount: u64, + pub from_seed: &'b String, + pub from_owner: &'c Pubkey, +} + +impl TransferSolWithSeed<'_, '_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 3] = [ + AccountMeta::new(self.source.key(), true, false), + AccountMeta::new(self.base_account.key(), false, true), + AccountMeta::new(self.destination.key(), true, false), + ]; + + let mut uninit_data = [UNINIT_BYTE; 0]; + write_bytes(&mut uninit_data[0..4], &11u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..12], &self.amount.to_le_bytes()); + + write_bytes(&mut uninit_data[0..32], self.from_owner.as_ref()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 32) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed( + &instruction, + &[&self.source, &self.base_account, &self.destination], + signers, + ) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/upgrade_nonce_account.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/upgrade_nonce_account.rs new file mode 100644 index 000000000..9dd6d6e2c --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/upgrade_nonce_account.rs @@ -0,0 +1,40 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `upgrade_nonce_account` instruction. +pub struct UpgradeNonceAccount<'a> { + pub nonce_account: &'a AccountInfo, +} + +impl UpgradeNonceAccount<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 1] = + [AccountMeta::new(self.nonce_account.key(), true, false)]; + + let data = &12u32.to_le_bytes(); + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[&self.nonce_account], signers) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/withdraw_nonce_account.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/withdraw_nonce_account.rs new file mode 100644 index 000000000..074f7abc6 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/instructions/withdraw_nonce_account.rs @@ -0,0 +1,66 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use super::write_bytes; +use super::UNINIT_BYTE; +use core::slice::from_raw_parts; +use pinocchio::account_info::AccountInfo; +use pinocchio::cpi::invoke_signed; +use pinocchio::instruction::AccountMeta; +use pinocchio::instruction::Instruction; +use pinocchio::instruction::Signer; +use pinocchio::ProgramResult; + +/// Helper for cross-program invocations of `withdraw_nonce_account` instruction. +pub struct WithdrawNonceAccount<'a> { + pub nonce_account: &'a AccountInfo, + pub recipient_account: &'a AccountInfo, + pub recent_blockhashes_sysvar: &'a AccountInfo, + pub rent_sysvar: &'a AccountInfo, + pub nonce_authority: &'a AccountInfo, + pub withdraw_amount: u64, +} + +impl WithdrawNonceAccount<'_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metas + let account_metas: [AccountMeta; 5] = [ + AccountMeta::new(self.nonce_account.key(), true, false), + AccountMeta::new(self.recipient_account.key(), true, false), + AccountMeta::new(self.recent_blockhashes_sysvar.key(), false, false), + AccountMeta::new(self.rent_sysvar.key(), false, false), + AccountMeta::new(self.nonce_authority.key(), false, true), + ]; + + let mut uninit_data = [UNINIT_BYTE; 12]; + write_bytes(&mut uninit_data[0..4], &5u32.to_le_bytes()); + write_bytes(&mut uninit_data[4..12], &self.withdraw_amount.to_le_bytes()); + let data = unsafe { from_raw_parts(uninit_data.as_ptr() as _, 12) }; + + let instruction = Instruction { + program_id: &crate::ID, + accounts: &account_metas, + data, + }; + + invoke_signed( + &instruction, + &[ + &self.nonce_account, + &self.recipient_account, + &self.recent_blockhashes_sysvar, + &self.rent_sysvar, + &self.nonce_authority, + ], + signers, + ) + } +} diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/mod.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/mod.rs new file mode 100644 index 000000000..c1b581407 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/mod.rs @@ -0,0 +1,8 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +pub mod instructions; +pub mod programs; diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/generated/programs/mod.rs b/packages/renderers-rust-cpi/test/e2e/system/src/generated/programs/mod.rs new file mode 100644 index 000000000..5470e7884 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/generated/programs/mod.rs @@ -0,0 +1,11 @@ +//! This code was AUTOGENERATED using the Codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun Codama to update it. +//! +//! + +use pinocchio::pubkey::Pubkey; +use pinocchio_pubkey::pubkey; + +/// `system` program ID. +pub const SYSTEM_ID: Pubkey = pubkey!("11111111111111111111111111111111"); diff --git a/packages/renderers-rust-cpi/test/e2e/system/src/lib.rs b/packages/renderers-rust-cpi/test/e2e/system/src/lib.rs new file mode 100644 index 000000000..45066cdd6 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/system/src/lib.rs @@ -0,0 +1,4 @@ +mod generated; + +pub use generated::programs::SYSTEM_ID as ID; +pub use generated::*; diff --git a/packages/renderers-rust-cpi/test/e2e/test.sh b/packages/renderers-rust-cpi/test/e2e/test.sh new file mode 100755 index 000000000..0f88ff654 --- /dev/null +++ b/packages/renderers-rust-cpi/test/e2e/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +function test_project() { + ./test/e2e/generate.cjs $1 + cd test/e2e/$1 + cargo check + cd ../../.. +} + +test_project dummy +test_project system +test_project memo diff --git a/packages/renderers-rust-cpi/test/exports/commonjs.cjs b/packages/renderers-rust-cpi/test/exports/commonjs.cjs new file mode 100644 index 000000000..a091a74b1 --- /dev/null +++ b/packages/renderers-rust-cpi/test/exports/commonjs.cjs @@ -0,0 +1,7 @@ +const { definedTypeNode, numberTypeNode } = require('@codama/nodes'); +const { visit } = require('@codama/visitors-core'); + +const { getRenderMapVisitor } = require('../../dist/index.node.cjs'); + +const node = definedTypeNode({ name: 'answerToLife', type: numberTypeNode('u8') }); +visit(node, getRenderMapVisitor()); diff --git a/packages/renderers-rust-cpi/test/exports/module.mjs b/packages/renderers-rust-cpi/test/exports/module.mjs new file mode 100644 index 000000000..87c6ff631 --- /dev/null +++ b/packages/renderers-rust-cpi/test/exports/module.mjs @@ -0,0 +1,10 @@ +// This ensures that we do not rely on `__dirname` in ES modules even when it is polyfilled. +globalThis.__dirname = 'DO_NOT_USE'; + +import { definedTypeNode, numberTypeNode } from '@codama/nodes'; +import { visit } from '@codama/visitors-core'; + +import { getRenderMapVisitor } from '../../dist/index.node.mjs'; + +const node = definedTypeNode({ name: 'answerToLife', type: numberTypeNode('u8') }); +visit(node, getRenderMapVisitor()); diff --git a/packages/renderers-rust-cpi/test/utils/traitOptions.test.ts b/packages/renderers-rust-cpi/test/utils/traitOptions.test.ts new file mode 100644 index 000000000..e7007fcb4 --- /dev/null +++ b/packages/renderers-rust-cpi/test/utils/traitOptions.test.ts @@ -0,0 +1,361 @@ +import { + accountNode, + definedTypeNode, + enumEmptyVariantTypeNode, + enumStructVariantTypeNode, + enumTypeNode, + numberTypeNode, + structFieldTypeNode, + structTypeNode, +} from '@codama/nodes'; +import { describe, expect, test } from 'vitest'; + +import { getTraitsFromNode, TraitOptions } from '../../src/utils'; + +describe('default values', () => { + test('it defaults to a set of traits for data enums', () => { + // Given a data enum defined type. + const node = definedTypeNode({ + name: 'Command', + type: enumTypeNode([ + enumStructVariantTypeNode( + 'Play', + structTypeNode([structFieldTypeNode({ name: 'guess', type: numberTypeNode('u16') })]), + ), + enumEmptyVariantTypeNode('Quit'), + ]), + }); + + // When we get the traits from the node using the default options. + const { content, imports } = getTraitsFromNode(node); + + // Then we expect the following traits to be rendered. + expect(content).toBe( + `#[derive(Clone, Debug, Eq, PartialEq)]\n` + + `#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]\n` + + `#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n`, + ); + + // And no additional imports. + expect(imports.isEmpty()); + }); + + test('it defaults to a set of traits for scalar enums', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node using the default options. + const { content, imports } = getTraitsFromNode(node); + + // Then we expect the following traits to be rendered. + expect(content).toBe( + `#[derive(Clone, Debug, Eq, PartialEq, Copy, PartialOrd, Hash, FromPrimitive)]\n` + + `#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]\n` + + `#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n`, + ); + + // And the following imports to be used. + expect([...imports.imports]).toStrictEqual(['num_derive::FromPrimitive']); + }); + + test('it defaults to a set of traits for structs', () => { + // Given an account node. + const node = accountNode({ + data: structTypeNode([ + structFieldTypeNode({ name: 'x', type: numberTypeNode('u64') }), + structFieldTypeNode({ name: 'y', type: numberTypeNode('u64') }), + ]), + name: 'Coordinates', + }); + + // When we get the traits from the node using the default options. + const { content, imports } = getTraitsFromNode(node); + + // Then we expect the following traits to be rendered. + expect(content).toBe( + `#[derive(Clone, Debug, Eq, PartialEq)]\n` + + `#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]\n` + + `#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n`, + ); + + // And no additional imports. + expect(imports.isEmpty()); + }); + + test('it does not use default traits if they are overridden', () => { + // Given a defined type node that should use custom traits. + const node = accountNode({ + data: structTypeNode([ + structFieldTypeNode({ name: 'x', type: numberTypeNode('u64') }), + structFieldTypeNode({ name: 'y', type: numberTypeNode('u64') }), + ]), + name: 'Coordinates', + }); + + // When we get the traits from the node using the + // default options with the overrides attribute. + const { content, imports } = getTraitsFromNode(node, { + overrides: { coordinates: ['My', 'special::Traits'] }, + }); + + // Then we expect the following traits to be rendered. + expect(content).toBe(`#[derive(My, Traits)]\n`); + + // And the following imports to be used. + expect([...imports.imports]).toStrictEqual(['special::Traits']); + }); + + test('it still uses feature flags for overridden traits', () => { + // Given a defined type node that should use custom traits. + const node = accountNode({ + data: structTypeNode([ + structFieldTypeNode({ name: 'x', type: numberTypeNode('u64') }), + structFieldTypeNode({ name: 'y', type: numberTypeNode('u64') }), + ]), + name: 'Coordinates', + }); + + // When we get the traits from the node using custom traits + // such that some are part of the feature flag defaults. + const { content } = getTraitsFromNode(node, { + overrides: { coordinates: ['My', 'special::Traits', 'serde::Serialize'] }, + }); + + // Then we expect the following traits to be rendered. + expect(content).toBe(`#[derive(My, Traits)]\n#[cfg_attr(feature = "serde", derive(serde::Serialize))]\n`); + }); +}); + +const RESET_OPTIONS: Required = { + baseDefaults: [], + dataEnumDefaults: [], + featureFlags: {}, + overrides: {}, + scalarEnumDefaults: [], + structDefaults: [], + useFullyQualifiedName: false, +}; + +describe('base traits', () => { + test('it uses both the base and data enum traits', () => { + // Given a data enum defined type. + const node = definedTypeNode({ + name: 'Command', + type: enumTypeNode([ + enumStructVariantTypeNode( + 'Play', + structTypeNode([structFieldTypeNode({ name: 'guess', type: numberTypeNode('u16') })]), + ), + enumEmptyVariantTypeNode('Quit'), + ]), + }); + + // When we get the traits from the node using custom base and data enum defaults. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['MyBaseTrait'], + dataEnumDefaults: ['MyDataEnumTrait'], + }); + + // Then we expect both the base and data enum traits to be rendered. + expect(content).toBe(`#[derive(MyBaseTrait, MyDataEnumTrait)]\n`); + }); + + test('it uses both the base and scalar enum traits', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node using custom base and scalar enum defaults. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['MyBaseTrait'], + scalarEnumDefaults: ['MyScalarEnumTrait'], + }); + + // Then we expect both the base and scalar enum traits to be rendered. + expect(content).toBe(`#[derive(MyBaseTrait, MyScalarEnumTrait)]\n`); + }); + + test('it uses both the base and struct traits', () => { + // Given an account node. + const node = accountNode({ + data: structTypeNode([ + structFieldTypeNode({ name: 'x', type: numberTypeNode('u64') }), + structFieldTypeNode({ name: 'y', type: numberTypeNode('u64') }), + ]), + name: 'Coordinates', + }); + + // When we get the traits from the node using custom base and struct defaults. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['MyBaseTrait'], + structDefaults: ['MyStructTrait'], + }); + + // Then we expect both the base and struct traits to be rendered. + expect(content).toBe(`#[derive(MyBaseTrait, MyStructTrait)]\n`); + }); + + test('it never uses traits for type aliases', () => { + // Given a defined type node that is not an enum or struct. + const node = definedTypeNode({ + name: 'Score', + type: numberTypeNode('u64'), + }); + + // When we get the traits from the node such that we have base defaults. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['MyBaseTrait'], + }); + + // Then we expect no traits to be rendered. + expect(content).toBe(''); + }); + + test('it identifies feature flags under all default traits', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node such that: + // - We provide custom base and scalar enum defaults. + // - We provide custom feature flags for traits in both categories. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['MyBaseTrait', 'MyNonFeatureTrait'], + featureFlags: { + base: ['MyBaseTrait'], + enum: ['MyScalarEnumTrait'], + }, + scalarEnumDefaults: ['MyScalarEnumTrait'], + }); + + // Then we expect both the base and enum traits to be rendered as separate feature flags. + expect(content).toBe( + `#[derive(MyNonFeatureTrait)]\n` + + `#[cfg_attr(feature = "base", derive(MyBaseTrait))]\n` + + `#[cfg_attr(feature = "enum", derive(MyScalarEnumTrait))]\n`, + ); + }); + + test('it renders traits correctly when they are all under feature flags', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node such that + // all traits are under feature flags. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['MyBaseTrait'], + featureFlags: { + base: ['MyBaseTrait'], + enum: ['MyScalarEnumTrait'], + }, + scalarEnumDefaults: ['MyScalarEnumTrait'], + }); + + // Then we expect the following traits to be rendered. + expect(content).toBe( + `#[cfg_attr(feature = "base", derive(MyBaseTrait))]\n#[cfg_attr(feature = "enum", derive(MyScalarEnumTrait))]\n`, + ); + }); +}); + +describe('overridden traits', () => { + test('it replaces all default traits with the overridden traits', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node such that: + // - We provide custom base and enum defaults. + // - We override the feedback type with custom traits. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['MyBaseTrait'], + overrides: { feedback: ['MyFeedbackTrait'] }, + scalarEnumDefaults: ['MyScalarEnumTrait'], + }); + + // Then we expect only the feedback traits to be rendered. + expect(content).toBe(`#[derive(MyFeedbackTrait)]\n`); + }); + + test('it finds traits to override when using pascal case', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node such that + // we use PascalCase for the type name. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + overrides: { Feedback: ['MyFeedbackTrait'] }, + }); + + // Then we still expect the custom feedback traits to be rendered. + expect(content).toBe(`#[derive(MyFeedbackTrait)]\n`); + }); + + test('it identifies feature flags under all overridden traits', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node such that: + // - We override the feedback type with custom traits. + // - We provide custom feature flags for these some of these custom traits. + const { content } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + featureFlags: { custom: ['MyFeedbackTrait'] }, + overrides: { feedback: ['MyFeedbackTrait', 'MyNonFeatureTrait'] }, + }); + + // Then we expect some of the overridden traits to be rendered under feature flags. + expect(content).toBe( + `#[derive(MyNonFeatureTrait)]\n#[cfg_attr(feature = "custom", derive(MyFeedbackTrait))]\n`, + ); + }); +}); + +describe('fully qualified name traits', () => { + test('it can use fully qualified names for traits instead of importing them', () => { + // Given a scalar enum defined type. + const node = definedTypeNode({ + name: 'Feedback', + type: enumTypeNode([enumEmptyVariantTypeNode('Good'), enumEmptyVariantTypeNode('Bad')]), + }); + + // When we get the traits from the node such that we use fully qualified names. + const { content, imports } = getTraitsFromNode(node, { + ...RESET_OPTIONS, + baseDefaults: ['fruits::Apple', 'fruits::Banana', 'vegetables::Carrot'], + useFullyQualifiedName: true, + }); + + // Then we expect the fully qualified names to be used for the traits. + expect(content).toBe(`#[derive(fruits::Apple, fruits::Banana, vegetables::Carrot)]\n`); + + // And no imports should be used. + expect([...imports.imports]).toStrictEqual([]); + }); +}); diff --git a/packages/renderers-rust-cpi/tsconfig.declarations.json b/packages/renderers-rust-cpi/tsconfig.declarations.json new file mode 100644 index 000000000..dc2d27bb0 --- /dev/null +++ b/packages/renderers-rust-cpi/tsconfig.declarations.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./dist/types" + }, + "extends": "./tsconfig.json", + "include": ["src/index.ts", "src/types"] +} diff --git a/packages/renderers-rust-cpi/tsconfig.json b/packages/renderers-rust-cpi/tsconfig.json new file mode 100644 index 000000000..f84dde13d --- /dev/null +++ b/packages/renderers-rust-cpi/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { "lib": [] }, + "display": "@codama/renderers-rust", + "extends": "../../tsconfig.json", + "include": ["src", "test"], + "exclude": ["test/e2e"] +} diff --git a/packages/renderers-rust-cpi/tsup.config.ts b/packages/renderers-rust-cpi/tsup.config.ts new file mode 100644 index 000000000..55e99457f --- /dev/null +++ b/packages/renderers-rust-cpi/tsup.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'tsup'; + +import { getPackageBuildConfigs } from '../../tsup.config.base'; + +export default defineConfig(getPackageBuildConfigs()); diff --git a/packages/renderers-rust-cpi/vitest.config.mts b/packages/renderers-rust-cpi/vitest.config.mts new file mode 100644 index 000000000..8fd0137cc --- /dev/null +++ b/packages/renderers-rust-cpi/vitest.config.mts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; +import { getVitestConfig } from '../../vitest.config.base.mjs'; + +export default defineConfig({ + test: { + projects: [getVitestConfig('browser'), getVitestConfig('node'), getVitestConfig('react-native')], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb252efd2..b83953aa0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -326,6 +326,24 @@ importers: specifier: ^3.2.6 version: 3.2.6 + packages/renderers-rust-cpi: + dependencies: + '@codama/errors': + specifier: workspace:* + version: link:../errors + '@codama/nodes': + specifier: workspace:* + version: link:../nodes + '@codama/renderers-core': + specifier: workspace:* + version: link:../renderers-core + '@codama/visitors-core': + specifier: workspace:* + version: link:../visitors-core + '@solana/codecs-strings': + specifier: ^3.0.2 + version: 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + packages/renderers-vixen-parser: dependencies: '@codama/errors': @@ -1367,12 +1385,6 @@ packages: '@sinonjs/fake-timers@11.3.1': resolution: {integrity: sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==} - '@solana/codecs-core@3.0.2': - resolution: {integrity: sha512-vpy8ySWgPF8+APwpeEr4dQbU/EyHjisd/TywIw3rymguWeZ9/wcThS0WDRi08Z44W6InCIbtI08RdtuHQZoSag==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-core@3.0.3': resolution: {integrity: sha512-emKykJ3h1DmnDOY29Uv9eJXP8E/FHzvlUBJ6te+5EbKdFjj7vdlKYPfDxOI6iGdXTY+YC/ELtbNBh6QwF2uEDQ==} engines: {node: '>=20.18.0'} @@ -1404,13 +1416,6 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/errors@3.0.2': - resolution: {integrity: sha512-0B4y9JUsX8/DzflhdSXSAF+UPUKyo1R1rrQ/ShS/8ApCOIWIFqZlve0TPatuTOGGNBememoukPOvDVtFy0ZYpg==} - engines: {node: '>=20.18.0'} - hasBin: true - peerDependencies: - typescript: '>=5.3.3' - '@solana/errors@3.0.3': resolution: {integrity: sha512-1l84xJlHNva6io62PcYfUamwWlc0eM95nHgCrKX0g0cLoC6D6QHYPCEbEVkR+C5UtP9JDgyQM8MFiv+Ei5tO9Q==} engines: {node: '>=20.18.0'} @@ -4586,11 +4591,6 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@solana/codecs-core@3.0.2(typescript@5.9.2)': - dependencies: - '@solana/errors': 3.0.2(typescript@5.9.2) - typescript: 5.9.2 - '@solana/codecs-core@3.0.3(typescript@5.9.2)': dependencies: '@solana/errors': 3.0.3(typescript@5.9.2) @@ -4628,12 +4628,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/errors@3.0.2(typescript@5.9.2)': - dependencies: - chalk: 5.6.2 - commander: 14.0.0 - typescript: 5.9.2 - '@solana/errors@3.0.3(typescript@5.9.2)': dependencies: chalk: 5.6.2 diff --git a/tsup.config.base.ts b/tsup.config.base.ts index a79565783..2888d0b34 100644 --- a/tsup.config.base.ts +++ b/tsup.config.base.ts @@ -35,7 +35,7 @@ export function getBuildConfig(options: BuildOptions): TsupConfig { }; } }, - external: ['node:fs', 'node:path', 'node:url'], + external: ['node:child_process', 'node:fs', 'node:path', 'node:url'], format, globalName: 'globalThis.codama', name: platform,