@typesol/cli 0.21.0 → 0.23.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,49 @@ 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.23.0 — 2026-10-28
9
+
10
+ Language limits lifted: `map` and `filter`, any number of growable Vecs per account, closures and arrow functions as
11
+ helper arguments, FixedVec arguments and list fields in events. Existing programs compile to the same bytes. Not
12
+ externally audited (see SECURITY.md).
13
+
14
+ ### Added
15
+ - `map` and `filter` on Vecs, FixedArrays, FixedVecs and Vec arguments. A chain used right away
16
+ (`xs.filter(f).map(g).reduce(h, 0n)`, `.length`, `for … of`, any list method) is one loop that builds no list;
17
+ callbacks after a filter get the index among the elements it let through, as in JS. Stored (`const ys = xs.map(f)`),
18
+ it is a FixedVec on the stack as big as the list it starts from, or as annotated (`const ys: FixedVec<u64, 16> = …`,
19
+ needed for a `Vec`); a list one element too small aborts with 5104.
20
+ - Arrow functions written as helper arguments (`applyTwice((x) => x * 3n, v)`), typed from the helper's parameter when
21
+ left unannotated. Given a closure (one that reads the caller's variables), the helper is expanded where it is called,
22
+ so the closure runs where it can.
23
+ - Any number of growable `Vec<T>` fields in an account, declared anywhere. They are stored after the fixed-size fields,
24
+ in the order declared (every other field keeps a fixed offset; the IDL lists the stored order), and a push, pop or
25
+ clear moves the Vecs after it. An account with one Vec is laid out as before.
26
+ - `FixedVec<T, N>` arguments (a Borsh Vec of at most N elements; more fail with 102), and `FixedVec` and `Vec` fields
27
+ in events.
28
+
29
+ ## 0.22.0 — 2026-10-28
30
+
31
+ Generated clients fill in the accounts Anchor's client still asked you to pass (signers from a default signer, PDAs
32
+ seeded from string, byte and argument fields, PDAs under a program given as an argument, a helper for PDAs seeded from
33
+ their own fields), read Anchor zero-copy accounts and generic types, and run in tests over LiteSVM. Programs and their
34
+ bytes are unchanged. Not externally audited (see SECURITY.md).
35
+
36
+ ### Added
37
+ - Generated clients fill in the accounts Anchor's client still asks for:
38
+ - `resolve.<instruction>(rpc, accounts, args?, { signer })` fills signer accounts that were left out with a default
39
+ signer (the wallet), as Anchor's client uses the provider's wallet.
40
+ - PDAs seeded from another account's string, byte and `FixedString` fields are derived by `resolve`, as scalar fields
41
+ already were.
42
+ - A PDA seeded from its own fields gets a `pdas.<name>({ field values })` helper (its address is needed to read it).
43
+ - A PDA whose program is a `Pubkey` argument is derived by the instruction builder.
44
+ - Clients generated from Anchor IDLs read and write zero-copy (`bytemuck`, `AccountLoader`) accounts at their
45
+ `#[repr(C)]` offsets on SBF (padding, `packed` and `align` included), checked byte for byte against an Anchor
46
+ program, and turn generic types into one concrete type per use (`Pair<u64, 2>` → `PairU64N2`).
47
+ - PDAs seeded from fields of a struct argument (`seeds = [params.id]`) are derived by the instruction builder.
48
+ - `typesol/testing`: `svmConnection(svm)` (and `t.connection`), a kit RPC and subscriptions pair answered by LiteSVM,
49
+ so a generated client sends, fetches, simulates, resolves and watches in tests as it does against a cluster.
50
+
8
51
  ## 0.21.0 — 2026-10-28
9
52
 
10
53
  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.23).** 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",