@typesol/cli 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ 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.21.0 — 2026-10-28
9
+
10
+ Clients from a program's address alone, and the last pieces Anchor's client resolves for you: `has_one` accounts,
11
+ PDAs seeded from another account's fields, and `emitCpi` events as they happen. Programs and their bytes are
12
+ unchanged. Not externally audited (see SECURITY.md).
13
+
14
+ ### Added
15
+ - `typesol client --address <program id> --url <rpc>`: a client from the IDL on chain, the canonical Program Metadata
16
+ IDL (`typesol idl publish`, Codama) or else Anchor's IDL account (`anchor idl init`).
17
+ - Generated clients: `resolve.<instruction>(rpc, accounts)` fills in `has_one` accounts that do not sign from the
18
+ account that names them, as Anchor's client resolves `relations`, and PDAs seeded from another account's fields
19
+ (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived).
20
+ - `watchEvents(rpcSubscriptions, { abortSignal, rpc })`: with an RPC, `emitCpi` events arrive live too (the transaction
21
+ is fetched when the program invoked itself).
22
+
8
23
  ## 0.20.0 — 2026-10-28
9
24
 
10
25
  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.20).** Releases follow the organization's
309
+ **TypeSOL is pre-1.0 (0.21).** 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: "Generate a typed TypeScript client (on @solana/kit) from an Anchor IDL (0.30+/1.x or TypeSOL). The IDL is validated first;\n what the client leaves out is printed as notes. `typesol build` already writes <name>.client.ts for TypeSOL programs.",
282
- options: { out: { type: "string" }, address: { type: "string" } },
283
- run(v, args) {
284
- const file = one(args, "<idl.json>");
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
- const res = generateClient(readJson(file), { address: v.address });
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",
@@ -723,6 +725,8 @@ export function generateClient(idl, opts = {}) {
723
725
  // ---- Accounts ---------------------------------------------------------------------------------------------
724
726
  const accountOut = [];
725
727
  const accountKeys = [];
728
+ /** Each account's decoder object and its Pubkey fields (snake_case), for has_one relations. */
729
+ const accountConsts = [];
726
730
  for (const a of idl.accounts ?? []) {
727
731
  if (!defs.has(a.name) || typeIssue(a.name) || special(a.name)) {
728
732
  notes.push(`account ${a.name}: ${typeIssue(a.name) ?? "no type definition"}; it is left out`);
@@ -771,6 +775,8 @@ export function generateClient(idl, opts = {}) {
771
775
  ` },\n` +
772
776
  `};\n`);
773
777
  accountKeys.push(`${prop(a.name)}: ${A}`);
778
+ const fields = (defs.get(a.name).type.fields ?? []).filter((f) => typeof f === "object" && f !== null && "name" in f);
779
+ accountConsts.push({ name: a.name, A, pubkeys: new Set(fields.filter((f) => f.type === "pubkey" || f.type === "publicKey").map((f) => f.name)) });
774
780
  }
775
781
  // ---- Events -----------------------------------------------------------------------------------------------
776
782
  const events = (idl.events ?? []).filter((e) => {
@@ -795,6 +801,7 @@ export function generateClient(idl, opts = {}) {
795
801
  const pdaFns = new Map();
796
802
  const returnKeys = [];
797
803
  const viewKeys = [];
804
+ const resolveKeys = [];
798
805
  const usedKeys = new Set();
799
806
  for (const ix of idl.instructions) {
800
807
  const argIssue = ix.args.map((a) => issue(a.type)).find(Boolean);
@@ -971,6 +978,113 @@ export function generateClient(idl, opts = {}) {
971
978
  lines.join("\n") +
972
979
  `\n return {\n programAddress: PROGRAM_ADDRESS,\n accounts: [\n${metas.join("\n")}\n ...remaining,\n ],\n data: ${data},\n } as Instruction;\n}\n`);
973
980
  ixKeys.push(`${prop(fn)}: build${P}`);
981
+ // has_one relations (Anchor's `relations`): a non-signing account another account of the instruction names in a
982
+ // Pubkey field can be read from that account on chain.
983
+ const rels = [];
984
+ for (const a of flat) {
985
+ if (auto(a) || a.optional || a.signer || !a.relations?.length)
986
+ continue;
987
+ for (const r of a.relations) {
988
+ const source = byPath(r, a.group);
989
+ if (!source || source.optional)
990
+ continue;
991
+ const readers = accountConsts
992
+ .filter((c) => c.pubkeys.has(a.name))
993
+ .map((c) => `(d) => (hasPrefix(d, ${c.A}.discriminator) ? ${c.A}.decode(d)[${JSON.stringify(camel(a.name))}] : undefined)`);
994
+ if (!readers.length)
995
+ continue;
996
+ rels.push({ a, source, readers });
997
+ break;
998
+ }
999
+ }
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
+ const fieldPdas = [];
1014
+ for (const a of flat) {
1015
+ if (!a.pda || a.address || resolvable.has(a.key) || a.optional)
1016
+ continue;
1017
+ const p = a.pda.program;
1018
+ if (p && p.kind !== "const")
1019
+ continue;
1020
+ const bases = new Map();
1021
+ const seeds = [];
1022
+ let ok = true;
1023
+ let usesArgs = false;
1024
+ for (const sd of a.pda.seeds) {
1025
+ if (sd.kind === "const")
1026
+ seeds.push(bytes(sd.value));
1027
+ else if (sd.kind === "arg") {
1028
+ const e = seedExpr(sd, a.group);
1029
+ if (typeof e !== "string")
1030
+ ok = false;
1031
+ else
1032
+ (seeds.push(e), (usesArgs = true));
1033
+ }
1034
+ else if (!sd.path.includes(".")) {
1035
+ const t = byPath(sd.path, a.group);
1036
+ if (!t || t === a || auto(t) || t.optional)
1037
+ ok = false;
1038
+ else
1039
+ seeds.push(`getAddressEncoder().encode(key(${inputOf(t)}))`);
1040
+ }
1041
+ else {
1042
+ const parts = sd.path.split(".");
1043
+ const base = parts.length === 2 ? byPath(parts[0], a.group) : undefined;
1044
+ const acc = accountConsts.find((c) => c.name === sd.account);
1045
+ const field = (defs.get(sd.account ?? "")?.type.fields ?? []).find((f) => typeof f === "object" && f !== null && f.name === parts[1]);
1046
+ if (!base || base === a || base.optional || auto(base) || !acc || !field || !scalarSeed(field.type))
1047
+ ok = false;
1048
+ else {
1049
+ bases.set(base.key, { base, A: acc.A });
1050
+ seeds.push(`${codec(field.type)}.encode(${"$$" + base.key}[${JSON.stringify(camel(parts[1]))}])`);
1051
+ }
1052
+ }
1053
+ if (!ok)
1054
+ break;
1055
+ }
1056
+ if (!ok || bases.size === 0)
1057
+ continue;
1058
+ const program = p ? `programFromBytes(${bytes(p.value)})` : "PROGRAM_ADDRESS";
1059
+ fieldPdas.push({
1060
+ a,
1061
+ usesArgs,
1062
+ lines: [
1063
+ ` const ${"$" + a.key} =`,
1064
+ ` ${inputOf(a)} ??`,
1065
+ ` (await (async () => {`,
1066
+ ...[...bases.values()].map(({ base, A }) => ` const ${"$$" + base.key} = await ${A}.fetch(rpc, key(${inputOf(base)}));`),
1067
+ ` return (await getProgramDerivedAddress({ programAddress: ${program}, seeds: [${seeds.join(", ")}] }))[0];`,
1068
+ ` })());`,
1069
+ ],
1070
+ });
1071
+ }
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(" | ");
1075
+ const withArgs = fieldPdas.some((f) => f.usesArgs);
1076
+ const what = [
1077
+ ...rels.map((r) => `${r.a.key} (from ${r.source.key}.${camel(r.a.name)})`),
1078
+ ...fieldPdas.map((f) => `${f.a.key} (a PDA seeded from account fields)`),
1079
+ ].join(", ");
1080
+ resolveKeys.push(` /** ${P}Accounts with ${what} read from the chain when left out. */\n` +
1081
+ ` async ${prop(fn)}(rpc: Rpc<GetAccountInfoApi>, input: Omit<${P}Accounts, ${keysOf}> & Partial<Pick<${P}Accounts, ${keysOf}>>${withArgs ? `, args: ${P}Args` : ""}): Promise<${P}Accounts> {\n` +
1082
+ [
1083
+ ...rels.map((r) => ` const ${"$" + r.a.key} = ${inputOf(r.a)} ?? (await readRelation(rpc, key(${inputOf(r.source)}), ${JSON.stringify(r.a.key)}, [${r.readers.join(", ")}]));`),
1084
+ ...fieldPdas.flatMap((f) => f.lines),
1085
+ ].join("\n") +
1086
+ `\n return { ...input, ${filled.map((a) => `${prop(a.key)}: ${"$" + a.key}`).join(", ")} } as ${P}Accounts;\n },`);
1087
+ }
974
1088
  if (ix.returns !== undefined) {
975
1089
  const why = issue(ix.returns);
976
1090
  if (why)
@@ -1041,6 +1155,21 @@ export function generateClient(idl, opts = {}) {
1041
1155
  ],
1042
1156
  },
1043
1157
  { name: "programFromBytes", lines: [`const programFromBytes = (b: Uint8Array): Address => getAddressDecoder().decode(b);`] },
1158
+ {
1159
+ name: "readRelation",
1160
+ lines: [
1161
+ `/** A Pubkey field of an account on chain, read by the first reader that knows its type. */`,
1162
+ `const readRelation = async (rpc: Rpc<GetAccountInfoApi>, at: Address, name: string, readers: ((data: ReadonlyUint8Array) => Address | undefined)[]): Promise<Address> => {`,
1163
+ ` const found = await fetchEncodedAccount(rpc, at);`,
1164
+ ` if (!found.exists) throw new Error("cannot fill in " + name + ": account " + at + " does not exist");`,
1165
+ ` for (const read of readers) {`,
1166
+ ` const v = read(found.data);`,
1167
+ ` if (v !== undefined) return v;`,
1168
+ ` }`,
1169
+ ` throw new Error("cannot fill in " + name + ": " + at + " is not an account that names it");`,
1170
+ `};`,
1171
+ ],
1172
+ },
1044
1173
  {
1045
1174
  name: "memcmp",
1046
1175
  lines: [
@@ -1221,10 +1350,32 @@ export function generateClient(idl, opts = {}) {
1221
1350
  ` if (!meta) return [];`,
1222
1351
  ` return [...parseEvents(meta.logMessages ?? []), ...parseCpiEvents((meta.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))];`,
1223
1352
  `}`,
1224
- `/** Events of every transaction that mentions the program, as their logs arrive, until \`abortSignal\` aborts. */`,
1225
- `export async function* watchEvents(rpcSubscriptions: RpcSubscriptions<LogsNotificationsApi>, options: { abortSignal: AbortSignal; commitment?: Commitment }): AsyncGenerator<{ signature: Signature; event: Event }> {`,
1353
+ `/**`,
1354
+ ` * Events of every transaction that mentions the program, as their logs arrive, until \`abortSignal\` aborts. With`,
1355
+ ` * \`rpc\`, \`emitCpi\` events too: a transaction in which the program invoked itself is fetched (getTransaction) and its`,
1356
+ ` * inner instructions read, after its log events.`,
1357
+ ` */`,
1358
+ `export async function* watchEvents(`,
1359
+ ` rpcSubscriptions: RpcSubscriptions<LogsNotificationsApi>,`,
1360
+ ` options: { abortSignal: AbortSignal; commitment?: Commitment; rpc?: Rpc<GetTransactionApi> },`,
1361
+ `): AsyncGenerator<{ signature: Signature; event: Event }> {`,
1226
1362
  ` const logs = await rpcSubscriptions.logsNotifications({ mentions: [PROGRAM_ADDRESS] }, options.commitment ? { commitment: options.commitment } : {}).subscribe({ abortSignal: options.abortSignal });`,
1227
- ` for await (const n of logs) for (const event of parseEvents(n.value.logs)) yield { signature: n.value.signature, event };`,
1363
+ ` const selfInvoke = new RegExp("^Program " + PROGRAM_ADDRESS + " invoke \\\\[([2-9]|\\\\d{2,})\\\\]$");`,
1364
+ ` for await (const n of logs) {`,
1365
+ ` for (const event of parseEvents(n.value.logs)) yield { signature: n.value.signature, event };`,
1366
+ ` if (!options.rpc || !n.value.logs.some((l) => selfInvoke.test(l))) continue;`,
1367
+ ` // The transaction can trail its logs by a moment: a few tries.`,
1368
+ ` for (let attempt = 0; attempt < 5 && !options.abortSignal.aborted; attempt++) {`,
1369
+ ` const tx = await options.rpc`,
1370
+ ` .getTransaction(n.value.signature, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: options.commitment === "finalized" ? "finalized" : "confirmed" })`,
1371
+ ` .send();`,
1372
+ ` if (tx) {`,
1373
+ ` for (const event of parseCpiEvents((tx.meta?.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))) yield { signature: n.value.signature, event };`,
1374
+ ` break;`,
1375
+ ` }`,
1376
+ ` await new Promise((resolve) => setTimeout(resolve, 400));`,
1377
+ ` }`,
1378
+ ` }`,
1228
1379
  `}`,
1229
1380
  "",
1230
1381
  `// ---- Transactions ----`,
@@ -1285,6 +1436,9 @@ export function generateClient(idl, opts = {}) {
1285
1436
  ` views: {`,
1286
1437
  ...viewKeys,
1287
1438
  ` },`,
1439
+ ` resolve: {`,
1440
+ ...resolveKeys,
1441
+ ` },`,
1288
1442
  ` errors: ERRORS,`,
1289
1443
  ` errorCode,`,
1290
1444
  ` isError,`,
@@ -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
+ }>;
@@ -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/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 }); // fills in what book's Pubkey 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
  ```
@@ -60,6 +66,7 @@ Kit's own transaction functions work with the instructions as well; `sendInstruc
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
67
  | `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives. |
62
68
  | `<program>.returns.<instruction>(bytes)` | Decodes return data. |
69
+ | `<program>.resolve.<instruction>(rpc, accounts, args?)` | Fills in what needs the chain: `has_one` accounts that do not sign (read from the account that names them) and PDAs seeded from another account's fields (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived). Given accounts are kept. |
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,12 @@ 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): accounts named by `has_one` relations, PDAs seeded from fields inside
86
- another account or inside an argument, and PDAs whose program comes from an argument. `typesol client` prints a note
87
- for each.
92
+ - **Not filled in automatically** (pass them): `has_one` accounts that sign (they have to sign), PDAs seeded from an
93
+ account's own fields or from fields inside an argument, PDAs seeded from non-scalar fields (strings, structs), and PDAs
94
+ whose program comes from an argument. `typesol client` prints a note for each. `has_one` accounts that do not sign and
95
+ PDAs seeded from another account's scalar fields are filled in by `resolve` (it reads the chain).
88
96
  - **Left out of a client generated from an Anchor IDL**, with a note: generic types and anything using them, and
89
97
  zero-copy (`bytemuck`) accounts.
90
98
  - **Not done yet**: a LiteSVM loader for tests (use `svm.addProgramFromFile(ledgerbook.address, soPath)`, or
91
- `typesol/testing`). `watchEvents` reads log events; `emitCpi` events are in inner instructions, which log
92
- subscriptions do not carry (use `eventsOf` on the fetched transaction).
99
+ `typesol/testing`). `watchEvents` reads log events; with `rpc` it also fetches each transaction in which the program
100
+ invoked itself and reads its `emitCpi` events (log subscriptions do not carry inner instructions).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",