-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathflows.ts
More file actions
1479 lines (1462 loc) · 53.2 KB
/
Copy pathflows.ts
File metadata and controls
1479 lines (1462 loc) · 53.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
export type RuntimeCheckStatus = "pass" | "warn" | "fail";
export interface RuntimeLabOption {
id: string;
label: string;
rationale: string;
}
export interface RuntimeLabCheck {
label: string;
status: RuntimeCheckStatus;
detail: string;
}
export interface RuntimeLabLog {
label: string;
detail: string;
tone: RuntimeCheckStatus;
}
export interface RuntimeLabFieldChange {
label: string;
before: string;
after: string;
}
export interface RuntimeLabAccountState {
id: string;
name: string;
role: string;
address: string;
owner: string;
summary: string;
chips: string[];
changes: RuntimeLabFieldChange[];
}
export interface RuntimeLabFailureMode {
id: string;
title: string;
error: string;
why: string;
fix: string;
}
export interface RuntimeLabStep {
id: string;
eyebrow: string;
title: string;
concept: string;
objective: string;
coachNote: string;
prompt: string;
options: RuntimeLabOption[];
correctOptionId: string;
checks: RuntimeLabCheck[];
logs: RuntimeLabLog[];
accounts: RuntimeLabAccountState[];
failures: RuntimeLabFailureMode[];
}
export interface RuntimeLabFlow {
id: string;
title: string;
tagline: string;
difficulty: string;
duration: string;
memoryHook: string;
outcomes: string[];
steps: RuntimeLabStep[];
}
export interface RuntimeLabProgram {
id: string;
name: string;
description: string;
difficulty: string;
focus: string;
flows: RuntimeLabFlow[];
}
export const vaultBootcampFlow: RuntimeLabFlow = {
id: "vault-bootcamp",
title: "Vault Bootcamp",
tagline: "Initialize a PDA vault, mutate state, mint a reward, then break it on purpose.",
difficulty: "Beginner",
duration: "12 min",
memoryHook: "Prediction first. Runtime second. State diff always.",
outcomes: [
"Understand what Solana validates before your instruction logic runs.",
"See how PDAs, writable accounts, and token accounts fit together.",
"Build the instinct to debug wrong signer, owner, and seed errors.",
],
steps: [
{
id: "initialize-vault",
eyebrow: "Stage 01",
title: "Initialize the vault PDA",
concept: "Account creation + rent + signer expectations",
objective: "Create a program-owned state account that will store vault data.",
coachNote:
"A new Solana dev usually thinks 'my instruction ran.' The runtime actually asks: who signed, who pays, who owns this account, and is it rent-safe?",
prompt:
"Which account must be both mutable and funded to create the vault PDA successfully?",
options: [
{
id: "payer-wallet",
label: "The payer wallet, because it signs and funds the new account allocation.",
rationale:
"Correct. The payer must sign and spend lamports so the PDA can be created rent-exempt.",
},
{
id: "system-program",
label: "The System Program, because it creates every new account.",
rationale:
"The System Program performs the instruction, but it does not provide lamports or mutability.",
},
{
id: "vault-pda",
label: "Only the vault PDA, because that is the account being initialized.",
rationale:
"The PDA is definitely mutable, but someone still has to fund it. That role belongs to the payer.",
},
],
correctOptionId: "payer-wallet",
checks: [
{
label: "Signer check",
status: "pass",
detail: "Payer signed the transaction, so the runtime allows lamports to be debited.",
},
{
label: "PDA derivation",
status: "pass",
detail:
"Seeds `[b\"vault\", user.key()]` plus bump derive a program-owned address with no private key.",
},
{
label: "Rent exemption",
status: "pass",
detail: "Enough lamports are moved into the new PDA to keep the account alive on-chain.",
},
],
logs: [
{
label: "Program log",
detail: "Creating vault PDA with 8-byte discriminator + 40 bytes of state.",
tone: "pass",
},
{
label: "System Program",
detail: "Transferred lamports from payer to vault PDA and allocated data space.",
tone: "pass",
},
{
label: "Anchor",
detail: "Account marked initialized and owned by your program.",
tone: "pass",
},
],
accounts: [
{
id: "payer",
name: "Payer Wallet",
role: "Funds the instruction",
address: "7Yx...payer",
owner: "System Program",
summary: "The human wallet covering rent and fees.",
chips: ["Signer", "Writable", "Fee payer"],
changes: [
{ label: "Lamports", before: "3.002 SOL", after: "2.999 SOL" },
{ label: "Data", before: "0 bytes", after: "0 bytes" },
],
},
{
id: "vault-pda",
name: "Vault PDA",
role: "Program state account",
address: "9aP...vault",
owner: "Vault Program",
summary: "Fresh PDA that will store vault metadata and counters.",
chips: ["PDA", "Initialized", "Program-owned"],
changes: [
{ label: "Lamports", before: "0 SOL", after: "0.0021 SOL" },
{ label: "Owner", before: "Unassigned", after: "Vault Program" },
{ label: "Counter", before: "N/A", after: "0" },
],
},
],
failures: [
{
id: "init-wrong-signer",
title: "Wrong signer",
error: "Signature verification failed",
why:
"The payer account was present but did not sign, so the runtime refused to debit lamports.",
fix: "Mark the payer as a signer in the client transaction and Anchor accounts struct.",
},
{
id: "init-low-lamports",
title: "Not enough rent",
error: "insufficient funds for rent exemption",
why:
"The PDA creation asked for more space than the payer could fund rent-exempt.",
fix: "Lower account size for the demo or airdrop more devnet SOL before retrying.",
},
],
},
{
id: "derive-pda",
eyebrow: "Stage 02",
title: "Derive the PDA again on read",
concept: "Seeds, bump, and deterministic addressing",
objective: "Prove you can find the same vault account without storing a private key.",
coachNote:
"Beginners often memorize 'PDAs are deterministic' without internalizing what is deterministic: the exact seed bytes, their order, the bump, and the program id.",
prompt:
"Why does Anchor care about the bump when validating an existing PDA account?",
options: [
{
id: "avoid-private-key",
label: "Because the bump proves the PDA sits off the ed25519 curve and matches the derived address.",
rationale:
"Correct. The bump is part of the deterministic search for a valid PDA address.",
},
{
id: "extra-security",
label: "Because every Solana account needs a bump for extra security, even wallet accounts.",
rationale:
"Only PDAs use bumps. Normal keypairs do not carry this concept.",
},
{
id: "save-compute",
label: "Because the runtime charges less compute when the bump is present.",
rationale:
"The bump helps derive the right PDA, but it is not a fee discount mechanic.",
},
],
correctOptionId: "avoid-private-key",
checks: [
{
label: "Seed order",
status: "pass",
detail: "The program derives `[b\"vault\", user.key()]` in the same order as initialization.",
},
{
label: "Program id match",
status: "pass",
detail: "Changing the program id would produce a different PDA, even with identical seeds.",
},
{
label: "Bump validation",
status: "pass",
detail: "Anchor recomputes the bump and confirms the passed-in account is the expected PDA.",
},
],
logs: [
{
label: "Program log",
detail: "Re-deriving vault PDA from canonical seeds before mutation.",
tone: "pass",
},
{
label: "Anchor",
detail: "ConstraintSeeds passed for account `vault_state`.",
tone: "pass",
},
{
label: "Coach",
detail: "If a client swaps seed order, you get a different address and the whole instruction stops here.",
tone: "warn",
},
],
accounts: [
{
id: "user",
name: "User Wallet",
role: "Seed input",
address: "4wT...user",
owner: "System Program",
summary: "The wallet pubkey becomes part of the PDA seed recipe.",
chips: ["Readable", "Seed source"],
changes: [
{ label: "Pubkey", before: "4wT...user", after: "4wT...user" },
{ label: "Lamports", before: "1.244 SOL", after: "1.244 SOL" },
],
},
{
id: "vault-pda-derive",
name: "Vault PDA",
role: "Derived target",
address: "9aP...vault",
owner: "Vault Program",
summary: "No private key exists. The program finds it from seeds and bump.",
chips: ["PDA", "Deterministic"],
changes: [
{ label: "Seeds", before: "[vault, user]", after: "[vault, user]" },
{ label: "Bump", before: "254", after: "254" },
],
},
],
failures: [
{
id: "pda-wrong-order",
title: "Wrong seed order",
error: "ConstraintSeeds",
why:
"The client derived `[user, vault]` while the program expected `[vault, user]`, so the addresses did not match.",
fix: "Centralize PDA helpers and never duplicate seed logic ad hoc in the frontend.",
},
{
id: "pda-wrong-program",
title: "Wrong program id",
error: "ConstraintSeeds",
why:
"The PDA was derived against an old or different program id after a redeploy.",
fix: "Regenerate client constants after redeploys and verify the deployed program id everywhere.",
},
],
},
{
id: "write-state",
eyebrow: "Stage 03",
title: "Write state into the vault",
concept: "Mutability, ownership, discriminator, and serialized data",
objective: "Increment a counter and record that the vault has been initialized.",
coachNote:
"This is where a lot of beginners collapse everything into 'the account exists.' Solana is stricter: it cares whether the right program owns the account and whether the transaction is allowed to write to it.",
prompt:
"Which pair of requirements is most important before your program updates vault state?",
options: [
{
id: "owner-and-mut",
label: "The vault must be owned by your program and passed as writable.",
rationale:
"Correct. Ownership decides authority over the data, and mutability allows the bytes to change.",
},
{
id: "signer-and-rent",
label: "The vault must sign and remain rent-exempt.",
rationale:
"A PDA cannot sign like a wallet unless invoked with seeds, and rent alone does not grant write permission.",
},
{
id: "mint-and-decimals",
label: "The token mint decimals must match the vault counter format.",
rationale:
"Token decimals matter later for token flows, not for writing plain program state.",
},
],
correctOptionId: "owner-and-mut",
checks: [
{
label: "Writable flag",
status: "pass",
detail: "The transaction marks `vault_state` mutable, so the runtime allows data bytes to change.",
},
{
label: "Program ownership",
status: "pass",
detail: "The vault account owner matches your program id, so your program may mutate it.",
},
{
label: "Discriminator decode",
status: "pass",
detail: "Anchor decodes the expected account type before applying the update.",
},
],
logs: [
{
label: "Program log",
detail: "Loaded vault state with counter = 0 and initialized = true.",
tone: "pass",
},
{
label: "Program log",
detail: "Incremented counter to 1 and stored `last_actor = user`.",
tone: "pass",
},
{
label: "Anchor",
detail: "Serialized updated account bytes back into `vault_state`.",
tone: "pass",
},
],
accounts: [
{
id: "vault-state-write",
name: "Vault PDA",
role: "Mutated account",
address: "9aP...vault",
owner: "Vault Program",
summary: "Program-owned state is now changing for the first time.",
chips: ["Writable", "Program-owned", "Decoded"],
changes: [
{ label: "Counter", before: "0", after: "1" },
{ label: "Initialized", before: "true", after: "true" },
{ label: "Last actor", before: "None", after: "4wT...user" },
],
},
{
id: "user-write",
name: "User Wallet",
role: "Authority context",
address: "4wT...user",
owner: "System Program",
summary: "No state bytes changed here, but this signer authorized the update.",
chips: ["Signer"],
changes: [
{ label: "Lamports", before: "1.244 SOL", after: "1.24399 SOL" },
],
},
],
failures: [
{
id: "write-owner",
title: "Wrong owner",
error: "AccountOwnedByWrongProgram",
why:
"The passed account looked like state, but it was not actually owned by your program.",
fix: "Verify account ownership before mutation and use strongly typed Anchor accounts wherever possible.",
},
{
id: "write-mut",
title: "Missing mut",
error: "instruction changed the balance of a read-only account",
why:
"The transaction included the account as read-only, so any attempted write was rejected.",
fix: "Mark the account `mut` in both the client instruction and the Anchor accounts struct.",
},
],
},
{
id: "mint-reward",
eyebrow: "Stage 04",
title: "Mint a reward to the user ATA",
concept: "SPL mint, token account ownership, CPI authority",
objective: "Reward the learner with demo tokens after the vault update succeeds.",
coachNote:
"Newcomers mix up the mint and the token account constantly. The mint defines the token. The token account holds somebody's balance.",
prompt:
"Where does the user's token balance actually increase when the reward is minted?",
options: [
{
id: "token-account",
label: "Inside the user's associated token account.",
rationale:
"Correct. The mint supply changes globally, but the owned balance sits in the token account.",
},
{
id: "mint-account",
label: "Inside the mint account, because the mint stores every user's tokens.",
rationale:
"The mint stores global metadata and total supply, not per-user balances.",
},
{
id: "vault-pda",
label: "Inside the vault PDA, because it initiated the reward instruction.",
rationale:
"Your program state can record metadata about the reward, but SPL balances live in token accounts.",
},
],
correctOptionId: "token-account",
checks: [
{
label: "Mint authority",
status: "pass",
detail: "The CPI uses the authority expected by the mint, so the token program accepts the instruction.",
},
{
label: "ATA ownership",
status: "pass",
detail: "The recipient token account belongs to the user and references the correct mint.",
},
{
label: "Supply update",
status: "pass",
detail: "Mint supply and token account amount both move in lockstep after the CPI succeeds.",
},
],
logs: [
{
label: "Program log",
detail: "Calling the Token Program to mint 10 demo tokens as a completion reward.",
tone: "pass",
},
{
label: "Token Program",
detail: "Minted 10 units to the user's associated token account.",
tone: "pass",
},
{
label: "Coach",
detail: "Notice how the mint supply changes globally while the ATA tracks the user's personal balance.",
tone: "warn",
},
],
accounts: [
{
id: "reward-mint",
name: "Reward Mint",
role: "Token definition",
address: "Mint...demo",
owner: "Token Program",
summary: "Defines the reward token and its total supply.",
chips: ["Mint", "Decimals: 0"],
changes: [
{ label: "Supply", before: "0", after: "10" },
{ label: "Mint authority", before: "Vault PDA", after: "Vault PDA" },
],
},
{
id: "reward-ata",
name: "User ATA",
role: "Balance holder",
address: "Ata...user",
owner: "Token Program",
summary: "The account that actually holds the user's reward balance.",
chips: ["Token account", "Writable"],
changes: [
{ label: "Amount", before: "0", after: "10" },
{ label: "Owner", before: "4wT...user", after: "4wT...user" },
],
},
],
failures: [
{
id: "mint-wrong-ata",
title: "Wrong token account",
error: "TokenInvalidAccountOwnerError",
why:
"The destination account did not belong to the expected owner or mint pairing.",
fix: "Derive the ATA from the wallet + mint pair instead of hardcoding token account addresses.",
},
{
id: "mint-auth",
title: "Wrong mint authority",
error: "owner does not match",
why:
"The CPI tried to mint with an authority that the mint did not recognize.",
fix: "Align the mint authority setup with the signer seeds used during the CPI.",
},
],
},
{
id: "break-it-safely",
eyebrow: "Stage 05",
title: "Break the flow and read the failure",
concept: "Atomicity and debug instincts",
objective: "See how the transaction rolls back when one validation fails.",
coachNote:
"This is the habit you want to build: when something fails, do not panic. Ask what the runtime checked last, which account that check referred to, and whether any state could have committed before the failure.",
prompt:
"The transaction fails with `ConstraintSeeds` before the reward CPI. What happens to the earlier state update in the same transaction?",
options: [
{
id: "rollback",
label: "Everything rolls back. No vault write or reward mint is committed.",
rationale:
"Correct. Solana transactions are atomic: one failed instruction means all prior writes in that transaction disappear.",
},
{
id: "state-persists",
label: "The vault write stays, but the reward mint is skipped.",
rationale:
"That would be partial commit behavior. Solana transactions do not work that way.",
},
{
id: "logs-reset-only",
label: "Only the logs are discarded. The state changes still land.",
rationale:
"Logs can still be emitted for debugging, but failed transactions do not commit state changes.",
},
],
correctOptionId: "rollback",
checks: [
{
label: "Seed validation",
status: "fail",
detail: "The PDA passed from the client does not match the PDA the program derived.",
},
{
label: "Instruction short-circuit",
status: "warn",
detail: "Execution stops before the token CPI, so the mint path never runs.",
},
{
label: "Atomic rollback",
status: "pass",
detail: "All earlier writes in the same transaction are discarded, keeping on-chain state unchanged.",
},
],
logs: [
{
label: "Anchor",
detail: "ConstraintSeeds: expected `9aP...vault`, got `85x...wrong`.",
tone: "fail",
},
{
label: "Runtime",
detail: "Instruction aborted before any token CPI could execute.",
tone: "warn",
},
{
label: "Coach",
detail: "If you inspect the accounts after failure, the vault counter and token balance remain unchanged.",
tone: "pass",
},
],
accounts: [
{
id: "rollback-vault",
name: "Vault PDA",
role: "Rolled-back state",
address: "9aP...vault",
owner: "Vault Program",
summary: "This account looks exactly like it did before the failed transaction began.",
chips: ["Rollback verified"],
changes: [
{ label: "Counter", before: "1", after: "1" },
{ label: "Last actor", before: "4wT...user", after: "4wT...user" },
],
},
{
id: "rollback-ata",
name: "User ATA",
role: "Unchanged token balance",
address: "Ata...user",
owner: "Token Program",
summary: "No extra reward arrives because the CPI never completed.",
chips: ["Unchanged"],
changes: [
{ label: "Amount", before: "10", after: "10" },
],
},
],
failures: [
{
id: "fail-seeds",
title: "Seeds mismatch",
error: "ConstraintSeeds",
why:
"The frontend or test derived the PDA incorrectly, so the program rejected the passed account.",
fix: "Share PDA derivation helpers between program tests, frontend code, and docs examples.",
},
{
id: "fail-owner",
title: "Owner mismatch",
error: "AccountOwnedByWrongProgram",
why:
"A different account with similar data was passed in, but ownership proved it was not valid state.",
fix: "Read the owner field before anything else when debugging mysterious state account failures.",
},
{
id: "fail-signer",
title: "Missing signer",
error: "Signature verification failed",
why:
"The transaction reached an auth check without the expected signer bit set on the required account.",
fix: "Compare the Anchor accounts struct to the client-side `accounts` and `signers` lists one by one.",
},
],
},
],
};
const tokenProgramFlow: RuntimeLabFlow = {
id: "token-reward-run",
title: "Token Reward Run",
tagline: "Create an ATA, mint a reward, and understand where balances actually live.",
difficulty: "Beginner",
duration: "10 min",
memoryHook: "Mint defines. Token account holds. Authority approves.",
outcomes: [
"Separate mint metadata from per-user balances.",
"See why ATAs are deterministic and safer than hardcoded token accounts.",
"Debug common token authority and destination-account mistakes.",
],
steps: [
{
id: "derive-ata",
eyebrow: "Stage 01",
title: "Derive the associated token account",
concept: "ATA derivation and owner + mint pairing",
objective: "Find the destination account that should hold the user's reward balance.",
coachNote:
"A lot of beginners think 'token account' is just any account with tokens in it. The real relationship is wallet + mint -> deterministic ATA.",
prompt: "Why is deriving the ATA safer than hardcoding a token account address?",
options: [
{
id: "wallet-mint-pair",
label: "Because the ATA is derived from the wallet and mint pair, so the destination is predictable and correct.",
rationale:
"Correct. You remove guesswork and reduce the chance of minting to the wrong destination account.",
},
{
id: "cheaper",
label: "Because ATA derivation uses less compute than any other token account operation.",
rationale:
"Compute is not the main reason. The real benefit is correctness and deterministic addressing.",
},
{
id: "private-key",
label: "Because ATAs have their own private keys managed by the token program.",
rationale:
"ATAs do not introduce a new private key model for the user here. They are deterministic token accounts.",
},
],
correctOptionId: "wallet-mint-pair",
checks: [
{
label: "Owner + mint derivation",
status: "pass",
detail: "The ATA address is derived from the wallet pubkey and the reward mint pubkey.",
},
{
label: "Destination account type",
status: "pass",
detail: "The selected destination account is an SPL token account, not a system wallet account.",
},
],
logs: [
{
label: "Client",
detail: "Derived ATA for wallet `4wT...user` and mint `Mint...reward`.",
tone: "pass",
},
{
label: "Coach",
detail: "If you hardcode an address here, you often end up debugging the wrong problem later.",
tone: "warn",
},
],
accounts: [
{
id: "token-user",
name: "User Wallet",
role: "ATA owner",
address: "4wT...user",
owner: "System Program",
summary: "The wallet identity that owns the eventual token balance.",
chips: ["Wallet", "Owner"],
changes: [
{ label: "Pubkey", before: "4wT...user", after: "4wT...user" },
],
},
{
id: "token-ata",
name: "Reward ATA",
role: "Destination token account",
address: "Ata...reward",
owner: "Token Program",
summary: "The token account that will hold the user's reward balance.",
chips: ["ATA", "Token account"],
changes: [
{ label: "Amount", before: "0", after: "0" },
],
},
],
failures: [
{
id: "wrong-destination",
title: "Wrong destination",
error: "InvalidAccountData",
why:
"The destination was not the expected token account for this wallet + mint pair.",
fix: "Always derive the ATA from the owner and mint instead of copying addresses by hand.",
},
],
},
{
id: "mint-reward-token",
eyebrow: "Stage 02",
title: "Mint into the ATA",
concept: "Mint authority and token balance updates",
objective: "Update the mint supply and the user's ATA balance in one token instruction path.",
coachNote:
"The mint does not store 'Alice has 10'. The mint stores supply and authorities. The ATA stores Alice's actual balance.",
prompt: "Where does the user's personal token balance change after minting succeeds?",
options: [
{
id: "ata-balance",
label: "Inside the user's ATA.",
rationale:
"Correct. The ATA holds the user's balance, while the mint tracks global token metadata and supply.",
},
{
id: "mint-only",
label: "Only inside the mint account.",
rationale:
"The mint supply changes too, but per-user balances do not live there.",
},
{
id: "wallet-native",
label: "Inside the user's system wallet because the wallet owns the ATA.",
rationale:
"Ownership is not the same thing as storage location. SPL balances live in token accounts.",
},
],
correctOptionId: "ata-balance",
checks: [
{
label: "Mint authority",
status: "pass",
detail: "The signer or PDA authority used for minting matches the mint's configured authority.",
},
{
label: "Supply increment",
status: "pass",
detail: "Mint supply and ATA amount both move after the mint instruction succeeds.",
},
],
logs: [
{
label: "Token Program",
detail: "Minted 25 reward units into the user's ATA.",
tone: "pass",
},
{
label: "Coach",
detail: "This is the split to memorize: supply on the mint, owned balance on the ATA.",
tone: "warn",
},
],
accounts: [
{
id: "reward-mint-supply",
name: "Reward Mint",
role: "Token definition",
address: "Mint...reward",
owner: "Token Program",
summary: "Defines the token and tracks total supply.",
chips: ["Mint"],
changes: [
{ label: "Supply", before: "0", after: "25" },
],
},
{
id: "reward-ata-balance",
name: "User ATA",
role: "Balance holder",
address: "Ata...reward",
owner: "Token Program",
summary: "Stores the user's personal reward balance.",
chips: ["Writable", "ATA"],
changes: [
{ label: "Amount", before: "0", after: "25" },
],
},
],
failures: [
{
id: "wrong-authority",
title: "Wrong mint authority",
error: "owner does not match",
why:
"The mint instruction was attempted with an authority the mint does not trust.",
fix: "Check the mint authority stored on the mint account and line it up with the signer or PDA seeds used in the CPI.",
},
],
},
{
id: "transfer-reward",
eyebrow: "Stage 03",
title: "Transfer from one ATA to another",
concept: "Token account ownership and signer authority",
objective: "Move tokens between token accounts while learning who must authorize the transfer.",
coachNote:
"The key mental model is simple: token accounts store balances, but the owner of that token account authorizes spending.",
prompt: "Who normally signs a token transfer from the user's ATA?",
options: [
{
id: "ata-owner",
label: "The owner of the source token account.",
rationale:
"Correct. The token program expects authority from the owner or an approved delegate.",
},
{
id: "mint-authority",
label: "The mint authority, because the mint created the tokens.",
rationale:
"Mint authority controls minting, not normal transfers between token accounts.",
},
{
id: "token-program",
label: "The token program itself, because it owns both token accounts.",
rationale:
"Program ownership is not the same thing as transfer authority.",
},
],
correctOptionId: "ata-owner",
checks: [
{
label: "Source authority",
status: "pass",
detail: "The owner of the source ATA signed, so the token program accepts the transfer.",
},
{
label: "Mint consistency",
status: "pass",
detail: "Source and destination token accounts both reference the same mint.",
},
],
logs: [
{
label: "Token Program",
detail: "Transferred 10 reward units from the source ATA to the destination ATA.",
tone: "pass",
},
],
accounts: [
{
id: "source-ata",
name: "Source ATA",
role: "Sending balance",
address: "Ata...source",
owner: "Token Program",
summary: "The user's token account before spending.",
chips: ["Source", "ATA"],
changes: [
{ label: "Amount", before: "25", after: "15" },
],
},
{
id: "destination-ata",
name: "Destination ATA",
role: "Receiving balance",
address: "Ata...dest",
owner: "Token Program",
summary: "The destination token account receiving the transfer.",
chips: ["Destination", "ATA"],
changes: [
{ label: "Amount", before: "0", after: "10" },
],
},
],
failures: [
{
id: "missing-transfer-authority",
title: "Missing transfer authority",
error: "owner does not match",
why:
"The transfer was attempted without the owner or delegate authority for the source ATA.",
fix: "Trace the source ATA owner field first, then compare it to the signer list in the transaction.",
},
],
},
],
};
const pdaStateFlow: RuntimeLabFlow = {
id: "counter-lifecycle",
title: "Counter Lifecycle",
tagline: "Initialize state, re-derive the PDA, mutate the counter, then fail on ownership assumptions.",
difficulty: "Beginner",
duration: "11 min",
memoryHook: "Seeds find state. Ownership protects state. Mutability changes state.",
outcomes: [
"Build confidence around PDA-derived program state.",
"See how ownership and mutability combine during writes.",
"Recognize the most common state-account failure modes quickly.",
],
steps: [
{
id: "counter-init",
eyebrow: "Stage 01",
title: "Initialize the counter account",
concept: "PDA init and rent-funded state",
objective: "Create a PDA that will store a simple program counter.",
coachNote:
"This is the smallest useful state pattern in Solana: derive a PDA, allocate space, and let the program own the bytes.",
prompt: "What gives your program the right to write custom bytes into a PDA state account later?",
options: [
{
id: "program-ownership",
label: "The account is owned by your program.",
rationale:
"Correct. Program ownership is the authority model for mutating custom account data.",
},
{
id: "payer-signature",
label: "The payer signed during initialization.",
rationale:
"The payer can fund creation, but that alone does not grant future write authority over program state.",
},
{
id: "rent-exemption",
label: "The account is rent-exempt.",
rationale:
"Rent keeps the account alive, but ownership is what controls writes.",
},
],
correctOptionId: "program-ownership",
checks: [
{
label: "PDA creation",
status: "pass",
detail: "The counter PDA is derived and created with enough space for the discriminator and state.",
},
{