Skip to content

Commit d2bbd09

Browse files
authored
feat: instruction decoder (#2191)
* feat: add instruction decoder * feat: add instruction decoder derive
1 parent 7c7a831 commit d2bbd09

File tree

90 files changed

+7186
-2985
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+7186
-2985
lines changed

Cargo.lock

Lines changed: 42 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ members = [
3333
"sdk-libs/sdk-types",
3434
"sdk-libs/photon-api",
3535
"sdk-libs/program-test",
36+
"sdk-libs/instruction-decoder",
37+
"sdk-libs/instruction-decoder-derive",
3638
"xtask",
3739
"program-tests/account-compression-test",
3840
"program-tests/batched-merkle-tree-test",
@@ -132,6 +134,7 @@ pinocchio = { version = "0.9" }
132134
pinocchio-pubkey = { version = "0.3.0" }
133135
pinocchio-system = { version = "0.3.0" }
134136
bs58 = "^0.5.1"
137+
sha2 = "0.10"
135138
litesvm = "0.7"
136139
# Anchor
137140
anchor-lang = { version = "0.31.1" }
@@ -150,6 +153,7 @@ proc-macro2 = "1.0"
150153
quote = "1.0"
151154
syn = { version = "2.0", features = ["visit", "visit-mut", "full"] }
152155
darling = "0.21"
156+
heck = "0.5"
153157

154158
# Async ecosystem
155159
futures = "0.3.31"
@@ -223,6 +227,8 @@ create-address-test-program = { path = "program-tests/create-address-test-progra
223227
"cpi",
224228
] }
225229
light-program-test = { path = "sdk-libs/program-test", version = "0.18.0" }
230+
light-instruction-decoder = { path = "sdk-libs/instruction-decoder", version = "0.1.0" }
231+
light-instruction-decoder-derive = { path = "sdk-libs/instruction-decoder-derive", version = "0.1.0" }
226232
light-batched-merkle-tree = { path = "program-libs/batched-merkle-tree", version = "0.8.0" }
227233
light-merkle-tree-metadata = { path = "program-libs/merkle-tree-metadata", version = "0.8.0" }
228234
aligned-sized = { path = "program-libs/aligned-sized", version = "1.1.0" }

program-tests/compressed-token-test/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ default = []
2020
[dependencies]
2121
anchor-lang = { workspace = true }
2222
light-sdk = { workspace = true, features = ["anchor"] }
23+
light-instruction-decoder = { workspace = true }
2324

2425
[dev-dependencies]
2526
light-compressed-token = { workspace = true }

program-tests/compressed-token-test/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
#![allow(deprecated)]
44

55
use anchor_lang::{prelude::*, solana_program::instruction::Instruction};
6+
use light_instruction_decoder::instruction_decoder;
67

78
declare_id!("CompressedTokenTestProgram11111111111111111");
89

10+
#[instruction_decoder]
911
#[program]
1012
pub mod compressed_token_test {
1113
use super::*;

program-tests/create-address-test-program/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ account-compression = { workspace = true, features = ["cpi"] }
2727
light-compressed-account = { workspace = true, features = ["anchor"] }
2828
light-sdk = { workspace = true, features = ["anchor", "v2"] }
2929
light-sdk-types = { workspace = true }
30+
light-instruction-decoder = { workspace = true }

program-tests/create-address-test-program/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use anchor_lang::{
88
solana_program::{instruction::Instruction, pubkey::Pubkey},
99
InstructionData,
1010
};
11+
use light_instruction_decoder::instruction_decoder;
1112
use light_sdk::{
1213
cpi::{v2::CpiAccounts, CpiAccountsConfig, CpiSigner},
1314
derive_light_cpi_signer,
@@ -59,6 +60,7 @@ declare_id!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy");
5960
pub const LIGHT_CPI_SIGNER: CpiSigner =
6061
derive_light_cpi_signer!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy");
6162

63+
#[instruction_decoder]
6264
#[program]
6365
pub mod system_cpi_test {
6466

program-tests/system-cpi-test/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ light-merkle-tree-metadata = { workspace = true, features = ["anchor"] }
3535
light-account-checks = { workspace = true }
3636
light-sdk = { workspace = true, features = ["v2", "cpi-context"] }
3737
light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] }
38+
light-instruction-decoder = { workspace = true }
3839

3940
[target.'cfg(not(target_os = "solana"))'.dependencies]
4041
solana-sdk = { workspace = true }

program-tests/system-cpi-test/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use light_compressed_account::{
2424
data::{NewAddressParamsPacked, PackedReadOnlyAddress},
2525
},
2626
};
27+
use light_instruction_decoder::instruction_decoder;
2728
use light_sdk::derive_light_cpi_signer;
2829
use light_sdk_types::CpiSigner;
2930

@@ -32,6 +33,7 @@ declare_id!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy");
3233
pub const LIGHT_CPI_SIGNER: CpiSigner =
3334
derive_light_cpi_signer!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy");
3435

36+
#[instruction_decoder]
3537
#[program]
3638
pub mod system_cpi_test {
3739

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# light-instruction-decoder-derive
2+
3+
Procedural macros for generating `InstructionDecoder` implementations.
4+
5+
## Overview
6+
7+
This crate provides two macros for generating instruction decoders:
8+
9+
| Macro | Type | Purpose |
10+
|-------|------|---------|
11+
| `#[derive(InstructionDecoder)]` | Derive | Generate decoder for instruction enums |
12+
| `#[instruction_decoder]` | Attribute | Auto-generate from Anchor program modules |
13+
14+
## Module Structure
15+
16+
```
17+
src/
18+
├── lib.rs # Macro entry points only (~100 lines)
19+
├── utils.rs # Case conversion, discriminator, error handling
20+
├── parsing.rs # Darling-based attribute parsing structs
21+
├── builder.rs # InstructionDecoderBuilder (code generation)
22+
├── derive_impl.rs # #[derive(InstructionDecoder)] implementation
23+
├── attribute_impl.rs # #[instruction_decoder] attribute implementation
24+
└── crate_context.rs # Recursive crate parsing for Accounts struct discovery
25+
```
26+
27+
## Key Features
28+
29+
### Multiple Discriminator Sizes
30+
31+
- **1 byte**: Native programs with simple instruction indices
32+
- **4 bytes**: System-style programs (little-endian u32)
33+
- **8 bytes**: Anchor programs (SHA256 prefix, default)
34+
35+
### Explicit Discriminators
36+
37+
Two syntax forms for specifying explicit discriminators:
38+
39+
1. **Integer**: `#[discriminator = 5]` - for 1-byte and 4-byte modes
40+
2. **Array**: `#[discriminator(26, 16, 169, 7, 21, 202, 242, 25)]` - for 8-byte mode with custom discriminators
41+
42+
### Account Names Extraction
43+
44+
Two ways to specify account names:
45+
46+
1. **Accounts type reference**: `accounts = MyAccountsStruct` - extracts field names at compile time
47+
2. **Inline names**: Direct array `["source", "dest", "authority"]`
48+
49+
When using `accounts = SomeType`, the macro uses `CrateContext` to parse the crate at macro expansion time and extract field names from the struct definition. This works for any struct with named fields (including standard Anchor `#[derive(Accounts)]` structs) without requiring any special trait implementation.
50+
51+
### Off-chain Only
52+
53+
All generated code is gated with `#[cfg(not(target_os = "solana"))]` since instruction decoding is only needed for logging/debugging.
54+
55+
## Usage Examples
56+
57+
### Derive Macro
58+
59+
```rust
60+
use light_instruction_decoder_derive::InstructionDecoder;
61+
62+
#[derive(InstructionDecoder)]
63+
#[instruction_decoder(
64+
program_id = "MyProgramId111111111111111111111111111111111",
65+
program_name = "My Program", // optional
66+
discriminator_size = 8 // optional: 1, 4, or 8
67+
)]
68+
pub enum MyInstruction {
69+
// Reference Accounts struct for account names
70+
#[instruction_decoder(accounts = CreateRecord, params = CreateRecordParams)]
71+
CreateRecord,
72+
73+
// Inline account names
74+
#[instruction_decoder(account_names = ["source", "dest"])]
75+
Transfer,
76+
77+
// Explicit integer discriminator (for 1-byte or 4-byte modes)
78+
#[discriminator = 5]
79+
Close,
80+
81+
// Explicit array discriminator (for 8-byte mode with custom discriminators)
82+
#[discriminator(26, 16, 169, 7, 21, 202, 242, 25)]
83+
#[instruction_decoder(account_names = ["fee_payer", "authority"])]
84+
CustomInstruction,
85+
}
86+
```
87+
88+
### Attribute Macro (Anchor Programs)
89+
90+
```rust
91+
use light_instruction_decoder_derive::instruction_decoder;
92+
93+
#[instruction_decoder] // or #[instruction_decoder(program_id = crate::ID)]
94+
#[program]
95+
pub mod my_program {
96+
pub fn create_record(ctx: Context<CreateRecord>, params: CreateParams) -> Result<()> { ... }
97+
pub fn transfer(ctx: Context<Transfer>) -> Result<()> { ... }
98+
}
99+
```
100+
101+
This generates `MyProgramInstructionDecoder` that:
102+
- Gets program_id from `crate::ID` (or `declare_id!` if found)
103+
- Extracts function names and converts to discriminators
104+
- Discovers Accounts struct field names from the crate
105+
- Decodes params using borsh if specified
106+
107+
## Architecture
108+
109+
### Darling-Based Parsing
110+
111+
Attributes are parsed using the `darling` crate for:
112+
- Declarative struct-based definitions
113+
- Automatic validation
114+
- Better error messages with span preservation
115+
116+
### Builder Pattern
117+
118+
`InstructionDecoderBuilder` separates:
119+
- **Parsing**: Extract and validate attributes
120+
- **Code Generation**: Produce TokenStream output
121+
122+
This follows the pattern from `sdk-libs/macros`.
123+
124+
### Crate Context
125+
126+
`CrateContext` recursively parses all module files at macro expansion time to discover structs by name. This enables both macros to automatically find field names:
127+
128+
- **Derive macro**: When `accounts = SomeType` is specified, extracts struct field names
129+
- **Attribute macro**: Discovers Accounts structs from `Context<T>` parameters
130+
131+
The struct lookup finds any struct with named fields - no special trait implementation required. This makes the macro completely independent and works with any Anchor program.
132+
133+
## Testing
134+
135+
```bash
136+
# Unit tests
137+
cargo test -p light-instruction-decoder-derive
138+
139+
# Integration tests (verifies generated code compiles and works)
140+
cargo test-sbf -p csdk-anchor-full-derived-test --test instruction_decoder_test
141+
```
142+
143+
## Dependencies
144+
145+
- `darling`: Attribute parsing
146+
- `syn/quote/proc-macro2`: Token manipulation
147+
- `sha2`: Anchor discriminator computation
148+
- `bs58`: Program ID encoding
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "light-instruction-decoder-derive"
3+
version = "0.1.0"
4+
description = "Derive macros for InstructionDecoder implementations in Light Protocol"
5+
repository = "https://github.com/Lightprotocol/light-protocol"
6+
license = "Apache-2.0"
7+
edition = "2021"
8+
9+
[dependencies]
10+
bs58 = { workspace = true }
11+
darling = { workspace = true }
12+
heck = { workspace = true }
13+
proc-macro2 = { workspace = true }
14+
quote = { workspace = true }
15+
sha2 = "0.10"
16+
syn = { workspace = true }
17+
18+
[dev-dependencies]
19+
light-instruction-decoder = { workspace = true }
20+
21+
[lib]
22+
proc-macro = true

0 commit comments

Comments
 (0)