@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 +43 -0
- package/README.md +1 -1
- package/dist/clientgen.js +357 -53
- package/dist/compiler/analyze.js +93 -15
- package/dist/compiler/emit/base.d.ts +11 -1
- package/dist/compiler/emit/base.js +21 -0
- package/dist/compiler/emit/calls.d.ts +11 -1
- package/dist/compiler/emit/calls.js +68 -6
- package/dist/compiler/emit/cpi.js +42 -4
- package/dist/compiler/emit/expressions.js +9 -0
- package/dist/compiler/emit/function.js +5 -3
- package/dist/compiler/emit/program.js +9 -2
- package/dist/compiler/emit/shared.d.ts +32 -2
- package/dist/compiler/emit/shared.js +43 -0
- package/dist/compiler/emit/statements.d.ts +35 -0
- package/dist/compiler/emit/statements.js +237 -79
- package/dist/compiler/emit/storage.js +39 -13
- package/dist/compiler/model.d.ts +18 -1
- package/dist/svmrpc.d.ts +26 -0
- package/dist/svmrpc.js +150 -0
- package/dist/testing.d.ts +4 -1
- package/dist/testing.js +3 -0
- package/docs/client.md +14 -12
- package/docs/language.md +9 -9
- package/docs/test-your-program.md +4 -1
- package/lib/typesol.d.ts +8 -0
- package/package.json +1 -1
- package/runtime/typesol_rt.h +19 -0
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.
|
|
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
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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 ${
|
|
847
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
982
|
-
//
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 ||
|
|
1280
|
+
if (!t || t === a || t.optional)
|
|
1037
1281
|
ok = false;
|
|
1038
|
-
else
|
|
1039
|
-
|
|
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
|
|
1045
|
-
const
|
|
1046
|
-
|
|
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:
|
|
1050
|
-
seeds.push(
|
|
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
|
|
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
|
-
|
|
1344
|
+
renote(a, `resolve.${fn} fills it in`);
|
|
1059
1345
|
fieldPdas.push({
|
|
1060
1346
|
a,
|
|
1061
1347
|
usesArgs,
|
|
1062
1348
|
lines: [
|
|
1063
|
-
`
|
|
1064
|
-
` ${
|
|
1065
|
-
` (await (
|
|
1066
|
-
|
|
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}
|
|
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
|
-
...
|
|
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
|
|
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",
|