@typesol/cli 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/dist/cli.js +29 -6
- package/dist/clientgen.js +472 -14
- package/dist/idlpublish.d.ts +12 -0
- package/dist/idlpublish.js +38 -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/cli.md +1 -1
- package/docs/client.md +21 -11
- package/docs/test-your-program.md +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ until 1.0, a minor version may change the language, the IDL layout or error code
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.22.0 — 2026-10-28
|
|
9
|
+
|
|
10
|
+
Generated clients fill in the accounts Anchor's client still asked you to pass (signers from a default signer, PDAs
|
|
11
|
+
seeded from string, byte and argument fields, PDAs under a program given as an argument, a helper for PDAs seeded from
|
|
12
|
+
their own fields), read Anchor zero-copy accounts and generic types, and run in tests over LiteSVM. Programs and their
|
|
13
|
+
bytes are unchanged. Not externally audited (see SECURITY.md).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Generated clients fill in the accounts Anchor's client still asks for:
|
|
17
|
+
- `resolve.<instruction>(rpc, accounts, args?, { signer })` fills signer accounts that were left out with a default
|
|
18
|
+
signer (the wallet), as Anchor's client uses the provider's wallet.
|
|
19
|
+
- PDAs seeded from another account's string, byte and `FixedString` fields are derived by `resolve`, as scalar fields
|
|
20
|
+
already were.
|
|
21
|
+
- A PDA seeded from its own fields gets a `pdas.<name>({ field values })` helper (its address is needed to read it).
|
|
22
|
+
- A PDA whose program is a `Pubkey` argument is derived by the instruction builder.
|
|
23
|
+
- Clients generated from Anchor IDLs read and write zero-copy (`bytemuck`, `AccountLoader`) accounts at their
|
|
24
|
+
`#[repr(C)]` offsets on SBF (padding, `packed` and `align` included), checked byte for byte against an Anchor
|
|
25
|
+
program, and turn generic types into one concrete type per use (`Pair<u64, 2>` → `PairU64N2`).
|
|
26
|
+
- PDAs seeded from fields of a struct argument (`seeds = [params.id]`) are derived by the instruction builder.
|
|
27
|
+
- `typesol/testing`: `svmConnection(svm)` (and `t.connection`), a kit RPC and subscriptions pair answered by LiteSVM,
|
|
28
|
+
so a generated client sends, fetches, simulates, resolves and watches in tests as it does against a cluster.
|
|
29
|
+
|
|
30
|
+
## 0.21.0 — 2026-10-28
|
|
31
|
+
|
|
32
|
+
Clients from a program's address alone, and the last pieces Anchor's client resolves for you: `has_one` accounts,
|
|
33
|
+
PDAs seeded from another account's fields, and `emitCpi` events as they happen. Programs and their bytes are
|
|
34
|
+
unchanged. Not externally audited (see SECURITY.md).
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `typesol client --address <program id> --url <rpc>`: a client from the IDL on chain, the canonical Program Metadata
|
|
38
|
+
IDL (`typesol idl publish`, Codama) or else Anchor's IDL account (`anchor idl init`).
|
|
39
|
+
- Generated clients: `resolve.<instruction>(rpc, accounts)` fills in `has_one` accounts that do not sign from the
|
|
40
|
+
account that names them, as Anchor's client resolves `relations`, and PDAs seeded from another account's fields
|
|
41
|
+
(`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived).
|
|
42
|
+
- `watchEvents(rpcSubscriptions, { abortSignal, rpc })`: with an RPC, `emitCpi` events arrive live too (the transaction
|
|
43
|
+
is fetched when the program invoked itself).
|
|
44
|
+
|
|
8
45
|
## 0.20.0 — 2026-10-28
|
|
9
46
|
|
|
10
47
|
The generated TypeScript clients catch up with Anchor's: remaining accounts, listing and watching accounts, sending
|
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.22).** Releases follow the organization's
|
|
310
310
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
311
311
|
change.
|
|
312
312
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
package/dist/cli.js
CHANGED
|
@@ -277,14 +277,37 @@ const COMMANDS = {
|
|
|
277
277
|
},
|
|
278
278
|
},
|
|
279
279
|
client: {
|
|
280
|
-
usage: "typesol client <idl.json> [--out <file.ts>] [--address <base58>]",
|
|
281
|
-
help:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
usage: "typesol client <idl.json> [--out <file.ts>] [--address <base58>]\n typesol client --address <program id> [--url <rpc>] [--config <solana cli config>] [--out <file.ts>]",
|
|
281
|
+
help: [
|
|
282
|
+
"Generate a typed TypeScript client (on @solana/kit) from an Anchor IDL (0.30+/1.x or TypeSOL). The IDL is validated first;",
|
|
283
|
+
"what the client leaves out is printed as notes. `typesol build` already writes <name>.client.ts for TypeSOL programs.",
|
|
284
|
+
"Without an IDL file, the IDL is read from the chain for the program at --address: the canonical Program Metadata IDL",
|
|
285
|
+
"(`typesol idl publish`, Codama), else Anchor's IDL account (`anchor idl init`).",
|
|
286
|
+
].join("\n "),
|
|
287
|
+
options: { out: { type: "string" }, address: { type: "string" }, url: { type: "string" }, config: { type: "string" } },
|
|
288
|
+
async run(v, args) {
|
|
285
289
|
if (v.address && !decodeAddress(v.address))
|
|
286
290
|
throw new UsageError(`--address "${v.address}" is not a base58 Solana address`);
|
|
287
|
-
|
|
291
|
+
let idl;
|
|
292
|
+
if (args.length === 0) {
|
|
293
|
+
if (!v.address)
|
|
294
|
+
throw new UsageError("pass an IDL file, or --address to read the program's IDL from the chain");
|
|
295
|
+
const rpcUrl = v.url ?? solanaConfig(v.config).url;
|
|
296
|
+
if (!rpcUrl)
|
|
297
|
+
throw new Error("no RPC: pass --url, or set json_rpc_url in the Solana CLI config");
|
|
298
|
+
// Loaded only here: the other commands do not need an RPC client.
|
|
299
|
+
const kit = await import("@solana/kit");
|
|
300
|
+
const { fetchProgramIdl } = await import("./idlpublish.js");
|
|
301
|
+
const found = await fetchProgramIdl(kit.createSolanaRpc(clusterUrl(rpcUrl)), kit.address(v.address));
|
|
302
|
+
console.error(` IDL from ${found.source === "anchor" ? "Anchor's IDL account" : "the Program Metadata program"}`);
|
|
303
|
+
idl = found.idl;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
if (v.url)
|
|
307
|
+
throw new UsageError("--url reads the IDL from the chain: leave out the IDL file");
|
|
308
|
+
idl = readJson(one(args, "<idl.json>"));
|
|
309
|
+
}
|
|
310
|
+
const res = generateClient(idl, { address: v.address });
|
|
288
311
|
if (v.out) {
|
|
289
312
|
writeFileSync(v.out, res.source);
|
|
290
313
|
log(` wrote ${v.out}`);
|
package/dist/clientgen.js
CHANGED
|
@@ -180,6 +180,7 @@ const KIT_TYPES = [
|
|
|
180
180
|
"GetMultipleAccountsApi",
|
|
181
181
|
"GetProgramAccountsApi",
|
|
182
182
|
"GetSignatureStatusesApi",
|
|
183
|
+
"GetTransactionApi",
|
|
183
184
|
"LogsNotificationsApi",
|
|
184
185
|
"RpcSubscriptions",
|
|
185
186
|
"SendTransactionApi",
|
|
@@ -257,6 +258,7 @@ const PROGRAM_KEYS = new Set([
|
|
|
257
258
|
"pdas",
|
|
258
259
|
"returns",
|
|
259
260
|
"views",
|
|
261
|
+
"resolve",
|
|
260
262
|
"errors",
|
|
261
263
|
"errorCode",
|
|
262
264
|
"isError",
|
|
@@ -417,9 +419,123 @@ class Names {
|
|
|
417
419
|
}
|
|
418
420
|
}
|
|
419
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
|
+
}
|
|
420
535
|
export function generateClient(idl, opts = {}) {
|
|
421
536
|
const programAddress = opts.address ?? idl.address;
|
|
422
537
|
validate(idl, programAddress);
|
|
538
|
+
idl = monomorphize(idl);
|
|
423
539
|
if (!programAddress)
|
|
424
540
|
throw new ClientIdlError("the IDL has no program address; pass one");
|
|
425
541
|
const typesol = idl.metadata?.description === "Built with TypeSOL";
|
|
@@ -446,6 +562,70 @@ export function generateClient(idl, opts = {}) {
|
|
|
446
562
|
return { kind: "fenum", base: n.slice("FixedEnum_".length), size: 1 + arrayLen(f[1].type) };
|
|
447
563
|
return undefined;
|
|
448
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
|
+
}
|
|
449
629
|
// ---- What can be expressed --------------------------------------------------------------------------------
|
|
450
630
|
const typeIssues = new Map();
|
|
451
631
|
/** Why a type cannot be generated, or undefined when it can. */
|
|
@@ -481,9 +661,14 @@ export function generateClient(idl, opts = {}) {
|
|
|
481
661
|
if (!d)
|
|
482
662
|
why = `type ${n} is not defined in the IDL`;
|
|
483
663
|
else if (d.generics?.length)
|
|
484
|
-
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
|
+
}
|
|
485
670
|
else if (d.serialization && d.serialization !== "borsh")
|
|
486
|
-
why = `type ${n} uses ${d.serialization} serialization
|
|
671
|
+
why = `type ${n} uses ${d.serialization} serialization, which is not supported`;
|
|
487
672
|
else if (special(n)?.kind === "foption")
|
|
488
673
|
why = issue(special(n).inner);
|
|
489
674
|
else if (d.type.kind === "struct")
|
|
@@ -545,6 +730,7 @@ export function generateClient(idl, opts = {}) {
|
|
|
545
730
|
let needsFixedOption = false;
|
|
546
731
|
let needsFixedVec = false;
|
|
547
732
|
let needsUnitEnum = false;
|
|
733
|
+
let needsCStruct = false;
|
|
548
734
|
function codec(t) {
|
|
549
735
|
if (typeof t === "string") {
|
|
550
736
|
if (t in INTS || t in FLOATS) {
|
|
@@ -638,6 +824,10 @@ export function generateClient(idl, opts = {}) {
|
|
|
638
824
|
const d = defs.get(n);
|
|
639
825
|
if (!d)
|
|
640
826
|
return undefined;
|
|
827
|
+
if (zeroCopy(d)) {
|
|
828
|
+
const l = structLayout(d);
|
|
829
|
+
return typeof l === "string" ? undefined : l.size;
|
|
830
|
+
}
|
|
641
831
|
if (isUnitEnum(d))
|
|
642
832
|
return 1;
|
|
643
833
|
if (d.type.kind !== "struct")
|
|
@@ -674,6 +864,16 @@ export function generateClient(idl, opts = {}) {
|
|
|
674
864
|
continue;
|
|
675
865
|
}
|
|
676
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
|
+
}
|
|
677
877
|
use("getStructCodec");
|
|
678
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` +
|
|
679
879
|
`export interface ${T}Args {\n${named.map((f) => ` ${prop(camel(f.name))}: ${tsType(f.type, "in")};`).join("\n")}\n}\n` +
|
|
@@ -723,6 +923,8 @@ export function generateClient(idl, opts = {}) {
|
|
|
723
923
|
// ---- Accounts ---------------------------------------------------------------------------------------------
|
|
724
924
|
const accountOut = [];
|
|
725
925
|
const accountKeys = [];
|
|
926
|
+
/** Each account's decoder object and its Pubkey fields (snake_case), for has_one relations. */
|
|
927
|
+
const accountConsts = [];
|
|
726
928
|
for (const a of idl.accounts ?? []) {
|
|
727
929
|
if (!defs.has(a.name) || typeIssue(a.name) || special(a.name)) {
|
|
728
930
|
notes.push(`account ${a.name}: ${typeIssue(a.name) ?? "no type definition"}; it is left out`);
|
|
@@ -771,6 +973,8 @@ export function generateClient(idl, opts = {}) {
|
|
|
771
973
|
` },\n` +
|
|
772
974
|
`};\n`);
|
|
773
975
|
accountKeys.push(`${prop(a.name)}: ${A}`);
|
|
976
|
+
const fields = (defs.get(a.name).type.fields ?? []).filter((f) => typeof f === "object" && f !== null && "name" in f);
|
|
977
|
+
accountConsts.push({ name: a.name, A, pubkeys: new Set(fields.filter((f) => f.type === "pubkey" || f.type === "publicKey").map((f) => f.name)) });
|
|
774
978
|
}
|
|
775
979
|
// ---- Events -----------------------------------------------------------------------------------------------
|
|
776
980
|
const events = (idl.events ?? []).filter((e) => {
|
|
@@ -795,6 +999,7 @@ export function generateClient(idl, opts = {}) {
|
|
|
795
999
|
const pdaFns = new Map();
|
|
796
1000
|
const returnKeys = [];
|
|
797
1001
|
const viewKeys = [];
|
|
1002
|
+
const resolveKeys = [];
|
|
798
1003
|
const usedKeys = new Set();
|
|
799
1004
|
for (const ix of idl.instructions) {
|
|
800
1005
|
const argIssue = ix.args.map((a) => issue(a.type)).find(Boolean);
|
|
@@ -832,12 +1037,20 @@ export function generateClient(idl, opts = {}) {
|
|
|
832
1037
|
if (s.kind === "const")
|
|
833
1038
|
return bytes(s.value);
|
|
834
1039
|
if (s.kind === "arg") {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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);
|
|
838
1043
|
if (t === undefined)
|
|
839
|
-
return { why: `the seed argument ${
|
|
840
|
-
|
|
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
|
+
}
|
|
841
1054
|
if (t === "string")
|
|
842
1055
|
return (use("getUtf8Encoder"), `getUtf8Encoder().encode(${v})`);
|
|
843
1056
|
if (t === "bytes" || (typeof t === "object" && t.vec !== undefined && isU8(t.vec)))
|
|
@@ -867,8 +1080,10 @@ export function generateClient(idl, opts = {}) {
|
|
|
867
1080
|
const target = p.path.includes(".") ? undefined : byPath(p.path, a.group);
|
|
868
1081
|
program = target ? `key($${target.key})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
|
|
869
1082
|
}
|
|
870
|
-
else if (p?.kind === "arg")
|
|
871
|
-
|
|
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
|
+
}
|
|
872
1087
|
const why = [...seeds, ...(program === undefined ? [] : [program])].find((x) => typeof x === "object");
|
|
873
1088
|
if (why) {
|
|
874
1089
|
notes.push(`instruction ${ix.name}: ${a.name} is not filled in (${why.why}); pass it`);
|
|
@@ -935,8 +1150,10 @@ export function generateClient(idl, opts = {}) {
|
|
|
935
1150
|
const inputs = [...a.pda.seeds, ...(a.pda.program ? [a.pda.program] : [])].filter((s) => s.kind !== "const");
|
|
936
1151
|
const params = [
|
|
937
1152
|
...new Map(inputs.map((s) => {
|
|
938
|
-
if (s.kind === "arg")
|
|
939
|
-
|
|
1153
|
+
if (s.kind === "arg") {
|
|
1154
|
+
const head = s.path.split(".")[0];
|
|
1155
|
+
return [camel(head), `${camel(head)}: ${tsType(argType.get(head), "in")}`];
|
|
1156
|
+
}
|
|
940
1157
|
const k = byPath(s.path, a.group).key;
|
|
941
1158
|
return [k, `${k}: Address | TransactionSigner`];
|
|
942
1159
|
})).values(),
|
|
@@ -971,6 +1188,190 @@ export function generateClient(idl, opts = {}) {
|
|
|
971
1188
|
lines.join("\n") +
|
|
972
1189
|
`\n return {\n programAddress: PROGRAM_ADDRESS,\n accounts: [\n${metas.join("\n")}\n ...remaining,\n ],\n data: ${data},\n } as Instruction;\n}\n`);
|
|
973
1190
|
ixKeys.push(`${prop(fn)}: build${P}`);
|
|
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);
|
|
1223
|
+
const rels = [];
|
|
1224
|
+
for (const a of flat) {
|
|
1225
|
+
if (auto(a) || a.optional || a.signer || !a.relations?.length)
|
|
1226
|
+
continue;
|
|
1227
|
+
for (const r of a.relations) {
|
|
1228
|
+
const source = byPath(r, a.group);
|
|
1229
|
+
if (!source || source.optional)
|
|
1230
|
+
continue;
|
|
1231
|
+
const readers = accountConsts
|
|
1232
|
+
.filter((c) => c.pubkeys.has(a.name))
|
|
1233
|
+
.map((c) => `(d) => (hasPrefix(d, ${c.A}.discriminator) ? ${c.A}.decode(d)[${JSON.stringify(camel(a.name))}] : undefined)`);
|
|
1234
|
+
if (!readers.length)
|
|
1235
|
+
continue;
|
|
1236
|
+
rels.push({ a, source, readers });
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const fieldPdas = [];
|
|
1241
|
+
for (const a of flat) {
|
|
1242
|
+
if (!a.pda || a.address || resolvable.has(a.key) || a.optional)
|
|
1243
|
+
continue;
|
|
1244
|
+
const p = a.pda.program;
|
|
1245
|
+
if (p && p.kind === "account")
|
|
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
|
+
}
|
|
1256
|
+
const bases = new Map();
|
|
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;
|
|
1262
|
+
let ok = true;
|
|
1263
|
+
let usesArgs = p?.kind === "arg";
|
|
1264
|
+
for (const sd of a.pda.seeds) {
|
|
1265
|
+
if (sd.kind === "const")
|
|
1266
|
+
seeds.push(bytes(sd.value));
|
|
1267
|
+
else if (sd.kind === "arg") {
|
|
1268
|
+
const e = seedExpr(sd, a.group);
|
|
1269
|
+
if (typeof e !== "string")
|
|
1270
|
+
ok = false;
|
|
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
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
else if (!sd.path.includes(".")) {
|
|
1279
|
+
const t = byPath(sd.path, a.group);
|
|
1280
|
+
if (!t || t === a || t.optional)
|
|
1281
|
+
ok = false;
|
|
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
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
const parts = sd.path.split(".");
|
|
1292
|
+
const base = parts.length === 2 ? byPath(parts[0], a.group) : undefined;
|
|
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))
|
|
1304
|
+
ok = false;
|
|
1305
|
+
else {
|
|
1306
|
+
bases.set(base.key, { base, A: consts.A });
|
|
1307
|
+
seeds.push(enc);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!ok)
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
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)
|
|
1343
|
+
continue;
|
|
1344
|
+
renote(a, `resolve.${fn} fills it in`);
|
|
1345
|
+
fieldPdas.push({
|
|
1346
|
+
a,
|
|
1347
|
+
usesArgs,
|
|
1348
|
+
lines: [
|
|
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
|
+
` }`,
|
|
1353
|
+
],
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
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(" | ");
|
|
1359
|
+
const withArgs = fieldPdas.some((f) => f.usesArgs);
|
|
1360
|
+
const what = [
|
|
1361
|
+
...(signers.length ? [`${signers.map((a) => a.key).join(", ")} (options.signer when left out)`] : []),
|
|
1362
|
+
...rels.map((r) => `${r.a.key} (from ${r.source.key}.${camel(r.a.name)})`),
|
|
1363
|
+
...fieldPdas.map((f) => `${f.a.key} (a PDA seeded from account fields)`),
|
|
1364
|
+
].join(", ");
|
|
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` +
|
|
1368
|
+
[
|
|
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(", ")}]);`),
|
|
1371
|
+
...fieldPdas.flatMap((f) => f.lines),
|
|
1372
|
+
].join("\n") +
|
|
1373
|
+
`\n void rpc;\n return acc as ${P}Accounts;\n },`);
|
|
1374
|
+
}
|
|
974
1375
|
if (ix.returns !== undefined) {
|
|
975
1376
|
const why = issue(ix.returns);
|
|
976
1377
|
if (why)
|
|
@@ -1041,6 +1442,21 @@ export function generateClient(idl, opts = {}) {
|
|
|
1041
1442
|
],
|
|
1042
1443
|
},
|
|
1043
1444
|
{ name: "programFromBytes", lines: [`const programFromBytes = (b: Uint8Array): Address => getAddressDecoder().decode(b);`] },
|
|
1445
|
+
{
|
|
1446
|
+
name: "readRelation",
|
|
1447
|
+
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> => {`,
|
|
1450
|
+
` const found = await fetchEncodedAccount(rpc, at);`,
|
|
1451
|
+
` if (!found.exists) throw new Error("cannot fill in " + name + ": account " + at + " does not exist");`,
|
|
1452
|
+
` for (const read of readers) {`,
|
|
1453
|
+
` const v = read(found.data);`,
|
|
1454
|
+
` if (v !== undefined) return v;`,
|
|
1455
|
+
` }`,
|
|
1456
|
+
` throw new Error("cannot fill in " + name + ": " + at + " is not an account that names it");`,
|
|
1457
|
+
`};`,
|
|
1458
|
+
],
|
|
1459
|
+
},
|
|
1044
1460
|
{
|
|
1045
1461
|
name: "memcmp",
|
|
1046
1462
|
lines: [
|
|
@@ -1108,6 +1524,23 @@ export function generateClient(idl, opts = {}) {
|
|
|
1108
1524
|
` });`,
|
|
1109
1525
|
],
|
|
1110
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
|
+
});
|
|
1111
1544
|
if (needsUnitEnum)
|
|
1112
1545
|
helperDefs.push({
|
|
1113
1546
|
name: "unitEnum",
|
|
@@ -1221,10 +1654,32 @@ export function generateClient(idl, opts = {}) {
|
|
|
1221
1654
|
` if (!meta) return [];`,
|
|
1222
1655
|
` return [...parseEvents(meta.logMessages ?? []), ...parseCpiEvents((meta.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))];`,
|
|
1223
1656
|
`}`,
|
|
1224
|
-
|
|
1225
|
-
`
|
|
1657
|
+
`/**`,
|
|
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.`,
|
|
1661
|
+
` */`,
|
|
1662
|
+
`export async function* watchEvents(`,
|
|
1663
|
+
` rpcSubscriptions: RpcSubscriptions<LogsNotificationsApi>,`,
|
|
1664
|
+
` options: { abortSignal: AbortSignal; commitment?: Commitment; rpc?: Rpc<GetTransactionApi> },`,
|
|
1665
|
+
`): AsyncGenerator<{ signature: Signature; event: Event }> {`,
|
|
1226
1666
|
` const logs = await rpcSubscriptions.logsNotifications({ mentions: [PROGRAM_ADDRESS] }, options.commitment ? { commitment: options.commitment } : {}).subscribe({ abortSignal: options.abortSignal });`,
|
|
1227
|
-
`
|
|
1667
|
+
` const selfInvoke = new RegExp("^Program " + PROGRAM_ADDRESS + " invoke \\\\[([2-9]|\\\\d{2,})\\\\]$");`,
|
|
1668
|
+
` for await (const n of logs) {`,
|
|
1669
|
+
` for (const event of parseEvents(n.value.logs)) yield { signature: n.value.signature, event };`,
|
|
1670
|
+
` if (!options.rpc || !n.value.logs.some((l) => selfInvoke.test(l))) continue;`,
|
|
1671
|
+
` // The transaction can trail its logs by a moment: a few tries.`,
|
|
1672
|
+
` for (let attempt = 0; attempt < 5 && !options.abortSignal.aborted; attempt++) {`,
|
|
1673
|
+
` const tx = await options.rpc`,
|
|
1674
|
+
` .getTransaction(n.value.signature, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: options.commitment === "finalized" ? "finalized" : "confirmed" })`,
|
|
1675
|
+
` .send();`,
|
|
1676
|
+
` 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 };`,
|
|
1678
|
+
` break;`,
|
|
1679
|
+
` }`,
|
|
1680
|
+
` await new Promise((resolve) => setTimeout(resolve, 400));`,
|
|
1681
|
+
` }`,
|
|
1682
|
+
` }`,
|
|
1228
1683
|
`}`,
|
|
1229
1684
|
"",
|
|
1230
1685
|
`// ---- Transactions ----`,
|
|
@@ -1285,6 +1740,9 @@ export function generateClient(idl, opts = {}) {
|
|
|
1285
1740
|
` views: {`,
|
|
1286
1741
|
...viewKeys,
|
|
1287
1742
|
` },`,
|
|
1743
|
+
` resolve: {`,
|
|
1744
|
+
...resolveKeys,
|
|
1745
|
+
` },`,
|
|
1288
1746
|
` errors: ERRORS,`,
|
|
1289
1747
|
` errorCode,`,
|
|
1290
1748
|
` isError,`,
|
package/dist/idlpublish.d.ts
CHANGED
|
@@ -27,3 +27,15 @@ export declare function publishIdl(chain: Chain, opts: PublishOptions): Promise<
|
|
|
27
27
|
export declare function metadataAddress(program: Address, authority: Address | null): Promise<Address>;
|
|
28
28
|
/** Read an IDL back (canonical, or the one `authority` wrote). */
|
|
29
29
|
export declare function fetchIdl(rpc: Chain["rpc"], program: Address, authority?: Address | null): Promise<object>;
|
|
30
|
+
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
31
|
+
export declare function anchorIdlAddress(program: Address): Promise<Address>;
|
|
32
|
+
/** The IDL in Anchor's IDL account (`anchor idl init`), or null when the program has none. */
|
|
33
|
+
export declare function fetchAnchorIdl(rpc: Chain["rpc"], program: Address): Promise<object | null>;
|
|
34
|
+
/**
|
|
35
|
+
* A program's IDL from the chain: the canonical Program Metadata IDL (`typesol idl publish`, Codama) first, else
|
|
36
|
+
* Anchor's IDL account (`anchor idl init`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function fetchProgramIdl(rpc: Chain["rpc"], program: Address): Promise<{
|
|
39
|
+
idl: object;
|
|
40
|
+
source: "program-metadata" | "anchor";
|
|
41
|
+
}>;
|
package/dist/idlpublish.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Works against an RPC (`rpcChain`) or anything else that can read accounts and send transactions (tests use LiteSVM).
|
|
7
7
|
*/
|
|
8
|
+
import { inflateSync } from "node:zlib";
|
|
8
9
|
import { Compression, Encoding, Format, fetchMetadataContent, findMetadataPda, packDirectData, writeMetadata } from "@solana-program/program-metadata";
|
|
9
|
-
import { createClient, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, extendClient, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit";
|
|
10
|
+
import { createAddressWithSeed, createClient, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, extendClient, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit";
|
|
10
11
|
export const PROGRAM_METADATA_ID = "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S";
|
|
11
12
|
/** A Chain over an RPC endpoint (polls signature status: no websocket needed). */
|
|
12
13
|
export function rpcChain(rpc) {
|
|
@@ -114,3 +115,39 @@ export async function metadataAddress(program, authority) {
|
|
|
114
115
|
export async function fetchIdl(rpc, program, authority = null) {
|
|
115
116
|
return JSON.parse(await fetchMetadataContent(rpc, program, "idl", authority));
|
|
116
117
|
}
|
|
118
|
+
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
119
|
+
export async function anchorIdlAddress(program) {
|
|
120
|
+
const [base] = await getProgramDerivedAddress({ programAddress: program, seeds: [] });
|
|
121
|
+
return createAddressWithSeed({ baseAddress: base, programAddress: program, seed: "anchor:idl" });
|
|
122
|
+
}
|
|
123
|
+
/** The IDL in Anchor's IDL account (`anchor idl init`), or null when the program has none. */
|
|
124
|
+
export async function fetchAnchorIdl(rpc, program) {
|
|
125
|
+
const { value } = await rpc.getAccountInfo(await anchorIdlAddress(program), { encoding: "base64" }).send();
|
|
126
|
+
if (!value)
|
|
127
|
+
return null;
|
|
128
|
+
const data = Buffer.from(value.data[0], "base64");
|
|
129
|
+
// IdlAccount: 8-byte discriminator, authority (32 bytes), a u32 length, then the zlib-compressed JSON.
|
|
130
|
+
if (data.length < 44)
|
|
131
|
+
throw new Error("Anchor IDL account is too short");
|
|
132
|
+
const len = data.readUInt32LE(40);
|
|
133
|
+
if (44 + len > data.length)
|
|
134
|
+
throw new Error("Anchor IDL account is truncated");
|
|
135
|
+
return JSON.parse(inflateSync(data.subarray(44, 44 + len)).toString("utf8"));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A program's IDL from the chain: the canonical Program Metadata IDL (`typesol idl publish`, Codama) first, else
|
|
139
|
+
* Anchor's IDL account (`anchor idl init`).
|
|
140
|
+
*/
|
|
141
|
+
export async function fetchProgramIdl(rpc, program) {
|
|
142
|
+
let metadataError;
|
|
143
|
+
try {
|
|
144
|
+
return { idl: await fetchIdl(rpc, program), source: "program-metadata" };
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
metadataError = e;
|
|
148
|
+
}
|
|
149
|
+
const anchor = await fetchAnchorIdl(rpc, program);
|
|
150
|
+
if (anchor)
|
|
151
|
+
return { idl: anchor, source: "anchor" };
|
|
152
|
+
throw new Error(`no IDL on chain for ${program}: none in the Program Metadata program (${metadataError instanceof Error ? metadataError.message : String(metadataError)}) and no Anchor IDL account at ${await anchorIdlAddress(program)}`);
|
|
153
|
+
}
|
package/dist/svmrpc.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests as they run against a
|
|
3
|
+
* cluster: `sendInstructions`, `simulate`, `views`, `accounts.<Name>.fetch*` and `watch`, `resolve`, `eventsOf` and
|
|
4
|
+
* `watchEvents`. Only the methods those helpers call are answered.
|
|
5
|
+
*
|
|
6
|
+
* const { rpc, rpcSubscriptions } = svmConnection(svm);
|
|
7
|
+
* await counter.sendInstructions({ rpc, rpcSubscriptions }, alice, [await counter.initialize({ authority: alice })]);
|
|
8
|
+
* await counter.accounts.Counter.fetch(rpc, address);
|
|
9
|
+
*
|
|
10
|
+
* Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
|
|
11
|
+
* the accounts transactions sent through this connection named, plus those passed to `track`.
|
|
12
|
+
*/
|
|
13
|
+
import { type Address, type Rpc, type RpcSubscriptions, type SolanaRpcApi, type SolanaRpcSubscriptionsApi } from "@solana/kit";
|
|
14
|
+
import { type LiteSVM, type TransactionMetadata } from "litesvm";
|
|
15
|
+
export interface SvmConnection {
|
|
16
|
+
rpc: Rpc<SolanaRpcApi>;
|
|
17
|
+
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
|
|
18
|
+
/** Every transaction sent through `rpc` that succeeded, in order. */
|
|
19
|
+
transactions: {
|
|
20
|
+
signature: string;
|
|
21
|
+
meta: TransactionMetadata;
|
|
22
|
+
}[];
|
|
23
|
+
/** Add accounts for `getProgramAccounts` to scan (accounts the transactions named are scanned already). */
|
|
24
|
+
track(...addresses: Address[]): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function svmConnection(svm: LiteSVM): SvmConnection;
|
package/dist/svmrpc.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests as they run against a
|
|
3
|
+
* cluster: `sendInstructions`, `simulate`, `views`, `accounts.<Name>.fetch*` and `watch`, `resolve`, `eventsOf` and
|
|
4
|
+
* `watchEvents`. Only the methods those helpers call are answered.
|
|
5
|
+
*
|
|
6
|
+
* const { rpc, rpcSubscriptions } = svmConnection(svm);
|
|
7
|
+
* await counter.sendInstructions({ rpc, rpcSubscriptions }, alice, [await counter.initialize({ authority: alice })]);
|
|
8
|
+
* await counter.accounts.Counter.fetch(rpc, address);
|
|
9
|
+
*
|
|
10
|
+
* Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
|
|
11
|
+
* the accounts transactions sent through this connection named, plus those passed to `track`.
|
|
12
|
+
*/
|
|
13
|
+
import { getAddressDecoder, getBase58Decoder, getBase58Encoder, getBase64Decoder, getBase64Encoder, getCompiledTransactionMessageDecoder, getTransactionDecoder, } from "@solana/kit";
|
|
14
|
+
import { FailedTransactionMetadata } from "litesvm";
|
|
15
|
+
const b64 = (bytes) => getBase64Decoder().decode(bytes);
|
|
16
|
+
const reply = (value) => ({ send: async () => value });
|
|
17
|
+
export function svmConnection(svm) {
|
|
18
|
+
const transactions = [];
|
|
19
|
+
const known = new Set();
|
|
20
|
+
const decode = (wire) => {
|
|
21
|
+
const tx = getTransactionDecoder().decode(getBase64Encoder().encode(wire));
|
|
22
|
+
return { tx, keys: getCompiledTransactionMessageDecoder().decode(tx.messageBytes).staticAccounts };
|
|
23
|
+
};
|
|
24
|
+
const encoded = (a) => {
|
|
25
|
+
const acct = svm.getAccount(a);
|
|
26
|
+
if (!acct.exists)
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
data: [b64(acct.data), "base64"],
|
|
30
|
+
executable: acct.executable,
|
|
31
|
+
lamports: acct.lamports,
|
|
32
|
+
owner: acct.programAddress,
|
|
33
|
+
space: BigInt(acct.data.length),
|
|
34
|
+
rentEpoch: 0n,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const rpc = {
|
|
38
|
+
getLatestBlockhash: () => reply({ context: { slot: 0n }, value: { blockhash: svm.latestBlockhash(), lastValidBlockHeight: 1000000n } }),
|
|
39
|
+
getEpochInfo: () => reply({ absoluteSlot: 0n, blockHeight: 0n, epoch: 0n, slotIndex: 0n, slotsInEpoch: 432000n, transactionCount: null }),
|
|
40
|
+
getSignatureStatuses: (sigs) => reply({
|
|
41
|
+
context: { slot: 0n },
|
|
42
|
+
value: sigs.map((s) => transactions.some((t) => t.signature === s)
|
|
43
|
+
? { confirmationStatus: "confirmed", confirmations: null, err: null, slot: 0n, status: { Ok: null } }
|
|
44
|
+
: null),
|
|
45
|
+
}),
|
|
46
|
+
getMinimumBalanceForRentExemption: (space) => reply(svm.minimumBalanceForRentExemption(space)),
|
|
47
|
+
getBalance: (a) => reply({ context: { slot: 0n }, value: svm.getBalance(a) ?? 0n }),
|
|
48
|
+
sendTransaction: (wire) => ({
|
|
49
|
+
send: async () => {
|
|
50
|
+
const { tx, keys } = decode(wire);
|
|
51
|
+
const res = svm.sendTransaction(tx);
|
|
52
|
+
svm.expireBlockhash();
|
|
53
|
+
if (res instanceof FailedTransactionMetadata)
|
|
54
|
+
throw new Error(`transaction failed: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
|
|
55
|
+
for (const k of keys)
|
|
56
|
+
known.add(k);
|
|
57
|
+
const signature = getBase58Decoder().decode(res.signature());
|
|
58
|
+
transactions.push({ signature, meta: res });
|
|
59
|
+
return signature;
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
simulateTransaction: (wire, config = {}) => ({
|
|
63
|
+
send: async () => {
|
|
64
|
+
// Simulations are usually unsigned (sigVerify: false): turn LiteSVM's check off for this one only.
|
|
65
|
+
const inner = svm.inner;
|
|
66
|
+
const restore = config.sigVerify === false && inner?.getSigverify?.() === true;
|
|
67
|
+
if (restore)
|
|
68
|
+
svm.withSigverify(false);
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
res = svm.simulateTransaction(decode(wire).tx);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (restore)
|
|
75
|
+
svm.withSigverify(true);
|
|
76
|
+
}
|
|
77
|
+
if (res instanceof FailedTransactionMetadata)
|
|
78
|
+
return {
|
|
79
|
+
context: { slot: 0n },
|
|
80
|
+
value: { err: String(res.err()), logs: res.meta().logs(), unitsConsumed: res.meta().computeUnitsConsumed(), returnData: null },
|
|
81
|
+
};
|
|
82
|
+
const meta = res.meta();
|
|
83
|
+
const ret = meta.returnData();
|
|
84
|
+
return {
|
|
85
|
+
context: { slot: 0n },
|
|
86
|
+
value: {
|
|
87
|
+
err: null,
|
|
88
|
+
logs: meta.logs(),
|
|
89
|
+
unitsConsumed: meta.computeUnitsConsumed(),
|
|
90
|
+
returnData: ret.data().length ? { programId: getAddressDecoder().decode(ret.programId()), data: [b64(ret.data()), "base64"] } : null,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
getAccountInfo: (a) => reply({ context: { slot: 0n }, value: encoded(a) }),
|
|
96
|
+
getMultipleAccounts: (addresses) => reply({ context: { slot: 0n }, value: addresses.map(encoded) }),
|
|
97
|
+
getProgramAccounts: (program, config = {}) => reply([...known]
|
|
98
|
+
.map((a) => ({ pubkey: a, account: encoded(a) }))
|
|
99
|
+
.filter(({ account }) => account && account.owner === program)
|
|
100
|
+
.filter(({ account }) => {
|
|
101
|
+
const data = getBase64Encoder().encode(account.data[0]);
|
|
102
|
+
return (config.filters ?? []).every(({ memcmp, dataSize }) => {
|
|
103
|
+
if (dataSize !== undefined)
|
|
104
|
+
return BigInt(data.length) === BigInt(dataSize);
|
|
105
|
+
if (!memcmp)
|
|
106
|
+
return true;
|
|
107
|
+
const want = getBase58Encoder().encode(memcmp.bytes);
|
|
108
|
+
return want.every((b, i) => data[Number(memcmp.offset) + i] === b);
|
|
109
|
+
});
|
|
110
|
+
})),
|
|
111
|
+
// The json shape: log messages, and inner instructions with base58 data (what `eventsOf` reads).
|
|
112
|
+
getTransaction: (signature) => {
|
|
113
|
+
const t = transactions.find((x) => x.signature === signature);
|
|
114
|
+
return reply(t
|
|
115
|
+
? {
|
|
116
|
+
slot: 0n,
|
|
117
|
+
blockTime: null,
|
|
118
|
+
meta: {
|
|
119
|
+
err: null,
|
|
120
|
+
logMessages: t.meta.logs(),
|
|
121
|
+
computeUnitsConsumed: t.meta.computeUnitsConsumed(),
|
|
122
|
+
innerInstructions: t.meta
|
|
123
|
+
.innerInstructions()
|
|
124
|
+
.map((group, index) => ({ index, instructions: group.map((i) => ({ data: getBase58Decoder().decode(i.instruction().data()) })) })),
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
: null);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
/** Notifications that end when the subscriber aborts. */
|
|
131
|
+
const stream = (items) => ({
|
|
132
|
+
subscribe: async ({ abortSignal }) => (async function* () {
|
|
133
|
+
for (const item of items())
|
|
134
|
+
yield item;
|
|
135
|
+
await new Promise((resolve) => abortSignal.addEventListener("abort", resolve, { once: true }));
|
|
136
|
+
})(),
|
|
137
|
+
});
|
|
138
|
+
const rpcSubscriptions = {
|
|
139
|
+
signatureNotifications: () => stream(() => [{ context: { slot: 0n }, value: { err: null } }]),
|
|
140
|
+
slotNotifications: () => stream(() => []),
|
|
141
|
+
logsNotifications: () => stream(() => transactions.map((t) => ({ context: { slot: 0n }, value: { signature: t.signature, logs: t.meta.logs(), err: null } }))),
|
|
142
|
+
accountNotifications: (a) => stream(() => [{ context: { slot: 0n }, value: encoded(a) ?? { data: ["", "base64"], lamports: 0n } }]),
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
rpc: rpc,
|
|
146
|
+
rpcSubscriptions: rpcSubscriptions,
|
|
147
|
+
transactions,
|
|
148
|
+
track: (...addresses) => addresses.forEach((a) => known.add(a)),
|
|
149
|
+
};
|
|
150
|
+
}
|
package/dist/testing.d.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import anchor, { type Idl } from "@anchor-lang/core";
|
|
20
20
|
import { type Address, type Instruction, type KeyPairSigner } from "@solana/kit";
|
|
21
21
|
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
|
|
22
|
+
import { type SvmConnection, svmConnection } from "./svmrpc.js";
|
|
23
|
+
export { type SvmConnection, svmConnection };
|
|
22
24
|
declare const BorshCoder: typeof anchor.BorshCoder;
|
|
23
25
|
type BorshCoder = InstanceType<typeof BorshCoder>;
|
|
24
26
|
export type Result = TransactionMetadata | FailedTransactionMetadata;
|
|
@@ -58,9 +60,10 @@ export interface TestProgram {
|
|
|
58
60
|
returned<T = unknown>(instruction: string, res: Result): T;
|
|
59
61
|
/** Lamports of any account (0 when it does not exist). */
|
|
60
62
|
balance(address: Address): bigint;
|
|
63
|
+
/** A kit RPC and subscriptions pair over this LiteSVM, for the generated client (`svmConnection`). */
|
|
64
|
+
connection: SvmConnection;
|
|
61
65
|
}
|
|
62
66
|
/** Build `program` of the project around `from` (a file URL such as import.meta.url, or a directory) and load it. */
|
|
63
67
|
export declare function testProgram(program: string, from: string | URL): Promise<TestProgram>;
|
|
64
68
|
/** The same helpers for a program already loaded into `svm` at `programId` with this IDL. */
|
|
65
69
|
export declare function programHarness(svm: LiteSVM, programId: Address, idl: Idl): TestProgram;
|
|
66
|
-
export {};
|
package/dist/testing.js
CHANGED
|
@@ -22,7 +22,9 @@ import { FailedTransactionMetadata, LiteSVM } from "litesvm";
|
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { svmConnection } from "./svmrpc.js";
|
|
25
26
|
import { buildWorkspace, findWorkspace } from "./workspace.js";
|
|
27
|
+
export { svmConnection };
|
|
26
28
|
// @anchor-lang/core is CommonJS: Node 22 cannot import its classes by name from an installed ES module.
|
|
27
29
|
const { BN, BorshCoder } = anchor;
|
|
28
30
|
const BIG = new Set(["u64", "i64", "u128", "i128"]);
|
|
@@ -332,6 +334,7 @@ export function programHarness(svm, programId, idl) {
|
|
|
332
334
|
balance(address) {
|
|
333
335
|
return svm.getBalance(address) ?? 0n;
|
|
334
336
|
},
|
|
337
|
+
connection: svmConnection(svm),
|
|
335
338
|
};
|
|
336
339
|
return harness;
|
|
337
340
|
}
|
package/docs/cli.md
CHANGED
|
@@ -15,7 +15,7 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
|
|
|
15
15
|
| `typesol idl publish [<program>…] [--url <rpc>] [--keypair <authority.json>] [--config <cli config>] [--non-canonical]` | Compile (the binary is not rebuilt), then write each IDL to the Program Metadata program (seed `idl`). Canonical IDLs must be signed by the upgrade authority. Defaults come from the Solana CLI config (`~/.config/solana/cli/config.yml`). |
|
|
16
16
|
| `typesol cpi <idl.json> [--out <file.ts>] [--prefix <name>] [--address <base58>]` | Generate TypeSOL helpers that call another program from its IDL. Unsupported instructions are listed on stderr. |
|
|
17
17
|
| `typesol port <idl.json> [--out <file.ts>]` | A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program: account structs, enums, errors and one function per instruction with its accounts and arguments, each body `require(false, …NotPorted)` until written. Instruction discriminators (an explicit `@discriminator` when the name derives another), account names and error codes are kept, so existing clients keep working where the layouts match; notes on stderr list where they do not (Borsh `String`/`Option` in account data, a `Vec` that is not the last field, custom discriminators or error codes). |
|
|
18
|
-
| `typesol client <idl.json> [--out <file.ts>] [--address <base58>]` | Generate a typed TypeScript client on `@solana/kit` from an Anchor IDL (0.30+/1.x or TypeSOL); the IDL is validated first, and what the client leaves out is printed as notes. See [client.md](client.md). |
|
|
18
|
+
| `typesol client <idl.json> [--out <file.ts>] [--address <base58>]`, `typesol client --address <program id> [--url <rpc>] [--config <cli config>] [--out <file.ts>]` | Generate a typed TypeScript client on `@solana/kit` from an Anchor IDL (0.30+/1.x or TypeSOL); the IDL is validated first, and what the client leaves out is printed as notes. Without a file, the IDL is read from the chain: the canonical Program Metadata IDL, else Anchor's IDL account. See [client.md](client.md). |
|
|
19
19
|
| `typesol verify <program\|file.ts> [<deployed.so>] [--address <program id>] [--url <rpc>] [--config <cli config>] [--sbpf …]` | Rebuild and compare with what is deployed. Without a `.so`, reads the program from the cluster (`--url`, cluster monikers such as `mainnet-beta` included, else the Solana CLI config) at `--address` or the address `typesol.json` records, under any loader (v2, upgradeable v3, v4); prints the upgrade authority (or that the program is immutable) and the last deploy slot, and compares the IDL published with `typesol idl publish` (a published IDL that differs is a mismatch). With a `.so` (e.g. `solana program dump`), compares with that file. Trailing zero padding is ignored; the SBPF version is read from the deployed ELF unless `--sbpf` is given. |
|
|
20
20
|
| `typesol profile <logs.txt\|-> [--program <id>]` | Compute units per TypeScript line from the logs of a `--profile` build's transaction (exit 1 when the logs have no markers). |
|
|
21
21
|
| `typesol help <command>` | Same as `typesol <command> --help`. |
|
package/docs/client.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
Every `typesol build` writes a typed client next to the program: `<out>/<name>/<name>.client.ts`. It is one file, it
|
|
4
4
|
depends only on [`@solana/kit`](https://github.com/anza-xyz/kit) (8.5 or later), and it is rewritten on every build, so
|
|
5
5
|
it always matches the program. `typesol client <idl.json>` writes the same kind of client from an Anchor IDL (Anchor 0.30+
|
|
6
|
-
and 1.x), so one client style covers TypeSOL programs and the Anchor programs they talk to.
|
|
6
|
+
and 1.x), so one client style covers TypeSOL programs and the Anchor programs they talk to. `typesol client --address
|
|
7
|
+
<program id> --url <rpc>` reads the IDL from the chain instead: the canonical Program Metadata IDL (`typesol idl
|
|
8
|
+
publish`, Codama), else Anchor's IDL account (`anchor idl init`), so any deployed Anchor or TypeSOL program with a
|
|
9
|
+
published IDL gets a client from its address alone.
|
|
7
10
|
|
|
8
11
|
```ts
|
|
9
12
|
import { ledgerbook } from "./target/ledgerbook/ledgerbook.client.js";
|
|
@@ -39,11 +42,14 @@ const several = await ledgerbook.accounts.Book.fetchMultiple(rpc, [a, b]); // (B
|
|
|
39
42
|
|
|
40
43
|
// Live updates, until the AbortSignal aborts
|
|
41
44
|
for await (const book of ledgerbook.accounts.Book.watch(rpcSubscriptions, address, { abortSignal })) render(book);
|
|
42
|
-
for await (const { signature, event } of ledgerbook.watchEvents(rpcSubscriptions, { abortSignal })) show(event);
|
|
45
|
+
for await (const { signature, event } of ledgerbook.watchEvents(rpcSubscriptions, { abortSignal, rpc })) show(event); // with rpc: emitCpi events too
|
|
43
46
|
|
|
44
47
|
// Remaining accounts go after the instruction's own (Anchor's `.remainingAccounts([...])`)
|
|
45
48
|
await ledgerbook.approveMany({ approver: signer, book }, { indices: [0n, 1n] }, [{ address: extra, role: AccountRole.WRITABLE }]);
|
|
46
49
|
|
|
50
|
+
// has_one accounts that do not sign, read from the account that names them (Anchor resolves `relations` the same way)
|
|
51
|
+
const accounts = await ledgerbook.resolve.someInstruction(rpc, { book }, { signer }); // signers, and what book's fields name
|
|
52
|
+
|
|
47
53
|
// Events of a fetched transaction (getTransaction, json): log events and emitCpi events from inner instructions
|
|
48
54
|
ledgerbook.eventsOf(await rpc.getTransaction(signature, { maxSupportedTransactionVersion: 0 }).send());
|
|
49
55
|
```
|
|
@@ -58,8 +64,9 @@ Kit's own transaction functions work with the instructions as well; `sendInstruc
|
|
|
58
64
|
| `<program>.<instruction>(accounts, args)` | Builds the `Instruction`. Accounts with a fixed address and PDAs whose seeds are constants, arguments or other accounts' keys are optional and filled in; pass one to override. An optional account passed as `null` is left out (sent as the program id, the convention TypeSOL and Anchor share); left undefined, it is filled in like any other. |
|
|
59
65
|
| `<program>.<instruction>(accounts, args, remaining?)` | The same, with remaining accounts appended in order (kit `AccountMeta`s). |
|
|
60
66
|
| `<program>.accounts.<Name>` | `decode(bytes)`, `encode(value)`, `fetch(rpc, address)`, `fetchMaybe(rpc, address)`, `fetchMultiple(rpc, addresses)`, `fetchAll(rpc, filters?)` (getProgramAccounts by discriminator, plus `{ offset, bytes }` filters), `watch(rpcSubscriptions, address, { abortSignal })`, `discriminator`. `decode` checks the discriminator. |
|
|
61
|
-
| `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives. |
|
|
67
|
+
| `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives, also one seeded from its own fields (from the field values). |
|
|
62
68
|
| `<program>.returns.<instruction>(bytes)` | Decodes return data. |
|
|
69
|
+
| `<program>.resolve.<instruction>(rpc, accounts, args?, { signer? })` | Fills in what needs the chain or the wallet: signer accounts left out (`options.signer`, the default signer, like Anchor's provider wallet), `has_one` accounts that do not sign (read from the account that names them), and PDAs seeded from another account's fields, scalars, strings or bytes (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived). Given accounts are kept. |
|
|
63
70
|
| `<program>.views.<instruction>(rpc, feePayer, accounts, args)` | Simulates an instruction that returns a value and decodes it (throws with the logs if it fails). |
|
|
64
71
|
| `<program>.sendInstructions(connection, feePayer, instructions)`, `simulate(rpc, feePayer, instructions)` | One transaction sent and confirmed (kit's sendAndConfirm), or simulated without signatures (error, logs, units, this program's return data). |
|
|
65
72
|
| `<program>.errors`, `errorCode(e)`, `isError(e, name)` | The program's errors (6000+), and for TypeSOL programs also every Anchor framework error (`AnchorError` and the constraints raise them) and TypeSOL's runtime errors. |
|
|
@@ -82,11 +89,14 @@ Anchor IDLs are refused with a hint to convert them (`anchor idl convert`).
|
|
|
82
89
|
|
|
83
90
|
- **Errors and events are matched by code and discriminator, not by program.** A program this one calls can raise the
|
|
84
91
|
same error code, and `parseEvents` reads every `Program data:` line that decodes as one of this program's events.
|
|
85
|
-
- **Not filled in automatically** (pass them):
|
|
86
|
-
|
|
87
|
-
for each.
|
|
88
|
-
- **
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`
|
|
92
|
-
|
|
92
|
+
- **Not filled in automatically** (pass them): PDAs seeded from struct fields (Anchor cannot seed with them either),
|
|
93
|
+
PDAs whose program is another account's field, and a PDA seeded from its own fields (derive it with its `pdas`
|
|
94
|
+
helper from the values). `typesol client` prints a note for each.
|
|
95
|
+
- **Anchor IDLs**: zero-copy (`bytemuck`) accounts decode and encode at their `#[repr(C)]` offsets as the program sees
|
|
96
|
+
them on SBF (padding included; `packed` and `align` honored; 128-bit integers align to 8). Generic types become one
|
|
97
|
+
type per use with concrete arguments (`Pair<u64, 2>` is `PairU64N2`). Left out, with a note: zero-copy types with
|
|
98
|
+
Rust's own layout (`repr(Rust)`) or holding a non-zero-copy type, and the generic definitions themselves.
|
|
99
|
+
- **In tests**: `svmConnection(svm)` from `typesol/testing` answers the client's RPC calls from LiteSVM, and
|
|
100
|
+
`testProgram(...)` has it as `t.connection` ([test-your-program.md](test-your-program.md)).
|
|
101
|
+
- `watchEvents` reads log events; with `rpc` it also fetches each transaction in which the program invoked itself and
|
|
102
|
+
reads its `emitCpi` events (log subscriptions do not carry inner instructions).
|
|
@@ -44,9 +44,12 @@ it("counts", async () => {
|
|
|
44
44
|
| `t.account(type, address)` | the account decoded by its IDL type (bigints, address strings), or `null` once closed. TypeSOL fixed layouts come back as their IDL structs: a `FixedString<N>` as `{ len, bytes }`, a `FixedVec<T, N>` as `{ len, items }` (the generated client gives strings and arrays) |
|
|
45
45
|
| `t.returned(instruction, res)` | the instruction's return data: an integer, boolean, Pubkey or struct |
|
|
46
46
|
| `t.balance(address)` | lamports |
|
|
47
|
+
| `t.connection` | `{ rpc, rpcSubscriptions }` answered by this LiteSVM, for the generated client: `counter.sendInstructions(t.connection, alice, [ix])`, `counter.accounts.Counter.fetch(t.connection.rpc, address)`, views, `resolve`, `watch` and `watchEvents` |
|
|
47
48
|
|
|
48
49
|
`t.svm`, `t.programId`, `t.idl` and `t.coder` are there for anything else, e.g. writing a mint straight into the VM
|
|
49
50
|
with `t.svm.setAccount` (see the escrow template). `programHarness(svm, programId, idl)` gives the same helpers for a
|
|
50
|
-
program you loaded yourself.
|
|
51
|
+
program you loaded yourself, and `svmConnection(svm)` the RPC pair alone (for any LiteSVM, e.g. one with an Anchor
|
|
52
|
+
program loaded). Transactions sent through it run at once; `getProgramAccounts` (`fetchAll`) sees the accounts those
|
|
53
|
+
transactions named, plus any you `track(...)`.
|
|
51
54
|
|
|
52
55
|
The module needs `litesvm` and `@anchor-lang/core`, which `typesol init` adds to `devDependencies`.
|