Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/.ghaignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# uses generated client from shank, can't rewrite to solana-bankrun
tools/shank-and-solita/native

# build failed - program outdated
tokens/token-extensions/metadata/anchor

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ node_modules/
# Exception: escrow native is a standalone (non-workspace) crate, so it keeps
# its Cargo.lock tracked for reproducible builds.
!finance/escrow/native/Cargo.lock
# Exception: shank-and-codama native is also a standalone (non-workspace) crate.
!tools/shank-and-codama/native/program/Cargo.lock

**/*/.anchor
**/*/.DS_Store
Expand Down
128 changes: 128 additions & 0 deletions tools/shank-and-codama/native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Shank and Codama

[Shank](https://github.com/metaplex-foundation/shank) lets a **native** Solana
[program](https://solana.com/docs/terminology#program) export an IDL the same
way [Anchor](https://solana.com/docs/terminology#anchor) does. Once you have an
IDL, [Codama](https://github.com/codama-idl/codama) turns it into a typed client
in the language of your choice.

This example is a small "car rental service" program. It is annotated with Shank
macros, Shank extracts the IDL, and Codama renders a TypeScript client
(`@solana/kit`-based) from that IDL. An in-process [LiteSVM](https://github.com/litesvm/litesvm)
test then drives the program through the generated client — no validator or
devnet required, so it runs in CI.

> This example used to use [Solita](https://github.com/metaplex-foundation/solita)
> to generate the client. Solita is unmaintained and does not work on the current
> toolchain, so it has been replaced with Codama. The Shank half of the lesson is
> unchanged.

## Shank

[Shank](https://github.com/metaplex-foundation/shank) is a set of Rust derive
macros plus a CLI that generates an IDL for your program.

Mark a struct as an [account](https://solana.com/docs/terminology#account):

```rust
#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, ShankAccount)]
pub struct Car {
pub year: u16,
pub make: String,
pub model: String,
}
```

Mark an enum as your [instruction](https://solana.com/docs/terminology#instruction) set,
using `#[account(...)]` attributes to describe each instruction's accounts:

```rust
#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, ShankInstruction)]
pub enum CarRentalServiceInstruction {
#[account(0, writable, name = "car_account", desc = "The account that will represent the Car being created")]
#[account(1, writable, name = "payer", desc = "Fee payer")]
#[account(2, name = "system_program", desc = "The System Program")]
AddCar(AddCarArgs),
// ...
}
```

> Shank needs `declare_id!` in your program for the IDL generation to work:
>
> ```rust
> declare_id!("8avNGHVXDwsELJaWMSoUZ44CirQd4zyU9Ez4ZmP4jNjZ");
> ```

Install the CLI and generate the IDL:

```bash
cargo install shank-cli
pnpm generate-idl # runs: shank idl --crate-root ./program --out-dir ./program/idl
```

The IDL lands in `program/idl/car_rental_service.json` (committed to the repo so
the client can be regenerated without the Rust CLI). Its `metadata.origin` is
`"shank"`, and each instruction carries an explicit single-byte (`u8`)
`discriminant` — this is what distinguishes a Shank IDL from an Anchor IDL.

### A note on PDAs and `#[seeds(...)]`

Shank 0.0.x used a `#[seeds(...)]` attribute on a `ShankAccount` to *generate*
`shank_pda` / `shank_seeds_with_bump` helper methods. As of Shank 0.4.x that PDA
code-generation produces unparsable tokens and fails to compile, and the seeds
are not emitted into the IDL either. So this example keeps PDA derivation
explicit in `program/src/state/mod.rs` (`Car::find_pda`, `RentalOrder::find_pda`)
and no longer uses the `#[seeds(...)]` attribute. `ShankAccount` is still used —
it is what tells Shank to include the account layout in the IDL.

## Codama

[Codama](https://github.com/codama-idl/codama) reads an IDL and renders a client.
It understands Shank IDLs out of the box.

Install the pieces used here:

```bash
pnpm add codama @codama/nodes-from-anchor @codama/renderers-js @solana/kit
```

The generator script ([`codama.ts`](./codama.ts)) reads the Shank IDL, sets its
`origin` to `"shank"` so the `u8` discriminants are honoured, builds a Codama
root node, and renders a TypeScript client:

```ts
import { rootNodeFromAnchor } from "@codama/nodes-from-anchor";
import { renderVisitor } from "@codama/renderers-js";
import { createFromRoot } from "codama";

const idl = JSON.parse(readFileSync(idlPath, "utf-8"));
const codama = createFromRoot(
rootNodeFromAnchor({ ...idl, metadata: { ...idl.metadata, origin: "shank" } }),
);
await codama.accept(renderVisitor(outDir, { deleteFolderBeforeRendering: true }));
```

> Codama also ships `@codama/renderers-rust` if you want a Rust client instead of
> a TypeScript one — swap `renderVisitor` from `@codama/renderers-js` for the Rust
> renderer.

Generate the client:

```bash
pnpm generate-client
```

The generated TypeScript client lands in `tests/generated/`.

## Build and test

```bash
pnpm install
pnpm build # cargo build-sbf -> program/target/so/car_rental_service.so
pnpm build-and-test # build, regenerate the client, then run the LiteSVM test
```

The test ([`tests/test.ts`](./tests/test.ts)) loads the compiled `.so` into a
[LiteSVM](https://github.com/litesvm/litesvm) instance and exercises `add_car`,
`book_rental`, and `pick_up_car` through the generated client, asserting on the
resulting on-chain account state.
44 changes: 44 additions & 0 deletions tools/shank-and-codama/native/codama.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Codama client generator.
//
// Reads the Shank-generated IDL (program/idl/car_rental_service.json) and emits
// a TypeScript client built on @solana/kit into tests/generated/.
//
// Flow: read IDL -> rootNodeFromAnchor (origin = "shank" so the u8 instruction
// discriminants are interpreted correctly) -> createFromRoot -> render JS.
//
// Run with: pnpm generate-client

import { readFileSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { type AnchorIdl, rootNodeFromAnchor } from "@codama/nodes-from-anchor";
import { renderVisitor } from "@codama/renderers-js";
import { createFromRoot } from "codama";

const here = dirname(fileURLToPath(import.meta.url));
const idlPath = join(here, "program", "idl", "car_rental_service.json");
const outDir = join(here, "tests", "generated");

const idl = JSON.parse(readFileSync(idlPath, "utf-8")) as AnchorIdl;

// Make sure Codama treats this as a Shank IDL. Shank uses single-byte (u8)
// instruction discriminants rather than Anchor's 8-byte hashes, and the
// "origin" field is what tells nodes-from-anchor to honour the explicit
// `discriminant` values in the IDL.
const idlWithOrigin = {
...idl,
metadata: { ...idl.metadata, origin: "shank" },
} as AnchorIdl;

const codama = createFromRoot(rootNodeFromAnchor(idlWithOrigin));

await codama.accept(renderVisitor(outDir, { deleteFolderBeforeRendering: true }));

// The renderer drops a standalone `package.json` (declaring an implicit CommonJS
// package) at the output root. That would shadow this example's
// `"type": "module"` setting and break ESM resolution of the generated `.ts`
// files when the test imports them via tsx, so remove it.
rmSync(join(outDir, "package.json"), { force: true });

console.log(`Codama: generated TypeScript client in ${outDir}`);
24 changes: 24 additions & 0 deletions tools/shank-and-codama/native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"type": "module",
"scripts": {
"build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so",
"generate-idl": "shank idl --crate-root ./program --out-dir ./program/idl",
"generate-client": "tsx ./codama.ts",
"test": "node --import tsx --test ./tests/test.ts",
"build-and-test": "pnpm build && pnpm generate-client && pnpm test"
},
"dependencies": {
"@codama/nodes-from-anchor": "^1.5.0",
"@codama/renderers-js": "^2.2.0",
"@solana-program/system": "^0.12.2",
"@solana/program-client-core": "^6.9.0",
"@solana/kit": "^6.9.0",
"codama": "^1.7.0",
"litesvm": "^1.1.0"
},
"devDependencies": {
"@types/node": "^25.9.1",
"tsx": "^4.22.4",
"typescript": "^5.9.0"
}
}
Loading
Loading