@typesol/cli 0.14.0 → 0.16.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 +81 -0
- package/README.md +35 -5
- package/dist/cli.js +161 -24
- package/dist/compiler/analyze.d.ts +4 -0
- package/dist/compiler/analyze.js +51 -9
- package/dist/compiler/emit/base.js +1 -1
- package/dist/compiler/emit/calls.js +7 -0
- package/dist/compiler/emit/cpi.js +188 -9
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +107 -2
- package/dist/compiler/emit/function.js +11 -1
- package/dist/compiler/emit/shared.js +3 -2
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +131 -5
- package/dist/compiler/emit/storage.d.ts +2 -1
- package/dist/compiler/emit/storage.js +24 -7
- package/dist/compiler/idl.js +28 -14
- package/dist/compiler/model.d.ts +16 -3
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/idlpublish.js +43 -9
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +311 -0
- package/dist/templates.d.ts +9 -0
- package/dist/templates.js +324 -0
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.d.ts +3 -2
- package/dist/workspace.js +18 -106
- package/docs/architecture.md +3 -0
- package/docs/cli.md +17 -3
- package/docs/language.md +11 -10
- package/docs/mainnet.md +100 -0
- package/docs/migrate-from-anchor.md +104 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +10 -2
- package/lib/typesol.d.ts +68 -12
- package/package.json +19 -1
- package/runtime/typesol_rt.h +25 -5
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,87 @@ 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.16.0 — 2026-10-28
|
|
9
|
+
|
|
10
|
+
Everyday TypeScript that Anchor developers reach for (`for … of`, `??`, destructuring, `**`, structs returned from
|
|
11
|
+
helpers and instructions, strings in enum variants), tools to move an Anchor program over (`typesol port`, a migration
|
|
12
|
+
guide) and to work on it (`typesol/testing`, an editor plugin, `verify` against the cluster), Token-2022 extension
|
|
13
|
+
setup and Metaplex creators. No IDL or error-code changes for existing programs. Not externally audited (see
|
|
14
|
+
SECURITY.md).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Metaplex `createV3(…, isMutable, { creators, collection }, signerSeeds?)`: 1–5 creators (shares checked to add up to 100
|
|
18
|
+
when literal) and an unverified collection mint, checked against the real Token Metadata program.
|
|
19
|
+
- Data-enum variants can hold a `string` (`type Cmd = { kind: "Say"; text: string } | …`), as Anchor enums with `String`
|
|
20
|
+
payloads: Borsh in instruction arguments and events, a value in code (a copy when built from account text), refused
|
|
21
|
+
in account data. The IDL describes them as Borsh enums only.
|
|
22
|
+
- `typesol port <idl.json>`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, one function per
|
|
23
|
+
instruction with its accounts and arguments). Discriminators, account names and error codes are kept, so existing
|
|
24
|
+
clients keep working where layouts match; notes list where Anchor's Borsh layout differs. Round-trips every example
|
|
25
|
+
(example → IDL → port → IDL) with the same instructions, accounts, layouts and error codes.
|
|
26
|
+
- Token-2022 extension initializers on `Token2022Program` / `TokenInterface` (refused on `TokenProgram`): transfer fee,
|
|
27
|
+
mint close authority, non-transferable, permanent delegate, default account state, metadata / group / group-member
|
|
28
|
+
pointers, transfer hook, interest-bearing, immutable owner. Byte layouts checked against the official
|
|
29
|
+
@solana-program/token-2022 decoder on real mints.
|
|
30
|
+
- `typesol/ts-plugin`: a TypeScript language-service plugin that shows the compiler's own errors in the editor, for
|
|
31
|
+
the unsaved text, once TypeScript has none. `typesol init` projects enable it in `programs/tsconfig.json` (the
|
|
32
|
+
editor must use the workspace's TypeScript). `typesol check` gains `--json` and `--stdin`.
|
|
33
|
+
- `typesol/testing`: `testProgram(name, import.meta.url)` builds a project program into LiteSVM, with `wallet()`, `pda(…)`,
|
|
34
|
+
`ix(name, args, accounts)` (accounts by name, flags, fixed addresses and PDAs from the IDL, plain argument values),
|
|
35
|
+
`send`, `ok`, `errorCode`, `account` (decoded to bigints and address strings), `returned` and `balance`. The
|
|
36
|
+
`typesol init` templates' tests use it (the counter test went from 70 lines to 20). See docs/test-your-program.md.
|
|
37
|
+
- `a ** b` on integers, every multiplication checked (5100 on overflow or a negative exponent, where JS throws). A
|
|
38
|
+
literal exponent unrolls at compile time; otherwise a square-and-multiply loop runs.
|
|
39
|
+
- Object destructuring: `const { owner, pos: p } = acct` (`let` for scalars) from a name or a field path, read like
|
|
40
|
+
`acct.owner` (scalars copied, nested structs referenced). Rest elements, defaults and nested patterns are refused.
|
|
41
|
+
- Instructions can return a fixed-size struct as return data (a literal, a stored struct or a helper's result), at
|
|
42
|
+
most 1024 bytes. It goes out in its fixed layout, which the IDL describes under `returns`, so Anchor clients and the
|
|
43
|
+
generated TypeScript client decode it.
|
|
44
|
+
- Helpers can return a fixed-size struct: `return { x, y }`, a stored struct or another helper's result. The caller
|
|
45
|
+
gets its own copy to bind (`const p: Point = at(1n, 2n)`), store (`acct.home = at(…)`, `arr.set(i, at(…))`) or read
|
|
46
|
+
(`at(…).x`). Structs with a Vec or variable-length strings cannot be returned.
|
|
47
|
+
- `a ?? b` on Options of integers, booleans and Pubkeys (b is evaluated only when a is empty; the result is an
|
|
48
|
+
Option when b can be null), and `x!` on an Option (its value; an empty one aborts with 5108).
|
|
49
|
+
- `for (const x of list)` over a `Vec` (account field or argument) or a `FixedArray`: each `x` is what `list.get(i)`
|
|
50
|
+
gives (a value, or a reference into account data for structs, with the same aliasing checks), the length is read
|
|
51
|
+
again before every step as in JS, and `break` / `continue` work. Remaining accounts are still iterated by index.
|
|
52
|
+
- `typesol verify <program>` without a `.so` reads the deployed program from the cluster (`--address`, or the address
|
|
53
|
+
in `typesol.json`; `--url`, cluster monikers or the Solana CLI config) under loader v2, v3 or v4, prints its upgrade
|
|
54
|
+
authority or that it is immutable and its last deploy slot, and compares the IDL published on chain with the built
|
|
55
|
+
one. A published IDL that differs is a mismatch (exit 3). No `solana program dump` step needed.
|
|
56
|
+
- `typesol idl publish --url` accepts the Solana CLI's cluster monikers (`mainnet-beta`, `devnet`, …).
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- `createV3` evaluates its arguments in source order (a creator share computed by a helper that changes account text
|
|
60
|
+
no longer changes the name or fee read before it).
|
|
61
|
+
- A function whose stack frame exceeds SBPF v0's 4 KiB used to build and then corrupt memory at runtime (the toolchain
|
|
62
|
+
only printed a note). The build now fails, naming the instruction or helper and its frame size (v1/v2 frames grow
|
|
63
|
+
dynamically and are not limited).
|
|
64
|
+
|
|
65
|
+
### Docs
|
|
66
|
+
- docs/migrate-from-anchor.md: Anchor constructs, account data, code and tooling mapped to TypeSOL, with the `typesol port`
|
|
67
|
+
workflow and which account layouts differ.
|
|
68
|
+
|
|
69
|
+
## 0.15.0 — 2026-10-27
|
|
70
|
+
|
|
71
|
+
Project templates for `typesol init`, a mainnet deployment guide, and clearer `typesol idl publish` failures. No
|
|
72
|
+
language, IDL or error-code changes. Not externally audited (see SECURITY.md).
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
- `typesol init --template counter|vault|escrow`: start from a PDA counter (the default, as before), a SOL vault or an
|
|
76
|
+
SPL token escrow. Each template is the matching `examples/` program with a LiteSVM test of its main flow and of a
|
|
77
|
+
refused call; the escrow project also gets `@solana-program/token`.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- `typesol idl publish` names the reason a write failed (the transaction's error and its last log lines) instead of
|
|
81
|
+
"The provided transaction plan failed to execute".
|
|
82
|
+
|
|
83
|
+
### Docs
|
|
84
|
+
- docs/mainnet.md: from `typesol init` to a verified program on mainnet-beta (wallet, cost, deploy, IDL, `verify`,
|
|
85
|
+
freezing and closing).
|
|
86
|
+
- README: the official escrow on mainnet-beta in all four versions, with each transaction's compute units linked to
|
|
87
|
+
Solscan; the TypeSOL program's IDL is published on chain.
|
|
88
|
+
|
|
8
89
|
## 0.14.0 — 2026-10-27
|
|
9
90
|
|
|
10
91
|
PDAs are derived without the PDA syscalls, so instructions that create, check or search for PDAs use up to a few
|
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">
|
|
@@ -152,6 +152,9 @@ npx typesol build # target/counter/{counter.so, coun
|
|
|
152
152
|
npx typesol deploy --url http://127.0.0.1:8899 --keypair ~/.config/solana/id.json
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
+
`--template vault` or `--template escrow` starts from a SOL vault or an SPL token escrow instead of the counter, each
|
|
156
|
+
with a passing LiteSVM test.
|
|
157
|
+
|
|
155
158
|
The npm package is [`@typesol/cli`](https://www.npmjs.com/package/@typesol/cli). Projects install it under the name
|
|
156
159
|
`typesol`, which is what programs import (`import { … } from "typesol"`); `typesol init` sets that up. To add it to an
|
|
157
160
|
existing project: `npm install -D typesol@npm:@typesol/cli`.
|
|
@@ -159,6 +162,10 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
|
|
|
159
162
|
Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
|
|
160
163
|
program's address.
|
|
161
164
|
|
|
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).
|
|
168
|
+
|
|
162
169
|
## How it works
|
|
163
170
|
|
|
164
171
|
```
|
|
@@ -185,7 +192,7 @@ Runtime failures point back to your source: `Error 6000 Unauthorized (counter.ts
|
|
|
185
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 |
|
|
186
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 |
|
|
187
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 |
|
|
188
|
-
| **Control flow** | `if`, `for`, `while`, `switch` (with narrowing on data enums), ternaries, helper functions across multiple files |
|
|
195
|
+
| **Control flow** | `if`, `for`, `for … of`, `while`, `switch` (with narrowing on data enums), ternaries, `??`, destructuring, helper functions (returning structs too) across multiple files |
|
|
189
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`) |
|
|
190
197
|
| **Events** | `emit` (log-based) and `emitCpi` (Anchor `emit_cpi!`, through a self-CPI) |
|
|
191
198
|
| **Crypto & oracles** | `sha256`, `keccak256`, Merkle proofs, Ed25519 and secp256k1 signature checks, Pyth pull oracle, Switchboard |
|
|
@@ -204,7 +211,9 @@ The full reference, with every type, check and built-in, is in [docs/language.md
|
|
|
204
211
|
| `typesol idl publish` | upload the IDL to the Program Metadata program (canonical when the upgrade authority signs) |
|
|
205
212
|
| `typesol cpi <idl.json>` | generate typed CPI helpers for another program |
|
|
206
213
|
| `typesol client <idl.json>` | generate a typed TypeScript client from an Anchor IDL (`typesol build` writes one per program) |
|
|
207
|
-
| `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`) |
|
|
208
217
|
| `typesol profile` | compute units per TypeScript line, from a `--profile` build's logs |
|
|
209
218
|
| `typesol emit-c` | print the generated C |
|
|
210
219
|
|
|
@@ -246,6 +255,27 @@ CPI account records only for the accounts a call lists. It uses fewer compute un
|
|
|
246
255
|
instruction here, while also refusing duplicate writable accounts as Anchor does. Fewer compute
|
|
247
256
|
units are not a safety claim. The differential tests also run the escrow against its Anchor twin ([docs/testing.md](docs/testing.md)).
|
|
248
257
|
|
|
258
|
+
### On mainnet
|
|
259
|
+
|
|
260
|
+
The official escrow ([tokens/escrow](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow)),
|
|
261
|
+
deployed to Solana mainnet-beta in all four versions on 2026-10-27 and called with the same mints, amounts and accounts,
|
|
262
|
+
one instruction per transaction. Each figure is the compute units of that transaction, linked to Solscan; the lowest per row is in bold. The deployed
|
|
263
|
+
programs match these builds byte for byte (`typesol verify` for TypeSOL), and the TypeSOL program's IDL is published
|
|
264
|
+
on chain with `typesol idl publish`.
|
|
265
|
+
|
|
266
|
+
| Transaction | TypeSOL | Pinocchio | Native Rust | Anchor |
|
|
267
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
268
|
+
| make_offer (offer 1) | [20,304](https://solscan.io/tx/32PVva77dT1rSWrMWX2sHTMPZiwztdEZYtpANaxrsuJxPcd3hnV6pDgJJaV4LkL3bX6ALDoHMNF8fjngtodYHBS1) | [**19,645**](https://solscan.io/tx/528BdMTrnLWauSXVh2vaFfy4XwKFpbyt95hQ6w3fwjtpkT85RPkRhGydunAizNDRqTTLPxcEVMrdcDvRer4H9LRE) | [30,698](https://solscan.io/tx/4MQgt75MvojaAkfxnvrE2yJWabGpE8bsZhNq2CeGJxHZg6uD6A64Kv71JmrsgiGnaEovvv8K34DYL49owLjhs4QJ) | [38,648](https://solscan.io/tx/9T1ZY9JaSMb1GMqv1i22AuZuJQN6B5XcgqeDv5tGqEm5Uox3MBTdXsECt59GwcXkFG9QKgXJyqndTEzpSXhbAVM) |
|
|
269
|
+
| take_offer (offer 1) | [**7,998**](https://solscan.io/tx/49EQwgnKShFdsAs37AEzYmv6tLY8DipcCjmRgqwj4fVTFEgVVWdEXw5AvuEufL5ymJYs4T4hVFdkENW9XK9een5m) | [25,206](https://solscan.io/tx/21MGkXpKRe4c7EeA1hC6JHevsk78xUdbcWmyqG47EbLeCNKSMTzTjkJQqtydnQmV8uHPmr6bJHnvQmmQTSZB7Am2) | [28,612](https://solscan.io/tx/B1hu1EL4Y5vWhBVWdyZEpzRNJb8kd6G9CgN7KtyNNGpQ84o9dfn1sdy9Z1UCHECNtp24Mw8bEH2iYz2YADdEUjx) | [32,835](https://solscan.io/tx/3ZLuQQz7Bx5YdgmFXpVQWHn5kyc8v39jr2BnfofH6sqenzfJpAdVm4h38uYcHwYygnfso92kJe7HRHeASsABoKPQ) |
|
|
270
|
+
| make_offer (offer 2) | [21,804](https://solscan.io/tx/xuu9KXbZvzGDpTEEcDE9LYvBqmPen81y6ty92e6crRxT8m4Mc4XyxhjWHbjVNsr9RE3QY4M1j1Sw9cH7PRMKKuc) | [**19,645**](https://solscan.io/tx/LLzS13AJrqACGoT1VHZN278ij39pNkttb1Z4Wh2AYXknWxwjaiaiMeD7WZomjP6TBv18RHfQHUfogQzApvXr2mC) | [35,198](https://solscan.io/tx/4xiKfzyccVatef1T52tZ3VAJwcx5R7iWgHWezBzZ3p2WpvoLzMp1PSUgr7x3sspE53mDE5JaghaNPKuUohVgPuwc) | [47,648](https://solscan.io/tx/4mxW8D5zpY8t4fL8JRbvhp6ejU6CUvB3iWmFjACipzexW6tZfFeatfnEnDTX3Y97BxsvAftoX9eQauiVu84xrLxh) |
|
|
271
|
+
| refund_offer (offer 2) | [**5,961**](https://solscan.io/tx/4kY7s3y95Lrd2WGCWGPWW3zFBVtEKH6EskdscuYmRCRRgPt2rJTZ1CmZMGUUkHs7aai9AygEiTSUG23EMUrFr7rg) | [13,387](https://solscan.io/tx/QUXWwdbKRcT6WhZvSe4U8nxr4GAMaPNV6pZ1fwrC28kRxdJm7HKXVr2DFRuKD9bEzoS6hFdiA2rLr1mQWyrN3oz) | [17,731](https://solscan.io/tx/5zj7E4c61j1JzUsUPC1fNs9ECFLVmCug9rsdYsVLn7YD1RSxdbAJUQDam1BX3epTzW6ed2uFGwTiNhwaYo7vqdwm) | [23,576](https://solscan.io/tx/5Kjwnq5tN8mzj5s67VGxSry4kRzZk6HLdgXW65FyTvpUeC1D7aQoDGTwzrA7kqyYEvV9y9znnZmLaqU98zFrJJo4) |
|
|
272
|
+
| program | [65ybkcg6…](https://solscan.io/account/65ybkcg6mQa9t5EuHtirRdvf2qAu12qRiK6wYBbGk16P) | [FrQwq3iU…](https://solscan.io/account/FrQwq3iUa8ZkWEne1y27webXTAyLgH4Hj6t986t6VS2G) | [5pnmi1Md…](https://solscan.io/account/5pnmi1MdxzXKmgX7ooRSE212rzZK8rb7nNpRTSMqP8V1) | [EG7M3sLL…](https://solscan.io/account/EG7M3sLLwB7MhmtzwYsaxbhZaFDbshL9hmVK79bQVyVq) |
|
|
273
|
+
|
|
274
|
+
Most of make_offer is the Associated Token Account program creating the vault (about 13,500 CU in every version); it
|
|
275
|
+
searches for the vault address's bump itself at 1,500 CU a try, which is why TypeSOL's offer 2 cost 1,500 more than
|
|
276
|
+
offer 1 (its own part was 6,719 CU both times). The Pinocchio version takes the offer PDA's bump from the client instead
|
|
277
|
+
of searching for it (its own part: 6,051 CU). Mainnet figures differ from the LiteSVM means above.
|
|
278
|
+
|
|
249
279
|
## How it is tested
|
|
250
280
|
|
|
251
281
|
- **Execution tests.** Every feature is compiled and run in LiteSVM.
|
|
@@ -276,7 +306,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
276
306
|
|
|
277
307
|
## Status
|
|
278
308
|
|
|
279
|
-
**TypeSOL is pre-1.0 (0.
|
|
309
|
+
**TypeSOL is pre-1.0 (0.16).** Releases follow the organization's
|
|
280
310
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
281
311
|
change.
|
|
282
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,12 +5,15 @@ 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";
|
|
13
15
|
import { formatProfile, profileLogs } from "./profile.js";
|
|
16
|
+
import { TEMPLATE_NAMES } from "./templates.js";
|
|
14
17
|
import { buildWorkspace, CONFIG, deployArgs, findWorkspace, programAddress, programForEntry, programPaths, scaffold } from "./workspace.js";
|
|
15
18
|
const ASSETS = fileURLToPath(new URL("../assets/", import.meta.url));
|
|
16
19
|
/** A usage problem: printed with the command's help, exit code 2. */
|
|
@@ -38,14 +41,23 @@ function banner() {
|
|
|
38
41
|
const SBPF = `--sbpf v0|v1|v2 SBPF version (default v0, or "sbpf" in ${CONFIG}); v1/v2 need clusters that enabled them`;
|
|
39
42
|
const COMMANDS = {
|
|
40
43
|
init: {
|
|
41
|
-
usage: "typesol init [<dir>] [--name <program>]",
|
|
42
|
-
help:
|
|
43
|
-
|
|
44
|
+
usage: "typesol init [<dir>] [--name <program>] [--template counter|vault|escrow]",
|
|
45
|
+
help: [
|
|
46
|
+
"Create a project: typesol.json, programs/<name>.ts, tests/<name>.test.ts (LiteSVM), package.json, tsconfig.json, .gitignore.",
|
|
47
|
+
"Refuses to overwrite files. <name> must be snake_case (default: derived from <dir>).",
|
|
48
|
+
"--template counter a PDA counter per user (default)",
|
|
49
|
+
"--template vault a SOL vault at a PDA: deposit, withdraw above rent, close",
|
|
50
|
+
"--template escrow an SPL token escrow: make, take, refund, with the offer PDA signing for its vault",
|
|
51
|
+
].join("\n "),
|
|
52
|
+
options: { name: { type: "string" }, template: { type: "string" } },
|
|
44
53
|
run(v, args) {
|
|
45
54
|
if (args.length > 1)
|
|
46
55
|
throw new UsageError("init takes at most one directory");
|
|
47
56
|
if (v.name !== undefined && !/^[a-z][a-z0-9_]*$/.test(v.name))
|
|
48
57
|
throw new UsageError(`--name "${v.name}" must be snake_case (a-z, 0-9, _; starting with a letter)`);
|
|
58
|
+
const template = v.template ?? "counter";
|
|
59
|
+
if (!TEMPLATE_NAMES.includes(template))
|
|
60
|
+
throw new UsageError(`--template "${template}" is not one of ${TEMPLATE_NAMES.join(", ")}`);
|
|
49
61
|
const root = path.resolve(args[0] ?? ".");
|
|
50
62
|
const derived = path
|
|
51
63
|
.basename(root)
|
|
@@ -53,7 +65,7 @@ const COMMANDS = {
|
|
|
53
65
|
.replace(/^_+|_+$/g, "")
|
|
54
66
|
.toLowerCase();
|
|
55
67
|
const name = v.name ?? (/^[a-z]/.test(derived) ? derived : `p_${derived || "program"}`);
|
|
56
|
-
for (const f of scaffold(root, name))
|
|
68
|
+
for (const f of scaffold(root, name, template))
|
|
57
69
|
log(` created ${path.join(root, f)}`);
|
|
58
70
|
log(`\n next: cd ${path.relative(process.cwd(), root) || "."} && npm install && npm test`);
|
|
59
71
|
return 0;
|
|
@@ -110,13 +122,35 @@ const COMMANDS = {
|
|
|
110
122
|
},
|
|
111
123
|
},
|
|
112
124
|
check: {
|
|
113
|
-
usage: "typesol check <program.ts>",
|
|
114
|
-
help:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|
|
120
154
|
},
|
|
121
155
|
},
|
|
122
156
|
"emit-c": {
|
|
@@ -185,7 +219,7 @@ const COMMANDS = {
|
|
|
185
219
|
const kit = await import("@solana/kit");
|
|
186
220
|
const { publishIdl, rpcChain } = await import("./idlpublish.js");
|
|
187
221
|
const signer = await kit.createKeyPairSignerFromBytes(readKeypair(keyFile));
|
|
188
|
-
const chain = rpcChain(kit.createSolanaRpc(rpcUrl));
|
|
222
|
+
const chain = rpcChain(kit.createSolanaRpc(clusterUrl(rpcUrl)));
|
|
189
223
|
for (const name of names(ws, args)) {
|
|
190
224
|
// The IDL only needs the compiled model and the program address: the deployed binary is left alone.
|
|
191
225
|
const { address } = programAddress(ws, name);
|
|
@@ -218,6 +252,30 @@ const COMMANDS = {
|
|
|
218
252
|
return 0;
|
|
219
253
|
},
|
|
220
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
|
+
},
|
|
221
279
|
client: {
|
|
222
280
|
usage: "typesol client <idl.json> [--out <file.ts>] [--address <base58>]",
|
|
223
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.",
|
|
@@ -239,41 +297,95 @@ const COMMANDS = {
|
|
|
239
297
|
},
|
|
240
298
|
},
|
|
241
299
|
verify: {
|
|
242
|
-
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]",
|
|
243
301
|
help: [
|
|
244
|
-
"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.",
|
|
245
307
|
"The SBPF version is read from the deployed binary unless --sbpf is given. Reproducible builds need the same",
|
|
246
308
|
"TypeSOL version and platform-tools as the original (see its <name>.build.json).",
|
|
247
309
|
].join("\n "),
|
|
248
|
-
options: { sbpf: { type: "string" } },
|
|
249
|
-
run(v, args) {
|
|
250
|
-
if (args.length
|
|
251
|
-
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");
|
|
252
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`);
|
|
253
319
|
const ws = findWorkspace(process.cwd());
|
|
254
320
|
const name = ws && (ws.programs[target] ? target : programForEntry(ws, target));
|
|
255
321
|
const entry = ws && name ? programPaths(ws, name).entry : target;
|
|
256
322
|
if (!existsSync(entry))
|
|
257
323
|
throw new UsageError(`${entry} does not exist`);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
}
|
|
261
346
|
// ELF e_flags (offset 0x30) hold the SBPF version: 0 for v0, 1 for v1, 2 for v2.
|
|
262
347
|
const flags = deployed.length > 0x34 && deployed.readUInt32BE(0) === 0x7f454c46 ? deployed.readUInt32LE(0x30) : 0;
|
|
263
348
|
const sbpf = sbpfOf(v) ?? SBPF_VERSIONS[flags] ?? "v0";
|
|
264
349
|
const dir = mkdtempSync(path.join(os.tmpdir(), "typesol-verify-"));
|
|
265
350
|
let res, same;
|
|
266
351
|
try {
|
|
267
|
-
res = build(entry, dir, "11111111111111111111111111111111", { sbpf });
|
|
352
|
+
res = build(entry, dir, onChain?.address ?? "11111111111111111111111111111111", { sbpf });
|
|
268
353
|
same = verifyBinary(deployed, readFileSync(res.soPath));
|
|
269
354
|
}
|
|
270
355
|
finally {
|
|
271
356
|
rmSync(dir, { recursive: true, force: true });
|
|
272
357
|
}
|
|
358
|
+
const what = deployedPath ??
|
|
359
|
+
`${onChain.address} (${onChain.info.dataAccount === onChain.address ? "" : `data ${onChain.info.dataAccount}, `}loader ${onChain.info.loader})`;
|
|
273
360
|
log(same
|
|
274
361
|
? ` match ${res.manifest.binary} (sbpf ${sbpf})`
|
|
275
|
-
: ` MISMATCH ${
|
|
276
|
-
|
|
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;
|
|
277
389
|
},
|
|
278
390
|
},
|
|
279
391
|
profile: {
|
|
@@ -346,6 +458,31 @@ function readJson(file) {
|
|
|
346
458
|
throw new Error(`${file}: not valid JSON (${e.message})`);
|
|
347
459
|
}
|
|
348
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
|
+
}
|
|
349
486
|
/** json_rpc_url / keypair_path from a Solana CLI config (the given file, or the CLI's default one). */
|
|
350
487
|
function solanaConfig(file) {
|
|
351
488
|
const configPath = file ?? path.join(os.homedir(), ".config/solana/cli/config.yml");
|
|
@@ -2,8 +2,12 @@ import ts from "typescript";
|
|
|
2
2
|
import { Reporter } from "./diagnostics.js";
|
|
3
3
|
import { type DataEnumDef, type EnumDef, 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;
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -17,7 +17,7 @@ const C_RESERVED = new Set([
|
|
|
17
17
|
..."NULL SUCCESS HEAP_LENGTH HEAP_START_ADDRESS MAX_PERMITTED_DATA_INCREASE MAX_RETURN_DATA MAX_SEED_LENGTH_EXCEEDED INVALID_SEEDS SIZE_PUBKEY memcpy memset strlen static_assert offsetof sol_log_64".split(" "),
|
|
18
18
|
]);
|
|
19
19
|
/** Why a name cannot be used as a C member, or undefined when it can. */
|
|
20
|
-
function cNameProblem(name) {
|
|
20
|
+
export function cNameProblem(name) {
|
|
21
21
|
if (name.includes("__") || name.startsWith("_"))
|
|
22
22
|
return `"${name}": names starting with "_" or containing "__" are reserved for generated code`;
|
|
23
23
|
if (name.includes("$"))
|
|
@@ -94,6 +94,8 @@ const MAX_ARRAY_LEN = 1_000_000;
|
|
|
94
94
|
const MAX_STRING_CAP = 10_240;
|
|
95
95
|
/** Solana's account size limit, and what a program can allocate itself through a CPI (Init<T>). */
|
|
96
96
|
export const MAX_ACCOUNT_SIZE = 10 * 1024 * 1024;
|
|
97
|
+
/** sol_set_return_data's limit. */
|
|
98
|
+
export const MAX_RETURN_DATA = 1024;
|
|
97
99
|
export const MAX_CPI_ALLOCATION = 10 * 1024;
|
|
98
100
|
export function snakeCase(name) {
|
|
99
101
|
return name
|
|
@@ -388,6 +390,9 @@ export function analyze(entry) {
|
|
|
388
390
|
instructions.push(ix);
|
|
389
391
|
for (const a of ix.args)
|
|
390
392
|
collectStructs(a.type, structs, dataEnums, argStructs);
|
|
393
|
+
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
394
|
+
if (ix.returns?.kind === "struct")
|
|
395
|
+
collectStructs(ix.returns, structs, dataEnums, argStructs);
|
|
391
396
|
for (const a of ix.accounts)
|
|
392
397
|
if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded")
|
|
393
398
|
accountStructs.add(a.kind.struct.name);
|
|
@@ -696,8 +701,8 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
696
701
|
if (problem)
|
|
697
702
|
reporter.fail(p.name, problem);
|
|
698
703
|
const t = fieldType(p.type);
|
|
699
|
-
if (t.kind === "
|
|
700
|
-
reporter.fail(p.type, "variant fields must have a fixed size (use
|
|
704
|
+
if (t.kind === "vec")
|
|
705
|
+
reporter.fail(p.type, "variant fields must have a fixed size (a Vec cannot be one; use FixedArray<T, N>)");
|
|
701
706
|
fields.push({ name: p.name.text, type: t });
|
|
702
707
|
}
|
|
703
708
|
if (!vname || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(vname))
|
|
@@ -734,11 +739,31 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
734
739
|
reporter.fail(at, `data enum ${e.name} contains itself`);
|
|
735
740
|
if (state.get(`enum:${e.name}`) !== "done") {
|
|
736
741
|
state.set(`enum:${e.name}`, "visiting");
|
|
742
|
+
// A string field is a TS_Str view (16 bytes) in the C payload and makes the enum variable-size.
|
|
743
|
+
const fieldSize = (f) => {
|
|
744
|
+
if (f.kind === "string") {
|
|
745
|
+
e.variable = true;
|
|
746
|
+
return 16;
|
|
747
|
+
}
|
|
748
|
+
if (f.kind === "dataEnum") {
|
|
749
|
+
const inner = dataEnums.get(f.name);
|
|
750
|
+
const size = sizeOf(f, e.node);
|
|
751
|
+
if (inner?.variable) {
|
|
752
|
+
e.variable = true;
|
|
753
|
+
return 1 + inner.payload;
|
|
754
|
+
}
|
|
755
|
+
return size;
|
|
756
|
+
}
|
|
757
|
+
return sizeOf(f, e.node);
|
|
758
|
+
};
|
|
737
759
|
for (const v of e.variants)
|
|
738
|
-
v.size = v.fields.reduce((sum, f) => sum +
|
|
760
|
+
v.size = v.fields.reduce((sum, f) => sum + fieldSize(f.type), 0);
|
|
739
761
|
e.payload = Math.max(0, ...e.variants.map((v) => v.size));
|
|
740
762
|
state.set(`enum:${e.name}`, "done");
|
|
741
763
|
}
|
|
764
|
+
// Variable enums have no fixed layout: a struct holding one is event-only, like one holding a string.
|
|
765
|
+
if (e.variable)
|
|
766
|
+
return undefined;
|
|
742
767
|
return 1 + e.payload;
|
|
743
768
|
}
|
|
744
769
|
case "vec":
|
|
@@ -801,10 +826,16 @@ function analyzeHelper(fn, reporter, scope, structs) {
|
|
|
801
826
|
if (fn.typeParameters?.length || fn.asteriskToken)
|
|
802
827
|
reporter.fail(fn, "helper functions cannot be generic or generators");
|
|
803
828
|
if (!fn.type)
|
|
804
|
-
reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean or
|
|
805
|
-
|
|
806
|
-
if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns)
|
|
807
|
-
|
|
829
|
+
reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
|
|
830
|
+
let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
|
|
831
|
+
if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
|
|
832
|
+
const s = ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments ? structs.get(fn.type.typeName.text) : undefined;
|
|
833
|
+
if (s && (s.size === undefined || s.vec))
|
|
834
|
+
reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
|
|
835
|
+
if (!s)
|
|
836
|
+
reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey or a struct");
|
|
837
|
+
returns = { kind: "struct", name: s.name };
|
|
838
|
+
}
|
|
808
839
|
const params = fn.parameters.map((p) => {
|
|
809
840
|
if (!ts.isIdentifier(p.name) || !p.type || p.initializer || p.questionToken || p.dotDotDotToken)
|
|
810
841
|
reporter.fail(p, "parameters need a plain name and an explicit type");
|
|
@@ -865,8 +896,19 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
865
896
|
let returns;
|
|
866
897
|
if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword) {
|
|
867
898
|
returns = resolveScalarType(fn.type, scope);
|
|
899
|
+
const s = !returns && ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments
|
|
900
|
+
? structs.get(fn.type.typeName.text)
|
|
901
|
+
: undefined;
|
|
902
|
+
if (s) {
|
|
903
|
+
if (s.size === undefined || s.vec)
|
|
904
|
+
reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be return data`);
|
|
905
|
+
// The runtime caps return data at 1024 bytes.
|
|
906
|
+
if (s.size > MAX_RETURN_DATA)
|
|
907
|
+
reporter.fail(fn.type, `${s.name} is ${s.size} bytes; return data holds at most ${MAX_RETURN_DATA}`);
|
|
908
|
+
returns = { kind: "struct", name: s.name };
|
|
909
|
+
}
|
|
868
910
|
if (!returns)
|
|
869
|
-
reporter.fail(fn.type, "instructions return void
|
|
911
|
+
reporter.fail(fn.type, "instructions return void, one integer, boolean or Pubkey, or a fixed-size struct (sent as return data)");
|
|
870
912
|
}
|
|
871
913
|
const accounts = [];
|
|
872
914
|
const args = [];
|
|
@@ -136,7 +136,7 @@ export class EmitterBase {
|
|
|
136
136
|
if ((ts.isPropertyAccessExpression(node) || ts.isCallExpression(node) || ts.isElementAccessExpression(node)) && node.questionDotToken)
|
|
137
137
|
this.fail(node, "optional chaining (?.) is not supported; check `x !== null` first");
|
|
138
138
|
if (ts.isNonNullExpression(node))
|
|
139
|
-
this.fail(node, "the ! assertion
|
|
139
|
+
this.fail(node, "the ! assertion works on Option values only; check `x !== null` so TypeScript narrows it");
|
|
140
140
|
}
|
|
141
141
|
lookup(name) {
|
|
142
142
|
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
@@ -905,6 +905,13 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
905
905
|
return undefined;
|
|
906
906
|
}
|
|
907
907
|
const t = this.tmp();
|
|
908
|
+
if (h.returns.kind === "struct") {
|
|
909
|
+
// The result lives in a local the caller owns: a reference to it reads and copies like any struct. Declared at
|
|
910
|
+
// the top of the function, so a reference taken inside a block (a ternary's branch) stays valid after it.
|
|
911
|
+
this.hoisted.push(` TSS_${h.returns.name} ${t};`);
|
|
912
|
+
this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
|
|
913
|
+
return { c: `(&${t})`, type: { kind: "structRef", struct: h.returns.name, writable: true } };
|
|
914
|
+
}
|
|
908
915
|
this.line(`${scalarC(h.returns)} ${t};`);
|
|
909
916
|
this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
|
|
910
917
|
return h.returns.kind === "pubkey" ? { c: `(&${t})`, type: h.returns } : { c: t, type: h.returns };
|