|
| 1 | +# Shank and Codama |
| 2 | + |
| 3 | +[Shank](https://github.com/metaplex-foundation/shank) lets a **native** Solana |
| 4 | +[program](https://solana.com/docs/terminology#program) export an IDL the same |
| 5 | +way [Anchor](https://solana.com/docs/terminology#anchor) does. Once you have an |
| 6 | +IDL, [Codama](https://github.com/codama-idl/codama) turns it into a typed client |
| 7 | +in the language of your choice. |
| 8 | + |
| 9 | +This example is a small "car rental service" program. It is annotated with Shank |
| 10 | +macros, Shank extracts the IDL, and Codama renders a TypeScript client |
| 11 | +(`@solana/kit`-based) from that IDL. An in-process [LiteSVM](https://github.com/litesvm/litesvm) |
| 12 | +test then drives the program through the generated client — no validator or |
| 13 | +devnet required, so it runs in CI. |
| 14 | + |
| 15 | +> This example used to use [Solita](https://github.com/metaplex-foundation/solita) |
| 16 | +> to generate the client. Solita is unmaintained and does not work on the current |
| 17 | +> toolchain, so it has been replaced with Codama. The Shank half of the lesson is |
| 18 | +> unchanged. |
| 19 | +
|
| 20 | +## Shank |
| 21 | + |
| 22 | +[Shank](https://github.com/metaplex-foundation/shank) is a set of Rust derive |
| 23 | +macros plus a CLI that generates an IDL for your program. |
| 24 | + |
| 25 | +Mark a struct as an [account](https://solana.com/docs/terminology#account): |
| 26 | + |
| 27 | +```rust |
| 28 | +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, ShankAccount)] |
| 29 | +pub struct Car { |
| 30 | + pub year: u16, |
| 31 | + pub make: String, |
| 32 | + pub model: String, |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +Mark an enum as your [instruction](https://solana.com/docs/terminology#instruction) set, |
| 37 | +using `#[account(...)]` attributes to describe each instruction's accounts: |
| 38 | + |
| 39 | +```rust |
| 40 | +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, ShankInstruction)] |
| 41 | +pub enum CarRentalServiceInstruction { |
| 42 | + #[account(0, writable, name = "car_account", desc = "The account that will represent the Car being created")] |
| 43 | + #[account(1, writable, name = "payer", desc = "Fee payer")] |
| 44 | + #[account(2, name = "system_program", desc = "The System Program")] |
| 45 | + AddCar(AddCarArgs), |
| 46 | + // ... |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +> Shank needs `declare_id!` in your program for the IDL generation to work: |
| 51 | +> |
| 52 | +> ```rust |
| 53 | +> declare_id!("8avNGHVXDwsELJaWMSoUZ44CirQd4zyU9Ez4ZmP4jNjZ"); |
| 54 | +> ``` |
| 55 | +
|
| 56 | +Install the CLI and generate the IDL: |
| 57 | +
|
| 58 | +```bash |
| 59 | +cargo install shank-cli |
| 60 | +pnpm generate-idl # runs: shank idl --crate-root ./program --out-dir ./program/idl |
| 61 | +``` |
| 62 | +
|
| 63 | +The IDL lands in `program/idl/car_rental_service.json` (committed to the repo so |
| 64 | +the client can be regenerated without the Rust CLI). Its `metadata.origin` is |
| 65 | +`"shank"`, and each instruction carries an explicit single-byte (`u8`) |
| 66 | +`discriminant` — this is what distinguishes a Shank IDL from an Anchor IDL. |
| 67 | + |
| 68 | +### A note on PDAs and `#[seeds(...)]` |
| 69 | + |
| 70 | +Shank 0.0.x used a `#[seeds(...)]` attribute on a `ShankAccount` to *generate* |
| 71 | +`shank_pda` / `shank_seeds_with_bump` helper methods. As of Shank 0.4.x that PDA |
| 72 | +code-generation produces unparsable tokens and fails to compile, and the seeds |
| 73 | +are not emitted into the IDL either. So this example keeps PDA derivation |
| 74 | +explicit in `program/src/state/mod.rs` (`Car::find_pda`, `RentalOrder::find_pda`) |
| 75 | +and no longer uses the `#[seeds(...)]` attribute. `ShankAccount` is still used — |
| 76 | +it is what tells Shank to include the account layout in the IDL. |
| 77 | + |
| 78 | +## Codama |
| 79 | + |
| 80 | +[Codama](https://github.com/codama-idl/codama) reads an IDL and renders a client. |
| 81 | +It understands Shank IDLs out of the box. |
| 82 | + |
| 83 | +Install the pieces used here: |
| 84 | + |
| 85 | +```bash |
| 86 | +pnpm add codama @codama/nodes-from-anchor @codama/renderers-js @solana/kit |
| 87 | +``` |
| 88 | + |
| 89 | +The generator script ([`codama.ts`](./codama.ts)) reads the Shank IDL, sets its |
| 90 | +`origin` to `"shank"` so the `u8` discriminants are honoured, builds a Codama |
| 91 | +root node, and renders a TypeScript client: |
| 92 | + |
| 93 | +```ts |
| 94 | +import { rootNodeFromAnchor } from "@codama/nodes-from-anchor"; |
| 95 | +import { renderVisitor } from "@codama/renderers-js"; |
| 96 | +import { createFromRoot } from "codama"; |
| 97 | + |
| 98 | +const idl = JSON.parse(readFileSync(idlPath, "utf-8")); |
| 99 | +const codama = createFromRoot( |
| 100 | + rootNodeFromAnchor({ ...idl, metadata: { ...idl.metadata, origin: "shank" } }), |
| 101 | +); |
| 102 | +await codama.accept(renderVisitor(outDir, { deleteFolderBeforeRendering: true })); |
| 103 | +``` |
| 104 | + |
| 105 | +> Codama also ships `@codama/renderers-rust` if you want a Rust client instead of |
| 106 | +> a TypeScript one — swap `renderVisitor` from `@codama/renderers-js` for the Rust |
| 107 | +> renderer. |
| 108 | +
|
| 109 | +Generate the client: |
| 110 | + |
| 111 | +```bash |
| 112 | +pnpm generate-client |
| 113 | +``` |
| 114 | + |
| 115 | +The generated TypeScript client lands in `tests/generated/`. |
| 116 | + |
| 117 | +## Build and test |
| 118 | + |
| 119 | +```bash |
| 120 | +pnpm install |
| 121 | +pnpm build # cargo build-sbf -> program/target/so/car_rental_service.so |
| 122 | +pnpm build-and-test # build, regenerate the client, then run the LiteSVM test |
| 123 | +``` |
| 124 | + |
| 125 | +The test ([`tests/test.ts`](./tests/test.ts)) loads the compiled `.so` into a |
| 126 | +[LiteSVM](https://github.com/litesvm/litesvm) instance and exercises `add_car`, |
| 127 | +`book_rental`, and `pick_up_car` through the generated client, asserting on the |
| 128 | +resulting on-chain account state. |
0 commit comments