@typesol/cli 0.24.1 → 0.26.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 (47) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +35 -16
  3. package/SECURITY.md +2 -0
  4. package/dist/cli.js +52 -37
  5. package/dist/compiler/analyze.js +11 -1
  6. package/dist/compiler/emit/calls.js +10 -10
  7. package/dist/compiler/emit/cpi.js +4 -0
  8. package/dist/compiler/emit/function.d.ts +9 -0
  9. package/dist/compiler/emit/function.js +169 -139
  10. package/dist/compiler/emit/program.js +3 -0
  11. package/dist/compiler/emit/shared.d.ts +1 -0
  12. package/dist/compiler/emit/shared.js +1 -0
  13. package/dist/compiler/emit/statements.d.ts +6 -0
  14. package/dist/compiler/emit/statements.js +35 -5
  15. package/dist/templates.js +1 -1
  16. package/dist/testing.d.ts +1 -1
  17. package/dist/testing.js +1 -1
  18. package/dist/{clientgen.js → tools/clientgen.js} +4 -1
  19. package/dist/tools/doctor.d.ts +45 -0
  20. package/dist/tools/doctor.js +229 -0
  21. package/dist/{portgen.js → tools/portgen.js} +1 -1
  22. package/dist/workspace.js +1 -1
  23. package/docs/architecture.md +17 -17
  24. package/docs/audit.md +90 -0
  25. package/docs/case-study-escrow.md +224 -0
  26. package/docs/cli.md +3 -2
  27. package/docs/errors.md +26 -1
  28. package/docs/language.md +12 -8
  29. package/docs/migrate-from-anchor.md +3 -1
  30. package/docs/runtime.md +1 -1
  31. package/docs/security-model.md +97 -0
  32. package/docs/testing.md +5 -4
  33. package/lib/typesol.d.ts +35 -13
  34. package/package.json +13 -13
  35. package/runtime/typesol_rt.h +33 -4
  36. /package/dist/{clientgen.d.ts → tools/clientgen.d.ts} +0 -0
  37. /package/dist/{cpigen.d.ts → tools/cpigen.d.ts} +0 -0
  38. /package/dist/{cpigen.js → tools/cpigen.js} +0 -0
  39. /package/dist/{idlpublish.d.ts → tools/idlpublish.d.ts} +0 -0
  40. /package/dist/{idlpublish.js → tools/idlpublish.js} +0 -0
  41. /package/dist/{onchain.d.ts → tools/onchain.d.ts} +0 -0
  42. /package/dist/{onchain.js → tools/onchain.js} +0 -0
  43. /package/dist/{portgen.d.ts → tools/portgen.d.ts} +0 -0
  44. /package/dist/{profile.d.ts → tools/profile.d.ts} +0 -0
  45. /package/dist/{profile.js → tools/profile.js} +0 -0
  46. /package/dist/{svmrpc.d.ts → tools/svmrpc.d.ts} +0 -0
  47. /package/dist/{svmrpc.js → tools/svmrpc.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,57 @@ codes, and says so here.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.26.0 — 2026-10-29
10
+
11
+ A first-run experience pass, the security model and audit package, and more fuzzing. The compiler's output is the
12
+ same as 0.25.0 for every program.
13
+
14
+ - New: `typesol doctor` checks Node.js, the platform-tools, the Solana CLI, its keypair, the RPC and the keypair's
15
+ balance there, and the project, and prints the command that fixes each problem (exit 1 when building is impossible).
16
+ - `typesol init` prints the files it creates relative to where it runs, warns with the install commands when the
17
+ platform-tools are missing, and lists the next steps.
18
+ - `typesol deploy` checks, before sending anything, that the RPC answers (else: start `solana-test-validator`, or
19
+ `--url devnet`) and that the payer can pay the rent (else: how much, and the `solana airdrop` command on devnet or a
20
+ local validator). RPC URLs in these messages show only the host.
21
+ - Docs: [security model](docs/security-model.md) (the guarantees a compiled program can rely on, trust boundaries,
22
+ what is left to the author) and [audit package](docs/audit.md) (scope, priorities, earlier reviews, evidence);
23
+ README: deploying on devnet and on a local validator.
24
+ - Tests: `test/verification/fuzz-layout.test.ts`, model-based fuzzing of an account with two growable Vecs and of
25
+ `loadAccountMut` aliasing through remaining accounts (also in `pnpm fuzz`).
26
+ - Repository: tests grouped by subject under `test/`, the Anchor twins in `test/anchor-twins` (`pnpm build:twins`),
27
+ the generators and chain tooling in `src/tools/` (the package's import paths are unchanged); Dependabot for npm and
28
+ GitHub Actions.
29
+
30
+ ## 0.25.0 — 2026-10-29
31
+
32
+ Security review (October 2026) fixes, and error codes aligned with Anchor. Some refused transactions now report a
33
+ different error code (below): a minor version.
34
+
35
+ - Fixed: `loadAccountMut` on the same remaining account twice (`rest.get(i)`, `rest.get(j)` with `i == j`) gave two
36
+ writable views of one account; a program reading both before writing both could be made to create value. The second
37
+ load is now refused with 2040.
38
+ - Fixed: a Vec element written with a right side that resizes the Vec (`book.bids.get(i).qty += settle(book)` where the
39
+ helper pops, a closure, or a `push` / `pop` / `clear` in the expression) wrote through the element's old position,
40
+ into the bytes of the next Vec. Such an assignment is now a compile error ("store its result in a local first"),
41
+ as it already was for a plain `=` with a helper.
42
+ - Fixed: `Ata<Init<…>>` / `Ata<InitIfNeeded<…>>` creation and `Ata<AccountInfo, …>` with two token program parameters
43
+ used the first one; they now need exactly one (use `TokenInterface`).
44
+ - New: `TokenOf<…, …, …, "tokenProgram">` / `MintOf<…, …, …, "tokenProgram">`, Anchor's `token::token_program` /
45
+ `mint::token_program` (2021 / 2022). The escrow port uses it.
46
+ - New: `pythPrice(update, feedId, maxAge, exponent)` refuses a feed whose exponent is not the one given;
47
+ `switchboardValue(…, minSamples)` refuses a result resting on fewer oracle samples.
48
+ - Changed: each account's constraints are checked in Anchor's order (seeds, associated token account, has_one, owner,
49
+ executable, address, token::, mint::), so a transaction breaking several fails with Anchor's code: e.g. a stranger's
50
+ call on a `Seeds<HasOne<…>>` account now fails with 2006 (ConstraintSeeds) instead of 2001, and an `Ata<…>` of
51
+ another wallet with 2015 instead of 2009. A `Mut<…>` account passed read-only fails with 2000 (ConstraintMut), as
52
+ Anchor's `#[account(mut)]`, instead of 3006. docs/errors.md lists the order and the differences left.
53
+ - Docs: unchecked raw Pyth fields, `requirePda` with a bump from the arguments, Merkle leaves, and what
54
+ `createIfNeeded` leaves unchecked on an existing account.
55
+ - Docs: [Case study: porting Anchor's escrow](docs/case-study-escrow.md), one Anchor program ported end to end, with the
56
+ differences, the checks run against the original and the cost.
57
+ - Releases: the release workflow checks that CHANGELOG.md has the version, and after publishing installs the package
58
+ from npm, scaffolds a project from each template and runs its typecheck, build and test.
59
+
9
60
  ## 0.24.1 — 2026-10-28
10
61
 
11
62
  Smaller programs and fewer compute units, with the same results; no change to the language, IDLs or error codes. Also
package/README.md CHANGED
@@ -149,9 +149,20 @@ npx @typesol/cli init my-app --name counter # typesol.json, programs/counter.t
149
149
  cd my-app && npm install
150
150
  npm test # builds the program and runs it in LiteSVM, an in-process Solana runtime
151
151
  npx typesol build # target/counter/{counter.so, counter.json (IDL), counter.client.ts, counter.build.json}
152
- npx typesol deploy --url http://127.0.0.1:8899 --keypair ~/.config/solana/id.json
153
152
  ```
154
153
 
154
+ `npx typesol doctor` checks all of this (Node.js, platform-tools, Solana CLI, keypair, RPC) and prints the command
155
+ that fixes anything missing. To deploy, on devnet:
156
+
157
+ ```sh
158
+ solana-keygen new # once, if you have no keypair (~/.config/solana/id.json)
159
+ solana airdrop 2 --url devnet # test SOL for the rent (rate-limited: https://faucet.solana.com also works)
160
+ npx typesol deploy --url devnet
161
+ ```
162
+
163
+ or on a local validator: `solana-test-validator` in another terminal, `solana airdrop 5 --url localhost`, then
164
+ `npx typesol deploy --url localhost`. `deploy` checks the RPC and the payer's balance before sending anything.
165
+
155
166
  `--template vault` or `--template escrow` starts from a SOL vault or an SPL token escrow instead of the counter, each
156
167
  with a passing LiteSVM test.
157
168
 
@@ -162,8 +173,9 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
162
173
  Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
163
174
  program's address.
164
175
 
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
176
+ Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct, `typesol port`
177
+ starts from your program's IDL, and [docs/case-study-escrow.md](docs/case-study-escrow.md) follows one complete Anchor
178
+ program through the port. Tests use `typesol/testing` (accounts by name, plain values; [docs/test-your-program.md](docs/test-your-program.md)). To go to
167
179
  mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
168
180
 
169
181
  ## How it works
@@ -261,23 +273,28 @@ units are not a safety claim. The differential tests also run the escrow against
261
273
  ### On mainnet
262
274
 
263
275
  The official escrow ([tokens/escrow](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow)),
264
- deployed to Solana mainnet-beta in all four versions on 2026-10-27 and called with the same mints, amounts and accounts,
265
- 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
266
- programs match these builds byte for byte (`typesol verify` for TypeSOL), and the TypeSOL program's IDL is published
267
- on chain with `typesol idl publish`.
276
+ deployed to Solana mainnet-beta in all four versions and called with the same mints, amounts and accounts, one
277
+ instruction per transaction. The TypeSOL program was upgraded in place to 0.25.0 on 2026-10-29; the others are as first
278
+ deployed on 2026-10-27. Each figure is the compute units of that transaction, linked to Solscan; the lowest per row is
279
+ in bold. The deployed programs match these builds byte for byte (`typesol verify` for TypeSOL), and the TypeSOL
280
+ program's IDL is published on chain with `typesol idl publish`.
268
281
 
269
282
  | Transaction | TypeSOL | Pinocchio | Native Rust | Anchor |
270
283
  | --- | ---: | ---: | ---: | ---: |
271
- | 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) |
272
- | 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) |
273
- | 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) |
274
- | 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) |
284
+ | make_offer (offer 1) | [29,205](https://solscan.io/tx/4AaenpnDGYmT2JXAm8HuN5D9GkEWLYeqyMNxJq6D9aYk6ssunSwNTigFmZoCJtY7MZ3FFBXLsig8jo1YmjUFVTpX) | [**21,145**](https://solscan.io/tx/3tWjcMQxTja6QjEmAvi1MRF9qCsnPBsAmQNPnd5RcNTwnkkJYmk9JFFnZoJbTAc6MEr2QYQd7KtgPGtaNr3inrCG) | [45,698](https://solscan.io/tx/4AgQzePrXL4f4qYrFQBzE65d8cszpmqSiQVt9yVsAotbrYTnartXuM2g2PFg7VhUGaZpbhfH9jzXgwXdRLXwB7tk) | [38,648](https://solscan.io/tx/4Hm62foYd5hoNqmEfLk51HhVVb5VHa5oTr81rjAKNNXyKFYwSrpXZhzFVwbk3WxRzEsiqGxHbn7B4eBop6RC1Sgx) |
285
+ | take_offer (offer 1) | [**9,951**](https://solscan.io/tx/52ej6M3zFt5kgMGKUef9VGZdmZWqJdSoHCvCEB9P3btg7DSbvHkM24uk95W7HXjSX12XHmEmXATC7GLcQQ1U22kK) | [26,706](https://solscan.io/tx/4KLfVqTM2KmwgqF4xECWmNuZWYufsWcocdT5saamWdZQfRYhY7aTsyEyk5pVTxug6LJnngJhWDU7NMiPnZxu9sH2) | [31,628](https://solscan.io/tx/5f2VxEcPUfZbCVHpbYbUntVwfUN4k6ieetxSjdpJVwfck2tDuv2QTGbFFtNcy34Nn8d6bKSp5efzKDSbg7xj5TbJ) | [32,835](https://solscan.io/tx/3sJVTUvGRRtvYisFyqthvYQUZpeLmNN2UtXJq9TsQHW2TLP8YwtiuFGoPvu51w5YoZyJjKc3dM3zF6JXTFRFjc6H) |
286
+ | make_offer (offer 2) | [20,205](https://solscan.io/tx/36mSJoMESxSLoxL5apVkoPzptAgFmUM9z2ZVuKpCadQajxbTF3T6BT93bw2ugHfLX7qSXvcBQ96kd8m22y3DsbTH) | [**19,645**](https://solscan.io/tx/PXPGNGCN3RN15L812K9Ho99Xz5YmUgMDaDy8aXuSg1kFgtdX42Z6foLLJkn8X9XB68xgeQsTBQhoDdUfRPKks2f) | [29,198](https://solscan.io/tx/2n38Urd1FsijNB2kbQy1b2cNHrvU7sXwYe5zH9SeyZUJRhUaD8TXSJGkvjEGYCTy3bdmAvEqqE27Ufgcxg3S3uFe) | [38,648](https://solscan.io/tx/185ZpYg3H1Ku4upHPBoBavrTczwzwxQiz7wjqqYWXd8MmxbimbLz7VvjuQdnqAGaAMy11G8M1QqPzXbrPof8gQb) |
287
+ | refund_offer (offer 2) | [**5,478**](https://solscan.io/tx/55Q7pTfsew4F4N9DPB21BEqeHPANhkgQmUGAYVerFP73D7qMTp4eeo5CPMpYQqU7SLS4e49fuKtLkZ9XowbyoKET) | [13,387](https://solscan.io/tx/2cNkvA4xuFJBQzFcFTnLrxRzDU1q5617T8TCF5sBS7JeNzCPUxCPfzS37uv1RUg7xs9Mqyya6KDV9GSZxGiToevT) | [17,731](https://solscan.io/tx/Nj8RGLqEgbj8SEhd9hLcB2JQJY7YwkP1CnMdeJFBdsFhftntFqm1j1wAD1Mc715wrkPv2pFCWu93AF29oHQUDYr) | [20,576](https://solscan.io/tx/5on9Fxe6mADcbWwVn43yQ5wYtdWyx2u3Ry2hSniLJjRqoj2J55Hbgv4AbZoVZ5CDp2VrrghEkKRBvmXxjv1TynmY) |
288
+ | escrow program's own part, 4 transactions | **28,118** | 36,956 | 93,273 | 102,926 |
275
289
  | 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) |
276
290
 
277
- Most of make_offer is the Associated Token Account program creating the vault (about 13,500 CU in every version); it
278
- 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
279
- offer 1 (its own part was 6,719 CU both times). The Pinocchio version takes the offer PDA's bump from the client instead
280
- of searching for it (its own part: 6,051 CU). Mainnet figures differ from the LiteSVM means above.
291
+ Most of make_offer is the Associated Token Account program creating the vault. It searches for the vault address's
292
+ bump itself at 1,500 CU a try, and every offer has its own vault, so that part varies from offer to offer in every
293
+ version (13,585 to 22,585 CU of the make_offers above); programs that check the vault's address re-derive it, so
294
+ take_offer and refund_offer vary with it too. The escrow program's own part is steadier: TypeSOL's make_offer spent
295
+ 6,620 CU of its own on both offers. The Pinocchio version takes the offer PDA's bump from the client instead of
296
+ searching for it. Mainnet figures differ from the LiteSVM means above. Every run, including the one before the
297
+ upgrade, is in [typesol/benchmarks](https://github.com/typesol/benchmarks) (`results/`).
281
298
 
282
299
  ## How it is tested
283
300
 
@@ -315,7 +332,9 @@ change.
315
332
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
316
333
  - They have **not been audited by an independent firm**. Do not put significant value behind programs built with
317
334
  TypeSOL until such an audit is published.
318
- - Audit your own programs as you would any Solana program.
335
+ - Audit your own programs as you would any Solana program. What a compiled program can rely on, and what is left to
336
+ you, is in [docs/security-model.md](docs/security-model.md); [docs/audit.md](docs/audit.md) is the package for an
337
+ external audit.
319
338
  - Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
320
339
 
321
340
  Not supported yet: see the end of [docs/language.md](docs/language.md#not-supported-yet).
package/SECURITY.md CHANGED
@@ -9,6 +9,8 @@ TypeSOL generates programs that hold funds. Treat it accordingly.
9
9
  firm.** Do not put significant value behind programs built with it until an external audit of the compiler and
10
10
  runtime is published, and audit your own programs as you would any Solana program.
11
11
  - Only the latest 0.x release receives security fixes.
12
+ - What a compiled program can rely on is specified in [docs/security-model.md](docs/security-model.md); the package
13
+ for an external audit is [docs/audit.md](docs/audit.md).
12
14
 
13
15
  ## Reporting a vulnerability
14
16
 
package/dist/cli.js CHANGED
@@ -7,14 +7,15 @@ import { fileURLToPath } from "node:url";
7
7
  import { parseArgs } from "node:util";
8
8
  import ts from "typescript";
9
9
  import { build, CompileError, COMPILER_VERSION, generate, SBPF_VERSIONS, verifyBinary } from "./compiler/index.js";
10
- import { generateCpi } from "./cpigen.js";
11
- import { generatePort } from "./portgen.js";
12
- import { generateClient, writeClient } from "./clientgen.js";
10
+ import { generateCpi } from "./tools/cpigen.js";
11
+ import { generatePort } from "./tools/portgen.js";
12
+ import { generateClient, writeClient } from "./tools/clientgen.js";
13
13
  import { buildIdl } from "./compiler/idl.js";
14
14
  import { decodeAddress, loadOrCreateKeypair, readKeypair } from "./keys.js";
15
- import { formatProfile, profileLogs } from "./profile.js";
15
+ import { formatProfile, profileLogs } from "./tools/profile.js";
16
16
  import { TEMPLATE_NAMES } from "./templates.js";
17
17
  import { buildWorkspace, CONFIG, deployArgs, findWorkspace, programAddress, programForEntry, programPaths, scaffold } from "./workspace.js";
18
+ import { clusterUrl, deployPreflight, doctorChecks, formatChecks, INSTALL_AGAVE, solanaConfig as readSolanaConfig, toolchainCheck } from "./tools/doctor.js";
18
19
  const ASSETS = fileURLToPath(new URL("../assets/", import.meta.url));
19
20
  /** A usage problem: printed with the command's help, exit code 2. */
20
21
  class UsageError extends Error {
@@ -65,9 +66,18 @@ const COMMANDS = {
65
66
  .replace(/^_+|_+$/g, "")
66
67
  .toLowerCase();
67
68
  const name = v.name ?? (/^[a-z]/.test(derived) ? derived : `p_${derived || "program"}`);
69
+ const rel = path.relative(process.cwd(), root) || ".";
68
70
  for (const f of scaffold(root, name, template))
69
- log(` created ${path.join(root, f)}`);
70
- log(`\n next: cd ${path.relative(process.cwd(), root) || "."} && npm install && npm test`);
71
+ log(` created ${path.join(rel, f)}`);
72
+ const tools = toolchainCheck();
73
+ if (!tools.ok)
74
+ log(`\n ! The Solana platform-tools are not installed yet; npm test compiles with them. Install once:\n` +
75
+ (tools.fix ?? []).map((f) => ` ${f}`).join("\n"));
76
+ log(`\n next:\n` +
77
+ (rel === "." ? "" : ` cd ${rel}\n`) +
78
+ ` npm install\n` +
79
+ ` npm test # builds ${name} and runs its test in LiteSVM, a Solana runtime in-process\n` +
80
+ ` npx typesol doctor # checks the toolchain, keypair and RPC before a deploy`);
71
81
  return 0;
72
82
  },
73
83
  },
@@ -166,6 +176,20 @@ const COMMANDS = {
166
176
  return 0;
167
177
  },
168
178
  },
179
+ doctor: {
180
+ usage: "typesol doctor [--url <rpc>] [--config <solana cli config>]",
181
+ help: [
182
+ "Check what building, testing and deploying need: Node.js, the platform-tools, the Solana CLI, its keypair, the RPC",
183
+ "(and the keypair's balance there) and the project. Each problem comes with the command that fixes it. Exit code 1",
184
+ "when building is impossible (Node.js or the platform-tools); a missing deploy piece is a warning.",
185
+ ].join("\n "),
186
+ options: { url: { type: "string" }, config: { type: "string" } },
187
+ async run(v) {
188
+ const checks = await doctorChecks({ cwd: process.cwd(), url: v.url, config: v.config });
189
+ log(formatChecks(checks));
190
+ return checks.some((c) => !c.ok && !c.optional) ? 1 : 0;
191
+ },
192
+ },
169
193
  deploy: {
170
194
  usage: "typesol deploy [<program>…] [--url <rpc>] [--keypair <payer.json>] [--config <solana cli config>] [--sbpf v0|v1|v2] [--dry-run]",
171
195
  help: [
@@ -174,9 +198,20 @@ const COMMANDS = {
174
198
  "Needs the Agave CLI (`solana`) on PATH.",
175
199
  ].join("\n "),
176
200
  options: { url: { type: "string" }, keypair: { type: "string" }, config: { type: "string" }, "dry-run": { type: "boolean" }, sbpf: { type: "string" } },
177
- run(v, args) {
201
+ async run(v, args) {
178
202
  const ws = needWorkspace("deploy");
179
203
  const built = buildWorkspace(ws, names(ws, args), { sbpf: sbpfOf(v) });
204
+ if (!v["dry-run"]) {
205
+ // Before any transaction: the RPC answers and the payer can afford it, with what to do if not.
206
+ const cfg = solanaConfig(v.config);
207
+ const url = v.url ?? cfg.url;
208
+ if (url)
209
+ await deployPreflight({
210
+ url: clusterUrl(url),
211
+ payer: v.keypair ?? cfg.keypair ?? path.join(os.homedir(), ".config/solana/id.json"),
212
+ programs: built.map((b) => ({ soPath: b.result.soPath, address: b.address, name: b.paths.name })),
213
+ });
214
+ }
180
215
  for (const b of built) {
181
216
  const cliArgs = deployArgs(b.result.soPath, b.paths.keypair, { url: v.url, payer: v.keypair, config: v.config });
182
217
  if (v["dry-run"]) {
@@ -189,7 +224,7 @@ const COMMANDS = {
189
224
  }
190
225
  catch (e) {
191
226
  if (e.code === "ENOENT")
192
- throw new Error("`solana` was not found on PATH. Install the Agave CLI: https://docs.anza.xyz/cli/install");
227
+ throw new Error(`\`solana\` was not found on PATH. Install the Agave CLI: ${INSTALL_AGAVE}`);
193
228
  throw new Error(`solana program deploy failed for ${b.paths.name} (see its output above)`);
194
229
  }
195
230
  }
@@ -217,7 +252,7 @@ const COMMANDS = {
217
252
  throw new Error(`no signer keypair${keyFile ? ` at ${keyFile}` : ""}: pass --keypair, or set keypair_path in the Solana CLI config`);
218
253
  // Loaded only here: the other commands do not need an RPC client.
219
254
  const kit = await import("@solana/kit");
220
- const { publishIdl, rpcChain } = await import("./idlpublish.js");
255
+ const { publishIdl, rpcChain } = await import("./tools/idlpublish.js");
221
256
  const signer = await kit.createKeyPairSignerFromBytes(readKeypair(keyFile));
222
257
  const chain = rpcChain(kit.createSolanaRpc(clusterUrl(rpcUrl)));
223
258
  for (const name of names(ws, args)) {
@@ -297,7 +332,7 @@ const COMMANDS = {
297
332
  throw new Error("no RPC: pass --url, or set json_rpc_url in the Solana CLI config");
298
333
  // Loaded only here: the other commands do not need an RPC client.
299
334
  const kit = await import("@solana/kit");
300
- const { fetchProgramIdl } = await import("./idlpublish.js");
335
+ const { fetchProgramIdl } = await import("./tools/idlpublish.js");
301
336
  const found = await fetchProgramIdl(kit.createSolanaRpc(clusterUrl(rpcUrl)), kit.address(v.address));
302
337
  console.error(` IDL from ${found.source === "anchor" ? "Anchor's IDL account" : "the Program Metadata program"}`);
303
338
  idl = found.idl;
@@ -360,7 +395,7 @@ const COMMANDS = {
360
395
  throw new Error("no RPC: pass --url, or set json_rpc_url in the Solana CLI config");
361
396
  // Loaded only here: the other commands do not need an RPC client.
362
397
  const kit = await import("@solana/kit");
363
- const { fetchDeployed } = await import("./onchain.js");
398
+ const { fetchDeployed } = await import("./tools/onchain.js");
364
399
  const rpc = kit.createSolanaRpc(clusterUrl(rpcUrl));
365
400
  const info = await fetchDeployed(rpc, address);
366
401
  onChain = { address, info, rpc };
@@ -387,8 +422,8 @@ const COMMANDS = {
387
422
  return same ? 0 : 3;
388
423
  const { info } = onChain;
389
424
  log(` authority ${info.authority ?? "none: the program is immutable"}${info.slot !== undefined ? `; last deployed in slot ${info.slot}` : ""}`);
390
- const { fetchIdl, metadataAddress } = await import("./idlpublish.js");
391
- const { canonicalJson } = await import("./onchain.js");
425
+ const { fetchIdl, metadataAddress } = await import("./tools/idlpublish.js");
426
+ const { canonicalJson } = await import("./tools/onchain.js");
392
427
  // No canonical metadata account means nothing was published; one that cannot be read is an error, not "none".
393
428
  const at = await metadataAddress(onChain.address, null);
394
429
  const exists = (await onChain.rpc.getAccountInfo(at, { encoding: "base64" }).send()).value !== null;
@@ -492,32 +527,12 @@ function overrideFile(file, text) {
492
527
  sys.fileExists = fileExists;
493
528
  };
494
529
  }
495
- /** The Solana CLI's cluster monikers, as `solana --url` accepts them; anything else is used as given. */
496
- function clusterUrl(url) {
497
- const monikers = {
498
- "mainnet-beta": "https://api.mainnet-beta.solana.com",
499
- m: "https://api.mainnet-beta.solana.com",
500
- devnet: "https://api.devnet.solana.com",
501
- d: "https://api.devnet.solana.com",
502
- testnet: "https://api.testnet.solana.com",
503
- t: "https://api.testnet.solana.com",
504
- localhost: "http://127.0.0.1:8899",
505
- l: "http://127.0.0.1:8899",
506
- };
507
- return monikers[url] ?? url;
508
- }
509
530
  /** json_rpc_url / keypair_path from a Solana CLI config (the given file, or the CLI's default one). */
510
531
  function solanaConfig(file) {
511
- const configPath = file ?? path.join(os.homedir(), ".config/solana/cli/config.yml");
512
- if (!existsSync(configPath)) {
513
- if (file)
514
- throw new UsageError(`${file} does not exist`);
515
- return {};
516
- }
517
- const text = readFileSync(configPath, "utf8");
518
- const get = (k) => new RegExp(`^${k}:\\s*["']?([^"'\\n]+?)["']?\\s*$`, "m").exec(text)?.[1];
519
- const expand = (p) => (p?.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p);
520
- return { url: get("json_rpc_url"), keypair: expand(get("keypair_path")) };
532
+ const cfg = readSolanaConfig(file);
533
+ if (file && !cfg.exists)
534
+ throw new UsageError(`${file} does not exist`);
535
+ return { url: cfg.url, keypair: cfg.keypair };
521
536
  }
522
537
  export async function main(argv) {
523
538
  const [cmd, ...rest] = argv;
@@ -1473,6 +1473,13 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1473
1473
  if (entry.includes("=")) {
1474
1474
  if (!accounts.some((o) => o.name === param))
1475
1475
  reporter.fail(a.node, `add an account parameter named "${param}"`);
1476
+ if (f === "tokenProgram") {
1477
+ if (a.kind.kind !== "token" && a.kind.kind !== "mint")
1478
+ reporter.fail(a.node, "TokenOf / MintOf apply to a TokenAccount / Mint");
1479
+ if (accounts.find((o) => o.name === param).kind.kind !== "tokenProgram")
1480
+ reporter.fail(a.node, `"${param}" must be a TokenProgram, Token2022Program or TokenInterface parameter`);
1481
+ continue;
1482
+ }
1476
1483
  if (a.kind.kind === "token" ? f !== "mint" && f !== "owner" : a.kind.kind === "mint" ? false : true)
1477
1484
  reporter.fail(a.node, a.kind.kind === "token" || a.kind.kind === "mint" ? "TokenOf<…> applies to a TokenAccount" : "TokenOf / MintOf apply to a TokenAccount / Mint");
1478
1485
  if (a.kind.kind === "mint" && f !== "mintAuthority" && f !== "freezeAuthority")
@@ -1610,12 +1617,15 @@ function unwrapConstraints(node, reporter, imports, out) {
1610
1617
  const one = (t) => (t && t.kind !== ts.SyntaxKind.NeverKeyword ? lit(t)[0] : undefined);
1611
1618
  const p1 = one(args[1]);
1612
1619
  const p2 = one(args[2]);
1613
- if (!p1 && !p2)
1620
+ const p3 = one(args[3]);
1621
+ if (!p1 && !p2 && !p3)
1614
1622
  reporter.fail(node, `${name}<…> names at least one parameter`);
1615
1623
  if (p1)
1616
1624
  out.hasOne.push(`${first}=${p1}`);
1617
1625
  if (p2)
1618
1626
  out.hasOne.push(`${second}=${p2}`);
1627
+ if (p3)
1628
+ out.hasOne.push(`tokenProgram=${p3}`);
1619
1629
  }
1620
1630
  if (name === "CloseTo")
1621
1631
  out.closeTo = lit(args[1])[0];
@@ -3,7 +3,7 @@ import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
3
3
  import { ANCHOR_ERRORS } from "../anchor-errors.js";
4
4
  import { decodeAddress } from "../../keys.js";
5
5
  import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
6
- import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
6
+ import { U64, U8, I64, I32, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
7
7
  import { ExpressionEmitter } from "./expressions.js";
8
8
  /**
9
9
  * Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
@@ -344,29 +344,29 @@ export class CallEmitter extends ExpressionEmitter {
344
344
  return { c: `ts_secp256k1_verify(${h.c}, (uint64_t)${id.c}, ${r.c}, ${sv.c}, ${eth.c})`, type: BOOL };
345
345
  }
346
346
  case "pythPrice": {
347
- if (args.length !== 3)
348
- this.fail(node, "pythPrice(update, feedId, maxAgeSeconds) takes three arguments");
347
+ if (args.length !== 3 && args.length !== 4)
348
+ this.fail(node, "pythPrice(update, feedId, maxAgeSeconds, exponent?) takes three or four arguments");
349
349
  const u = this.accountParam(args[0], "the price update");
350
350
  if (u.kind.kind !== "pyth")
351
351
  this.fail(args[0], "pass a PythPriceUpdate parameter");
352
- const [feed, age] = this.lowerInOrder(args.slice(1), [HASH, U64]);
352
+ const [feed, age, expo] = this.lowerInOrder(args.slice(1), [HASH, U64, I32]);
353
353
  const t = this.tmp("px");
354
354
  this.line(`int64_t ${t};`);
355
- this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, &${t}));`);
355
+ this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, ${expo ? `true, ${expo.c}` : "false, 0"}, &${t}));`);
356
356
  return { c: t, type: I64 };
357
357
  }
358
358
  case "switchboardValue": {
359
359
  // Anyone can create a Switchboard feed: like pythPrice's feed id, the expected feed address is required.
360
- if (args.length !== 4)
361
- this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals) takes four arguments");
360
+ if (args.length !== 4 && args.length !== 5)
361
+ this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals, minSamples?) takes four or five arguments");
362
362
  const f = this.accountParam(args[0], "the feed");
363
363
  if (f.kind.kind !== "switchboard")
364
364
  this.fail(args[0], "pass a SwitchboardFeed parameter");
365
- const [expected, stale, dec] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8]);
365
+ const [expected, stale, dec, min] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8, U8]);
366
366
  const t = this.tmp("sb");
367
367
  this.line(`int64_t ${t};`);
368
368
  this.line(`if (!ts_key_eq(&a_${f.name}->key, ${expected.c})) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: not the expected Switchboard feed");`);
369
- this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, &${t}));`);
369
+ this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, ${min ? min.c : "1"}, &${t}));`);
370
370
  return { c: t, type: I64 };
371
371
  }
372
372
  case "associatedTokenAddress": {
@@ -980,7 +980,7 @@ export class CallEmitter extends ExpressionEmitter {
980
980
  this.line(l);
981
981
  if (mut) {
982
982
  this.line(`TS_TRY(ts_check_writable(${h}));`);
983
- this.line(`TS_TRY(ts_check_unaliased(params, ${h}));`);
983
+ this.line(`TS_TRY(ts_claim_mut(params, ${h}));`);
984
984
  }
985
985
  return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
986
986
  }
@@ -774,6 +774,10 @@ export class CpiEmitter extends CallEmitter {
774
774
  const ataProgram = need("ataProgram", "an AssociatedTokenProgram");
775
775
  const system = need("system", "a SystemProgram");
776
776
  const tokenProgram = need("tokenProgram", "a token program (TokenProgram, Token2022Program or TokenInterface)");
777
+ // The address is derived under the token program: with two, which one the mint uses is not known here.
778
+ const tokenPrograms = this.ix.accounts.filter((o) => o.kind.kind === "tokenProgram");
779
+ if (tokenPrograms.length > 1)
780
+ this.fail(node, `${call}(...) needs exactly one token program parameter (found ${tokenPrograms.map((o) => o.name).join(", ")}); use TokenInterface`);
777
781
  const { owner, mint } = target.constraints.ata;
778
782
  const a = `a_${target.name}`;
779
783
  const create = `TS_TRY(ts_ata_create(params, a_${ataProgram.name}, a_${payer.name}, ${a}, a_${owner}, a_${mint}, a_${system.name}, a_${tokenProgram.name}, false));`;
@@ -9,7 +9,16 @@ export declare class FunctionEmitter extends StatementEmitter {
9
9
  emitInstruction(): string;
10
10
  /** CloseTo<…> accounts are closed when the instruction succeeds, at every return. */
11
11
  protected epilogue(): string[];
12
+ /**
13
+ * Each account's constraints, account by account in parameter order and, within one account, in Anchor's order
14
+ * (anchor-syn's `linearize`): seeds, associated token, has_one, owner, executable, address, token::, mint::.
15
+ * A transaction that breaks several checks then fails with the same error code as under Anchor.
16
+ */
12
17
  protected constraintChecks(ix: Instruction): string[];
18
+ /** Ata<…>: the account's owner (2015) and address (2009) as Anchor checks them, then its mint. */
19
+ private ataCheck;
20
+ /** Seeds<…>: the account is the PDA of its seeds (with the stored bump, or the canonical one found here). */
21
+ private seedsCheck;
13
22
  /** Optional account parameters a piece of generated C refers to (a_name / d_name). */
14
23
  protected optionalIn(code: string): string[];
15
24
  }