@typesol/cli 0.21.0 → 0.22.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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ until 1.0, a minor version may change the language, the IDL layout or error code
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.22.0 — 2026-10-28
9
+
10
+ Generated clients fill in the accounts Anchor's client still asked you to pass (signers from a default signer, PDAs
11
+ seeded from string, byte and argument fields, PDAs under a program given as an argument, a helper for PDAs seeded from
12
+ their own fields), read Anchor zero-copy accounts and generic types, and run in tests over LiteSVM. Programs and their
13
+ bytes are unchanged. Not externally audited (see SECURITY.md).
14
+
15
+ ### Added
16
+ - Generated clients fill in the accounts Anchor's client still asks for:
17
+ - `resolve.<instruction>(rpc, accounts, args?, { signer })` fills signer accounts that were left out with a default
18
+ signer (the wallet), as Anchor's client uses the provider's wallet.
19
+ - PDAs seeded from another account's string, byte and `FixedString` fields are derived by `resolve`, as scalar fields
20
+ already were.
21
+ - A PDA seeded from its own fields gets a `pdas.<name>({ field values })` helper (its address is needed to read it).
22
+ - A PDA whose program is a `Pubkey` argument is derived by the instruction builder.
23
+ - Clients generated from Anchor IDLs read and write zero-copy (`bytemuck`, `AccountLoader`) accounts at their
24
+ `#[repr(C)]` offsets on SBF (padding, `packed` and `align` included), checked byte for byte against an Anchor
25
+ program, and turn generic types into one concrete type per use (`Pair<u64, 2>` → `PairU64N2`).
26
+ - PDAs seeded from fields of a struct argument (`seeds = [params.id]`) are derived by the instruction builder.
27
+ - `typesol/testing`: `svmConnection(svm)` (and `t.connection`), a kit RPC and subscriptions pair answered by LiteSVM,
28
+ so a generated client sends, fetches, simulates, resolves and watches in tests as it does against a cluster.
29
+
8
30
  ## 0.21.0 — 2026-10-28
9
31
 
10
32
  Clients from a program's address alone, and the last pieces Anchor's client resolves for you: `has_one` accounts,
package/README.md CHANGED
@@ -306,7 +306,7 @@ Details are in [docs/testing.md](docs/testing.md).
306
306
 
307
307
  ## Status
308
308
 
309
- **TypeSOL is pre-1.0 (0.21).** Releases follow the organization's
309
+ **TypeSOL is pre-1.0 (0.22).** Releases follow the organization's
310
310
  [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
311
311
  change.
312
312
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
package/dist/clientgen.js CHANGED
@@ -419,9 +419,123 @@ class Names {
419
419
  }
420
420
  }
421
421
  }
422
+ /**
423
+ * Generic types (Anchor `struct Wrap<T, const N: usize>`) as concrete ones: each use with type and const arguments
424
+ * (`Wrap<u64, 4>`) becomes its own type definition (`WrapU64N4`), and the uses point at it. Generic definitions
425
+ * themselves are left as they are (and out of the client); uses whose arguments are still generic are kept.
426
+ */
427
+ function monomorphize(idl) {
428
+ const defs = new Map((idl.types ?? []).map((t) => [t.name, t]));
429
+ if (![...defs.values()].some((d) => d.generics?.length))
430
+ return idl;
431
+ const taken = new Set(defs.keys());
432
+ const made = new Map(); // instantiation key → new type name
433
+ const extra = [];
434
+ const label = (t) => {
435
+ if (typeof t === "number")
436
+ return `N${t}`;
437
+ if (typeof t === "string")
438
+ return pascal(t.replace(/[^A-Za-z0-9_]/g, ""));
439
+ if (t.vec !== undefined)
440
+ return `Vec${label(t.vec)}`;
441
+ if (t.option !== undefined)
442
+ return `Option${label(t.option)}`;
443
+ if (t.coption !== undefined)
444
+ return `COption${label(t.coption)}`;
445
+ if (t.array)
446
+ return `Array${label(t.array[0])}N${String(t.array[1]).replace(/[^0-9]/g, "")}`;
447
+ return pascal((definedName(t) ?? "T").replace(/[^A-Za-z0-9_]/g, ""));
448
+ };
449
+ const hasGeneric = (t) => JSON.stringify(t ?? null).includes('"generic"');
450
+ const inst = (t, env) => {
451
+ if (typeof t === "string")
452
+ return t;
453
+ if (t.generic !== undefined) {
454
+ const v = env.get(t.generic);
455
+ return v === undefined || typeof v === "number" ? t : v;
456
+ }
457
+ if (t.vec !== undefined)
458
+ return { vec: inst(t.vec, env) };
459
+ if (t.option !== undefined)
460
+ return { option: inst(t.option, env) };
461
+ if (t.coption !== undefined)
462
+ return { coption: inst(t.coption, env) };
463
+ if (t.array) {
464
+ const len = t.array[1];
465
+ const g = typeof len === "object" && len !== null ? len.generic : undefined;
466
+ const n = g !== undefined ? env.get(g) : len;
467
+ return { array: [inst(t.array[0], env), typeof n === "number" ? n : len] };
468
+ }
469
+ const name = definedName(t);
470
+ const args = typeof t.defined === "object" ? (t.defined.generics ?? []) : [];
471
+ if (name === undefined || !args.length)
472
+ return t;
473
+ const values = args.map((a) => a?.kind === "const"
474
+ ? Number.isSafeInteger(Number(a.value)) && /^\d+$/.test(String(a.value))
475
+ ? Number(a.value)
476
+ : undefined
477
+ : a?.type === undefined
478
+ ? undefined
479
+ : inst(a.type, env));
480
+ const def = defs.get(name);
481
+ const params = (def?.generics ?? []);
482
+ if (!def ||
483
+ values.some((v) => v === undefined || hasGeneric(v)) ||
484
+ params.length !== values.length ||
485
+ params.some((p) => typeof p?.name !== "string" || !NAME.test(p.name)))
486
+ return { defined: { name, generics: args } };
487
+ const key = JSON.stringify([name, values]);
488
+ let made1 = made.get(key);
489
+ if (!made1) {
490
+ let base = `${name}${values.map((v) => label(v)).join("")}`;
491
+ if (!NAME.test(base))
492
+ base = `${name}Instance`;
493
+ made1 = base;
494
+ for (let i = 2; taken.has(made1); i++)
495
+ made1 = `${base}${i}`;
496
+ taken.add(made1);
497
+ made.set(key, made1);
498
+ const env2 = new Map(params.map((p, i) => [p.name, values[i]]));
499
+ const body = def.type;
500
+ const field = (f) => (isField(f) ? { ...f, type: inst(f.type, env2) } : inst(f, env2));
501
+ extra.push({
502
+ ...def,
503
+ name: made1,
504
+ generics: undefined,
505
+ type: {
506
+ ...body,
507
+ ...(body.fields ? { fields: body.fields.map(field) } : {}),
508
+ ...(body.variants ? { variants: body.variants.map((v) => ({ ...v, ...(v.fields ? { fields: v.fields.map(field) } : {}) })) } : {}),
509
+ ...(body.alias !== undefined ? { alias: inst(body.alias, env2) } : {}),
510
+ },
511
+ });
512
+ }
513
+ return { defined: made1 };
514
+ };
515
+ const none = new Map();
516
+ const field = (f) => (isField(f) ? { ...f, type: inst(f.type, none) } : inst(f, none));
517
+ const types = (idl.types ?? []).map((d) => d.generics?.length
518
+ ? d
519
+ : {
520
+ ...d,
521
+ type: {
522
+ ...d.type,
523
+ ...(d.type.fields ? { fields: d.type.fields.map(field) } : {}),
524
+ ...(d.type.variants ? { variants: d.type.variants.map((v) => ({ ...v, ...(v.fields ? { fields: v.fields.map(field) } : {}) })) } : {}),
525
+ ...(d.type.alias !== undefined ? { alias: inst(d.type.alias, none) } : {}),
526
+ },
527
+ });
528
+ const instructions = idl.instructions.map((ix) => ({
529
+ ...ix,
530
+ args: ix.args.map((a) => ({ ...a, type: inst(a.type, none) })),
531
+ ...(ix.returns !== undefined ? { returns: inst(ix.returns, none) } : {}),
532
+ }));
533
+ return { ...idl, instructions, types: [...types, ...extra] };
534
+ }
422
535
  export function generateClient(idl, opts = {}) {
423
536
  const programAddress = opts.address ?? idl.address;
424
537
  validate(idl, programAddress);
538
+ idl = monomorphize(idl);
425
539
  if (!programAddress)
426
540
  throw new ClientIdlError("the IDL has no program address; pass one");
427
541
  const typesol = idl.metadata?.description === "Built with TypeSOL";
@@ -448,6 +562,70 @@ export function generateClient(idl, opts = {}) {
448
562
  return { kind: "fenum", base: n.slice("FixedEnum_".length), size: 1 + arrayLen(f[1].type) };
449
563
  return undefined;
450
564
  };
565
+ // ---- Zero-copy (bytemuck) layouts: #[repr(C)] in memory, as the program sees it on SBF ------------------------
566
+ const zeroCopy = (d) => d?.serialization === "bytemuck" || d?.serialization === "bytemuckunsafe";
567
+ const layouts = new Map();
568
+ /** Size and alignment of a type inside a zero-copy struct, or why it cannot be in one. */
569
+ function cLayout(t) {
570
+ if (typeof t === "string") {
571
+ if (t in INTS)
572
+ return { size: INTS[t] / 8, align: Math.min(INTS[t] / 8, 8) }; // SBF aligns u128/i128 to 8, not 16
573
+ if (t in FLOATS)
574
+ return { size: FLOATS[t] / 8, align: FLOATS[t] / 8 };
575
+ if (t === "bool")
576
+ return { size: 1, align: 1 };
577
+ if (t === "pubkey" || t === "publicKey")
578
+ return { size: 32, align: 1 };
579
+ return `"${t}" has no fixed size`;
580
+ }
581
+ if (t.array) {
582
+ const e = cLayout(t.array[0]);
583
+ const n = Number(t.array[1]);
584
+ return typeof e === "string" ? e : { size: e.size * n, align: e.align };
585
+ }
586
+ const n = definedName(t);
587
+ const d = defs.get(n ?? "");
588
+ if (!n || !d || !zeroCopy(d))
589
+ return `${n ?? "the type"} is not a zero-copy type`;
590
+ const l = structLayout(d);
591
+ return typeof l === "string" ? l : { size: l.size, align: l.align };
592
+ }
593
+ function structLayout(d) {
594
+ const known = layouts.get(d.name);
595
+ if (known)
596
+ return known;
597
+ layouts.set(d.name, `type ${d.name} contains itself`);
598
+ const repr = (d.repr ?? {});
599
+ const fields = d.type.kind === "struct" ? (d.type.fields ?? []) : undefined;
600
+ let out;
601
+ if (!fields || !fields.every(isField))
602
+ out = `zero-copy type ${d.name} is not a struct with named fields`;
603
+ else if (repr.kind === "rust")
604
+ out = `zero-copy type ${d.name} has Rust's unspecified layout`;
605
+ else {
606
+ let offset = 0;
607
+ let align = 1;
608
+ const offsets = [];
609
+ let why;
610
+ for (const f of fields) {
611
+ const l = cLayout(f.type);
612
+ if (typeof l === "string") {
613
+ why = `${d.name}.${f.name}: ${l}`;
614
+ break;
615
+ }
616
+ const a = repr.packed ? 1 : l.align;
617
+ offset = Math.ceil(offset / a) * a;
618
+ offsets.push(offset);
619
+ offset += l.size;
620
+ align = Math.max(align, a);
621
+ }
622
+ if (Number.isInteger(repr.align) && repr.align > 0)
623
+ align = Math.max(align, repr.align);
624
+ out = why ?? { size: Math.ceil(offset / align) * align, align, offsets };
625
+ }
626
+ layouts.set(d.name, out);
627
+ return out;
628
+ }
451
629
  // ---- What can be expressed --------------------------------------------------------------------------------
452
630
  const typeIssues = new Map();
453
631
  /** Why a type cannot be generated, or undefined when it can. */
@@ -483,9 +661,14 @@ export function generateClient(idl, opts = {}) {
483
661
  if (!d)
484
662
  why = `type ${n} is not defined in the IDL`;
485
663
  else if (d.generics?.length)
486
- why = `type ${n} is generic`;
664
+ why = `type ${n} is generic (each use with concrete arguments is generated as its own type)`;
665
+ else if (zeroCopy(d)) {
666
+ const l = structLayout(d);
667
+ if (typeof l === "string")
668
+ why = l;
669
+ }
487
670
  else if (d.serialization && d.serialization !== "borsh")
488
- why = `type ${n} uses ${d.serialization} serialization (zero-copy layouts are not supported)`;
671
+ why = `type ${n} uses ${d.serialization} serialization, which is not supported`;
489
672
  else if (special(n)?.kind === "foption")
490
673
  why = issue(special(n).inner);
491
674
  else if (d.type.kind === "struct")
@@ -547,6 +730,7 @@ export function generateClient(idl, opts = {}) {
547
730
  let needsFixedOption = false;
548
731
  let needsFixedVec = false;
549
732
  let needsUnitEnum = false;
733
+ let needsCStruct = false;
550
734
  function codec(t) {
551
735
  if (typeof t === "string") {
552
736
  if (t in INTS || t in FLOATS) {
@@ -640,6 +824,10 @@ export function generateClient(idl, opts = {}) {
640
824
  const d = defs.get(n);
641
825
  if (!d)
642
826
  return undefined;
827
+ if (zeroCopy(d)) {
828
+ const l = structLayout(d);
829
+ return typeof l === "string" ? undefined : l.size;
830
+ }
643
831
  if (isUnitEnum(d))
644
832
  return 1;
645
833
  if (d.type.kind !== "struct")
@@ -676,6 +864,16 @@ export function generateClient(idl, opts = {}) {
676
864
  continue;
677
865
  }
678
866
  const named = fields;
867
+ if (zeroCopy(d)) {
868
+ // Fields at their #[repr(C)] offsets (padding written as zeros), not one after another as Borsh would.
869
+ const l = structLayout(d);
870
+ needsCStruct = true;
871
+ typeOut.push(`${doc(d.docs)}export interface ${T} {\n${named.map((f) => `${doc(f.docs, " ")} ${prop(camel(f.name))}: ${tsType(f.type, "out")};`).join("\n")}\n}\n` +
872
+ `export interface ${T}Args {\n${named.map((f) => ` ${prop(camel(f.name))}: ${tsType(f.type, "in")};`).join("\n")}\n}\n` +
873
+ `/** Zero-copy (#[repr(C)]) layout: ${l.size} bytes. */\n` +
874
+ `export function get${T}Codec(): Codec<${T}Args, ${T}> {\n return cStruct([${named.map((f, i) => `[${JSON.stringify(camel(f.name))}, ${codec(f.type)}, ${l.offsets[i]}]`).join(", ")}], ${l.size}) ${cast};\n}\n`);
875
+ continue;
876
+ }
679
877
  use("getStructCodec");
680
878
  typeOut.push(`${doc(d.docs)}export interface ${T} {\n${named.map((f) => `${doc(f.docs, " ")} ${prop(camel(f.name))}: ${tsType(f.type, "out")};`).join("\n")}\n}\n` +
681
879
  `export interface ${T}Args {\n${named.map((f) => ` ${prop(camel(f.name))}: ${tsType(f.type, "in")};`).join("\n")}\n}\n` +
@@ -839,12 +1037,20 @@ export function generateClient(idl, opts = {}) {
839
1037
  if (s.kind === "const")
840
1038
  return bytes(s.value);
841
1039
  if (s.kind === "arg") {
842
- if (s.path.includes("."))
843
- return { why: `the seed ${s.path} is a field of an argument` };
844
- const t = argType.get(s.path);
1040
+ // `params.id`: a field (of a field…) of a struct argument.
1041
+ const [head, ...rest] = s.path.split(".");
1042
+ let t = argType.get(head);
845
1043
  if (t === undefined)
846
- return { why: `the seed argument ${s.path} does not exist` };
847
- const v = `args.${camel(s.path)}`;
1044
+ return { why: `the seed argument ${head} does not exist` };
1045
+ let v = `args.${camel(head)}`;
1046
+ for (const part of rest) {
1047
+ const d = defs.get(definedName(t) ?? "");
1048
+ const f = d?.type.kind === "struct" ? (d.type.fields ?? []).find((x) => isField(x) && x.name === part) : undefined;
1049
+ if (!f || !NAME.test(part))
1050
+ return { why: `the seed ${s.path} is not a field of a struct argument` };
1051
+ t = f.type;
1052
+ v += `[${JSON.stringify(camel(part))}]`;
1053
+ }
848
1054
  if (t === "string")
849
1055
  return (use("getUtf8Encoder"), `getUtf8Encoder().encode(${v})`);
850
1056
  if (t === "bytes" || (typeof t === "object" && t.vec !== undefined && isU8(t.vec)))
@@ -874,8 +1080,10 @@ export function generateClient(idl, opts = {}) {
874
1080
  const target = p.path.includes(".") ? undefined : byPath(p.path, a.group);
875
1081
  program = target ? `key($${target.key})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
876
1082
  }
877
- else if (p?.kind === "arg")
878
- program = { why: "the PDA program comes from an argument" };
1083
+ else if (p?.kind === "arg") {
1084
+ const t = p.path.includes(".") ? undefined : argType.get(p.path);
1085
+ program = t === "pubkey" || t === "publicKey" ? `args.${camel(p.path)}` : { why: "the PDA program comes from an argument that is not a Pubkey" };
1086
+ }
879
1087
  const why = [...seeds, ...(program === undefined ? [] : [program])].find((x) => typeof x === "object");
880
1088
  if (why) {
881
1089
  notes.push(`instruction ${ix.name}: ${a.name} is not filled in (${why.why}); pass it`);
@@ -942,8 +1150,10 @@ export function generateClient(idl, opts = {}) {
942
1150
  const inputs = [...a.pda.seeds, ...(a.pda.program ? [a.pda.program] : [])].filter((s) => s.kind !== "const");
943
1151
  const params = [
944
1152
  ...new Map(inputs.map((s) => {
945
- if (s.kind === "arg")
946
- return [camel(s.path), `${camel(s.path)}: ${tsType(argType.get(s.path), "in")}`];
1153
+ if (s.kind === "arg") {
1154
+ const head = s.path.split(".")[0];
1155
+ return [camel(head), `${camel(head)}: ${tsType(argType.get(head), "in")}`];
1156
+ }
947
1157
  const k = byPath(s.path, a.group).key;
948
1158
  return [k, `${k}: Address | TransactionSigner`];
949
1159
  })).values(),
@@ -978,8 +1188,38 @@ export function generateClient(idl, opts = {}) {
978
1188
  lines.join("\n") +
979
1189
  `\n return {\n programAddress: PROGRAM_ADDRESS,\n accounts: [\n${metas.join("\n")}\n ...remaining,\n ],\n data: ${data},\n } as Instruction;\n}\n`);
980
1190
  ixKeys.push(`${prop(fn)}: build${P}`);
981
- // has_one relations (Anchor's `relations`): a non-signing account another account of the instruction names in a
982
- // Pubkey field can be read from that account on chain.
1191
+ // `resolve.<instruction>`: what needs the chain or the caller's wallet, filled in on a copy of the accounts in this
1192
+ // order: signers (a default signer), has_one accounts, then PDAs seeded from other accounts' fields.
1193
+ const acc = (f) => `acc${NAME.test(f.key) && !JS_RESERVED.has(f.key) ? `.${f.key}` : `[${JSON.stringify(f.key)}]`}`;
1194
+ // A field's value as seed bytes: scalars by their codec, strings as UTF-8, byte fields as they are.
1195
+ const seedOfField = (t, v) => {
1196
+ if (typeof t === "string") {
1197
+ if (t in INTS || t === "bool" || t === "pubkey" || t === "publicKey")
1198
+ return `${codec(t)}.encode(${v})`;
1199
+ if (t === "string")
1200
+ return (use("getUtf8Encoder"), `getUtf8Encoder().encode(${v})`);
1201
+ if (t === "bytes")
1202
+ return `Uint8Array.from(${v})`;
1203
+ return undefined;
1204
+ }
1205
+ if ((t.vec !== undefined && isU8(t.vec)) || (t.array && isU8(t.array[0])))
1206
+ return `Uint8Array.from(${v})`;
1207
+ const name = definedName(t);
1208
+ if (name === undefined)
1209
+ return undefined;
1210
+ if (special(name)?.kind === "fstring")
1211
+ return (use("getUtf8Encoder"), `getUtf8Encoder().encode(${v})`);
1212
+ const d = defs.get(name);
1213
+ return d?.type.kind === "enum" && (d.type.variants ?? []).every((x) => !x.fields?.length) ? `${codec(t)}.encode(${v})` : undefined;
1214
+ };
1215
+ // The builder's "pass it" note for an account, pointing at what fills it in instead.
1216
+ const renote = (a, how) => {
1217
+ const i = notes.findIndex((n) => n.startsWith(`instruction ${ix.name}: ${a.name} is not filled in (`));
1218
+ if (i >= 0)
1219
+ notes[i] = `${notes[i].replace(/; pass it$/, "")}; ${how}`;
1220
+ };
1221
+ const fieldOf = (accountType, field) => (defs.get(accountType ?? "")?.type.fields ?? []).find((f) => typeof f === "object" && f !== null && f.name === field);
1222
+ const signers = flat.filter((a) => a.signer && !auto(a) && !a.optional);
983
1223
  const rels = [];
984
1224
  for (const a of flat) {
985
1225
  if (auto(a) || a.optional || a.signer || !a.relations?.length)
@@ -997,30 +1237,30 @@ export function generateClient(idl, opts = {}) {
997
1237
  break;
998
1238
  }
999
1239
  }
1000
- // PDAs seeded from fields of another account of the instruction (Anchor `seeds = [b"vault", book.owner]`): the
1001
- // account is read on chain, then the PDA derived. Only scalar fields (integers, bool, Pubkey, unit enums).
1002
- const inputOf = (f) => `input${NAME.test(f.key) && !JS_RESERVED.has(f.key) ? `.${f.key}` : `[${JSON.stringify(f.key)}]`}`;
1003
- const scalarSeed = (t) => {
1004
- if (t === undefined)
1005
- return false;
1006
- if (typeof t === "string")
1007
- return t in INTS || t === "bool" || t === "pubkey" || t === "publicKey";
1008
- if (!t.defined)
1009
- return false;
1010
- const d = defs.get(definedName(t) ?? "");
1011
- return d?.type.kind === "enum" && (d.type.variants ?? []).every((v) => !v.fields?.length);
1012
- };
1013
1240
  const fieldPdas = [];
1014
1241
  for (const a of flat) {
1015
1242
  if (!a.pda || a.address || resolvable.has(a.key) || a.optional)
1016
1243
  continue;
1017
1244
  const p = a.pda.program;
1018
- if (p && p.kind !== "const")
1245
+ if (p && p.kind === "account")
1019
1246
  continue;
1247
+ let program = "PROGRAM_ADDRESS";
1248
+ if (p?.kind === "const")
1249
+ program = `programFromBytes(${bytes(p.value)})`;
1250
+ else if (p?.kind === "arg") {
1251
+ const t = argType.get(p.path ?? "");
1252
+ if (t !== "pubkey" && t !== "publicKey")
1253
+ continue;
1254
+ program = `args.${camel(p.path)}`;
1255
+ }
1020
1256
  const bases = new Map();
1021
1257
  const seeds = [];
1258
+ const own = [];
1259
+ // Inputs of a `pdas` helper for a PDA seeded from its own fields: its own field values, accounts and arguments.
1260
+ const params = new Map();
1261
+ let autoDep = false;
1022
1262
  let ok = true;
1023
- let usesArgs = false;
1263
+ let usesArgs = p?.kind === "arg";
1024
1264
  for (const sd of a.pda.seeds) {
1025
1265
  if (sd.kind === "const")
1026
1266
  seeds.push(bytes(sd.value));
@@ -1028,62 +1268,109 @@ export function generateClient(idl, opts = {}) {
1028
1268
  const e = seedExpr(sd, a.group);
1029
1269
  if (typeof e !== "string")
1030
1270
  ok = false;
1031
- else
1032
- (seeds.push(e), (usesArgs = true));
1271
+ else {
1272
+ seeds.push(e);
1273
+ usesArgs = true;
1274
+ const head = sd.path.split(".")[0];
1275
+ params.set(camel(head), `${camel(head)}: ${tsType(argType.get(head), "in")}`);
1276
+ }
1033
1277
  }
1034
1278
  else if (!sd.path.includes(".")) {
1035
1279
  const t = byPath(sd.path, a.group);
1036
- if (!t || t === a || auto(t) || t.optional)
1280
+ if (!t || t === a || t.optional)
1037
1281
  ok = false;
1038
- else
1039
- seeds.push(`getAddressEncoder().encode(key(${inputOf(t)}))`);
1282
+ else {
1283
+ use("getAddressEncoder");
1284
+ seeds.push(`getAddressEncoder().encode(key(${acc(t)}!))`);
1285
+ params.set(t.key, `${t.key}: Address | TransactionSigner`);
1286
+ if (auto(t))
1287
+ autoDep = true;
1288
+ }
1040
1289
  }
1041
1290
  else {
1042
1291
  const parts = sd.path.split(".");
1043
1292
  const base = parts.length === 2 ? byPath(parts[0], a.group) : undefined;
1044
- const acc = accountConsts.find((c) => c.name === sd.account);
1045
- const field = (defs.get(sd.account ?? "")?.type.fields ?? []).find((f) => typeof f === "object" && f !== null && f.name === parts[1]);
1046
- if (!base || base === a || base.optional || auto(base) || !acc || !field || !scalarSeed(field.type))
1293
+ const field = fieldOf(sd.account, parts[1]);
1294
+ const consts = accountConsts.find((c) => c.name === sd.account);
1295
+ const enc = field && seedOfField(field.type, base === a ? `seeds.${camel(parts[1])}` : `${"$$" + (base?.key ?? "")}[${JSON.stringify(camel(parts[1]))}]`);
1296
+ if (!base || !field || !enc || base.optional)
1297
+ ok = false;
1298
+ else if (base === a) {
1299
+ own.push({ field: parts[1], type: field.type });
1300
+ params.set(camel(parts[1]), `${camel(parts[1])}: ${tsType(field.type, "in")}`);
1301
+ seeds.push(enc);
1302
+ }
1303
+ else if (!consts || auto(base))
1047
1304
  ok = false;
1048
1305
  else {
1049
- bases.set(base.key, { base, A: acc.A });
1050
- seeds.push(`${codec(field.type)}.encode(${"$$" + base.key}[${JSON.stringify(camel(parts[1]))}])`);
1306
+ bases.set(base.key, { base, A: consts.A });
1307
+ seeds.push(enc);
1051
1308
  }
1052
1309
  }
1053
1310
  if (!ok)
1054
1311
  break;
1055
1312
  }
1056
- if (!ok || bases.size === 0)
1313
+ if (!ok)
1314
+ continue;
1315
+ if (own.length) {
1316
+ // Seeded from its own fields: the address is needed to read them, so the caller gives the values instead.
1317
+ if (bases.size)
1318
+ continue;
1319
+ const json = JSON.stringify(a.pda);
1320
+ let pn = camel(a.name);
1321
+ if (pdaFns.has(pn) && pdaFns.get(pn).json !== json) {
1322
+ pn = `${fn}${pascal(a.name)}`;
1323
+ for (let i = 2; pdaFns.has(pn) && pdaFns.get(pn).json !== json; i++)
1324
+ pn = `${fn}${pascal(a.name)}${i}`;
1325
+ }
1326
+ renote(a, `pass it (pdas.${pn} derives it from the field values)`);
1327
+ if (pdaFns.has(pn))
1328
+ continue;
1329
+ if (p?.kind === "arg")
1330
+ params.set(camel(p.path), `${camel(p.path)}: Address`);
1331
+ const rewrite = (e) => e
1332
+ .replace(/\bargs\.([A-Za-z0-9_$]+)/g, "seeds.$1")
1333
+ .replace(/key\(acc\.([A-Za-z0-9_$]+)!\)/g, "key(seeds.$1)")
1334
+ .replace(/key\(acc\["([^"]+)"\]!\)/g, 'key(seeds["$1"])');
1335
+ use("type ProgramDerivedAddress", "getProgramDerivedAddress");
1336
+ pdaFns.set(pn, {
1337
+ json,
1338
+ source: ` /** The ${a.name} PDA (as ${ix.name} derives it), from the values of its own fields it is seeded with (${own.map((o) => camel(o.field)).join(", ")}). */\n ${prop(pn)}(seeds: { ${[...params.values()].join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${rewrite(program)}, seeds: [${seeds.map(rewrite).join(", ")}] });\n },`,
1339
+ });
1340
+ continue;
1341
+ }
1342
+ if (bases.size === 0 || autoDep)
1057
1343
  continue;
1058
- const program = p ? `programFromBytes(${bytes(p.value)})` : "PROGRAM_ADDRESS";
1344
+ renote(a, `resolve.${fn} fills it in`);
1059
1345
  fieldPdas.push({
1060
1346
  a,
1061
1347
  usesArgs,
1062
1348
  lines: [
1063
- ` const ${"$" + a.key} =`,
1064
- ` ${inputOf(a)} ??`,
1065
- ` (await (async () => {`,
1066
- ...[...bases.values()].map(({ base, A }) => ` const ${"$$" + base.key} = await ${A}.fetch(rpc, key(${inputOf(base)}));`),
1067
- ` return (await getProgramDerivedAddress({ programAddress: ${program}, seeds: [${seeds.join(", ")}] }))[0];`,
1068
- ` })());`,
1349
+ ` if (${acc(a)} === undefined) {`,
1350
+ ...[...bases.values()].map(({ base, A }) => ` const ${"$$" + base.key} = await ${A}.fetch(rpc, key(${acc(base)}!));`),
1351
+ ` ${acc(a)} = (await getProgramDerivedAddress({ programAddress: ${program}, seeds: [${seeds.join(", ")}] }))[0];`,
1352
+ ` }`,
1069
1353
  ],
1070
1354
  });
1071
1355
  }
1072
- if (rels.length || fieldPdas.length) {
1073
- const filled = [...rels.map((r) => r.a), ...fieldPdas.map((f) => f.a)];
1074
- const keysOf = filled.map((a) => JSON.stringify(a.key)).join(" | ");
1356
+ if (rels.length || fieldPdas.length || signers.length) {
1357
+ const filled = [...signers, ...rels.map((r) => r.a), ...fieldPdas.map((f) => f.a)];
1358
+ const keysOf = [...new Set(filled.map((a) => JSON.stringify(a.key)))].join(" | ");
1075
1359
  const withArgs = fieldPdas.some((f) => f.usesArgs);
1076
1360
  const what = [
1361
+ ...(signers.length ? [`${signers.map((a) => a.key).join(", ")} (options.signer when left out)`] : []),
1077
1362
  ...rels.map((r) => `${r.a.key} (from ${r.source.key}.${camel(r.a.name)})`),
1078
1363
  ...fieldPdas.map((f) => `${f.a.key} (a PDA seeded from account fields)`),
1079
1364
  ].join(", ");
1080
- resolveKeys.push(` /** ${P}Accounts with ${what} read from the chain when left out. */\n` +
1081
- ` async ${prop(fn)}(rpc: Rpc<GetAccountInfoApi>, input: Omit<${P}Accounts, ${keysOf}> & Partial<Pick<${P}Accounts, ${keysOf}>>${withArgs ? `, args: ${P}Args` : ""}): Promise<${P}Accounts> {\n` +
1365
+ resolveKeys.push(` /** ${P}Accounts with ${what} filled in. */\n` +
1366
+ ` async ${prop(fn)}(rpc: Rpc<GetAccountInfoApi>, input: Omit<${P}Accounts, ${keysOf}> & Partial<Pick<${P}Accounts, ${keysOf}>>${withArgs ? `, args: ${P}Args` : ""}, options: { signer?: TransactionSigner } = {}): Promise<${P}Accounts> {\n` +
1367
+ ` const acc: Partial<${P}Accounts> = { ...input };\n` +
1082
1368
  [
1083
- ...rels.map((r) => ` const ${"$" + r.a.key} = ${inputOf(r.a)} ?? (await readRelation(rpc, key(${inputOf(r.source)}), ${JSON.stringify(r.a.key)}, [${r.readers.join(", ")}]));`),
1369
+ ...signers.map((a) => ` if (${acc(a)} === undefined) {\n if (!options.signer) throw new Error(${JSON.stringify(`${ix.name}: pass ${a.key}, or a default signer as options.signer`)});\n ${acc(a)} = options.signer;\n }`),
1370
+ ...rels.map((r) => ` ${acc(r.a)} ??= await readRelation(rpc, key(${acc(r.source)}!), ${JSON.stringify(r.a.key)}, [${r.readers.join(", ")}]);`),
1084
1371
  ...fieldPdas.flatMap((f) => f.lines),
1085
1372
  ].join("\n") +
1086
- `\n return { ...input, ${filled.map((a) => `${prop(a.key)}: ${"$" + a.key}`).join(", ")} } as ${P}Accounts;\n },`);
1373
+ `\n void rpc;\n return acc as ${P}Accounts;\n },`);
1087
1374
  }
1088
1375
  if (ix.returns !== undefined) {
1089
1376
  const why = issue(ix.returns);
@@ -1237,6 +1524,23 @@ export function generateClient(idl, opts = {}) {
1237
1524
  ` });`,
1238
1525
  ],
1239
1526
  });
1527
+ if (needsCStruct)
1528
+ helperDefs.push({
1529
+ name: "cStruct",
1530
+ lines: [
1531
+ `/** A zero-copy struct: each field at its #[repr(C)] offset in a block of \`size\` bytes (padding is zero). */`,
1532
+ `const cStruct = (fields: [string, Codec<any, any>, number][], size: number): Codec<Record<string, unknown>, Record<string, unknown>> =>`,
1533
+ ` createCodec<Record<string, unknown>, Record<string, unknown>>({`,
1534
+ ` fixedSize: size,`,
1535
+ ` write: (value, bytes, offset) => {`,
1536
+ ` bytes.fill(0, offset, offset + size);`,
1537
+ ` for (const [k, c, at] of fields) c.write(value[k], bytes, offset + at);`,
1538
+ ` return offset + size;`,
1539
+ ` },`,
1540
+ ` read: (bytes, offset) => [Object.fromEntries(fields.map(([k, c, at]) => [k, c.read(bytes, offset + at)[0]])), offset + size],`,
1541
+ ` });`,
1542
+ ],
1543
+ });
1240
1544
  if (needsUnitEnum)
1241
1545
  helperDefs.push({
1242
1546
  name: "unitEnum",
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests as they run against a
3
+ * cluster: `sendInstructions`, `simulate`, `views`, `accounts.<Name>.fetch*` and `watch`, `resolve`, `eventsOf` and
4
+ * `watchEvents`. Only the methods those helpers call are answered.
5
+ *
6
+ * const { rpc, rpcSubscriptions } = svmConnection(svm);
7
+ * await counter.sendInstructions({ rpc, rpcSubscriptions }, alice, [await counter.initialize({ authority: alice })]);
8
+ * await counter.accounts.Counter.fetch(rpc, address);
9
+ *
10
+ * Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
11
+ * the accounts transactions sent through this connection named, plus those passed to `track`.
12
+ */
13
+ import { type Address, type Rpc, type RpcSubscriptions, type SolanaRpcApi, type SolanaRpcSubscriptionsApi } from "@solana/kit";
14
+ import { type LiteSVM, type TransactionMetadata } from "litesvm";
15
+ export interface SvmConnection {
16
+ rpc: Rpc<SolanaRpcApi>;
17
+ rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
18
+ /** Every transaction sent through `rpc` that succeeded, in order. */
19
+ transactions: {
20
+ signature: string;
21
+ meta: TransactionMetadata;
22
+ }[];
23
+ /** Add accounts for `getProgramAccounts` to scan (accounts the transactions named are scanned already). */
24
+ track(...addresses: Address[]): void;
25
+ }
26
+ export declare function svmConnection(svm: LiteSVM): SvmConnection;
package/dist/svmrpc.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * A kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests as they run against a
3
+ * cluster: `sendInstructions`, `simulate`, `views`, `accounts.<Name>.fetch*` and `watch`, `resolve`, `eventsOf` and
4
+ * `watchEvents`. Only the methods those helpers call are answered.
5
+ *
6
+ * const { rpc, rpcSubscriptions } = svmConnection(svm);
7
+ * await counter.sendInstructions({ rpc, rpcSubscriptions }, alice, [await counter.initialize({ authority: alice })]);
8
+ * await counter.accounts.Counter.fetch(rpc, address);
9
+ *
10
+ * Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
11
+ * the accounts transactions sent through this connection named, plus those passed to `track`.
12
+ */
13
+ import { getAddressDecoder, getBase58Decoder, getBase58Encoder, getBase64Decoder, getBase64Encoder, getCompiledTransactionMessageDecoder, getTransactionDecoder, } from "@solana/kit";
14
+ import { FailedTransactionMetadata } from "litesvm";
15
+ const b64 = (bytes) => getBase64Decoder().decode(bytes);
16
+ const reply = (value) => ({ send: async () => value });
17
+ export function svmConnection(svm) {
18
+ const transactions = [];
19
+ const known = new Set();
20
+ const decode = (wire) => {
21
+ const tx = getTransactionDecoder().decode(getBase64Encoder().encode(wire));
22
+ return { tx, keys: getCompiledTransactionMessageDecoder().decode(tx.messageBytes).staticAccounts };
23
+ };
24
+ const encoded = (a) => {
25
+ const acct = svm.getAccount(a);
26
+ if (!acct.exists)
27
+ return null;
28
+ return {
29
+ data: [b64(acct.data), "base64"],
30
+ executable: acct.executable,
31
+ lamports: acct.lamports,
32
+ owner: acct.programAddress,
33
+ space: BigInt(acct.data.length),
34
+ rentEpoch: 0n,
35
+ };
36
+ };
37
+ const rpc = {
38
+ getLatestBlockhash: () => reply({ context: { slot: 0n }, value: { blockhash: svm.latestBlockhash(), lastValidBlockHeight: 1000000n } }),
39
+ getEpochInfo: () => reply({ absoluteSlot: 0n, blockHeight: 0n, epoch: 0n, slotIndex: 0n, slotsInEpoch: 432000n, transactionCount: null }),
40
+ getSignatureStatuses: (sigs) => reply({
41
+ context: { slot: 0n },
42
+ value: sigs.map((s) => transactions.some((t) => t.signature === s)
43
+ ? { confirmationStatus: "confirmed", confirmations: null, err: null, slot: 0n, status: { Ok: null } }
44
+ : null),
45
+ }),
46
+ getMinimumBalanceForRentExemption: (space) => reply(svm.minimumBalanceForRentExemption(space)),
47
+ getBalance: (a) => reply({ context: { slot: 0n }, value: svm.getBalance(a) ?? 0n }),
48
+ sendTransaction: (wire) => ({
49
+ send: async () => {
50
+ const { tx, keys } = decode(wire);
51
+ const res = svm.sendTransaction(tx);
52
+ svm.expireBlockhash();
53
+ if (res instanceof FailedTransactionMetadata)
54
+ throw new Error(`transaction failed: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
55
+ for (const k of keys)
56
+ known.add(k);
57
+ const signature = getBase58Decoder().decode(res.signature());
58
+ transactions.push({ signature, meta: res });
59
+ return signature;
60
+ },
61
+ }),
62
+ simulateTransaction: (wire, config = {}) => ({
63
+ send: async () => {
64
+ // Simulations are usually unsigned (sigVerify: false): turn LiteSVM's check off for this one only.
65
+ const inner = svm.inner;
66
+ const restore = config.sigVerify === false && inner?.getSigverify?.() === true;
67
+ if (restore)
68
+ svm.withSigverify(false);
69
+ let res;
70
+ try {
71
+ res = svm.simulateTransaction(decode(wire).tx);
72
+ }
73
+ finally {
74
+ if (restore)
75
+ svm.withSigverify(true);
76
+ }
77
+ if (res instanceof FailedTransactionMetadata)
78
+ return {
79
+ context: { slot: 0n },
80
+ value: { err: String(res.err()), logs: res.meta().logs(), unitsConsumed: res.meta().computeUnitsConsumed(), returnData: null },
81
+ };
82
+ const meta = res.meta();
83
+ const ret = meta.returnData();
84
+ return {
85
+ context: { slot: 0n },
86
+ value: {
87
+ err: null,
88
+ logs: meta.logs(),
89
+ unitsConsumed: meta.computeUnitsConsumed(),
90
+ returnData: ret.data().length ? { programId: getAddressDecoder().decode(ret.programId()), data: [b64(ret.data()), "base64"] } : null,
91
+ },
92
+ };
93
+ },
94
+ }),
95
+ getAccountInfo: (a) => reply({ context: { slot: 0n }, value: encoded(a) }),
96
+ getMultipleAccounts: (addresses) => reply({ context: { slot: 0n }, value: addresses.map(encoded) }),
97
+ getProgramAccounts: (program, config = {}) => reply([...known]
98
+ .map((a) => ({ pubkey: a, account: encoded(a) }))
99
+ .filter(({ account }) => account && account.owner === program)
100
+ .filter(({ account }) => {
101
+ const data = getBase64Encoder().encode(account.data[0]);
102
+ return (config.filters ?? []).every(({ memcmp, dataSize }) => {
103
+ if (dataSize !== undefined)
104
+ return BigInt(data.length) === BigInt(dataSize);
105
+ if (!memcmp)
106
+ return true;
107
+ const want = getBase58Encoder().encode(memcmp.bytes);
108
+ return want.every((b, i) => data[Number(memcmp.offset) + i] === b);
109
+ });
110
+ })),
111
+ // The json shape: log messages, and inner instructions with base58 data (what `eventsOf` reads).
112
+ getTransaction: (signature) => {
113
+ const t = transactions.find((x) => x.signature === signature);
114
+ return reply(t
115
+ ? {
116
+ slot: 0n,
117
+ blockTime: null,
118
+ meta: {
119
+ err: null,
120
+ logMessages: t.meta.logs(),
121
+ computeUnitsConsumed: t.meta.computeUnitsConsumed(),
122
+ innerInstructions: t.meta
123
+ .innerInstructions()
124
+ .map((group, index) => ({ index, instructions: group.map((i) => ({ data: getBase58Decoder().decode(i.instruction().data()) })) })),
125
+ },
126
+ }
127
+ : null);
128
+ },
129
+ };
130
+ /** Notifications that end when the subscriber aborts. */
131
+ const stream = (items) => ({
132
+ subscribe: async ({ abortSignal }) => (async function* () {
133
+ for (const item of items())
134
+ yield item;
135
+ await new Promise((resolve) => abortSignal.addEventListener("abort", resolve, { once: true }));
136
+ })(),
137
+ });
138
+ const rpcSubscriptions = {
139
+ signatureNotifications: () => stream(() => [{ context: { slot: 0n }, value: { err: null } }]),
140
+ slotNotifications: () => stream(() => []),
141
+ logsNotifications: () => stream(() => transactions.map((t) => ({ context: { slot: 0n }, value: { signature: t.signature, logs: t.meta.logs(), err: null } }))),
142
+ accountNotifications: (a) => stream(() => [{ context: { slot: 0n }, value: encoded(a) ?? { data: ["", "base64"], lamports: 0n } }]),
143
+ };
144
+ return {
145
+ rpc: rpc,
146
+ rpcSubscriptions: rpcSubscriptions,
147
+ transactions,
148
+ track: (...addresses) => addresses.forEach((a) => known.add(a)),
149
+ };
150
+ }
package/dist/testing.d.ts CHANGED
@@ -19,6 +19,8 @@
19
19
  import anchor, { type Idl } from "@anchor-lang/core";
20
20
  import { type Address, type Instruction, type KeyPairSigner } from "@solana/kit";
21
21
  import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
22
+ import { type SvmConnection, svmConnection } from "./svmrpc.js";
23
+ export { type SvmConnection, svmConnection };
22
24
  declare const BorshCoder: typeof anchor.BorshCoder;
23
25
  type BorshCoder = InstanceType<typeof BorshCoder>;
24
26
  export type Result = TransactionMetadata | FailedTransactionMetadata;
@@ -58,9 +60,10 @@ export interface TestProgram {
58
60
  returned<T = unknown>(instruction: string, res: Result): T;
59
61
  /** Lamports of any account (0 when it does not exist). */
60
62
  balance(address: Address): bigint;
63
+ /** A kit RPC and subscriptions pair over this LiteSVM, for the generated client (`svmConnection`). */
64
+ connection: SvmConnection;
61
65
  }
62
66
  /** Build `program` of the project around `from` (a file URL such as import.meta.url, or a directory) and load it. */
63
67
  export declare function testProgram(program: string, from: string | URL): Promise<TestProgram>;
64
68
  /** The same helpers for a program already loaded into `svm` at `programId` with this IDL. */
65
69
  export declare function programHarness(svm: LiteSVM, programId: Address, idl: Idl): TestProgram;
66
- export {};
package/dist/testing.js CHANGED
@@ -22,7 +22,9 @@ import { FailedTransactionMetadata, LiteSVM } from "litesvm";
22
22
  import { createHash } from "node:crypto";
23
23
  import path from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
+ import { svmConnection } from "./svmrpc.js";
25
26
  import { buildWorkspace, findWorkspace } from "./workspace.js";
27
+ export { svmConnection };
26
28
  // @anchor-lang/core is CommonJS: Node 22 cannot import its classes by name from an installed ES module.
27
29
  const { BN, BorshCoder } = anchor;
28
30
  const BIG = new Set(["u64", "i64", "u128", "i128"]);
@@ -332,6 +334,7 @@ export function programHarness(svm, programId, idl) {
332
334
  balance(address) {
333
335
  return svm.getBalance(address) ?? 0n;
334
336
  },
337
+ connection: svmConnection(svm),
335
338
  };
336
339
  return harness;
337
340
  }
package/docs/client.md CHANGED
@@ -48,7 +48,7 @@ for await (const { signature, event } of ledgerbook.watchEvents(rpcSubscriptions
48
48
  await ledgerbook.approveMany({ approver: signer, book }, { indices: [0n, 1n] }, [{ address: extra, role: AccountRole.WRITABLE }]);
49
49
 
50
50
  // has_one accounts that do not sign, read from the account that names them (Anchor resolves `relations` the same way)
51
- const accounts = await ledgerbook.resolve.someInstruction(rpc, { book }); // fills in what book's Pubkey fields name
51
+ const accounts = await ledgerbook.resolve.someInstruction(rpc, { book }, { signer }); // signers, and what book's fields name
52
52
 
53
53
  // Events of a fetched transaction (getTransaction, json): log events and emitCpi events from inner instructions
54
54
  ledgerbook.eventsOf(await rpc.getTransaction(signature, { maxSupportedTransactionVersion: 0 }).send());
@@ -64,9 +64,9 @@ Kit's own transaction functions work with the instructions as well; `sendInstruc
64
64
  | `<program>.<instruction>(accounts, args)` | Builds the `Instruction`. Accounts with a fixed address and PDAs whose seeds are constants, arguments or other accounts' keys are optional and filled in; pass one to override. An optional account passed as `null` is left out (sent as the program id, the convention TypeSOL and Anchor share); left undefined, it is filled in like any other. |
65
65
  | `<program>.<instruction>(accounts, args, remaining?)` | The same, with remaining accounts appended in order (kit `AccountMeta`s). |
66
66
  | `<program>.accounts.<Name>` | `decode(bytes)`, `encode(value)`, `fetch(rpc, address)`, `fetchMaybe(rpc, address)`, `fetchMultiple(rpc, addresses)`, `fetchAll(rpc, filters?)` (getProgramAccounts by discriminator, plus `{ offset, bytes }` filters), `watch(rpcSubscriptions, address, { abortSignal })`, `discriminator`. `decode` checks the discriminator. |
67
- | `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives. |
67
+ | `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives, also one seeded from its own fields (from the field values). |
68
68
  | `<program>.returns.<instruction>(bytes)` | Decodes return data. |
69
- | `<program>.resolve.<instruction>(rpc, accounts, args?)` | Fills in what needs the chain: `has_one` accounts that do not sign (read from the account that names them) and PDAs seeded from another account's fields (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived). Given accounts are kept. |
69
+ | `<program>.resolve.<instruction>(rpc, accounts, args?, { signer? })` | Fills in what needs the chain or the wallet: signer accounts left out (`options.signer`, the default signer, like Anchor's provider wallet), `has_one` accounts that do not sign (read from the account that names them), and PDAs seeded from another account's fields, scalars, strings or bytes (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived). Given accounts are kept. |
70
70
  | `<program>.views.<instruction>(rpc, feePayer, accounts, args)` | Simulates an instruction that returns a value and decodes it (throws with the logs if it fails). |
71
71
  | `<program>.sendInstructions(connection, feePayer, instructions)`, `simulate(rpc, feePayer, instructions)` | One transaction sent and confirmed (kit's sendAndConfirm), or simulated without signatures (error, logs, units, this program's return data). |
72
72
  | `<program>.errors`, `errorCode(e)`, `isError(e, name)` | The program's errors (6000+), and for TypeSOL programs also every Anchor framework error (`AnchorError` and the constraints raise them) and TypeSOL's runtime errors. |
@@ -89,12 +89,14 @@ Anchor IDLs are refused with a hint to convert them (`anchor idl convert`).
89
89
 
90
90
  - **Errors and events are matched by code and discriminator, not by program.** A program this one calls can raise the
91
91
  same error code, and `parseEvents` reads every `Program data:` line that decodes as one of this program's events.
92
- - **Not filled in automatically** (pass them): `has_one` accounts that sign (they have to sign), PDAs seeded from an
93
- account's own fields or from fields inside an argument, PDAs seeded from non-scalar fields (strings, structs), and PDAs
94
- whose program comes from an argument. `typesol client` prints a note for each. `has_one` accounts that do not sign and
95
- PDAs seeded from another account's scalar fields are filled in by `resolve` (it reads the chain).
96
- - **Left out of a client generated from an Anchor IDL**, with a note: generic types and anything using them, and
97
- zero-copy (`bytemuck`) accounts.
98
- - **Not done yet**: a LiteSVM loader for tests (use `svm.addProgramFromFile(ledgerbook.address, soPath)`, or
99
- `typesol/testing`). `watchEvents` reads log events; with `rpc` it also fetches each transaction in which the program
100
- invoked itself and reads its `emitCpi` events (log subscriptions do not carry inner instructions).
92
+ - **Not filled in automatically** (pass them): PDAs seeded from struct fields (Anchor cannot seed with them either),
93
+ PDAs whose program is another account's field, and a PDA seeded from its own fields (derive it with its `pdas`
94
+ helper from the values). `typesol client` prints a note for each.
95
+ - **Anchor IDLs**: zero-copy (`bytemuck`) accounts decode and encode at their `#[repr(C)]` offsets as the program sees
96
+ them on SBF (padding included; `packed` and `align` honored; 128-bit integers align to 8). Generic types become one
97
+ type per use with concrete arguments (`Pair<u64, 2>` is `PairU64N2`). Left out, with a note: zero-copy types with
98
+ Rust's own layout (`repr(Rust)`) or holding a non-zero-copy type, and the generic definitions themselves.
99
+ - **In tests**: `svmConnection(svm)` from `typesol/testing` answers the client's RPC calls from LiteSVM, and
100
+ `testProgram(...)` has it as `t.connection` ([test-your-program.md](test-your-program.md)).
101
+ - `watchEvents` reads log events; with `rpc` it also fetches each transaction in which the program invoked itself and
102
+ reads its `emitCpi` events (log subscriptions do not carry inner instructions).
@@ -44,9 +44,12 @@ it("counts", async () => {
44
44
  | `t.account(type, address)` | the account decoded by its IDL type (bigints, address strings), or `null` once closed. TypeSOL fixed layouts come back as their IDL structs: a `FixedString<N>` as `{ len, bytes }`, a `FixedVec<T, N>` as `{ len, items }` (the generated client gives strings and arrays) |
45
45
  | `t.returned(instruction, res)` | the instruction's return data: an integer, boolean, Pubkey or struct |
46
46
  | `t.balance(address)` | lamports |
47
+ | `t.connection` | `{ rpc, rpcSubscriptions }` answered by this LiteSVM, for the generated client: `counter.sendInstructions(t.connection, alice, [ix])`, `counter.accounts.Counter.fetch(t.connection.rpc, address)`, views, `resolve`, `watch` and `watchEvents` |
47
48
 
48
49
  `t.svm`, `t.programId`, `t.idl` and `t.coder` are there for anything else, e.g. writing a mint straight into the VM
49
50
  with `t.svm.setAccount` (see the escrow template). `programHarness(svm, programId, idl)` gives the same helpers for a
50
- program you loaded yourself.
51
+ program you loaded yourself, and `svmConnection(svm)` the RPC pair alone (for any LiteSVM, e.g. one with an Anchor
52
+ program loaded). Transactions sent through it run at once; `getProgramAccounts` (`fetchAll`) sees the accounts those
53
+ transactions named, plus any you `track(...)`.
51
54
 
52
55
  The module needs `litesvm` and `@anchor-lang/core`, which `typesol init` adds to `devDependencies`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",