agent-bober 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +495 -0
- package/agents/bober-evaluator.md +323 -0
- package/agents/bober-generator.md +245 -0
- package/agents/bober-planner.md +248 -0
- package/dist/cli/commands/eval.d.ts +6 -0
- package/dist/cli/commands/eval.d.ts.map +1 -0
- package/dist/cli/commands/eval.js +129 -0
- package/dist/cli/commands/eval.js.map +1 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +547 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +5 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +87 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/run.d.ts +5 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +120 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/sprint.d.ts +6 -0
- package/dist/cli/commands/sprint.d.ts.map +1 -0
- package/dist/cli/commands/sprint.js +206 -0
- package/dist/cli/commands/sprint.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +124 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/defaults.d.ts +15 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +226 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +18 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +189 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +904 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +181 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/contracts/eval-result.d.ts +205 -0
- package/dist/contracts/eval-result.d.ts.map +1 -0
- package/dist/contracts/eval-result.js +87 -0
- package/dist/contracts/eval-result.js.map +1 -0
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +16 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/spec.d.ts +101 -0
- package/dist/contracts/spec.d.ts.map +1 -0
- package/dist/contracts/spec.js +51 -0
- package/dist/contracts/spec.js.map +1 -0
- package/dist/contracts/sprint-contract.d.ts +141 -0
- package/dist/contracts/sprint-contract.d.ts.map +1 -0
- package/dist/contracts/sprint-contract.js +80 -0
- package/dist/contracts/sprint-contract.js.map +1 -0
- package/dist/evaluators/builtin/api-check.d.ts +13 -0
- package/dist/evaluators/builtin/api-check.d.ts.map +1 -0
- package/dist/evaluators/builtin/api-check.js +152 -0
- package/dist/evaluators/builtin/api-check.js.map +1 -0
- package/dist/evaluators/builtin/build-check.d.ts +17 -0
- package/dist/evaluators/builtin/build-check.d.ts.map +1 -0
- package/dist/evaluators/builtin/build-check.js +155 -0
- package/dist/evaluators/builtin/build-check.js.map +1 -0
- package/dist/evaluators/builtin/command-runner.d.ts +26 -0
- package/dist/evaluators/builtin/command-runner.d.ts.map +1 -0
- package/dist/evaluators/builtin/command-runner.js +114 -0
- package/dist/evaluators/builtin/command-runner.js.map +1 -0
- package/dist/evaluators/builtin/lint.d.ts +17 -0
- package/dist/evaluators/builtin/lint.d.ts.map +1 -0
- package/dist/evaluators/builtin/lint.js +264 -0
- package/dist/evaluators/builtin/lint.js.map +1 -0
- package/dist/evaluators/builtin/playwright.d.ts +16 -0
- package/dist/evaluators/builtin/playwright.d.ts.map +1 -0
- package/dist/evaluators/builtin/playwright.js +238 -0
- package/dist/evaluators/builtin/playwright.js.map +1 -0
- package/dist/evaluators/builtin/typescript-check.d.ts +12 -0
- package/dist/evaluators/builtin/typescript-check.d.ts.map +1 -0
- package/dist/evaluators/builtin/typescript-check.js +155 -0
- package/dist/evaluators/builtin/typescript-check.js.map +1 -0
- package/dist/evaluators/builtin/unit-test.d.ts +18 -0
- package/dist/evaluators/builtin/unit-test.d.ts.map +1 -0
- package/dist/evaluators/builtin/unit-test.js +279 -0
- package/dist/evaluators/builtin/unit-test.js.map +1 -0
- package/dist/evaluators/index.d.ts +11 -0
- package/dist/evaluators/index.d.ts.map +1 -0
- package/dist/evaluators/index.js +13 -0
- package/dist/evaluators/index.js.map +1 -0
- package/dist/evaluators/plugin-interface.d.ts +50 -0
- package/dist/evaluators/plugin-interface.d.ts.map +1 -0
- package/dist/evaluators/plugin-interface.js +2 -0
- package/dist/evaluators/plugin-interface.js.map +1 -0
- package/dist/evaluators/plugin-loader.d.ts +18 -0
- package/dist/evaluators/plugin-loader.d.ts.map +1 -0
- package/dist/evaluators/plugin-loader.js +107 -0
- package/dist/evaluators/plugin-loader.js.map +1 -0
- package/dist/evaluators/registry.d.ts +78 -0
- package/dist/evaluators/registry.d.ts.map +1 -0
- package/dist/evaluators/registry.js +238 -0
- package/dist/evaluators/registry.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/context-handoff.d.ts +543 -0
- package/dist/orchestrator/context-handoff.d.ts.map +1 -0
- package/dist/orchestrator/context-handoff.js +133 -0
- package/dist/orchestrator/context-handoff.js.map +1 -0
- package/dist/orchestrator/evaluator-agent.d.ts +15 -0
- package/dist/orchestrator/evaluator-agent.d.ts.map +1 -0
- package/dist/orchestrator/evaluator-agent.js +233 -0
- package/dist/orchestrator/evaluator-agent.js.map +1 -0
- package/dist/orchestrator/generator-agent.d.ts +16 -0
- package/dist/orchestrator/generator-agent.d.ts.map +1 -0
- package/dist/orchestrator/generator-agent.js +147 -0
- package/dist/orchestrator/generator-agent.js.map +1 -0
- package/dist/orchestrator/pipeline.d.ts +24 -0
- package/dist/orchestrator/pipeline.d.ts.map +1 -0
- package/dist/orchestrator/pipeline.js +290 -0
- package/dist/orchestrator/pipeline.js.map +1 -0
- package/dist/orchestrator/planner-agent.d.ts +10 -0
- package/dist/orchestrator/planner-agent.d.ts.map +1 -0
- package/dist/orchestrator/planner-agent.js +187 -0
- package/dist/orchestrator/planner-agent.js.map +1 -0
- package/dist/state/helpers.d.ts +5 -0
- package/dist/state/helpers.d.ts.map +1 -0
- package/dist/state/helpers.js +8 -0
- package/dist/state/helpers.js.map +1 -0
- package/dist/state/history.d.ts +39 -0
- package/dist/state/history.d.ts.map +1 -0
- package/dist/state/history.js +162 -0
- package/dist/state/history.js.map +1 -0
- package/dist/state/index.d.ts +8 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +22 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/plan-state.d.ts +21 -0
- package/dist/state/plan-state.d.ts.map +1 -0
- package/dist/state/plan-state.js +108 -0
- package/dist/state/plan-state.js.map +1 -0
- package/dist/state/sprint-state.d.ts +20 -0
- package/dist/state/sprint-state.d.ts.map +1 -0
- package/dist/state/sprint-state.js +98 -0
- package/dist/state/sprint-state.js.map +1 -0
- package/dist/utils/fs.d.ts +31 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +67 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +35 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +84 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +73 -0
- package/dist/utils/logger.js.map +1 -0
- package/hooks/hooks.json +10 -0
- package/package.json +67 -0
- package/scripts/detect-stack.sh +287 -0
- package/scripts/init-project.sh +206 -0
- package/scripts/run-eval.sh +175 -0
- package/skills/bober.anchor/SKILL.md +365 -0
- package/skills/bober.anchor/references/anchor-guide.md +567 -0
- package/skills/bober.brownfield/SKILL.md +422 -0
- package/skills/bober.brownfield/references/codebase-analysis.md +304 -0
- package/skills/bober.eval/SKILL.md +235 -0
- package/skills/bober.eval/references/eval-strategies.md +407 -0
- package/skills/bober.eval/references/feedback-format.md +182 -0
- package/skills/bober.plan/SKILL.md +244 -0
- package/skills/bober.plan/references/clarification-guide.md +124 -0
- package/skills/bober.plan/references/spec-schema.md +253 -0
- package/skills/bober.react/SKILL.md +330 -0
- package/skills/bober.react/references/react-scaffold.md +344 -0
- package/skills/bober.run/SKILL.md +303 -0
- package/skills/bober.solidity/SKILL.md +416 -0
- package/skills/bober.solidity/references/solidity-guide.md +487 -0
- package/skills/bober.sprint/SKILL.md +280 -0
- package/skills/bober.sprint/references/contract-schema.md +251 -0
- package/templates/base/CLAUDE.md +20 -0
- package/templates/base/bober.config.json +35 -0
- package/templates/brownfield/CLAUDE.md +34 -0
- package/templates/brownfield/bober.config.json +37 -0
- package/templates/presets/anchor/CLAUDE.md +163 -0
- package/templates/presets/anchor/bober.config.json +9 -0
- package/templates/presets/api-node/CLAUDE.md +153 -0
- package/templates/presets/api-node/bober.config.json +10 -0
- package/templates/presets/nextjs/CLAUDE.md +82 -0
- package/templates/presets/nextjs/bober.config.json +14 -0
- package/templates/presets/python-api/CLAUDE.md +202 -0
- package/templates/presets/python-api/bober.config.json +9 -0
- package/templates/presets/react-vite/CLAUDE.md +71 -0
- package/templates/presets/react-vite/bober.config.json +53 -0
- package/templates/presets/react-vite/scaffold/package.json +45 -0
- package/templates/presets/react-vite/scaffold/server/index.ts +38 -0
- package/templates/presets/react-vite/scaffold/server/tsconfig.json +24 -0
- package/templates/presets/react-vite/scaffold/src/App.tsx +37 -0
- package/templates/presets/react-vite/scaffold/src/index.html +12 -0
- package/templates/presets/react-vite/scaffold/src/main.tsx +12 -0
- package/templates/presets/react-vite/scaffold/tsconfig.json +27 -0
- package/templates/presets/react-vite/scaffold/vite.config.ts +34 -0
- package/templates/presets/solidity/CLAUDE.md +106 -0
- package/templates/presets/solidity/bober.config.json +9 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
# Anchor / Solana Development Reference Guide
|
|
2
|
+
|
|
3
|
+
## Anchor Project Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
project-root/
|
|
7
|
+
programs/ # Solana programs (one or more)
|
|
8
|
+
my-program/
|
|
9
|
+
src/
|
|
10
|
+
lib.rs # Program entry point, declares modules
|
|
11
|
+
instructions/ # Instruction handler modules
|
|
12
|
+
mod.rs # Re-exports all instructions
|
|
13
|
+
initialize.rs
|
|
14
|
+
transfer.rs
|
|
15
|
+
state/ # Account struct definitions
|
|
16
|
+
mod.rs
|
|
17
|
+
user_account.rs
|
|
18
|
+
vault.rs
|
|
19
|
+
errors.rs # Custom error definitions
|
|
20
|
+
constants.rs # Seeds, sizes, and other constants
|
|
21
|
+
Cargo.toml # Rust dependencies for this program
|
|
22
|
+
tests/ # Integration tests (TypeScript)
|
|
23
|
+
my-program.ts
|
|
24
|
+
helpers/
|
|
25
|
+
setup.ts
|
|
26
|
+
utils.ts
|
|
27
|
+
app/ # Optional client application
|
|
28
|
+
sdk/ # Optional TypeScript SDK
|
|
29
|
+
migrations/ # Anchor migration scripts
|
|
30
|
+
deploy.ts
|
|
31
|
+
target/ # Build output (generated)
|
|
32
|
+
idl/ # Generated IDL files
|
|
33
|
+
types/ # Generated TypeScript types
|
|
34
|
+
deploy/ # Program keypairs
|
|
35
|
+
Anchor.toml # Anchor workspace configuration
|
|
36
|
+
Cargo.toml # Workspace Cargo.toml
|
|
37
|
+
package.json
|
|
38
|
+
tsconfig.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key config (`Anchor.toml`):**
|
|
42
|
+
```toml
|
|
43
|
+
[features]
|
|
44
|
+
seeds = false
|
|
45
|
+
skip-lint = false
|
|
46
|
+
|
|
47
|
+
[programs.localnet]
|
|
48
|
+
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
|
|
49
|
+
|
|
50
|
+
[programs.devnet]
|
|
51
|
+
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
|
|
52
|
+
|
|
53
|
+
[registry]
|
|
54
|
+
url = "https://api.apr.dev"
|
|
55
|
+
|
|
56
|
+
[provider]
|
|
57
|
+
cluster = "Localnet"
|
|
58
|
+
wallet = "~/.config/solana/id.json"
|
|
59
|
+
|
|
60
|
+
[scripts]
|
|
61
|
+
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Account Types and Constraints
|
|
65
|
+
|
|
66
|
+
### Common Account Types
|
|
67
|
+
|
|
68
|
+
```rust
|
|
69
|
+
use anchor_lang::prelude::*;
|
|
70
|
+
|
|
71
|
+
#[derive(Accounts)]
|
|
72
|
+
pub struct Initialize<'info> {
|
|
73
|
+
// Mutable signer -- the user paying for account creation
|
|
74
|
+
#[account(mut)]
|
|
75
|
+
pub payer: Signer<'info>,
|
|
76
|
+
|
|
77
|
+
// New account to be created (init)
|
|
78
|
+
#[account(
|
|
79
|
+
init,
|
|
80
|
+
payer = payer,
|
|
81
|
+
space = 8 + UserAccount::INIT_SPACE, // 8 bytes for discriminator
|
|
82
|
+
seeds = [b"user", payer.key().as_ref()],
|
|
83
|
+
bump,
|
|
84
|
+
)]
|
|
85
|
+
pub user_account: Account<'info, UserAccount>,
|
|
86
|
+
|
|
87
|
+
// Existing account (validated by ownership and type)
|
|
88
|
+
pub config: Account<'info, GlobalConfig>,
|
|
89
|
+
|
|
90
|
+
// System program (required for account creation)
|
|
91
|
+
pub system_program: Program<'info, System>,
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Account Data Struct
|
|
96
|
+
|
|
97
|
+
```rust
|
|
98
|
+
#[account]
|
|
99
|
+
#[derive(InitSpace)]
|
|
100
|
+
pub struct UserAccount {
|
|
101
|
+
pub authority: Pubkey, // 32 bytes
|
|
102
|
+
pub balance: u64, // 8 bytes
|
|
103
|
+
pub is_active: bool, // 1 byte
|
|
104
|
+
pub bump: u8, // 1 byte
|
|
105
|
+
#[max_len(32)]
|
|
106
|
+
pub name: String, // 4 + 32 bytes (prefix + max chars)
|
|
107
|
+
pub created_at: i64, // 8 bytes
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Constraint Reference
|
|
112
|
+
|
|
113
|
+
```rust
|
|
114
|
+
#[derive(Accounts)]
|
|
115
|
+
pub struct UpdateUser<'info> {
|
|
116
|
+
// Must be a signer
|
|
117
|
+
pub authority: Signer<'info>,
|
|
118
|
+
|
|
119
|
+
// Must be mutable, must match authority field, PDA with seeds+bump
|
|
120
|
+
#[account(
|
|
121
|
+
mut,
|
|
122
|
+
has_one = authority,
|
|
123
|
+
seeds = [b"user", authority.key().as_ref()],
|
|
124
|
+
bump = user_account.bump,
|
|
125
|
+
)]
|
|
126
|
+
pub user_account: Account<'info, UserAccount>,
|
|
127
|
+
|
|
128
|
+
// Custom constraint with error
|
|
129
|
+
#[account(
|
|
130
|
+
constraint = vault.balance > 0 @ CustomError::VaultEmpty
|
|
131
|
+
)]
|
|
132
|
+
pub vault: Account<'info, Vault>,
|
|
133
|
+
|
|
134
|
+
// Close an account and send rent to a destination
|
|
135
|
+
#[account(
|
|
136
|
+
mut,
|
|
137
|
+
close = authority,
|
|
138
|
+
has_one = authority,
|
|
139
|
+
)]
|
|
140
|
+
pub account_to_close: Account<'info, TemporaryAccount>,
|
|
141
|
+
|
|
142
|
+
// Realloc (resize) an account
|
|
143
|
+
#[account(
|
|
144
|
+
mut,
|
|
145
|
+
realloc = 8 + UserAccount::INIT_SPACE + new_data_len,
|
|
146
|
+
realloc::payer = authority,
|
|
147
|
+
realloc::zero = false,
|
|
148
|
+
)]
|
|
149
|
+
pub resizable_account: Account<'info, UserAccount>,
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Token Account Constraints
|
|
154
|
+
|
|
155
|
+
```rust
|
|
156
|
+
use anchor_spl::token::{Mint, Token, TokenAccount};
|
|
157
|
+
use anchor_spl::associated_token::AssociatedToken;
|
|
158
|
+
|
|
159
|
+
#[derive(Accounts)]
|
|
160
|
+
pub struct TransferTokens<'info> {
|
|
161
|
+
#[account(mut)]
|
|
162
|
+
pub authority: Signer<'info>,
|
|
163
|
+
|
|
164
|
+
// Mint account
|
|
165
|
+
pub mint: Account<'info, Mint>,
|
|
166
|
+
|
|
167
|
+
// Source token account (must be owned by authority)
|
|
168
|
+
#[account(
|
|
169
|
+
mut,
|
|
170
|
+
associated_token::mint = mint,
|
|
171
|
+
associated_token::authority = authority,
|
|
172
|
+
)]
|
|
173
|
+
pub source_ata: Account<'info, TokenAccount>,
|
|
174
|
+
|
|
175
|
+
// Destination token account (init if needed)
|
|
176
|
+
#[account(
|
|
177
|
+
init_if_needed,
|
|
178
|
+
payer = authority,
|
|
179
|
+
associated_token::mint = mint,
|
|
180
|
+
associated_token::authority = recipient,
|
|
181
|
+
)]
|
|
182
|
+
pub destination_ata: Account<'info, TokenAccount>,
|
|
183
|
+
|
|
184
|
+
/// CHECK: recipient can be any account
|
|
185
|
+
pub recipient: UncheckedAccount<'info>,
|
|
186
|
+
|
|
187
|
+
pub token_program: Program<'info, Token>,
|
|
188
|
+
pub associated_token_program: Program<'info, AssociatedToken>,
|
|
189
|
+
pub system_program: Program<'info, System>,
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## PDA Patterns
|
|
194
|
+
|
|
195
|
+
### Basic PDA Derivation
|
|
196
|
+
|
|
197
|
+
```rust
|
|
198
|
+
// In constants.rs
|
|
199
|
+
pub const USER_SEED: &[u8] = b"user";
|
|
200
|
+
pub const VAULT_SEED: &[u8] = b"vault";
|
|
201
|
+
pub const CONFIG_SEED: &[u8] = b"config";
|
|
202
|
+
|
|
203
|
+
// In instruction context
|
|
204
|
+
#[account(
|
|
205
|
+
init,
|
|
206
|
+
payer = payer,
|
|
207
|
+
space = 8 + UserAccount::INIT_SPACE,
|
|
208
|
+
seeds = [USER_SEED, authority.key().as_ref()],
|
|
209
|
+
bump,
|
|
210
|
+
)]
|
|
211
|
+
pub user_account: Account<'info, UserAccount>,
|
|
212
|
+
|
|
213
|
+
// In instruction handler -- store the bump for future use
|
|
214
|
+
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
|
|
215
|
+
let user = &mut ctx.accounts.user_account;
|
|
216
|
+
user.authority = ctx.accounts.authority.key();
|
|
217
|
+
user.bump = ctx.bumps.user_account;
|
|
218
|
+
Ok(())
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### PDA as Signer (for CPIs)
|
|
223
|
+
|
|
224
|
+
```rust
|
|
225
|
+
// When a PDA needs to sign a CPI
|
|
226
|
+
pub fn transfer_from_vault(ctx: Context<TransferFromVault>, amount: u64) -> Result<()> {
|
|
227
|
+
let vault = &ctx.accounts.vault;
|
|
228
|
+
let seeds = &[
|
|
229
|
+
VAULT_SEED,
|
|
230
|
+
vault.authority.as_ref(),
|
|
231
|
+
&[vault.bump],
|
|
232
|
+
];
|
|
233
|
+
let signer_seeds = &[&seeds[..]];
|
|
234
|
+
|
|
235
|
+
// CPI with PDA signer
|
|
236
|
+
let cpi_accounts = Transfer {
|
|
237
|
+
from: ctx.accounts.vault_token_account.to_account_info(),
|
|
238
|
+
to: ctx.accounts.user_token_account.to_account_info(),
|
|
239
|
+
authority: ctx.accounts.vault.to_account_info(),
|
|
240
|
+
};
|
|
241
|
+
let cpi_program = ctx.accounts.token_program.to_account_info();
|
|
242
|
+
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
|
|
243
|
+
|
|
244
|
+
token::transfer(cpi_ctx, amount)?;
|
|
245
|
+
Ok(())
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Multi-Seed PDAs
|
|
250
|
+
|
|
251
|
+
```rust
|
|
252
|
+
// PDA derived from multiple seeds
|
|
253
|
+
#[account(
|
|
254
|
+
init,
|
|
255
|
+
payer = payer,
|
|
256
|
+
space = 8 + StakeRecord::INIT_SPACE,
|
|
257
|
+
seeds = [
|
|
258
|
+
b"stake",
|
|
259
|
+
pool.key().as_ref(),
|
|
260
|
+
user.key().as_ref(),
|
|
261
|
+
&pool.stake_count.to_le_bytes(),
|
|
262
|
+
],
|
|
263
|
+
bump,
|
|
264
|
+
)]
|
|
265
|
+
pub stake_record: Account<'info, StakeRecord>,
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Client-Side PDA Derivation (TypeScript)
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { PublicKey } from "@solana/web3.js";
|
|
272
|
+
|
|
273
|
+
// Derive a PDA
|
|
274
|
+
const [userAccountPda, bump] = PublicKey.findProgramAddressSync(
|
|
275
|
+
[Buffer.from("user"), authority.toBuffer()],
|
|
276
|
+
programId
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Derive a PDA with multiple seeds
|
|
280
|
+
const [stakeRecordPda] = PublicKey.findProgramAddressSync(
|
|
281
|
+
[
|
|
282
|
+
Buffer.from("stake"),
|
|
283
|
+
poolPubkey.toBuffer(),
|
|
284
|
+
userPubkey.toBuffer(),
|
|
285
|
+
new anchor.BN(stakeCount).toArrayLike(Buffer, "le", 8),
|
|
286
|
+
],
|
|
287
|
+
programId
|
|
288
|
+
);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## CPI Patterns
|
|
292
|
+
|
|
293
|
+
### Token Transfer CPI
|
|
294
|
+
|
|
295
|
+
```rust
|
|
296
|
+
use anchor_spl::token::{self, Transfer, Token};
|
|
297
|
+
|
|
298
|
+
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
|
|
299
|
+
let cpi_accounts = Transfer {
|
|
300
|
+
from: ctx.accounts.source.to_account_info(),
|
|
301
|
+
to: ctx.accounts.destination.to_account_info(),
|
|
302
|
+
authority: ctx.accounts.authority.to_account_info(),
|
|
303
|
+
};
|
|
304
|
+
let cpi_program = ctx.accounts.token_program.to_account_info();
|
|
305
|
+
token::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)?;
|
|
306
|
+
Ok(())
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Mint Tokens CPI
|
|
311
|
+
|
|
312
|
+
```rust
|
|
313
|
+
use anchor_spl::token::{self, MintTo, Token};
|
|
314
|
+
|
|
315
|
+
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
|
|
316
|
+
let seeds = &[
|
|
317
|
+
b"mint_authority",
|
|
318
|
+
&[ctx.accounts.config.mint_authority_bump],
|
|
319
|
+
];
|
|
320
|
+
let signer_seeds = &[&seeds[..]];
|
|
321
|
+
|
|
322
|
+
let cpi_accounts = MintTo {
|
|
323
|
+
mint: ctx.accounts.mint.to_account_info(),
|
|
324
|
+
to: ctx.accounts.destination.to_account_info(),
|
|
325
|
+
authority: ctx.accounts.mint_authority.to_account_info(),
|
|
326
|
+
};
|
|
327
|
+
let cpi_program = ctx.accounts.token_program.to_account_info();
|
|
328
|
+
token::mint_to(
|
|
329
|
+
CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds),
|
|
330
|
+
amount,
|
|
331
|
+
)?;
|
|
332
|
+
Ok(())
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### System Program CPI (SOL Transfer)
|
|
337
|
+
|
|
338
|
+
```rust
|
|
339
|
+
use anchor_lang::system_program::{self, Transfer};
|
|
340
|
+
|
|
341
|
+
pub fn transfer_sol(ctx: Context<TransferSol>, amount: u64) -> Result<()> {
|
|
342
|
+
let cpi_accounts = Transfer {
|
|
343
|
+
from: ctx.accounts.from.to_account_info(),
|
|
344
|
+
to: ctx.accounts.to.to_account_info(),
|
|
345
|
+
};
|
|
346
|
+
let cpi_program = ctx.accounts.system_program.to_account_info();
|
|
347
|
+
system_program::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)?;
|
|
348
|
+
Ok(())
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Testing with Bankrun
|
|
353
|
+
|
|
354
|
+
Bankrun provides a fast, in-process Solana test environment.
|
|
355
|
+
|
|
356
|
+
### Setup
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
npm install --save-dev solana-bankrun @solana/web3.js @coral-xyz/anchor
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Basic Bankrun Test
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { startAnchor } from "solana-bankrun";
|
|
366
|
+
import { BankrunProvider } from "anchor-bankrun";
|
|
367
|
+
import { Program } from "@coral-xyz/anchor";
|
|
368
|
+
import { PublicKey, Keypair } from "@solana/web3.js";
|
|
369
|
+
import { MyProgram } from "../target/types/my_program";
|
|
370
|
+
import IDL from "../target/idl/my_program.json";
|
|
371
|
+
|
|
372
|
+
describe("my-program", () => {
|
|
373
|
+
let provider: BankrunProvider;
|
|
374
|
+
let program: Program<MyProgram>;
|
|
375
|
+
let payer: Keypair;
|
|
376
|
+
|
|
377
|
+
beforeAll(async () => {
|
|
378
|
+
const context = await startAnchor(".", [], []);
|
|
379
|
+
provider = new BankrunProvider(context);
|
|
380
|
+
program = new Program(IDL as MyProgram, provider);
|
|
381
|
+
payer = context.payer;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("initializes correctly", async () => {
|
|
385
|
+
const [userPda] = PublicKey.findProgramAddressSync(
|
|
386
|
+
[Buffer.from("user"), payer.publicKey.toBuffer()],
|
|
387
|
+
program.programId
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
await program.methods
|
|
391
|
+
.initialize()
|
|
392
|
+
.accounts({
|
|
393
|
+
payer: payer.publicKey,
|
|
394
|
+
userAccount: userPda,
|
|
395
|
+
systemProgram: SystemProgram.programId,
|
|
396
|
+
})
|
|
397
|
+
.signers([payer])
|
|
398
|
+
.rpc();
|
|
399
|
+
|
|
400
|
+
const account = await program.account.userAccount.fetch(userPda);
|
|
401
|
+
expect(account.authority.toString()).toEqual(payer.publicKey.toString());
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Testing with Local Validator
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import * as anchor from "@coral-xyz/anchor";
|
|
410
|
+
import { Program } from "@coral-xyz/anchor";
|
|
411
|
+
import { MyProgram } from "../target/types/my_program";
|
|
412
|
+
|
|
413
|
+
describe("my-program", () => {
|
|
414
|
+
const provider = anchor.AnchorProvider.env();
|
|
415
|
+
anchor.setProvider(provider);
|
|
416
|
+
|
|
417
|
+
const program = anchor.workspace.MyProgram as Program<MyProgram>;
|
|
418
|
+
|
|
419
|
+
it("initializes correctly", async () => {
|
|
420
|
+
const [userPda] = anchor.web3.PublicKey.findProgramAddressSync(
|
|
421
|
+
[Buffer.from("user"), provider.wallet.publicKey.toBuffer()],
|
|
422
|
+
program.programId
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const tx = await program.methods
|
|
426
|
+
.initialize()
|
|
427
|
+
.accounts({
|
|
428
|
+
payer: provider.wallet.publicKey,
|
|
429
|
+
userAccount: userPda,
|
|
430
|
+
})
|
|
431
|
+
.rpc();
|
|
432
|
+
|
|
433
|
+
console.log("Transaction signature:", tx);
|
|
434
|
+
|
|
435
|
+
const account = await program.account.userAccount.fetch(userPda);
|
|
436
|
+
assert.ok(account.authority.equals(provider.wallet.publicKey));
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("rejects unauthorized access", async () => {
|
|
440
|
+
const unauthorized = anchor.web3.Keypair.generate();
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
await program.methods
|
|
444
|
+
.adminOnlyInstruction()
|
|
445
|
+
.accounts({
|
|
446
|
+
authority: unauthorized.publicKey,
|
|
447
|
+
})
|
|
448
|
+
.signers([unauthorized])
|
|
449
|
+
.rpc();
|
|
450
|
+
assert.fail("Should have thrown");
|
|
451
|
+
} catch (err) {
|
|
452
|
+
assert.ok(err.message.includes("ConstraintHasOne") ||
|
|
453
|
+
err.message.includes("Unauthorized"));
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Common Solana-Specific Issues
|
|
460
|
+
|
|
461
|
+
### Rent
|
|
462
|
+
|
|
463
|
+
All accounts on Solana must maintain a minimum balance (rent-exempt threshold). This is proportional to the account's data size.
|
|
464
|
+
|
|
465
|
+
```rust
|
|
466
|
+
// Calculate space for an account (include 8-byte discriminator for Anchor accounts)
|
|
467
|
+
space = 8 + UserAccount::INIT_SPACE
|
|
468
|
+
|
|
469
|
+
// In TypeScript, calculate minimum rent:
|
|
470
|
+
const rentExemptBalance = await connection.getMinimumBalanceForRentExemption(space);
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Common pitfall:** Forgetting the 8-byte Anchor discriminator when calculating space. Every Anchor account needs `8 + actual_data_size` bytes.
|
|
474
|
+
|
|
475
|
+
### Compute Budget
|
|
476
|
+
|
|
477
|
+
Each transaction has a default compute budget of 200,000 compute units per instruction. Complex operations may exceed this.
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// Request additional compute units
|
|
481
|
+
import { ComputeBudgetProgram } from "@solana/web3.js";
|
|
482
|
+
|
|
483
|
+
const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
|
|
484
|
+
units: 400_000,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const tx = new Transaction()
|
|
488
|
+
.add(modifyComputeUnits)
|
|
489
|
+
.add(yourInstruction);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Common pitfall:** Excessive `msg!()` logging consumes compute units. Minimize logging in production.
|
|
493
|
+
|
|
494
|
+
### Transaction Size
|
|
495
|
+
|
|
496
|
+
Solana transactions are limited to 1232 bytes. This includes:
|
|
497
|
+
- Signatures (64 bytes each)
|
|
498
|
+
- Message header (3 bytes)
|
|
499
|
+
- Account keys (32 bytes each)
|
|
500
|
+
- Recent blockhash (32 bytes)
|
|
501
|
+
- Instructions (variable)
|
|
502
|
+
|
|
503
|
+
**Mitigations:**
|
|
504
|
+
- Use Address Lookup Tables (ALTs) to reduce account key sizes
|
|
505
|
+
- Split large operations into multiple transactions
|
|
506
|
+
- Use versioned transactions (`MessageV0`)
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// Using Address Lookup Tables
|
|
510
|
+
import { AddressLookupTableProgram, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
|
|
511
|
+
|
|
512
|
+
const messageV0 = new TransactionMessage({
|
|
513
|
+
payerKey: payer.publicKey,
|
|
514
|
+
recentBlockhash,
|
|
515
|
+
instructions,
|
|
516
|
+
}).compileToV0Message([lookupTableAccount]);
|
|
517
|
+
|
|
518
|
+
const tx = new VersionedTransaction(messageV0);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Account Size Limits
|
|
522
|
+
|
|
523
|
+
- Maximum account size: 10 MB (10,240 bytes for realloc per instruction)
|
|
524
|
+
- For large data, consider using multiple accounts or off-chain storage with on-chain hashes
|
|
525
|
+
|
|
526
|
+
### Clock and Timestamps
|
|
527
|
+
|
|
528
|
+
```rust
|
|
529
|
+
// Get current timestamp
|
|
530
|
+
let clock = Clock::get()?;
|
|
531
|
+
let current_timestamp = clock.unix_timestamp; // i64
|
|
532
|
+
let current_slot = clock.slot; // u64
|
|
533
|
+
|
|
534
|
+
// Use in constraints
|
|
535
|
+
#[account(
|
|
536
|
+
constraint = clock.unix_timestamp > stake.unlock_time @ CustomError::StillLocked
|
|
537
|
+
)]
|
|
538
|
+
pub stake: Account<'info, StakeRecord>,
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Common pitfall:** Solana's clock is based on validator consensus and can drift. Do not rely on exact second precision. Use slot numbers for ordering when possible.
|
|
542
|
+
|
|
543
|
+
### Serialization and Borsh
|
|
544
|
+
|
|
545
|
+
Anchor uses Borsh serialization. Be aware of:
|
|
546
|
+
- Strings are serialized as `length (4 bytes) + utf8 bytes`
|
|
547
|
+
- Vectors are serialized as `length (4 bytes) + elements`
|
|
548
|
+
- Options are serialized as `1 byte (tag) + value (if Some)`
|
|
549
|
+
- Enums are serialized as `1 byte (variant index) + variant data`
|
|
550
|
+
|
|
551
|
+
```rust
|
|
552
|
+
#[account]
|
|
553
|
+
#[derive(InitSpace)]
|
|
554
|
+
pub struct DataAccount {
|
|
555
|
+
pub authority: Pubkey, // 32 bytes
|
|
556
|
+
pub count: u64, // 8 bytes
|
|
557
|
+
pub is_active: bool, // 1 byte
|
|
558
|
+
pub bump: u8, // 1 byte
|
|
559
|
+
#[max_len(50)]
|
|
560
|
+
pub name: String, // 4 + 50 bytes
|
|
561
|
+
#[max_len(10)]
|
|
562
|
+
pub items: Vec<u64>, // 4 + (10 * 8) bytes
|
|
563
|
+
pub optional_field: Option<Pubkey>, // 1 + 32 bytes
|
|
564
|
+
}
|
|
565
|
+
// Total INIT_SPACE = 32 + 8 + 1 + 1 + 54 + 84 + 33 = 213 bytes
|
|
566
|
+
// Account space = 8 (discriminator) + 213 = 221 bytes
|
|
567
|
+
```
|