@typesol/cli 0.15.0 → 0.17.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +11 -7
  3. package/dist/cli.js +147 -20
  4. package/dist/clientgen.js +37 -0
  5. package/dist/compiler/analyze.d.ts +11 -1
  6. package/dist/compiler/analyze.js +307 -37
  7. package/dist/compiler/emit/base.d.ts +7 -0
  8. package/dist/compiler/emit/base.js +62 -2
  9. package/dist/compiler/emit/borsh.d.ts +13 -0
  10. package/dist/compiler/emit/borsh.js +232 -0
  11. package/dist/compiler/emit/calls.js +50 -15
  12. package/dist/compiler/emit/cpi.d.ts +32 -8
  13. package/dist/compiler/emit/cpi.js +706 -25
  14. package/dist/compiler/emit/expressions.d.ts +11 -0
  15. package/dist/compiler/emit/expressions.js +126 -4
  16. package/dist/compiler/emit/function.js +45 -12
  17. package/dist/compiler/emit/program.js +24 -6
  18. package/dist/compiler/emit/shared.d.ts +12 -0
  19. package/dist/compiler/emit/shared.js +22 -4
  20. package/dist/compiler/emit/statements.d.ts +13 -0
  21. package/dist/compiler/emit/statements.js +141 -7
  22. package/dist/compiler/emit/storage.d.ts +6 -1
  23. package/dist/compiler/emit/storage.js +82 -17
  24. package/dist/compiler/idl.js +61 -18
  25. package/dist/compiler/model.d.ts +56 -4
  26. package/dist/compiler/model.js +8 -2
  27. package/dist/compiler/toolchain.d.ts +2 -0
  28. package/dist/compiler/toolchain.js +27 -1
  29. package/dist/onchain.d.ts +30 -0
  30. package/dist/onchain.js +53 -0
  31. package/dist/portgen.d.ts +87 -0
  32. package/dist/portgen.js +367 -0
  33. package/dist/templates.js +49 -154
  34. package/dist/testing.d.ts +66 -0
  35. package/dist/testing.js +337 -0
  36. package/dist/workspace.js +11 -1
  37. package/docs/architecture.md +3 -0
  38. package/docs/cli.md +16 -2
  39. package/docs/errors.md +3 -2
  40. package/docs/language.md +19 -14
  41. package/docs/mainnet.md +5 -4
  42. package/docs/migrate-from-anchor.md +113 -0
  43. package/docs/test-your-program.md +52 -0
  44. package/docs/testing.md +1 -1
  45. package/docs/troubleshooting.md +7 -0
  46. package/lib/typesol.d.ts +325 -13
  47. package/package.json +19 -1
  48. package/runtime/typesol_rt.h +54 -9
  49. package/ts-plugin/index.js +112 -0
  50. package/ts-plugin/package.json +4 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,113 @@ 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.17.0 — 2026-10-28
9
+
10
+ Anchor's account bytes on request (`@layout borsh`, so a TypeSOL program can take over an Anchor program's accounts
11
+ without a migration), lists with a fixed capacity anywhere in account data (`FixedVec`), arrow functions and generic
12
+ helpers, Metaplex collection verification, Core plugins and Bubblegum V2, and a faster UTF-8 check. Existing programs
13
+ keep their layouts and error codes (3004 is new, raised only by `@layout borsh` accounts). Not externally audited (see
14
+ SECURITY.md).
15
+
16
+ ### Added
17
+ - `/** @layout borsh */` on an account struct stores it with Anchor's bytes (Borsh String / Vec / Option for
18
+ `FixedString` / `FixedVec` / `Option`), so a TypeSOL program can take over an Anchor program's accounts without a
19
+ migration; its IDL lists Anchor's own types. An instruction unpacks the account into the fixed layout when it starts
20
+ and packs it back when it returns (3003 for bytes that are not a valid value, 3004 when the account is too small).
21
+ Checked byte for byte against an Anchor twin, including switching programs in the middle of a sequence.
22
+ `typesol port` marks Anchor account structs that need it.
23
+ - `FixedVec<T, N>`: a list with room for N elements (u32 length + N slots, like `FixedString<N>`) as any account field,
24
+ several per struct and in nested structs, with `get` / `set` / `push` / `pop` / `clear` / `.length` / `for … of`;
25
+ `push` on a full one fails with 5104, `fixedVec([…])` writes a whole one. The IDL describes each as a
26
+ `FixedVec<N>_<T>` struct `{ len, items }`; the generated client reads and writes them as arrays. The
27
+ basics/favorites port uses one.
28
+ - Bubblegum V2: `createTreeV2`, `mintV2` (optionally into a Metaplex Core collection), `transferV2` and `burnV2`, which
29
+ take the leaf (root, data and creator hashes, nonce, index) and pass its Merkle proof on from the remaining accounts;
30
+ the Core `BubblegumV2` collection plugin. Checked against Bubblegum, mpl-account-compression, mpl-noop and Core
31
+ (`pnpm fetch:programs` now fetches the mpl-account-compression pair too).
32
+ - Metaplex Core plugins: a literal plugin list in `createAsset` / `createCollection` (Royalties, freeze / burn / transfer /
33
+ update delegates and their permanent forms, Attributes, Edition, AddBlocker, ImmutableMetadata, each with an optional
34
+ authority), and `addPlugin`, `updatePlugin`, `removePlugin` with their collection forms, checked against the real Core
35
+ program (a frozen asset cannot move until it is thawed).
36
+ - Arrow functions: `const f = (x: u64): u64 => …` (and `const f = function (…) {…}`) at the top level or inside a
37
+ function is a helper. Local ones capture nothing (a closure over the enclosing function's variables is refused with
38
+ the name it reads); functions are called, not passed around.
39
+ - Generic helpers: `function max<T extends bigint>(a: T, b: T): T` compiles once per set of types it is called with,
40
+ inferred from the arguments or written out (`max<u128>(x, 1n)`); type parameters stand for integers, booleans,
41
+ Pubkeys or enums.
42
+ - `x as T` (and `satisfies`): names a type the value already has, never a conversion; mainly `return (a + b) as T`
43
+ in a generic helper.
44
+ - Metaplex collections: `createV3`'s `isCollection: true` (CollectionDetails::V1), `createMasterEditionV3(…, maxSupply | null)`,
45
+ `verifyCollection(…)` (Verify with CollectionV1) and `masterEditionAddress(mint)`. The tokens/nft-operations port now
46
+ uses them instead of hand-encoded `invoke`s, checked against the real Token Metadata program.
47
+
48
+ ### Changed
49
+ - IDLs no longer list `FixedString*` / `FixedOption_*` / `FixedVec*` helper types that nothing in them refers to
50
+ (for example an `Option` used only as an instruction argument, which is Borsh there).
51
+ - UTF-8 checks of strings run eight ASCII bytes at a time: every official-example instruction with a string argument
52
+ gets cheaper (create_token_mint 40,919 → 39,828 CU, set_favorites 4,282 → 4,195).
53
+
54
+ ## 0.16.0 — 2026-10-28
55
+
56
+ Everyday TypeScript that Anchor developers reach for (`for … of`, `??`, destructuring, `**`, structs returned from
57
+ helpers and instructions, strings in enum variants), tools to move an Anchor program over (`typesol port`, a migration
58
+ guide) and to work on it (`typesol/testing`, an editor plugin, `verify` against the cluster), Token-2022 extension
59
+ setup and Metaplex creators. No IDL or error-code changes for existing programs. Not externally audited (see
60
+ SECURITY.md).
61
+
62
+ ### Added
63
+ - Metaplex `createV3(…, isMutable, { creators, collection }, signerSeeds?)`: 1–5 creators (shares checked to add up to 100
64
+ when literal) and an unverified collection mint, checked against the real Token Metadata program.
65
+ - Data-enum variants can hold a `string` (`type Cmd = { kind: "Say"; text: string } | …`), as Anchor enums with `String`
66
+ payloads: Borsh in instruction arguments and events, a value in code (a copy when built from account text), refused
67
+ in account data. The IDL describes them as Borsh enums only.
68
+ - `typesol port <idl.json>`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, one function per
69
+ instruction with its accounts and arguments). Discriminators, account names and error codes are kept, so existing
70
+ clients keep working where layouts match; notes list where Anchor's Borsh layout differs. Round-trips every example
71
+ (example → IDL → port → IDL) with the same instructions, accounts, layouts and error codes.
72
+ - Token-2022 extension initializers on `Token2022Program` / `TokenInterface` (refused on `TokenProgram`): transfer fee,
73
+ mint close authority, non-transferable, permanent delegate, default account state, metadata / group / group-member
74
+ pointers, transfer hook, interest-bearing, immutable owner. Byte layouts checked against the official
75
+ @solana-program/token-2022 decoder on real mints.
76
+ - `typesol/ts-plugin`: a TypeScript language-service plugin that shows the compiler's own errors in the editor, for
77
+ the unsaved text, once TypeScript has none. `typesol init` projects enable it in `programs/tsconfig.json` (the
78
+ editor must use the workspace's TypeScript). `typesol check` gains `--json` and `--stdin`.
79
+ - `typesol/testing`: `testProgram(name, import.meta.url)` builds a project program into LiteSVM, with `wallet()`, `pda(…)`,
80
+ `ix(name, args, accounts)` (accounts by name, flags, fixed addresses and PDAs from the IDL, plain argument values),
81
+ `send`, `ok`, `errorCode`, `account` (decoded to bigints and address strings), `returned` and `balance`. The
82
+ `typesol init` templates' tests use it (the counter test went from 70 lines to 20). See docs/test-your-program.md.
83
+ - `a ** b` on integers, every multiplication checked (5100 on overflow or a negative exponent, where JS throws). A
84
+ literal exponent unrolls at compile time; otherwise a square-and-multiply loop runs.
85
+ - Object destructuring: `const { owner, pos: p } = acct` (`let` for scalars) from a name or a field path, read like
86
+ `acct.owner` (scalars copied, nested structs referenced). Rest elements, defaults and nested patterns are refused.
87
+ - Instructions can return a fixed-size struct as return data (a literal, a stored struct or a helper's result), at
88
+ most 1024 bytes. It goes out in its fixed layout, which the IDL describes under `returns`, so Anchor clients and the
89
+ generated TypeScript client decode it.
90
+ - Helpers can return a fixed-size struct: `return { x, y }`, a stored struct or another helper's result. The caller
91
+ gets its own copy to bind (`const p: Point = at(1n, 2n)`), store (`acct.home = at(…)`, `arr.set(i, at(…))`) or read
92
+ (`at(…).x`). Structs with a Vec or variable-length strings cannot be returned.
93
+ - `a ?? b` on Options of integers, booleans and Pubkeys (b is evaluated only when a is empty; the result is an
94
+ Option when b can be null), and `x!` on an Option (its value; an empty one aborts with 5108).
95
+ - `for (const x of list)` over a `Vec` (account field or argument) or a `FixedArray`: each `x` is what `list.get(i)`
96
+ gives (a value, or a reference into account data for structs, with the same aliasing checks), the length is read
97
+ again before every step as in JS, and `break` / `continue` work. Remaining accounts are still iterated by index.
98
+ - `typesol verify <program>` without a `.so` reads the deployed program from the cluster (`--address`, or the address
99
+ in `typesol.json`; `--url`, cluster monikers or the Solana CLI config) under loader v2, v3 or v4, prints its upgrade
100
+ authority or that it is immutable and its last deploy slot, and compares the IDL published on chain with the built
101
+ one. A published IDL that differs is a mismatch (exit 3). No `solana program dump` step needed.
102
+ - `typesol idl publish --url` accepts the Solana CLI's cluster monikers (`mainnet-beta`, `devnet`, …).
103
+
104
+ ### Fixed
105
+ - `createV3` evaluates its arguments in source order (a creator share computed by a helper that changes account text
106
+ no longer changes the name or fee read before it).
107
+ - A function whose stack frame exceeds SBPF v0's 4 KiB used to build and then corrupt memory at runtime (the toolchain
108
+ only printed a note). The build now fails, naming the instruction or helper and its frame size (v1/v2 frames grow
109
+ dynamically and are not limited).
110
+
111
+ ### Docs
112
+ - docs/migrate-from-anchor.md: Anchor constructs, account data, code and tooling mapped to TypeSOL, with the `typesol port`
113
+ workflow and which account layouts differ.
114
+
8
115
  ## 0.15.0 — 2026-10-27
9
116
 
10
117
  Project templates for `typesol init`, a mainnet deployment guide, and clearer `typesol idl publish` failures. No
package/README.md CHANGED
@@ -34,8 +34,8 @@ clients read TypeSOL programs unchanged.
34
34
  - **Small and fast.** On the same programs, TypeSOL uses 5–46% of Anchor's compute units, with binaries about 10×
35
35
  smaller. It also uses fewer compute units than native Rust and than hand-tuned Pinocchio on every benchmarked
36
36
  instruction.
37
- - **Reproducible.** The same sources and toolchain produce the same bytes. `typesol verify` checks a deployed program
38
- against its source.
37
+ - **Reproducible.** The same sources and toolchain produce the same bytes. `typesol verify` reads a deployed program
38
+ from the cluster and checks it, and the IDL published on chain, against its source.
39
39
 
40
40
  <p align="center">
41
41
  <a href="#numbers">
@@ -162,7 +162,9 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
162
162
  Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
163
163
  program's address.
164
164
 
165
- To go to mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
165
+ Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct, and `typesol port`
166
+ starts from your program's IDL. Tests use `typesol/testing` (accounts by name, plain values; [docs/test-your-program.md](docs/test-your-program.md)). To go to
167
+ mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
166
168
 
167
169
  ## How it works
168
170
 
@@ -189,8 +191,8 @@ Runtime failures point back to your source: `Error 6000 Unauthorized (counter.ts
189
191
  | --- | --- |
190
192
  | **Accounts** | `Signer`, `Account<T>`, `Mut<T>`, `Init<T>`, `InitIfNeeded<T>` (PDAs and associated token accounts), `Zeroed<T>` (up to 10 MiB), optional accounts (`A \| null`), token accounts and mints, programs, sysvars |
191
193
  | **Constraints** | `HasOne`, `Seeds` (also from account fields and under another program; `pdaBump` like `ctx.bumps`), `CloseTo`, `Ata`, `Address`, `Owner`, `Executable`. They are checked before the body runs and recorded in the IDL |
192
- | **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account |
193
- | **Control flow** | `if`, `for`, `while`, `switch` (with narrowing on data enums), ternaries, helper functions across multiple files |
194
+ | **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account, `FixedVec` lists anywhere, and Anchor's own byte layout (`@layout borsh`) to take over existing Anchor accounts |
195
+ | **Control flow** | `if`, `for`, `for … of`, `while`, `switch` (with narrowing on data enums), ternaries, `??`, destructuring, helper functions (returning structs too, arrow functions, generics) across multiple files |
194
196
  | **CPIs** | System, SPL Token, Token-2022 (including extension reads and metadata), ATA, Metaplex Token Metadata, Core, Bubblegum. Generic `invoke` with PDA signing, return data, and typed bindings generated from any Anchor IDL (`typesol cpi`) |
195
197
  | **Events** | `emit` (log-based) and `emitCpi` (Anchor `emit_cpi!`, through a self-CPI) |
196
198
  | **Crypto & oracles** | `sha256`, `keccak256`, Merkle proofs, Ed25519 and secp256k1 signature checks, Pyth pull oracle, Switchboard |
@@ -209,7 +211,9 @@ The full reference, with every type, check and built-in, is in [docs/language.md
209
211
  | `typesol idl publish` | upload the IDL to the Program Metadata program (canonical when the upgrade authority signs) |
210
212
  | `typesol cpi <idl.json>` | generate typed CPI helpers for another program |
211
213
  | `typesol client <idl.json>` | generate a typed TypeScript client from an Anchor IDL (`typesol build` writes one per program) |
212
- | `typesol verify` | rebuild from source and compare with a deployed binary |
214
+ | `typesol port` | a TypeSOL skeleton of an Anchor program from its IDL, keeping discriminators and error codes |
215
+ | `typesol/ts-plugin` | the compiler's errors in the editor as you type ([docs/cli.md](docs/cli.md#editor)) |
216
+ | `typesol verify` | rebuild from source and compare with the program and IDL on chain (or a dumped `.so`) |
213
217
  | `typesol profile` | compute units per TypeScript line, from a `--profile` build's logs |
214
218
  | `typesol emit-c` | print the generated C |
215
219
 
@@ -302,7 +306,7 @@ Details are in [docs/testing.md](docs/testing.md).
302
306
 
303
307
  ## Status
304
308
 
305
- **TypeSOL is pre-1.0 (0.15).** Releases follow the organization's
309
+ **TypeSOL is pre-1.0 (0.17).** Releases follow the organization's
306
310
  [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
307
311
  change.
308
312
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
package/dist/cli.js CHANGED
@@ -5,8 +5,10 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parseArgs } from "node:util";
8
+ import ts from "typescript";
8
9
  import { build, CompileError, COMPILER_VERSION, generate, SBPF_VERSIONS, verifyBinary } from "./compiler/index.js";
9
10
  import { generateCpi } from "./cpigen.js";
11
+ import { generatePort } from "./portgen.js";
10
12
  import { generateClient, writeClient } from "./clientgen.js";
11
13
  import { buildIdl } from "./compiler/idl.js";
12
14
  import { decodeAddress, loadOrCreateKeypair, readKeypair } from "./keys.js";
@@ -120,13 +122,35 @@ const COMMANDS = {
120
122
  },
121
123
  },
122
124
  check: {
123
- usage: "typesol check <program.ts>",
124
- help: "Typecheck and validate the TypeSOL subset without building.",
125
- options: {},
126
- run(_, args) {
127
- const { model } = generate(mustExist(one(args, "<program.ts>")));
128
- log(`ok ${model.name}: ${model.instructions.length} instruction(s), ${model.accountStructs.size} account type(s), ${model.events.size} event(s)`);
129
- return 0;
125
+ usage: "typesol check <program.ts> [--json] [--stdin]",
126
+ help: [
127
+ "Typecheck and validate the TypeSOL subset without building.",
128
+ "--json print { ok, diagnostics: [{ file, line, column, message }] } (exit 1 when there are diagnostics)",
129
+ "--stdin read <program.ts>'s text from stdin instead of the file (an editor's unsaved buffer); its imports are read from disk",
130
+ ].join("\n "),
131
+ options: { json: { type: "boolean" }, stdin: { type: "boolean" } },
132
+ run(v, args) {
133
+ const file = path.resolve(one(args, "<program.ts>"));
134
+ const restore = v.stdin ? overrideFile(file, readFileSync(0, "utf8")) : undefined;
135
+ try {
136
+ if (!v.stdin)
137
+ mustExist(file);
138
+ const { model } = generate(file);
139
+ if (v.json)
140
+ log(JSON.stringify({ ok: true, diagnostics: [] }));
141
+ else
142
+ log(`ok ${model.name}: ${model.instructions.length} instruction(s), ${model.accountStructs.size} account type(s), ${model.events.size} event(s)`);
143
+ return 0;
144
+ }
145
+ catch (e) {
146
+ if (!v.json || !(e instanceof CompileError))
147
+ throw e;
148
+ log(JSON.stringify({ ok: false, diagnostics: e.diagnostics }));
149
+ return 1;
150
+ }
151
+ finally {
152
+ restore?.();
153
+ }
130
154
  },
131
155
  },
132
156
  "emit-c": {
@@ -195,7 +219,7 @@ const COMMANDS = {
195
219
  const kit = await import("@solana/kit");
196
220
  const { publishIdl, rpcChain } = await import("./idlpublish.js");
197
221
  const signer = await kit.createKeyPairSignerFromBytes(readKeypair(keyFile));
198
- const chain = rpcChain(kit.createSolanaRpc(rpcUrl));
222
+ const chain = rpcChain(kit.createSolanaRpc(clusterUrl(rpcUrl)));
199
223
  for (const name of names(ws, args)) {
200
224
  // The IDL only needs the compiled model and the program address: the deployed binary is left alone.
201
225
  const { address } = programAddress(ws, name);
@@ -228,6 +252,30 @@ const COMMANDS = {
228
252
  return 0;
229
253
  },
230
254
  },
255
+ port: {
256
+ usage: "typesol port <idl.json> [--out <file.ts>]",
257
+ help: [
258
+ "A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x): account structs, enums, errors, events and one function per",
259
+ "instruction with its accounts and arguments (bodies to fill in). Discriminators, account names and error order are kept,",
260
+ "so existing clients keep working where the layouts match; the notes (stderr) list where they do not.",
261
+ ].join("\n "),
262
+ options: { out: { type: "string" } },
263
+ run(v, args) {
264
+ const file = one(args, "<idl.json>");
265
+ const res = generatePort(readJson(file));
266
+ if (v.out) {
267
+ if (existsSync(v.out))
268
+ throw new UsageError(`${v.out} exists; refusing to overwrite it`);
269
+ writeFileSync(v.out, res.source);
270
+ log(` wrote ${v.out}`);
271
+ }
272
+ else
273
+ process.stdout.write(res.source);
274
+ for (const n of res.notes)
275
+ console.error(` note: ${n}`);
276
+ return 0;
277
+ },
278
+ },
231
279
  client: {
232
280
  usage: "typesol client <idl.json> [--out <file.ts>] [--address <base58>]",
233
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.",
@@ -249,41 +297,95 @@ const COMMANDS = {
249
297
  },
250
298
  },
251
299
  verify: {
252
- usage: "typesol verify <program.ts | program> <deployed.so> [--sbpf v0|v1|v2]",
300
+ usage: "typesol verify <program.ts | program> [<deployed.so>] [--address <program id>] [--url <rpc>] [--config <solana cli config>] [--sbpf v0|v1|v2]",
253
301
  help: [
254
- "Rebuild and compare with a deployed binary (e.g. from `solana program dump`). Exit 0 on a match, 3 on a mismatch.",
302
+ "Rebuild and compare with what is deployed. Exit 0 on a match, 3 on a mismatch.",
303
+ "With a .so (e.g. from `solana program dump`): compare with that file.",
304
+ "Without one: read the program from the cluster (--url, else the Solana CLI config) at --address, or at the address",
305
+ "typesol.json records for a project program. Also prints the upgrade authority and compares the IDL published",
306
+ "on chain (`typesol idl publish`) with the one the source builds; a published IDL that differs is a mismatch.",
255
307
  "The SBPF version is read from the deployed binary unless --sbpf is given. Reproducible builds need the same",
256
308
  "TypeSOL version and platform-tools as the original (see its <name>.build.json).",
257
309
  ].join("\n "),
258
- options: { sbpf: { type: "string" } },
259
- run(v, args) {
260
- if (args.length !== 2)
261
- throw new UsageError("verify takes the program and the deployed .so");
310
+ options: { sbpf: { type: "string" }, address: { type: "string" }, url: { type: "string" }, config: { type: "string" } },
311
+ async run(v, args) {
312
+ if (args.length < 1 || args.length > 2)
313
+ throw new UsageError("verify takes the program and, optionally, the deployed .so");
262
314
  const [target, deployedPath] = args;
315
+ if (deployedPath && (v.address || v.url))
316
+ throw new UsageError("give either a deployed .so or --address/--url, not both");
317
+ if (v.address && !decodeAddress(v.address))
318
+ throw new UsageError(`--address "${v.address}" is not a base58 Solana address`);
263
319
  const ws = findWorkspace(process.cwd());
264
320
  const name = ws && (ws.programs[target] ? target : programForEntry(ws, target));
265
321
  const entry = ws && name ? programPaths(ws, name).entry : target;
266
322
  if (!existsSync(entry))
267
323
  throw new UsageError(`${entry} does not exist`);
268
- if (!existsSync(deployedPath))
269
- throw new UsageError(`${deployedPath} does not exist`);
270
- const deployed = readFileSync(deployedPath);
324
+ let deployed;
325
+ let onChain;
326
+ if (deployedPath) {
327
+ if (!existsSync(deployedPath))
328
+ throw new UsageError(`${deployedPath} does not exist`);
329
+ deployed = readFileSync(deployedPath);
330
+ }
331
+ else {
332
+ const address = v.address ?? (ws && name ? ws.programs[name].address : undefined);
333
+ if (!address)
334
+ throw new UsageError("no program address: pass --address, a deployed .so, or name a project program that has been built");
335
+ const rpcUrl = v.url ?? solanaConfig(v.config).url;
336
+ if (!rpcUrl)
337
+ throw new Error("no RPC: pass --url, or set json_rpc_url in the Solana CLI config");
338
+ // Loaded only here: the other commands do not need an RPC client.
339
+ const kit = await import("@solana/kit");
340
+ const { fetchDeployed } = await import("./onchain.js");
341
+ const rpc = kit.createSolanaRpc(clusterUrl(rpcUrl));
342
+ const info = await fetchDeployed(rpc, address);
343
+ onChain = { address, info, rpc };
344
+ deployed = info.elf;
345
+ }
271
346
  // ELF e_flags (offset 0x30) hold the SBPF version: 0 for v0, 1 for v1, 2 for v2.
272
347
  const flags = deployed.length > 0x34 && deployed.readUInt32BE(0) === 0x7f454c46 ? deployed.readUInt32LE(0x30) : 0;
273
348
  const sbpf = sbpfOf(v) ?? SBPF_VERSIONS[flags] ?? "v0";
274
349
  const dir = mkdtempSync(path.join(os.tmpdir(), "typesol-verify-"));
275
350
  let res, same;
276
351
  try {
277
- res = build(entry, dir, "11111111111111111111111111111111", { sbpf });
352
+ res = build(entry, dir, onChain?.address ?? "11111111111111111111111111111111", { sbpf });
278
353
  same = verifyBinary(deployed, readFileSync(res.soPath));
279
354
  }
280
355
  finally {
281
356
  rmSync(dir, { recursive: true, force: true });
282
357
  }
358
+ const what = deployedPath ??
359
+ `${onChain.address} (${onChain.info.dataAccount === onChain.address ? "" : `data ${onChain.info.dataAccount}, `}loader ${onChain.info.loader})`;
283
360
  log(same
284
361
  ? ` match ${res.manifest.binary} (sbpf ${sbpf})`
285
- : ` MISMATCH ${deployedPath} is not what ${entry} builds to (${res.manifest.binary}, sbpf ${sbpf})`);
286
- return same ? 0 : 3;
362
+ : ` MISMATCH ${what} is not what ${entry} builds to (${res.manifest.binary}, sbpf ${sbpf})`);
363
+ if (!onChain)
364
+ return same ? 0 : 3;
365
+ const { info } = onChain;
366
+ log(` authority ${info.authority ?? "none: the program is immutable"}${info.slot !== undefined ? `; last deployed in slot ${info.slot}` : ""}`);
367
+ const { fetchIdl, metadataAddress } = await import("./idlpublish.js");
368
+ const { canonicalJson } = await import("./onchain.js");
369
+ // No canonical metadata account means nothing was published; one that cannot be read is an error, not "none".
370
+ const at = await metadataAddress(onChain.address, null);
371
+ const exists = (await onChain.rpc.getAccountInfo(at, { encoding: "base64" }).send()).value !== null;
372
+ if (!exists) {
373
+ log(" idl none published on chain (typesol idl publish)");
374
+ return same ? 0 : 3;
375
+ }
376
+ let published;
377
+ try {
378
+ published = await fetchIdl(onChain.rpc, onChain.address);
379
+ }
380
+ catch (e) {
381
+ log(` IDL ERROR the IDL at ${at} could not be read: ${e.message}`);
382
+ return 3;
383
+ }
384
+ const idlSame = canonicalJson(published) === canonicalJson(res.idl);
385
+ log(idlSame
386
+ ? " idl match: the published IDL is the one the source builds"
387
+ : " IDL MISMATCH the published IDL differs from the one the source builds");
388
+ return same && idlSame ? 0 : 3;
287
389
  },
288
390
  },
289
391
  profile: {
@@ -356,6 +458,31 @@ function readJson(file) {
356
458
  throw new Error(`${file}: not valid JSON (${e.message})`);
357
459
  }
358
460
  }
461
+ /** Make the compiler read `text` for `file` (instead of the disk) until the returned function is called. */
462
+ function overrideFile(file, text) {
463
+ const sys = ts.sys;
464
+ const { readFile, fileExists } = sys;
465
+ sys.readFile = (p, enc) => (path.resolve(p) === file ? text : readFile.call(sys, p, enc));
466
+ sys.fileExists = (p) => path.resolve(p) === file || fileExists.call(sys, p);
467
+ return () => {
468
+ sys.readFile = readFile;
469
+ sys.fileExists = fileExists;
470
+ };
471
+ }
472
+ /** The Solana CLI's cluster monikers, as `solana --url` accepts them; anything else is used as given. */
473
+ function clusterUrl(url) {
474
+ const monikers = {
475
+ "mainnet-beta": "https://api.mainnet-beta.solana.com",
476
+ m: "https://api.mainnet-beta.solana.com",
477
+ devnet: "https://api.devnet.solana.com",
478
+ d: "https://api.devnet.solana.com",
479
+ testnet: "https://api.testnet.solana.com",
480
+ t: "https://api.testnet.solana.com",
481
+ localhost: "http://127.0.0.1:8899",
482
+ l: "http://127.0.0.1:8899",
483
+ };
484
+ return monikers[url] ?? url;
485
+ }
359
486
  /** json_rpc_url / keypair_path from a Solana CLI config (the given file, or the CLI's default one). */
360
487
  function solanaConfig(file) {
361
488
  const configPath = file ?? path.join(os.homedir(), ".config/solana/cli/config.yml");
package/dist/clientgen.js CHANGED
@@ -43,6 +43,7 @@ export const TYPESOL_ERRORS = {
43
43
  3000: "AccountDiscriminatorAlreadySet",
44
44
  3002: "AccountDiscriminatorMismatch",
45
45
  3003: "AccountDidNotDeserialize",
46
+ 3004: "AccountDidNotSerialize",
46
47
  3005: "AccountNotEnoughKeys",
47
48
  3006: "AccountNotMutable",
48
49
  3007: "AccountOwnedByWrongProgram",
@@ -383,6 +384,8 @@ export function generateClient(idl, opts = {}) {
383
384
  const arrayLen = (t) => (typeof t === "object" && t.array ? Number(t.array[1]) : NaN);
384
385
  if (/^FixedString\d+$/.test(n) && f[0].name === "len" && Number.isInteger(arrayLen(f[1].type)))
385
386
  return { kind: "fstring", cap: arrayLen(f[1].type) };
387
+ if (/^FixedVec\d+_/.test(n) && f[0].name === "len" && f[1].name === "items" && Number.isInteger(arrayLen(f[1].type)))
388
+ return { kind: "fvec", elem: f[1].type.array[0], cap: arrayLen(f[1].type) };
386
389
  if (n.startsWith("FixedOption_") && f[0].name === "is_some")
387
390
  return { kind: "foption", inner: f[1].type };
388
391
  if (n.startsWith("FixedEnum_") && f[0].name === "tag" && defs.has(n.slice("FixedEnum_".length)) && Number.isInteger(arrayLen(f[1].type)))
@@ -476,6 +479,8 @@ export function generateClient(idl, opts = {}) {
476
479
  const s = special(n);
477
480
  if (s?.kind === "fstring")
478
481
  return "string";
482
+ if (s?.kind === "fvec")
483
+ return `Array<${tsType(s.elem, dir)}>`;
479
484
  if (s?.kind === "foption")
480
485
  return `${tsType(s.inner, dir)} | null`;
481
486
  if (s?.kind === "fenum")
@@ -484,6 +489,7 @@ export function generateClient(idl, opts = {}) {
484
489
  }
485
490
  let needsFixedString = false;
486
491
  let needsFixedOption = false;
492
+ let needsFixedVec = false;
487
493
  let needsUnitEnum = false;
488
494
  function codec(t) {
489
495
  if (typeof t === "string") {
@@ -525,6 +531,11 @@ export function generateClient(idl, opts = {}) {
525
531
  const s = special(n);
526
532
  if (s?.kind === "fstring")
527
533
  return ((needsFixedString = true), `fixedString(${s.cap})`);
534
+ if (s?.kind === "fvec") {
535
+ if (idlSize(s.elem) === undefined)
536
+ throw new ClientIdlError(`${n}: the elements have no fixed size`);
537
+ return ((needsFixedVec = true), `fixedVec(${codec(s.elem)}, ${idlSize(s.elem)}, ${s.cap})`);
538
+ }
528
539
  if (s?.kind === "foption") {
529
540
  const size = idlSize(s.inner);
530
541
  if (size === undefined)
@@ -560,6 +571,10 @@ export function generateClient(idl, opts = {}) {
560
571
  const sp = special(n);
561
572
  if (sp?.kind === "fstring")
562
573
  return 4 + sp.cap;
574
+ if (sp?.kind === "fvec") {
575
+ const e = idlSize(sp.elem);
576
+ return e === undefined ? undefined : 4 + e * sp.cap;
577
+ }
563
578
  if (sp?.kind === "fenum")
564
579
  return sp.size;
565
580
  if (sp?.kind === "foption") {
@@ -982,6 +997,28 @@ export function generateClient(idl, opts = {}) {
982
997
  ` });`,
983
998
  ],
984
999
  });
1000
+ if (needsFixedVec)
1001
+ helperDefs.push({
1002
+ name: "fixedVec",
1003
+ lines: [
1004
+ `/** TypeSOL FixedVec<T, N> in account data: u32 length, then room for N elements (unused ones zero). */`,
1005
+ `const fixedVec = <From, To extends From>(item: Codec<From, To>, size: number, cap: number): Codec<From[], To[]> =>`,
1006
+ ` createCodec<From[], To[]>({`,
1007
+ ` fixedSize: 4 + size * cap,`,
1008
+ ` write: (value, bytes, offset) => {`,
1009
+ ` if (value.length > cap) throw new Error(value.length + " elements; the field holds " + cap);`,
1010
+ ` bytes.fill(0, offset, offset + 4 + size * cap);`,
1011
+ ` getU32Codec().write(value.length, bytes, offset);`,
1012
+ ` value.forEach((v, i) => item.write(v, bytes, offset + 4 + i * size));`,
1013
+ ` return offset + 4 + size * cap;`,
1014
+ ` },`,
1015
+ ` read: (bytes, offset) => {`,
1016
+ ` const n = Math.min(getU32Codec().read(bytes, offset)[0], cap);`,
1017
+ ` return [Array.from({ length: n }, (_, i) => item.read(bytes, offset + 4 + i * size)[0]), offset + 4 + size * cap];`,
1018
+ ` },`,
1019
+ ` });`,
1020
+ ],
1021
+ });
985
1022
  if (needsUnitEnum)
986
1023
  helperDefs.push({
987
1024
  name: "unitEnum",
@@ -1,12 +1,18 @@
1
1
  import ts from "typescript";
2
2
  import { Reporter } from "./diagnostics.js";
3
- import { type DataEnumDef, type EnumDef, type ProgramModel, type ScalarType, type StructDef, type ValueType, isScalar } from "./model.js";
3
+ import { type DataEnumDef, type EnumDef, type GenericHelper, type Helper, type ProgramModel, type ScalarType, type StructDef, type ValueType, isScalar } from "./model.js";
4
4
  export declare const LIB_PATH: string;
5
+ /** Why a name cannot be used as a C member, or undefined when it can. */
6
+ export declare function cNameProblem(name: string): string | undefined;
5
7
  /** Solana's account size limit, and what a program can allocate itself through a CPI (Init<T>). */
6
8
  export declare const MAX_ACCOUNT_SIZE: number;
9
+ /** sol_set_return_data's limit. */
10
+ export declare const MAX_RETURN_DATA = 1024;
7
11
  export declare const MAX_CPI_ALLOCATION: number;
8
12
  export interface Analysis {
9
13
  model: ProgramModel;
14
+ /** A generic helper's instance for these type arguments (analyzed once, then kept in `model.helpers`). */
15
+ instantiate(g: GenericHelper, typeArgs: ScalarType[]): Helper;
10
16
  /** TypeScript's checker: used for narrowing (which variant a data-enum value is in a branch). */
11
17
  checker: ts.TypeChecker;
12
18
  sourceFile: ts.SourceFile;
@@ -19,6 +25,8 @@ export interface Analysis {
19
25
  /** What type resolution needs: imports, unit enums, and where to record Option<T> uses. */
20
26
  export interface TypeScope {
21
27
  imports: Map<string, string>;
28
+ /** Inside a generic helper's instance: type parameter -> the type it stands for. */
29
+ typeArgs?: Map<string, ScalarType>;
22
30
  enums: Map<string, EnumDef>;
23
31
  dataEnums?: Map<string, DataEnumDef>;
24
32
  options: Map<string, ScalarType | {
@@ -45,4 +53,6 @@ export declare function resolveOptionType(node: ts.TypeNode, scope: TypeScope):
45
53
  }) | undefined;
46
54
  /** Values a local, argument or helper parameter can hold. */
47
55
  export declare function resolveValueType(node: ts.TypeNode, scope: TypeScope, allowVec?: boolean): ValueType | undefined;
56
+ /** The helper (or generic) a call's callee names: a local function by its declaration, others by name. */
57
+ export declare function calleeName(callee: ts.Identifier, decls: Map<ts.Node, string>, checker: ts.TypeChecker): string | undefined;
48
58
  export { isScalar };