@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.
- package/CHANGELOG.md +107 -0
- package/README.md +11 -7
- package/dist/cli.js +147 -20
- package/dist/clientgen.js +37 -0
- package/dist/compiler/analyze.d.ts +11 -1
- package/dist/compiler/analyze.js +307 -37
- package/dist/compiler/emit/base.d.ts +7 -0
- package/dist/compiler/emit/base.js +62 -2
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +232 -0
- package/dist/compiler/emit/calls.js +50 -15
- package/dist/compiler/emit/cpi.d.ts +32 -8
- package/dist/compiler/emit/cpi.js +706 -25
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +126 -4
- package/dist/compiler/emit/function.js +45 -12
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +12 -0
- package/dist/compiler/emit/shared.js +22 -4
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +141 -7
- package/dist/compiler/emit/storage.d.ts +6 -1
- package/dist/compiler/emit/storage.js +82 -17
- package/dist/compiler/idl.js +61 -18
- package/dist/compiler/model.d.ts +56 -4
- package/dist/compiler/model.js +8 -2
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +367 -0
- package/dist/templates.js +49 -154
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.js +11 -1
- package/docs/architecture.md +3 -0
- package/docs/cli.md +16 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +19 -14
- package/docs/mainnet.md +5 -4
- package/docs/migrate-from-anchor.md +113 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +7 -0
- package/lib/typesol.d.ts +325 -13
- package/package.json +19 -1
- package/runtime/typesol_rt.h +54 -9
- package/ts-plugin/index.js +112 -0
- 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`
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 ${
|
|
286
|
-
|
|
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 };
|