@typesol/cli 0.23.0 → 0.24.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +77 -2
  2. package/README.md +18 -15
  3. package/SECURITY.md +1 -1
  4. package/dist/clientgen.js +222 -61
  5. package/dist/compiler/analyze.js +132 -20
  6. package/dist/compiler/emit/base.d.ts +27 -4
  7. package/dist/compiler/emit/base.js +68 -15
  8. package/dist/compiler/emit/calls.d.ts +7 -0
  9. package/dist/compiler/emit/calls.js +99 -9
  10. package/dist/compiler/emit/cpi.d.ts +33 -1
  11. package/dist/compiler/emit/cpi.js +353 -110
  12. package/dist/compiler/emit/expressions.d.ts +9 -1
  13. package/dist/compiler/emit/expressions.js +16 -12
  14. package/dist/compiler/emit/function.js +3 -1
  15. package/dist/compiler/emit/shared.d.ts +11 -1
  16. package/dist/compiler/emit/statements.d.ts +4 -2
  17. package/dist/compiler/emit/statements.js +128 -27
  18. package/dist/compiler/emit/storage.d.ts +6 -2
  19. package/dist/compiler/emit/storage.js +36 -32
  20. package/dist/compiler/model.d.ts +8 -0
  21. package/dist/idlpublish.d.ts +6 -1
  22. package/dist/idlpublish.js +47 -5
  23. package/dist/portgen.js +1 -1
  24. package/dist/svmrpc.d.ts +7 -1
  25. package/dist/svmrpc.js +133 -19
  26. package/dist/testing.js +23 -3
  27. package/docs/architecture.md +26 -2
  28. package/docs/cli.md +2 -2
  29. package/docs/client.md +13 -10
  30. package/docs/errors.md +9 -3
  31. package/docs/language.md +18 -17
  32. package/docs/mainnet.md +1 -1
  33. package/docs/migrate-from-anchor.md +8 -9
  34. package/docs/runtime.md +59 -0
  35. package/docs/test-your-program.md +6 -2
  36. package/docs/testing.md +1 -1
  37. package/docs/troubleshooting.md +3 -2
  38. package/lib/typesol.d.ts +120 -21
  39. package/package.json +1 -1
  40. package/runtime/typesol_rt.h +106 -28
package/dist/clientgen.js CHANGED
@@ -93,6 +93,11 @@ const TAKEN = [
93
93
  "required",
94
94
  "meta",
95
95
  "programFromBytes",
96
+ "ownProp",
97
+ "readRelation",
98
+ "memcmp",
99
+ "cpiData",
100
+ "FetchedTransaction",
96
101
  "fixedString",
97
102
  "fixedOption",
98
103
  "unitEnum",
@@ -279,6 +284,8 @@ const pascal = (s) => {
279
284
  return c.charAt(0).toUpperCase() + c.slice(1);
280
285
  };
281
286
  const prop = (s) => (NAME.test(s) && !JS_RESERVED.has(s) ? s : JSON.stringify(s));
287
+ /** Text safe to print on a terminal (notes, errors): C0 and C1 control characters and bidi overrides escaped. */
288
+ const printable = (s) => s.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
282
289
  /** Text that can sit inside a block comment: printable ASCII, no comment terminator. */
283
290
  const commentText = (s) => s.replace(/[^\x20-\x7e]/g, " ").replace(/\*\//g, "* /");
284
291
  const doc = (lines, indent = "") => Array.isArray(lines) && lines.length && lines.every((l) => typeof l === "string")
@@ -291,7 +298,7 @@ const sortCodeUnits = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
291
298
  /** Throws ClientIdlError unless every value that reaches the generated source is well-formed. */
292
299
  function validate(idl, programAddress) {
293
300
  const fail = (what) => {
294
- throw new ClientIdlError(`invalid IDL: ${what}`);
301
+ throw new ClientIdlError(printable(`invalid IDL: ${what}`));
295
302
  };
296
303
  const name = (n, where) => (typeof n === "string" && NAME.test(n) ? n : fail(`${where}: name ${JSON.stringify(n)} is not an identifier`));
297
304
  const byteList = (b, where) => Array.isArray(b) && b.every((x) => Number.isInteger(x) && x >= 0 && x <= 255) ? b : fail(`${where}: expected a list of bytes (0-255)`);
@@ -304,6 +311,12 @@ function validate(idl, programAddress) {
304
311
  if (o.array !== undefined) {
305
312
  if (!Array.isArray(o.array) || o.array.length !== 2)
306
313
  fail(`${where}: bad array type`);
314
+ const len = o.array[1];
315
+ // A length: a whole number, or a const generic parameter (`{ generic: "N" }`).
316
+ if (typeof len === "object" && len !== null)
317
+ name(len.generic, `${where} array length`);
318
+ else if (!(Number.isSafeInteger(len) && len >= 0))
319
+ fail(`${where}: array length ${JSON.stringify(len)} is not a whole number`);
307
320
  type(o.array[0], where);
308
321
  return;
309
322
  }
@@ -312,7 +325,24 @@ function validate(idl, programAddress) {
312
325
  return type(o[k], where);
313
326
  if (o.defined !== undefined) {
314
327
  const d = o.defined;
315
- name(typeof d === "string" ? d : d?.name, where);
328
+ if (typeof d === "string")
329
+ return void name(d, where);
330
+ if (typeof d !== "object" || d === null)
331
+ return fail(`${where}: bad defined type ${JSON.stringify(d)}`);
332
+ name(d.name, where);
333
+ // Generic arguments: `{ kind: "type", type }` or `{ kind: "const", value: "4" }`.
334
+ const args = d.generics;
335
+ if (args === undefined)
336
+ return;
337
+ if (!Array.isArray(args))
338
+ return fail(`${where}: generic arguments must be a list`);
339
+ for (const a of args) {
340
+ const g = (typeof a === "object" && a !== null ? a : {});
341
+ if (g.kind === "type")
342
+ type(g.type, `${where} generic argument`);
343
+ else if (g.kind !== "const" || !(typeof g.value === "string" && /^\d{1,15}$/.test(g.value)))
344
+ fail(`${where}: bad generic argument ${JSON.stringify(a)}`);
345
+ }
316
346
  return;
317
347
  }
318
348
  if (o.generic !== undefined)
@@ -380,6 +410,19 @@ function validate(idl, programAddress) {
380
410
  name(t.name, "type");
381
411
  if (typeof t.type !== "object" || t.type === null)
382
412
  fail(`type ${t.name} has no definition`);
413
+ if (t.generics !== undefined) {
414
+ if (!Array.isArray(t.generics))
415
+ fail(`type ${t.name}: generics must be a list`);
416
+ for (const g of t.generics)
417
+ name(g?.name, `type ${t.name} generic parameter`);
418
+ }
419
+ if (t.repr !== undefined) {
420
+ const align = t.repr?.align;
421
+ if (typeof t.repr !== "object" || t.repr === null)
422
+ fail(`type ${t.name}: bad repr`);
423
+ if (align !== undefined && !(Number.isSafeInteger(align) && align > 0 && Number.isInteger(Math.log2(align))))
424
+ fail(`type ${t.name}: repr align ${JSON.stringify(align)} is not a power of two`);
425
+ }
383
426
  fields(t.type.fields, `type ${t.name}`);
384
427
  for (const v of t.type.variants ?? []) {
385
428
  name(v.name, `type ${t.name} variant`);
@@ -447,7 +490,10 @@ function monomorphize(idl) {
447
490
  return pascal((definedName(t) ?? "T").replace(/[^A-Za-z0-9_]/g, ""));
448
491
  };
449
492
  const hasGeneric = (t) => JSON.stringify(t ?? null).includes('"generic"');
450
- const inst = (t, env) => {
493
+ // Instantiations can nest without end (`A<T> { next: Option<A<Vec<T>>> }`): cap their number and nesting.
494
+ const MAX_INSTANCES = 256;
495
+ const MAX_DEPTH = 16;
496
+ const inst = (t, env, depth = 0) => {
451
497
  if (typeof t === "string")
452
498
  return t;
453
499
  if (t.generic !== undefined) {
@@ -455,16 +501,16 @@ function monomorphize(idl) {
455
501
  return v === undefined || typeof v === "number" ? t : v;
456
502
  }
457
503
  if (t.vec !== undefined)
458
- return { vec: inst(t.vec, env) };
504
+ return { vec: inst(t.vec, env, depth) };
459
505
  if (t.option !== undefined)
460
- return { option: inst(t.option, env) };
506
+ return { option: inst(t.option, env, depth) };
461
507
  if (t.coption !== undefined)
462
- return { coption: inst(t.coption, env) };
508
+ return { coption: inst(t.coption, env, depth) };
463
509
  if (t.array) {
464
510
  const len = t.array[1];
465
511
  const g = typeof len === "object" && len !== null ? len.generic : undefined;
466
512
  const n = g !== undefined ? env.get(g) : len;
467
- return { array: [inst(t.array[0], env), typeof n === "number" ? n : len] };
513
+ return { array: [inst(t.array[0], env, depth), typeof n === "number" ? n : len] };
468
514
  }
469
515
  const name = definedName(t);
470
516
  const args = typeof t.defined === "object" ? (t.defined.generics ?? []) : [];
@@ -476,7 +522,7 @@ function monomorphize(idl) {
476
522
  : undefined
477
523
  : a?.type === undefined
478
524
  ? undefined
479
- : inst(a.type, env));
525
+ : inst(a.type, env, depth));
480
526
  const def = defs.get(name);
481
527
  const params = (def?.generics ?? []);
482
528
  if (!def ||
@@ -487,6 +533,8 @@ function monomorphize(idl) {
487
533
  const key = JSON.stringify([name, values]);
488
534
  let made1 = made.get(key);
489
535
  if (!made1) {
536
+ if (extra.length >= MAX_INSTANCES || depth >= MAX_DEPTH)
537
+ throw new ClientIdlError(`generic type ${name} is instantiated ${depth >= MAX_DEPTH ? `more than ${MAX_DEPTH} levels deep` : `more than ${MAX_INSTANCES} times`} (a generic type that uses itself with growing arguments?)`);
490
538
  let base = `${name}${values.map((v) => label(v)).join("")}`;
491
539
  if (!NAME.test(base))
492
540
  base = `${name}Instance`;
@@ -497,7 +545,7 @@ function monomorphize(idl) {
497
545
  made.set(key, made1);
498
546
  const env2 = new Map(params.map((p, i) => [p.name, values[i]]));
499
547
  const body = def.type;
500
- const field = (f) => (isField(f) ? { ...f, type: inst(f.type, env2) } : inst(f, env2));
548
+ const field = (f) => (isField(f) ? { ...f, type: inst(f.type, env2, depth + 1) } : inst(f, env2, depth + 1));
501
549
  extra.push({
502
550
  ...def,
503
551
  name: made1,
@@ -506,7 +554,7 @@ function monomorphize(idl) {
506
554
  ...body,
507
555
  ...(body.fields ? { fields: body.fields.map(field) } : {}),
508
556
  ...(body.variants ? { variants: body.variants.map((v) => ({ ...v, ...(v.fields ? { fields: v.fields.map(field) } : {}) })) } : {}),
509
- ...(body.alias !== undefined ? { alias: inst(body.alias, env2) } : {}),
557
+ ...(body.alias !== undefined ? { alias: inst(body.alias, env2, depth + 1) } : {}),
510
558
  },
511
559
  });
512
560
  }
@@ -533,6 +581,18 @@ function monomorphize(idl) {
533
581
  return { ...idl, instructions, types: [...types, ...extra] };
534
582
  }
535
583
  export function generateClient(idl, opts = {}) {
584
+ try {
585
+ const res = generate(idl, opts);
586
+ return { ...res, notes: res.notes.map(printable) };
587
+ }
588
+ catch (e) {
589
+ if (e instanceof ClientIdlError)
590
+ throw e;
591
+ // A malformed IDL that validation let through is still the IDL's fault: the same error, the original as cause.
592
+ throw new ClientIdlError(printable(`cannot generate a client from this IDL: ${e instanceof Error ? e.message : String(e)}`), { cause: e });
593
+ }
594
+ }
595
+ function generate(idl, opts) {
536
596
  const programAddress = opts.address ?? idl.address;
537
597
  validate(idl, programAddress);
538
598
  idl = monomorphize(idl);
@@ -633,7 +693,7 @@ export function generateClient(idl, opts = {}) {
633
693
  if (typeof t === "string") {
634
694
  if (t in INTS || t in FLOATS || ["bool", "pubkey", "publicKey", "string", "bytes"].includes(t))
635
695
  return undefined;
636
- return `type "${t}" is not supported`;
696
+ return `type ${JSON.stringify(t)} is not supported`;
637
697
  }
638
698
  if (t.generic !== undefined)
639
699
  return "generic parameters are not supported";
@@ -668,7 +728,7 @@ export function generateClient(idl, opts = {}) {
668
728
  why = l;
669
729
  }
670
730
  else if (d.serialization && d.serialization !== "borsh")
671
- why = `type ${n} uses ${d.serialization} serialization, which is not supported`;
731
+ why = `type ${n} uses ${JSON.stringify(d.serialization)} serialization, which is not supported`;
672
732
  else if (special(n)?.kind === "foption")
673
733
  why = issue(special(n).inner);
674
734
  else if (d.type.kind === "struct")
@@ -678,7 +738,7 @@ export function generateClient(idl, opts = {}) {
678
738
  else if (d.type.kind === "type" && d.type.alias !== undefined)
679
739
  why = issue(d.type.alias);
680
740
  else
681
- why = `type ${n} (${d.type.kind}) is not supported`;
741
+ why = `type ${n} (${JSON.stringify(d.type.kind)}) is not supported`;
682
742
  typeIssues.set(n, why);
683
743
  return why;
684
744
  }
@@ -788,7 +848,8 @@ export function generateClient(idl, opts = {}) {
788
848
  return (use("fixCodecSize"), `fixCodecSize(get${tsName(s.base)}Codec(), ${s.size})`);
789
849
  return `get${tsName(n)}Codec()`;
790
850
  }
791
- /** Byte size of a fixed-size IDL type (undefined when it varies). */
851
+ /** Byte size of a fixed-size IDL type (undefined when it varies). Memoized by type name, so shared types are sized once. */
852
+ const sizes = new Map();
792
853
  function idlSize(t) {
793
854
  if (typeof t === "string") {
794
855
  if (t in INTS)
@@ -808,6 +869,14 @@ export function generateClient(idl, opts = {}) {
808
869
  const n = definedName(t);
809
870
  if (n === undefined)
810
871
  return undefined;
872
+ if (sizes.has(n))
873
+ return sizes.get(n);
874
+ sizes.set(n, undefined); // a type that holds itself has no fixed size
875
+ const size = definedSize(n);
876
+ sizes.set(n, size);
877
+ return size;
878
+ }
879
+ function definedSize(n) {
811
880
  const sp = special(n);
812
881
  if (sp?.kind === "fstring")
813
882
  return 4 + sp.cap;
@@ -1033,7 +1102,10 @@ export function generateClient(idl, opts = {}) {
1033
1102
  // A seed or program path names an account: in the same group first, then at the top level.
1034
1103
  const byPath = (p, group) => flat.find((f) => `${f.group}${f.name}` === `${group}${p}`) ?? flat.find((f) => f.group === "" && f.name === p);
1035
1104
  const argType = new Map(ix.args.map((a) => [a.name, a.type]));
1036
- const seedExpr = (s, group) => {
1105
+ const seedsProp = (k) => `seeds${NAME.test(k) && !JS_RESERVED.has(k) ? `.${k}` : `[${JSON.stringify(k)}]`}`;
1106
+ const BUILDER = { arg: (n) => `args.${n}`, account: (k) => `$${k}` };
1107
+ const HELPER = { arg: (n) => seedsProp(n), account: (k) => seedsProp(k) };
1108
+ const seedExpr = (s, group, refs = BUILDER) => {
1037
1109
  if (s.kind === "const")
1038
1110
  return bytes(s.value);
1039
1111
  if (s.kind === "arg") {
@@ -1042,7 +1114,7 @@ export function generateClient(idl, opts = {}) {
1042
1114
  let t = argType.get(head);
1043
1115
  if (t === undefined)
1044
1116
  return { why: `the seed argument ${head} does not exist` };
1045
- let v = `args.${camel(head)}`;
1117
+ let v = refs.arg(camel(head));
1046
1118
  for (const part of rest) {
1047
1119
  const d = defs.get(definedName(t) ?? "");
1048
1120
  const f = d?.type.kind === "struct" ? (d.type.fields ?? []).find((x) => isField(x) && x.name === part) : undefined;
@@ -1065,25 +1137,30 @@ export function generateClient(idl, opts = {}) {
1065
1137
  if (!target)
1066
1138
  return { why: `the seed account ${s.path} is not an account of the instruction` };
1067
1139
  use("getAddressEncoder");
1068
- return `getAddressEncoder().encode(key($${target.key}))`;
1140
+ return `getAddressEncoder().encode(key(${refs.account(target.key)}))`;
1069
1141
  };
1070
- const resolvable = new Map();
1071
- for (const a of flat) {
1072
- if (!a.pda || a.address)
1073
- continue;
1074
- const seeds = a.pda.seeds.map((s) => seedExpr(s, a.group));
1075
- let program;
1142
+ /** A PDA's program as the code being written names it (undefined: this program). */
1143
+ const pdaProgram = (a, refs) => {
1076
1144
  const p = a.pda.program;
1077
1145
  if (p?.kind === "const")
1078
- program = (use("getAddressDecoder"), `programFromBytes(${bytes(p.value)})`);
1079
- else if (p?.kind === "account") {
1146
+ return (use("getAddressDecoder"), `programFromBytes(${bytes(p.value)})`);
1147
+ if (p?.kind === "account") {
1080
1148
  const target = p.path.includes(".") ? undefined : byPath(p.path, a.group);
1081
- program = target ? `key($${target.key})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
1149
+ return target ? `key(${refs.account(target.key)})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
1082
1150
  }
1083
- else if (p?.kind === "arg") {
1151
+ if (p?.kind === "arg") {
1084
1152
  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" };
1153
+ return t === "pubkey" || t === "publicKey" ? refs.arg(camel(p.path)) : { why: "the PDA program comes from an argument that is not a Pubkey" };
1086
1154
  }
1155
+ return undefined;
1156
+ };
1157
+ const resolvable = new Map();
1158
+ for (const a of flat) {
1159
+ if (!a.pda || a.address)
1160
+ continue;
1161
+ const seeds = a.pda.seeds.map((s) => seedExpr(s, a.group));
1162
+ const program = pdaProgram(a, BUILDER);
1163
+ const p = a.pda.program;
1087
1164
  const why = [...seeds, ...(program === undefined ? [] : [program])].find((x) => typeof x === "object");
1088
1165
  if (why) {
1089
1166
  notes.push(`instruction ${ix.name}: ${a.name} is not filled in (${why.why}); pass it`);
@@ -1121,8 +1198,9 @@ export function generateClient(idl, opts = {}) {
1121
1198
  ? `export interface ${P}Args {\n${argsField.map((a) => `${doc(a.docs, " ")} ${prop(a.key)}: ${tsType(a.type, "in")};`).join("\n")}\n}\n`
1122
1199
  : "";
1123
1200
  // Locals: given accounts and fixed addresses first, then PDAs in dependency order. `null` leaves an optional
1124
- // account out (sent as the program id); `undefined` fills it in.
1125
- const input = (a) => `input${NAME.test(a.key) && !JS_RESERVED.has(a.key) ? `.${a.key}` : `[${JSON.stringify(a.key)}]`}`;
1201
+ // account out (sent as the program id); `undefined` fills it in. Accounts are read as own properties, so one named
1202
+ // `constructor` or `toString` is not taken from Object.prototype.
1203
+ const input = (a) => `ownProp(input, ${JSON.stringify(a.key)})`;
1126
1204
  const fill = (a, value) => (a.optional ? `${input(a)} === null ? null : (${input(a)} ?? ${value})` : `${input(a)} ?? ${value}`);
1127
1205
  const lines = [];
1128
1206
  for (const a of flat) {
@@ -1158,11 +1236,13 @@ export function generateClient(idl, opts = {}) {
1158
1236
  return [k, `${k}: Address | TransactionSigner`];
1159
1237
  })).values(),
1160
1238
  ];
1161
- const rewrite = (e) => e.replace(/\bargs\.([A-Za-z0-9_$]+)/g, "seeds.$1").replace(/key\(\$([A-Za-z0-9_]+)\)/g, "key(seeds.$1)");
1239
+ // The same derivation, its inputs named as the helper's `seeds` parameter.
1240
+ const helperSeeds = a.pda.seeds.map((s) => seedExpr(s, a.group, HELPER));
1241
+ const helperProgram = pdaProgram(a, HELPER) ?? "PROGRAM_ADDRESS";
1162
1242
  use("type ProgramDerivedAddress");
1163
1243
  pdaFns.set(pn, {
1164
1244
  json,
1165
- source: ` /** The ${a.name} PDA (as ${ix.name} derives it). */\n ${prop(pn)}(seeds: { ${params.join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${rewrite(r.program ?? "PROGRAM_ADDRESS")}, seeds: [${r.seeds.map(rewrite).join(", ")}] });\n },`,
1245
+ source: ` /** The ${a.name} PDA (as ${ix.name} derives it). */\n ${prop(pn)}(seeds: { ${params.join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${helperProgram}, seeds: [${helperSeeds.join(", ")}] });\n },`,
1166
1246
  });
1167
1247
  }
1168
1248
  const metas = flat.map((a) => {
@@ -1190,7 +1270,8 @@ export function generateClient(idl, opts = {}) {
1190
1270
  ixKeys.push(`${prop(fn)}: build${P}`);
1191
1271
  // `resolve.<instruction>`: what needs the chain or the caller's wallet, filled in on a copy of the accounts in this
1192
1272
  // 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)}]`}`;
1273
+ const acc = (f) => `ownProp(acc, ${JSON.stringify(f.key)})`;
1274
+ const setAcc = (f) => `acc${NAME.test(f.key) && !JS_RESERVED.has(f.key) ? `.${f.key}` : `[${JSON.stringify(f.key)}]`}`;
1194
1275
  // A field's value as seed bytes: scalars by their codec, strings as UTF-8, byte fields as they are.
1195
1276
  const seedOfField = (t, v) => {
1196
1277
  if (typeof t === "string") {
@@ -1255,6 +1336,7 @@ export function generateClient(idl, opts = {}) {
1255
1336
  }
1256
1337
  const bases = new Map();
1257
1338
  const seeds = [];
1339
+ const helperSeeds = []; // the same seeds, named as a `pdas` helper's `seeds` (for one seeded from its own fields)
1258
1340
  const own = [];
1259
1341
  // Inputs of a `pdas` helper for a PDA seeded from its own fields: its own field values, accounts and arguments.
1260
1342
  const params = new Map();
@@ -1263,13 +1345,14 @@ export function generateClient(idl, opts = {}) {
1263
1345
  let usesArgs = p?.kind === "arg";
1264
1346
  for (const sd of a.pda.seeds) {
1265
1347
  if (sd.kind === "const")
1266
- seeds.push(bytes(sd.value));
1348
+ (seeds.push(bytes(sd.value)), helperSeeds.push(bytes(sd.value)));
1267
1349
  else if (sd.kind === "arg") {
1268
1350
  const e = seedExpr(sd, a.group);
1269
1351
  if (typeof e !== "string")
1270
1352
  ok = false;
1271
1353
  else {
1272
1354
  seeds.push(e);
1355
+ helperSeeds.push(seedExpr(sd, a.group, HELPER));
1273
1356
  usesArgs = true;
1274
1357
  const head = sd.path.split(".")[0];
1275
1358
  params.set(camel(head), `${camel(head)}: ${tsType(argType.get(head), "in")}`);
@@ -1282,6 +1365,7 @@ export function generateClient(idl, opts = {}) {
1282
1365
  else {
1283
1366
  use("getAddressEncoder");
1284
1367
  seeds.push(`getAddressEncoder().encode(key(${acc(t)}!))`);
1368
+ helperSeeds.push(`getAddressEncoder().encode(key(${seedsProp(t.key)}))`);
1285
1369
  params.set(t.key, `${t.key}: Address | TransactionSigner`);
1286
1370
  if (auto(t))
1287
1371
  autoDep = true;
@@ -1299,6 +1383,7 @@ export function generateClient(idl, opts = {}) {
1299
1383
  own.push({ field: parts[1], type: field.type });
1300
1384
  params.set(camel(parts[1]), `${camel(parts[1])}: ${tsType(field.type, "in")}`);
1301
1385
  seeds.push(enc);
1386
+ helperSeeds.push(enc);
1302
1387
  }
1303
1388
  else if (!consts || auto(base))
1304
1389
  ok = false;
@@ -1328,14 +1413,11 @@ export function generateClient(idl, opts = {}) {
1328
1413
  continue;
1329
1414
  if (p?.kind === "arg")
1330
1415
  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"])');
1416
+ const helperProgram = p?.kind === "arg" ? seedsProp(camel(p.path)) : program;
1335
1417
  use("type ProgramDerivedAddress", "getProgramDerivedAddress");
1336
1418
  pdaFns.set(pn, {
1337
1419
  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 },`,
1420
+ 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: ${helperProgram}, seeds: [${helperSeeds.join(", ")}] });\n },`,
1339
1421
  });
1340
1422
  continue;
1341
1423
  }
@@ -1348,17 +1430,38 @@ export function generateClient(idl, opts = {}) {
1348
1430
  lines: [
1349
1431
  ` if (${acc(a)} === undefined) {`,
1350
1432
  ...[...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];`,
1433
+ ` ${setAcc(a)} = (await getProgramDerivedAddress({ programAddress: ${program}, seeds: [${seeds.join(", ")}] }))[0];`,
1352
1434
  ` }`,
1353
1435
  ],
1354
1436
  });
1355
1437
  }
1438
+ // A has_one source the builder derives (a `config` PDA) is derived here too, after the accounts its seeds name,
1439
+ // so its fields can be read.
1440
+ const needed = new Set();
1441
+ const need = (k) => {
1442
+ if (!resolvable.has(k) || needed.has(k))
1443
+ return;
1444
+ needed.add(k);
1445
+ for (const d of resolvable.get(k).deps)
1446
+ need(d);
1447
+ };
1448
+ for (const r of rels)
1449
+ need(r.source.key);
1450
+ const derived = order.filter((a) => needed.has(a.key));
1451
+ const ref = (f) => (f.address ? `(${acc(f)} ?? address(${JSON.stringify(f.address)}))` : acc(f));
1452
+ const sub = (e) => e.replace(/key\(\$([A-Za-z0-9_]+)\)/g, (_, k) => `key(${ref(flat.find((f) => f.key === k))}, ${JSON.stringify(k)})`);
1453
+ const deriveLines = derived.map((a) => {
1454
+ const r = resolvable.get(a.key);
1455
+ return ` if (${acc(a)} === undefined) ${setAcc(a)} = (await getProgramDerivedAddress({ programAddress: ${sub(r.program ?? "PROGRAM_ADDRESS")}, seeds: [${r.seeds.map(sub).join(", ")}] }))[0];`;
1456
+ });
1356
1457
  if (rels.length || fieldPdas.length || signers.length) {
1357
1458
  const filled = [...signers, ...rels.map((r) => r.a), ...fieldPdas.map((f) => f.a)];
1358
1459
  const keysOf = [...new Set(filled.map((a) => JSON.stringify(a.key)))].join(" | ");
1359
- const withArgs = fieldPdas.some((f) => f.usesArgs);
1460
+ const withArgs = fieldPdas.some((f) => f.usesArgs) ||
1461
+ derived.some((a) => /\bargs\./.test([...resolvable.get(a.key).seeds, resolvable.get(a.key).program ?? ""].join(" ")));
1360
1462
  const what = [
1361
1463
  ...(signers.length ? [`${signers.map((a) => a.key).join(", ")} (options.signer when left out)`] : []),
1464
+ ...derived.map((a) => `${a.key} (derived, to read it)`),
1362
1465
  ...rels.map((r) => `${r.a.key} (from ${r.source.key}.${camel(r.a.name)})`),
1363
1466
  ...fieldPdas.map((f) => `${f.a.key} (a PDA seeded from account fields)`),
1364
1467
  ].join(", ");
@@ -1366,8 +1469,9 @@ export function generateClient(idl, opts = {}) {
1366
1469
  ` 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
1470
  ` const acc: Partial<${P}Accounts> = { ...input };\n` +
1368
1471
  [
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(", ")}]);`),
1472
+ ...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 ${setAcc(a)} = options.signer;\n }`),
1473
+ ...deriveLines,
1474
+ ...rels.map((r) => ` if (${acc(r.a)} === undefined) ${setAcc(r.a)} = await readRelation(rpc, ${ref(r.source)}, ${JSON.stringify(r.source.key)}, ${JSON.stringify(r.a.key)}, [${r.readers.join(", ")}]);`),
1371
1475
  ...fieldPdas.flatMap((f) => f.lines),
1372
1476
  ].join("\n") +
1373
1477
  `\n void rpc;\n return acc as ${P}Accounts;\n },`);
@@ -1420,8 +1524,8 @@ export function generateClient(idl, opts = {}) {
1420
1524
  {
1421
1525
  name: "key",
1422
1526
  lines: [
1423
- `const key = (v: TransactionSigner | Address | undefined | null): Address => {`,
1424
- ` if (v === undefined || v === null) throw new Error("a seed account is missing");`,
1527
+ `const key = (v: TransactionSigner | Address | undefined | null, name?: string): Address => {`,
1528
+ ` if (v === undefined || v === null) throw new Error(name === undefined ? "a seed account is missing" : "a seed account is missing: pass " + name);`,
1425
1529
  ` return isSigner(v) ? v.address : v;`,
1426
1530
  `};`,
1427
1531
  ],
@@ -1442,11 +1546,26 @@ export function generateClient(idl, opts = {}) {
1442
1546
  ],
1443
1547
  },
1444
1548
  { name: "programFromBytes", lines: [`const programFromBytes = (b: Uint8Array): Address => getAddressDecoder().decode(b);`] },
1549
+ {
1550
+ name: "ownProp",
1551
+ lines: [
1552
+ `/** An account given in \`o\` (an own property: never one inherited from Object.prototype, such as \`constructor\`). */`,
1553
+ `const ownProp = <T extends object, K extends keyof T>(o: T, k: K): T[K] | undefined => (Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined);`,
1554
+ ],
1555
+ },
1445
1556
  {
1446
1557
  name: "readRelation",
1447
1558
  lines: [
1448
- `/** A Pubkey field of an account on chain, read by the first reader that knows its type. */`,
1449
- `const readRelation = async (rpc: Rpc<GetAccountInfoApi>, at: Address, name: string, readers: ((data: ReadonlyUint8Array) => Address | undefined)[]): Promise<Address> => {`,
1559
+ `/** A Pubkey field (\`name\`) of an account on chain (\`from\`), read by the first reader that knows its type. */`,
1560
+ `const readRelation = async (`,
1561
+ ` rpc: Rpc<GetAccountInfoApi>,`,
1562
+ ` from: TransactionSigner | Address | undefined,`,
1563
+ ` fromName: string,`,
1564
+ ` name: string,`,
1565
+ ` readers: ((data: ReadonlyUint8Array) => Address | undefined)[],`,
1566
+ `): Promise<Address> => {`,
1567
+ ` if (from === undefined) throw new Error("cannot fill in " + name + ": pass " + fromName + ", the account it is read from");`,
1568
+ ` const at = key(from);`,
1450
1569
  ` const found = await fetchEncodedAccount(rpc, at);`,
1451
1570
  ` if (!found.exists) throw new Error("cannot fill in " + name + ": account " + at + " does not exist");`,
1452
1571
  ` for (const read of readers) {`,
@@ -1581,6 +1700,11 @@ export function generateClient(idl, opts = {}) {
1581
1700
  ` for (let cur: unknown = e, depth = 0; cur !== undefined && cur !== null && depth < 8; depth++) {`,
1582
1701
  ` if (typeof cur === "number") return cur;`,
1583
1702
  ` if (isSolanaError(cur, SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM)) return Number(cur.context.code);`,
1703
+ ` // The JSON an RPC node returns (simulations, signature statuses): { InstructionError: [0, { Custom: 6000 }] }.`,
1704
+ ` if (typeof cur === "object" && "InstructionError" in cur && Array.isArray(cur.InstructionError)) {`,
1705
+ ` const inner: unknown = cur.InstructionError[1];`,
1706
+ ` if (typeof inner === "object" && inner !== null && "Custom" in inner) return Number(inner.Custom);`,
1707
+ ` }`,
1584
1708
  ` if (typeof cur === "object" && "err" in cur && typeof cur.err === "function") cur = String(cur.err());`,
1585
1709
  ` if (typeof cur === "string" || cur instanceof Error) {`,
1586
1710
  ` const m = /custom program error: (0x[0-9a-f]+)|Custom(?:\\(|\\s*\\{\\s*code:\\s*)(\\d+)/i.exec(String(cur instanceof Error ? cur.message : cur));`,
@@ -1612,14 +1736,25 @@ export function generateClient(idl, opts = {}) {
1612
1736
  : ` void data;\n return null;`,
1613
1737
  `}`,
1614
1738
  `/**`,
1615
- ` * Events in a transaction's log messages (\`Program data:\` lines, as \`emit\` writes them). Lines are matched by`,
1616
- ` * discriminator, whichever program wrote them; lines that do not decode are skipped. \`emitCpi\` events travel in inner`,
1617
- ` * instructions: see parseCpiEvents and eventsOf.`,
1739
+ ` * Events in a transaction's log messages (\`Program data:\` lines, as \`emit\` writes them). Only this program's lines`,
1740
+ ` * count: the \`Program <id> invoke\` and \`success\` / \`failed\` lines are followed, and a data line is read only while`,
1741
+ ` * this program is the innermost one running (as Anchor's EventParser does). Lines that do not decode are skipped.`,
1742
+ ` * \`emitCpi\` events travel in inner instructions: see eventsOf.`,
1618
1743
  ` */`,
1619
1744
  `export function parseEvents(logs: readonly string[]): Event[] {`,
1620
1745
  ` const found: Event[] = [];`,
1746
+ ` const running: string[] = [];`,
1621
1747
  ` for (const line of logs) {`,
1622
- ` if (!line.startsWith("Program data: ")) continue;`,
1748
+ ` const invoke = /^Program (\\S+) invoke \\[\\d+\\]$/.exec(line);`,
1749
+ ` if (invoke) {`,
1750
+ ` running.push(invoke[1]!);`,
1751
+ ` continue;`,
1752
+ ` }`,
1753
+ ` if (/^Program \\S+ (success$|failed)/.test(line)) {`,
1754
+ ` running.pop();`,
1755
+ ` continue;`,
1756
+ ` }`,
1757
+ ` if (running.at(-1) !== PROGRAM_ADDRESS || !line.startsWith("Program data: ")) continue;`,
1623
1758
  ` let data: ReadonlyUint8Array;`,
1624
1759
  ` try {`,
1625
1760
  ` data = getBase64Encoder().encode(line.slice("Program data: ".length));`,
@@ -1631,7 +1766,10 @@ export function generateClient(idl, opts = {}) {
1631
1766
  ` }`,
1632
1767
  ` return found;`,
1633
1768
  `}`,
1634
- `/** \`emitCpi\` events: the data of inner instructions (bytes, or base58 as RPC returns them) that carry one. */`,
1769
+ `/**`,
1770
+ ` * \`emitCpi\` events in instruction data (bytes, or base58 as RPC returns them). This does not check which program the`,
1771
+ ` * instructions called, so give it only this program's inner instructions; eventsOf picks them from a transaction.`,
1772
+ ` */`,
1635
1773
  `export function parseCpiEvents(instructionData: Iterable<ReadonlyUint8Array | string>): Event[] {`,
1636
1774
  ` const tag = new Uint8Array([228, 69, 165, 46, 81, 203, 154, 29]);`,
1637
1775
  ` const found: Event[] = [];`,
@@ -1648,16 +1786,38 @@ export function generateClient(idl, opts = {}) {
1648
1786
  ` }`,
1649
1787
  ` return found;`,
1650
1788
  `}`,
1651
- `/** Every event of a fetched transaction (getTransaction, json encoding): its logs, then its inner instructions. */`,
1652
- `export function eventsOf(tx: { meta: { logMessages?: readonly string[] | null; innerInstructions?: readonly { instructions: readonly { data: string }[] }[] | null } | null } | null): Event[] {`,
1653
- ` const meta = tx?.meta;`,
1654
- ` if (!meta) return [];`,
1655
- ` return [...parseEvents(meta.logMessages ?? []), ...parseCpiEvents((meta.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))];`,
1789
+ `/** A transaction as getTransaction returns it (json encoding), as far as events need it. */`,
1790
+ `export interface FetchedTransaction {`,
1791
+ ` transaction?: { message: { accountKeys: readonly (string | { pubkey: string })[] } } | null;`,
1792
+ ` meta: {`,
1793
+ ` logMessages?: readonly string[] | null;`,
1794
+ ` innerInstructions?: readonly { instructions: readonly { programIdIndex?: number; data: string }[] }[] | null;`,
1795
+ ` loadedAddresses?: { writable: readonly string[]; readonly: readonly string[] } | null;`,
1796
+ ` } | null;`,
1797
+ `}`,
1798
+ `/** The data of the inner instructions of \`tx\` that call this program (found by their program's account index). */`,
1799
+ `function cpiData(tx: FetchedTransaction): string[] {`,
1800
+ ` const keys = [`,
1801
+ ` ...(tx.transaction?.message.accountKeys ?? []).map((k) => (typeof k === "string" ? k : k.pubkey)),`,
1802
+ ` ...(tx.meta?.loadedAddresses?.writable ?? []),`,
1803
+ ` ...(tx.meta?.loadedAddresses?.readonly ?? []),`,
1804
+ ` ];`,
1805
+ ` return (tx.meta?.innerInstructions ?? []).flatMap((g) =>`,
1806
+ ` g.instructions.filter((i) => i.programIdIndex !== undefined && keys[i.programIdIndex] === PROGRAM_ADDRESS).map((i) => i.data),`,
1807
+ ` );`,
1808
+ `}`,
1809
+ `/**`,
1810
+ ` * Every event of a fetched transaction (getTransaction, json encoding): its log events, then the \`emitCpi\` events of`,
1811
+ ` * its inner instructions that call this program (read only when \`tx.transaction\` gives the account keys).`,
1812
+ ` */`,
1813
+ `export function eventsOf(tx: FetchedTransaction | null): Event[] {`,
1814
+ ` if (!tx?.meta) return [];`,
1815
+ ` return [...parseEvents(tx.meta.logMessages ?? []), ...parseCpiEvents(cpiData(tx))];`,
1656
1816
  `}`,
1657
1817
  `/**`,
1658
- ` * Events of every transaction that mentions the program, as their logs arrive, until \`abortSignal\` aborts. With`,
1659
- ` * \`rpc\`, \`emitCpi\` events too: a transaction in which the program invoked itself is fetched (getTransaction) and its`,
1660
- ` * inner instructions read, after its log events.`,
1818
+ ` * Events of every transaction that mentions the program and succeeded, as their logs arrive, until \`abortSignal\``,
1819
+ ` * aborts (a failed transaction's events did not happen). With \`rpc\`, \`emitCpi\` events too: a transaction in which`,
1820
+ ` * the program invoked itself is fetched (getTransaction) and its inner instructions read, after its log events.`,
1661
1821
  ` */`,
1662
1822
  `export async function* watchEvents(`,
1663
1823
  ` rpcSubscriptions: RpcSubscriptions<LogsNotificationsApi>,`,
@@ -1666,6 +1826,7 @@ export function generateClient(idl, opts = {}) {
1666
1826
  ` const logs = await rpcSubscriptions.logsNotifications({ mentions: [PROGRAM_ADDRESS] }, options.commitment ? { commitment: options.commitment } : {}).subscribe({ abortSignal: options.abortSignal });`,
1667
1827
  ` const selfInvoke = new RegExp("^Program " + PROGRAM_ADDRESS + " invoke \\\\[([2-9]|\\\\d{2,})\\\\]$");`,
1668
1828
  ` for await (const n of logs) {`,
1829
+ ` if (n.value.err !== null) continue;`,
1669
1830
  ` for (const event of parseEvents(n.value.logs)) yield { signature: n.value.signature, event };`,
1670
1831
  ` if (!options.rpc || !n.value.logs.some((l) => selfInvoke.test(l))) continue;`,
1671
1832
  ` // The transaction can trail its logs by a moment: a few tries.`,
@@ -1674,7 +1835,7 @@ export function generateClient(idl, opts = {}) {
1674
1835
  ` .getTransaction(n.value.signature, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: options.commitment === "finalized" ? "finalized" : "confirmed" })`,
1675
1836
  ` .send();`,
1676
1837
  ` if (tx) {`,
1677
- ` for (const event of parseCpiEvents((tx.meta?.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))) yield { signature: n.value.signature, event };`,
1838
+ ` for (const event of parseCpiEvents(cpiData(tx))) yield { signature: n.value.signature, event };`,
1678
1839
  ` break;`,
1679
1840
  ` }`,
1680
1841
  ` await new Promise((resolve) => setTimeout(resolve, 400));`,