@typesol/cli 0.25.0 → 0.27.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 +39 -0
- package/README.md +32 -14
- package/SECURITY.md +2 -0
- package/dist/cli.js +52 -37
- package/dist/compiler/analyze.js +62 -5
- package/dist/compiler/emit/base.d.ts +2 -0
- package/dist/compiler/emit/base.js +4 -1
- package/dist/compiler/emit/calls.d.ts +16 -1
- package/dist/compiler/emit/calls.js +129 -1
- package/dist/compiler/emit/expressions.js +18 -0
- package/dist/compiler/emit/function.js +28 -0
- package/dist/compiler/emit/shared.js +2 -0
- package/dist/compiler/emit/storage.js +18 -0
- package/dist/compiler/model.d.ts +12 -0
- package/dist/compiler/model.js +3 -0
- package/dist/templates.js +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{clientgen.js → tools/clientgen.js} +1 -1
- package/dist/tools/doctor.d.ts +45 -0
- package/dist/tools/doctor.js +229 -0
- package/dist/{portgen.js → tools/portgen.js} +1 -1
- package/dist/workspace.js +1 -1
- package/docs/architecture.md +17 -17
- package/docs/audit.md +90 -0
- package/docs/case-study-escrow.md +4 -4
- package/docs/cli.md +3 -2
- package/docs/errors.md +1 -1
- package/docs/language.md +7 -2
- package/docs/migrate-from-anchor.md +4 -0
- package/docs/runtime.md +1 -1
- package/docs/security-model.md +105 -0
- package/docs/testing.md +5 -4
- package/lib/typesol.d.ts +55 -1
- package/package.json +13 -13
- package/runtime/typesol_rt.h +82 -21
- /package/dist/{clientgen.d.ts → tools/clientgen.d.ts} +0 -0
- /package/dist/{cpigen.d.ts → tools/cpigen.d.ts} +0 -0
- /package/dist/{cpigen.js → tools/cpigen.js} +0 -0
- /package/dist/{idlpublish.d.ts → tools/idlpublish.d.ts} +0 -0
- /package/dist/{idlpublish.js → tools/idlpublish.js} +0 -0
- /package/dist/{onchain.d.ts → tools/onchain.d.ts} +0 -0
- /package/dist/{onchain.js → tools/onchain.js} +0 -0
- /package/dist/{portgen.d.ts → tools/portgen.d.ts} +0 -0
- /package/dist/{profile.d.ts → tools/profile.d.ts} +0 -0
- /package/dist/{profile.js → tools/profile.js} +0 -0
- /package/dist/{svmrpc.d.ts → tools/svmrpc.d.ts} +0 -0
- /package/dist/{svmrpc.js → tools/svmrpc.js} +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* data enum, a COption) it says so in a note and a comment.
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
|
-
import { cNameProblem } from "
|
|
12
|
+
import { cNameProblem } from "../compiler/analyze.js";
|
|
13
13
|
const RESERVED = new Set("break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "));
|
|
14
14
|
const camel = (s) => s.replace(/[_-]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
15
15
|
const pascal = (s) => camel(s).replace(/^[a-z]/, (c) => c.toUpperCase());
|
package/dist/workspace.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import { writeClient } from "./clientgen.js";
|
|
16
|
+
import { writeClient } from "./tools/clientgen.js";
|
|
17
17
|
import { build, COMPILER_VERSION, SBPF_VERSIONS } from "./compiler/index.js";
|
|
18
18
|
import { loadOrCreateKeypair } from "./keys.js";
|
|
19
19
|
import { TEMPLATES } from "./templates.js";
|
package/docs/architecture.md
CHANGED
|
@@ -26,13 +26,13 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
26
26
|
|
|
27
27
|
| Path | Contents |
|
|
28
28
|
| --- | --- |
|
|
29
|
-
| `src/` | The compiler (`src/compiler/`) and the
|
|
29
|
+
| `src/` | The compiler (`src/compiler/`); the CLI, projects, init templates and `typesol/testing` at the top; generators and chain tooling in `src/tools/` (client, CPI and port generators, IDL publishing, on-chain reads, profiling, the LiteSVM RPC). Source map below |
|
|
30
30
|
| `runtime/` | `typesol_rt.h`, the C runtime every program includes ([runtime.md](runtime.md)) |
|
|
31
31
|
| `lib/` | `typesol.d.ts`, the language surface programs typecheck against |
|
|
32
32
|
| `ts-plugin/` | The editor plugin (`typesol/ts-plugin`) |
|
|
33
33
|
| `examples/` | Example programs; the `typesol init` templates are copies of three of them |
|
|
34
|
-
| `test/` | The test suite: `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated clients), `programs/` (third-party program binaries, see its SOURCES.md) |
|
|
35
|
-
| `
|
|
34
|
+
| `test/` | The test suite, by subject: `language/`, `security/`, `examples/`, `integrations/` (token, Metaplex, oracle and other CPIs), `client/`, `cli/`, `porting/`, `verification/` (differential and fuzz tests). Shared: `harness.ts` and the `*-helpers.ts`, `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated C and clients), `programs/` (third-party program binaries, see its SOURCES.md), `anchor-twins/` (below) |
|
|
35
|
+
| `test/anchor-twins/` | Anchor twins of the counter, vault and escrow examples, for the differential tests (`pnpm build:twins`) and the zero-copy client test; the benchmarks themselves are in [typesol/benchmarks](https://github.com/typesol/benchmarks) |
|
|
36
36
|
| `scripts/` | `smoke.ts` and `localnet.sh` (a local validator run), `fetch-programs.sh` (third-party binaries), `readme-bench.ts` (the README numbers) |
|
|
37
37
|
| `docs/` | User and contributor documentation (also published on typesol.com) |
|
|
38
38
|
| `assets/` | The CLI's logo and the README images (`readme/`, generated) |
|
|
@@ -53,17 +53,17 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
53
53
|
| `src/compiler/index.ts` | The compiler's API (`generate`, `build`, `verifyBinary`, the build manifest). |
|
|
54
54
|
| `runtime/typesol_rt.h` | The C runtime: error codes, word-wise memory helpers, the entry point's input parser (`ts_deserialize`), account checks, checked math, Borsh helpers, CPIs (System, Token, Token-2022, ATA, Metaplex), oracles and the heap allocator. Header-only and `static inline`, so unused code is dropped. |
|
|
55
55
|
| `src/workspace.ts` | `typesol.json` projects, program keypairs, `typesol init` templates and deploy arguments. |
|
|
56
|
-
| `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
|
|
57
|
-
| `src/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
|
|
58
|
-
| `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program, and reading a program's IDL back from the chain (Program Metadata or Anchor's IDL account) for `typesol client --address`. |
|
|
59
|
-
| `src/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
|
|
56
|
+
| `src/tools/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
|
|
57
|
+
| `src/tools/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
|
|
58
|
+
| `src/tools/idlpublish.ts` | `typesol idl publish` through the Program Metadata program, and reading a program's IDL back from the chain (Program Metadata or Anchor's IDL account) for `typesol client --address`. |
|
|
59
|
+
| `src/tools/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
|
|
60
60
|
| `src/templates.ts` | The programs and tests `typesol init --template` writes (copies of `examples/`; a test checks they match). |
|
|
61
|
-
| `src/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
|
|
61
|
+
| `src/tools/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
|
|
62
62
|
| `src/keys.ts` | Base58 and program keypair helpers. |
|
|
63
63
|
| `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
|
|
64
64
|
| `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
|
|
65
|
-
| `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
|
|
66
|
-
| `src/profile.ts` | Turns `--profile` logs into compute units per source line. |
|
|
65
|
+
| `src/tools/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
|
|
66
|
+
| `src/tools/profile.ts` | Turns `--profile` logs into compute units per source line. |
|
|
67
67
|
| `src/cli.ts` | Command parsing. Every command has strict options, `--help`, and exit codes 0 (success), 1 (failure), 2 (usage error) and 3 (verify mismatch). |
|
|
68
68
|
|
|
69
69
|
## The emitter
|
|
@@ -123,7 +123,7 @@ These invariants are easy to break. Each one has tests.
|
|
|
123
123
|
3. Put any C helper in `runtime/typesol_rt.h` as `static inline`, returning `uint64_t` (`SUCCESS` or an error code)
|
|
124
124
|
so that callers can wrap it in `TS_TRY`.
|
|
125
125
|
4. Add an execution test that compiles a fixture under `test/fixtures/` and runs it in LiteSVM. Add a diagnostics
|
|
126
|
-
case in `test/diagnostics.test.ts` for each misuse you reject.
|
|
126
|
+
case in `test/language/diagnostics.test.ts` for each misuse you reject.
|
|
127
127
|
5. Document it in `docs/language.md` and add a line to `CHANGELOG.md`.
|
|
128
128
|
|
|
129
129
|
### Add an account constraint
|
|
@@ -145,7 +145,7 @@ These invariants are easy to break. Each one has tests.
|
|
|
145
145
|
### Change generated C
|
|
146
146
|
|
|
147
147
|
`test/golden/counter.c` pins the output for the counter example. Run
|
|
148
|
-
`UPDATE_GOLDEN=1 pnpm test test/diagnostics.test.ts`, review the diff, and commit it with the change.
|
|
148
|
+
`UPDATE_GOLDEN=1 pnpm test test/language/diagnostics.test.ts`, review the diff, and commit it with the change.
|
|
149
149
|
|
|
150
150
|
For refactors that should not change behaviour, compare binaries: build every example and fixture before and after
|
|
151
151
|
the change. The sha256 values in the `*.build.json` manifests must not change.
|
|
@@ -154,12 +154,12 @@ the change. The sha256 values in the `*.build.json` manifests must not change.
|
|
|
154
154
|
|
|
155
155
|
| Files | What they cover |
|
|
156
156
|
| --- | --- |
|
|
157
|
-
| `test/<feature>.test.ts` | Execution tests. Each compiles fixtures and runs them in LiteSVM through `test/harness.ts`. |
|
|
158
|
-
| `test/diagnostics.test.ts` | Every rejected construct, plus the golden C. |
|
|
159
|
-
| `test/hardening.test.ts`, `test/regressions.test.ts` | Attacks and edge cases that once failed. Keep each as a negative test. |
|
|
160
|
-
| `test/fuzz-*.test.ts`, `test/differential.test.ts` | Randomized checks against BigInt, a TypeScript state model, and the Anchor twins in `
|
|
157
|
+
| `test/<subject>/<feature>.test.ts` | Execution tests. Each compiles fixtures and runs them in LiteSVM through `test/harness.ts`. |
|
|
158
|
+
| `test/language/diagnostics.test.ts` | Every rejected construct, plus the golden C. |
|
|
159
|
+
| `test/security/hardening.test.ts`, `test/security/regressions.test.ts` | Attacks and edge cases that once failed. Keep each as a negative test. |
|
|
160
|
+
| `test/verification/fuzz-*.test.ts`, `test/verification/differential.test.ts` | Randomized checks against BigInt, a TypeScript state model, and the Anchor twins in `test/anchor-twins`. See [testing.md](testing.md). |
|
|
161
161
|
| `scripts/smoke.ts` | End to end against a real `solana-test-validator` (`scripts/localnet.sh`). |
|
|
162
162
|
|
|
163
163
|
Some tests need optional inputs and are skipped without them:
|
|
164
164
|
- `pnpm fetch:programs` downloads the Metaplex programs, which cannot be redistributed.
|
|
165
|
-
- `pnpm
|
|
165
|
+
- `pnpm build:twins` builds the Anchor twins and needs Rust and Anchor.
|
package/docs/audit.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Audit package
|
|
2
|
+
|
|
3
|
+
For a security firm reviewing TypeSOL. TypeSOL compiles a subset of TypeScript to C and then to Solana SBF; every
|
|
4
|
+
program it builds includes one C runtime header. The question for an audit is whether a program compiled by TypeSOL
|
|
5
|
+
can be made to do something its source and [the security model](security-model.md) say it cannot. Programs written
|
|
6
|
+
with TypeSOL are out of scope; their authors audit them.
|
|
7
|
+
|
|
8
|
+
## Scope
|
|
9
|
+
|
|
10
|
+
In order of priority. Line counts at 0.25.0.
|
|
11
|
+
|
|
12
|
+
| Priority | Component | Files | Lines | Why |
|
|
13
|
+
| --- | --- | --- | ---: | --- |
|
|
14
|
+
| 1 | C runtime | `runtime/typesol_rt.h` | 1,740 | In every program: input parsing, account checks, checked arithmetic, PDAs, system and token CPIs, account creation, resizing and closing, oracles, signature checks, the heap |
|
|
15
|
+
| 1 | Instruction prologue and constraints | `src/compiler/emit/function.ts` | 584 | Which checks each account parameter gets, and their order |
|
|
16
|
+
| 1 | Account data, Vecs and assignment | `src/compiler/emit/storage.ts`, `statements.ts`, `base.ts` | 2,296 | Reads and writes of account data, growable Vec layout and resizing, references and aliasing, closure and helper expansion |
|
|
17
|
+
| 1 | Calls and CPIs | `src/compiler/emit/calls.ts`, `cpi.ts` | 4,276 | Built-ins, remaining accounts, `invoke` and its account flags, signer seeds, token and Metaplex CPIs, account creation |
|
|
18
|
+
| 1 | Expressions | `src/compiler/emit/expressions.ts`, `shared.ts`, `borsh.ts` | 1,512 | Checked arithmetic and casts, Borsh encoding of arguments and `@layout borsh` accounts |
|
|
19
|
+
| 2 | Front end | `src/compiler/analyze.ts`, `model.ts` | 2,202 | What the language accepts; constraints and account types as parsed from the source |
|
|
20
|
+
| 2 | Interface | `src/compiler/idl.ts`, `src/tools/clientgen.ts` | 2,211 | The IDL and the generated client must describe the program exactly (a mismatch can make clients sign the wrong thing) |
|
|
21
|
+
| 3 | Build and tooling | `src/compiler/toolchain.ts`, `src/cli.ts`, `src/workspace.ts`, `src/tools/` | | Build flags, deploy and verification commands |
|
|
22
|
+
|
|
23
|
+
The language surface is `lib/typesol.d.ts` (1,711 lines): every type and built-in a program can use, with its
|
|
24
|
+
documented behavior. [docs/language.md](language.md) is the reference; [docs/architecture.md](architecture.md) maps
|
|
25
|
+
the code, [docs/runtime.md](runtime.md) the runtime's conventions.
|
|
26
|
+
|
|
27
|
+
Out of scope: the Solana runtime, platform-tools (clang, lld), the TypeScript compiler, and programs written with
|
|
28
|
+
TypeSOL.
|
|
29
|
+
|
|
30
|
+
## What to check
|
|
31
|
+
|
|
32
|
+
The guarantees in [security-model.md](security-model.md), each of which should hold for every program the compiler
|
|
33
|
+
accepts. Areas where a bug would be most damaging, and where earlier internal reviews found the issues listed below:
|
|
34
|
+
|
|
35
|
+
- **Account checks**: every parameter type and constraint, also when an account only reaches a helper, a closure or a
|
|
36
|
+
CPI; optional accounts; the duplicate-writable check.
|
|
37
|
+
- **Aliasing**: two views of one account (parameters, remaining accounts, references held across a call), writes
|
|
38
|
+
through a place the right side moved.
|
|
39
|
+
- **Account data layout**: growable Vecs after the fixed fields, their lengths against the account size, resizing and
|
|
40
|
+
the Vecs that follow, `@layout borsh` accounts.
|
|
41
|
+
- **Arithmetic**: every operator and cast at every width, compound assignment, `mulDiv`, rent.
|
|
42
|
+
- **CPIs and signing**: signer-seed arrays (including their reuse across statements), account flags in `invoke`,
|
|
43
|
+
account creation on pre-funded addresses, closing.
|
|
44
|
+
- **The runtime's parsing**: instruction data, the serialized accounts, Token-2022 extension data, oracle accounts and
|
|
45
|
+
the instructions sysvar.
|
|
46
|
+
|
|
47
|
+
## Earlier reviews
|
|
48
|
+
|
|
49
|
+
Internal reviews, by the maintainers, in October 2026. Their findings and fixes are in the CHANGELOG:
|
|
50
|
+
|
|
51
|
+
- **0.25.0** (runtime and emitted checks): two Medium issues fixed, a remaining account loaded mutably twice through
|
|
52
|
+
the same slot, and a Vec element written after its right side resized the Vec; plus a token program ambiguity,
|
|
53
|
+
`mint::token_program`, oracle options and documentation of `requirePda`, Merkle leaves and `createIfNeeded`.
|
|
54
|
+
- **0.24.0** (compiler review): helper and closure expansion scoped to their own module, stale struct references
|
|
55
|
+
across Vec moves refused, account-shadowing parameters refused; client SDK hardening.
|
|
56
|
+
- Earlier releases: see `test/security/regressions.test.ts` and `test/security/hardening.test.ts`, which keep every
|
|
57
|
+
attack that once worked as a failing transaction.
|
|
58
|
+
|
|
59
|
+
## Evidence
|
|
60
|
+
|
|
61
|
+
- **Tests**: 570, by subject under `test/` (`docs/architecture.md`). `test/security/` holds the attacks.
|
|
62
|
+
- **Differential tests**: the same transactions to TypeSOL and to Anchor twins (`test/anchor-twins`), comparing
|
|
63
|
+
outcomes, account contents and balances after every step.
|
|
64
|
+
- **Model-based fuzzing** (`test/verification/`): arithmetic against BigInt at every width; honest and hostile action
|
|
65
|
+
sequences against state models with value conservation; account layout with two growable Vecs and aliasing through
|
|
66
|
+
remaining accounts. Each run prints its seed (`FUZZ_SEED`), `pnpm fuzz` runs them wide.
|
|
67
|
+
- **Ported programs**: all 46 programs of solana-developers/program-examples, run through their original tests' flows
|
|
68
|
+
and failure paths ([test/ports/README.md](../test/ports/README.md)).
|
|
69
|
+
- **Four-way comparison**: [typesol/benchmarks](https://github.com/typesol/benchmarks) runs 17 of those programs in
|
|
70
|
+
TypeSOL, Anchor, native Rust and Pinocchio, requires the same outcome, and records how each refuses the hostile calls
|
|
71
|
+
and with which error.
|
|
72
|
+
|
|
73
|
+
## Running it
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
git clone https://github.com/typesol/typesol && cd typesol
|
|
77
|
+
pnpm install && pnpm build
|
|
78
|
+
pnpm fetch:programs # Metaplex binaries for the token-metadata, Core and Bubblegum tests
|
|
79
|
+
pnpm build:twins # the Anchor twins (Rust + Anchor 1.0.2)
|
|
80
|
+
pnpm test # everything; CI runs it on Ubuntu (Node 22, 24) per change, macOS daily
|
|
81
|
+
pnpm fuzz # the randomized checks, wide
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`node dist/cli.js build <program.ts> --out <dir>` writes the generated C next to the binary, which is the most direct
|
|
85
|
+
way to review what a construct compiles to; `test/golden/counter.c` is a pinned example.
|
|
86
|
+
|
|
87
|
+
## Contact
|
|
88
|
+
|
|
89
|
+
Coordinate through a private advisory on https://github.com/typesol/typesol/security, or report findings the same way
|
|
90
|
+
(SECURITY.md).
|
|
@@ -166,7 +166,7 @@ is marked PORTED in [the ports table](../test/ports/README.md).
|
|
|
166
166
|
|
|
167
167
|
Two independent checks run the original and the port against each other.
|
|
168
168
|
|
|
169
|
-
- **The port's own test** ([`test/ports-tokens.test.ts`](../test/ports-tokens.test.ts)) runs the upstream flow with SPL
|
|
169
|
+
- **The port's own test** ([`test/porting/ports-tokens.test.ts`](../test/porting/ports-tokens.test.ts)) runs the upstream flow with SPL
|
|
170
170
|
Token mints and again with Token-2022 mints: make, take (creating the taker's and the maker's missing token accounts),
|
|
171
171
|
make again, a refund attempt by someone other than the maker, and the refund. It checks balances, the decoded offer
|
|
172
172
|
and that offer and vault are closed.
|
|
@@ -184,9 +184,9 @@ Both implementations refuse the hostile calls the comparison makes:
|
|
|
184
184
|
|
|
185
185
|
TypeSOL checks each account's constraints in Anchor's order, so a refused transaction usually gets the same error
|
|
186
186
|
code (see [which error a transaction gets](errors.md#which-error-a-transaction-gets)). The second call differs on
|
|
187
|
-
purpose: taker and maker are then the same writable account, which TypeSOL refuses for any two writable parameters
|
|
188
|
-
where Anchor only compares program and token accounts and fails later, on the maker's
|
|
189
|
-
matches on this code needs to accept either.
|
|
187
|
+
purpose: taker and maker are then the same writable account, which TypeSOL refuses for any two writable parameters
|
|
188
|
+
unless one is marked `Dup<…>`, where Anchor only compares program and token accounts and fails later, on the maker's
|
|
189
|
+
token account. A client that matches on this code needs to accept either.
|
|
190
190
|
|
|
191
191
|
## Cost
|
|
192
192
|
|
package/docs/cli.md
CHANGED
|
@@ -6,12 +6,13 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
|
|
|
6
6
|
|
|
7
7
|
| Command | What it does |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`, `programs/tsconfig.json` (with the editor plugin) and `.gitignore`. `<name>` must be snake_case; by default it is derived from `<dir>` (`my-app` → `my_app`). The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. |
|
|
9
|
+
| `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`, `programs/tsconfig.json` (with the editor plugin) and `.gitignore`. `<name>` must be snake_case; by default it is derived from `<dir>` (`my-app` → `my_app`). The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. It warns, with the install commands, when the platform-tools that building needs are missing. |
|
|
10
|
+
| `typesol doctor [--url <rpc>] [--config <cli config>]` | Check what building, testing and deploying need: Node.js, the platform-tools, the Solana CLI, its keypair, the RPC (`--url`, a cluster moniker, or the CLI config) and the keypair's balance there, and the project. Each problem is printed with the command that fixes it. Exit code 1 when building is impossible (Node.js or the platform-tools missing); deploy pieces are warnings. |
|
|
10
11
|
| `typesol build [<program>…] [--sbpf v0\|v1\|v2] [--profile] [--quiet]` | In a project: build every program (or those named) into `<out>/<name>/`: `<name>.so`, `<name>.json` (Anchor IDL), `<name>.client.ts` (TypeScript client, see [client.md](client.md)), `<name>.c`, `<name>.build.json`. The first build creates `<out>/deploy/<name>-keypair.json` and records the address in `typesol.json`; later builds refuse a missing or different keypair. |
|
|
11
12
|
| `typesol build <file.ts> [--out <dir>] [--program-id <keypair.json>] [--sbpf …] [--profile]` | Outside a project (or a file that is not one of its programs): build that file. A project program's `.ts` builds as that program. |
|
|
12
13
|
| `typesol check <file.ts> [--json] [--stdin]` | Type-check and validate without building. `--json` prints `{ ok, diagnostics: [{ file, line, column, message }] }`; `--stdin` checks the text given on stdin as that file (its imports come from disk). |
|
|
13
14
|
| `typesol emit-c <file.ts> [--out <file.c>] [--profile]` | Print or write the generated C. |
|
|
14
|
-
| `typesol deploy [<program>…] [--url <rpc>] [--keypair <payer.json>] [--config <cli config>] [--dry-run] [--sbpf …]` | Build, then `solana program deploy --use-rpc` at the program keypair's address. Deploying again upgrades (the payer must be the upgrade authority). `--dry-run` builds and prints the commands. Needs `solana` on PATH. |
|
|
15
|
+
| `typesol deploy [<program>…] [--url <rpc>] [--keypair <payer.json>] [--config <cli config>] [--dry-run] [--sbpf …]` | Build, then `solana program deploy --use-rpc` at the program keypair's address. Deploying again upgrades (the payer must be the upgrade authority). `--dry-run` builds and prints the commands. Before sending anything it checks that the RPC answers and that the payer holds enough for the rent (about twice the binary's on a first deploy, while the buffer exists), and says what to do if not (start `solana-test-validator`, `solana airdrop` on devnet or a local validator). Needs `solana` on PATH. |
|
|
15
16
|
| `typesol idl publish [<program>…] [--url <rpc>] [--keypair <authority.json>] [--config <cli config>] [--non-canonical]` | Compile (the binary is not rebuilt), then write each IDL to the Program Metadata program (seed `idl`). Canonical IDLs must be signed by the upgrade authority. Defaults come from the Solana CLI config (`~/.config/solana/cli/config.yml`). |
|
|
16
17
|
| `typesol cpi <idl.json> [--out <file.ts>] [--prefix <name>] [--address <base58>]` | Generate TypeSOL helpers that call another program from its IDL. Unsupported instructions are listed on stderr. |
|
|
17
18
|
| `typesol port <idl.json> [--out <file.ts>]` | A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program: account structs, enums, errors, events and one function per instruction with its accounts and arguments, each body `require(false, …NotPorted)` until written. Instruction discriminators (an explicit `@discriminator` when the name derives another), account names and error codes are kept, so existing clients keep working where the layouts match; notes on stderr list where they do not (Borsh `String`/`Option` in account data, a `Vec` that is not the last field, custom discriminators or error codes). |
|
package/docs/errors.md
CHANGED
|
@@ -70,5 +70,5 @@ Where TypeSOL's order differs, only a transaction that breaks two of these check
|
|
|
70
70
|
- Too few accounts fails with 3005 before any account is checked; Anchor fails on the first account it cannot find.
|
|
71
71
|
- Accounts the instruction creates (`Init<…>`, `create(…)`) are created in the body, after every other check, where
|
|
72
72
|
Anchor creates them before checking the other accounts.
|
|
73
|
-
- TypeSOL refuses any two writable parameters naming one account (2040)
|
|
73
|
+
- TypeSOL refuses any two writable parameters naming one account (2040) unless one is `Dup<…>`, where Anchor only compares program and token
|
|
74
74
|
accounts: a signer passed as two writable parameters fails with 2040 here and later, or not at all, under Anchor.
|
package/docs/language.md
CHANGED
|
@@ -38,7 +38,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
38
38
|
| Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
|
|
39
39
|
| Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full): a template is joined in one allocation, text known at compile time (literals, string and Pubkey constants) is folded in, and `s += …` grows the newest heap string in place |
|
|
40
40
|
| `switch` | on integers (128-bit ones too), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
|
|
41
|
-
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention; inner nodes and leaves are hashed alike, so hash leaves as OpenZeppelin does, `keccak256(keccak256(data))`, and never take the leaf itself from the arguments: an inner node would pass as a leaf); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
41
|
+
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention; inner nodes and leaves are hashed alike, so hash leaves as OpenZeppelin does, `keccak256(keccak256(data))`, and never take the leaf itself from the arguments: an inner node would pass as a leaf); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `stackHeight()` is 1 when the transaction calls the program directly, more under a CPI (`require(stackHeight() === 1n, …)` refuses CPI callers); `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
42
42
|
| Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds, exponent?)` (fully verified, feed-matched, fresh; with `exponent`, a feed at another scale is refused); raw `update.price / conf / exponent / publishTime` are unchecked (any feed, partially verified or stale: check `feedId`, `fullyVerified` and the age yourself, or use `pythPrice`); `switchboardValue(feed: SwitchboardFeed, expectedFeed, maxStaleSlots, decimals, minSamples?)` (the feed must be the expected one: anyone can create feeds; `minSamples`, default 1, the fewest oracle samples accepted) (mainnet program unless `SwitchboardFeed<"devnet">`) |
|
|
43
43
|
| Multiple files | `import { Side, min } from "./math"` — structs, enums, constants and helpers can live in other files; only the entry file's exported functions are instructions; names are program-wide, so imports from TypeSOL files cannot be renamed; error codes number imported files first |
|
|
44
44
|
| Loops | `for (let i = 0n; i < n; i++)`, `while`, `do … while`, `for (const x of list)` over a `Vec` or `FixedArray` (each `x` is what `list.get(i)` gives: a value, or a reference into account data for structs; the length is read again every step, as in JS), `break`, `continue` — bounded by the compute budget. Remaining accounts are iterated by index |
|
|
@@ -76,6 +76,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
76
76
|
| `A \| null` / `Optional<A>` | optional account (Anchor `Option<Account>`): the client passes this program's id to leave it out; its checks and constraints run only when present; `x !== null` before use, or `x!` / `x!.field` to assert it was passed (5115 otherwise); `optional` in the IDL |
|
|
77
77
|
| `ProgramData` | this program's ProgramData (address derived from the program id, upgradeable-loader owner and state checked); `upgradeAuthority`, `deploySlot` — gate admin setup on `programData.upgradeAuthority === admin.key` |
|
|
78
78
|
| `EventAuthority`, `SelfProgram` | Anchor's `["__event_authority"]` PDA, and this program's own address, for `emitCpi` |
|
|
79
|
+
| `Migrate<From, To>` | Anchor 1.0's `Migration`: an existing `From` account (owner, discriminator, size) rewritten as `To` by `acct.migrate(payer, value)`, a top-level statement the instruction must reach. `acct.from` (read-only) before it, `acct.to` after; the value is built in full before the rewrite, so it may read `acct.from`. Resized to `To` (the payer tops up rent when it grows, at most 10 KiB; shrinking keeps the lamports; growing needs `SystemProgram`). Fixed-layout structs without growable Vecs; no constraints yet; the account cannot be passed to a helper (use `acct.to` fields in the instruction). An account already migrated fails with 3002. Keep the old struct under a new name with its discriminator: `/** @discriminator account:Counter */ interface CounterV1 { … }` |
|
|
79
80
|
| `Zeroed<T>` | large accounts (up to 10 MiB) the client allocated with System `CreateAccount`: owned by this program, big enough, rent-exempt, discriminator still zero — then initialized; like Anchor's zero-copy `zero` |
|
|
80
81
|
| `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (its only token program parameter, or one passed as the last argument of `create` / `createIfNeeded`; plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. With `createIfNeeded` an existing mint must carry groupPointer / groupMemberPointer / metadataPointer / closeAuthority / permanentDelegate / transferHook with those keys (Anchor's 2024–2039); extensions not listed are not looked at, so an existing mint may carry others (a permanent delegate, say). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
|
|
81
82
|
| `Init<TokenAccount>`, `InitIfNeeded<TokenAccount>` (no `Ata<…>`) | Anchor `init` / `init_if_needed` with `token::mint` / `token::authority`: `vault.create(payer, ["vault", mint.key], mint, authority)` (the authority may be `vault.key` itself) creates a token account at that PDA (or a keypair account with `null`), sized for the mint's extensions under Token-2022. `createIfNeeded` checks an existing one has that mint and owner (2014 / 2015), not its delegate or close authority (as Anchor) |
|
|
@@ -86,6 +87,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
86
87
|
| `AssociatedTokenProgram` | `create` / `createIdempotent` CPIs; `associatedTokenAddress(owner, mint, tokenProgram)` |
|
|
87
88
|
| `TokenMetadataProgram` | Metaplex `createV3(...)` (CreateMetadataAccountV3), optionally with `{ creators: [{ address, verified, share }], collection: mint }` after `isMutable` (1–5 creators, shares adding up to 100; only the update authority, signing, can be `verified: true`; the collection is written unverified; `isCollection: true` makes the NFT a sized collection); `createMasterEditionV3(edition, mint, updateAuthority, mintAuthority, payer, metadata, tokenProgram, systemProgram, maxSupply \| null)`; `verifyCollection(authority, metadata, collectionMint, collectionMetadata, collectionMasterEdition, systemProgram, instructionsSysvar)` (Verify with CollectionV1); `metadataAddress(mint)`, `masterEditionAddress(mint)` |
|
|
88
89
|
| `InstructionsSysvar`, `PythPriceUpdate`, `SwitchboardFeed` | exact address / owner program + discriminator |
|
|
90
|
+
| `InstructionsSysvar` (introspection) | `ixs.length`, `ixs.currentIndex()` (the top-level instruction being executed), `ixs.get(i)` → a `TransactionInstruction`: `programId`, `accountsLength`, `dataLength`, `accountKey(j)` / `accountIsSigner(j)` / `accountIsWritable(j)`, `readU8/U16/U32/U64/readPubkey(offset)` (little-endian), `is(program, "name")` (that program's instruction by its default Anchor discriminator, sha256("global:<name>"); an instruction with its own `@discriminator` is matched with `programId` and `readU64(0)` instead). Every read is bounds-checked (5104). It is the transaction author's data: require things of the transaction with it (a repay instruction after a flash loan), never treat it as proof |
|
|
89
91
|
| `Program<"Address…">` | exact program address + executable |
|
|
90
92
|
| `UncheckedProgram` | executable only — check `.key` yourself |
|
|
91
93
|
| `AccountInfo` / `Mut<AccountInfo>` | nothing (writable for `Mut`); `owner`, `executable` |
|
|
@@ -94,7 +96,10 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
94
96
|
Every account exposes `key`, `lamports`, `dataLen`, `isSigner`, `isWritable`. At most 32 accounts per instruction (more is refused, 5113).
|
|
95
97
|
|
|
96
98
|
Two writable parameters can never be the same account: aliasing is refused with error 2040 (also when a remaining
|
|
97
|
-
account written through `.lamports` or `loadAccountMut` is another account of the instruction).
|
|
99
|
+
account written through `.lamports` or `loadAccountMut` is another account of the instruction). `Dup<Mut<T>>`
|
|
100
|
+
(Anchor's `dup`) lifts that for one parameter when both accounts are read and written in place (not created, closed,
|
|
101
|
+
a close destination or `@layout borsh`): a write through either is seen through the other, so read what you need
|
|
102
|
+
before writing. A remaining account
|
|
98
103
|
can be loaded with `loadAccountMut` once per instruction: loading the same one again (`rest.get(i)` and `rest.get(j)`
|
|
99
104
|
with `i == j`) is refused with 2040 too, since the two views would alias.
|
|
100
105
|
|
|
@@ -46,6 +46,8 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
46
46
|
| `Option<Account<'info, T>>` | `Mut<T> \| null` (the client passes the program id to leave it out) |
|
|
47
47
|
| `UncheckedAccount` / `AccountInfo` | `AccountInfo` / `Mut<AccountInfo>`: check what you rely on yourself |
|
|
48
48
|
| `SystemAccount<'info>` | `SystemAccount` / `Mut<SystemAccount>` |
|
|
49
|
+
| `Migration<'info, From, To>` | `Migrate<From, To>`, then `acct.migrate(payer, value)`; the old struct keeps its on-chain discriminator with `/** @discriminator account:<OldName> */` |
|
|
50
|
+
| `#[account(discriminator = …)]` | `/** @discriminator <preimage or 0x8 bytes> */` on the struct |
|
|
49
51
|
| `AccountLoader<T>` with `#[account(zero)]` | `Zeroed<T>` (TypeSOL reads every account in place: no copy to avoid) |
|
|
50
52
|
| `Program<'info, System>` | `SystemProgram` |
|
|
51
53
|
| `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
|
|
@@ -57,6 +59,8 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
57
59
|
| `#[account(init, payer = p, seeds = […], bump, token::mint = m, token::authority = a)]` | `Init<TokenAccount>`, then `vault.create(p, […], m, a.key)` (`InitIfNeeded<TokenAccount>` + `createIfNeeded`) |
|
|
58
60
|
| `associated_token::mint = m, associated_token::authority = a` | `Ata<TokenAccount, "a", "m">`; `Ata<Init<TokenAccount>, "a", "m">` creates it |
|
|
59
61
|
| `ctx.remaining_accounts` | `remainingAccounts()` |
|
|
62
|
+
| `#[account(mut, dup)]` | `Dup<Mut<T>>` (in-place accounts only) |
|
|
63
|
+
| `sysvar::instructions` (`load_current_index_checked`, `load_instruction_at_checked`), `get_stack_height()` | `InstructionsSysvar`: `currentIndex()`, `get(i)`, `.length`; `stackHeight()` |
|
|
60
64
|
| `ctx.bumps.vault` | the value `create` returns, or `acct.pdaBump` |
|
|
61
65
|
|
|
62
66
|
## Account data
|
package/docs/runtime.md
CHANGED
|
@@ -56,4 +56,4 @@ In file order:
|
|
|
56
56
|
- A change that alters behavior (a result, an error code, a log line, account bytes) is a language change and goes
|
|
57
57
|
in the CHANGELOG as one. One that only changes cost says so: "same results".
|
|
58
58
|
- New error codes: add the `#define` here, the row in [errors.md](errors.md) and the name in the compiler's runtime
|
|
59
|
-
error list (`TYPESOL_ERRORS` in `src/clientgen.ts`), so clients name it (a test checks every code has a name).
|
|
59
|
+
error list (`TYPESOL_ERRORS` in `src/tools/clientgen.ts`), so clients name it (a test checks every code has a name).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Security model
|
|
2
|
+
|
|
3
|
+
What a program compiled by TypeSOL can rely on, who is trusted, and what is left to the program's author. This is the
|
|
4
|
+
specification the tests check and an audit measures the compiler against; [audit.md](audit.md) is the package for
|
|
5
|
+
auditors, [SECURITY.md](../SECURITY.md) how to report a problem.
|
|
6
|
+
|
|
7
|
+
## Trust
|
|
8
|
+
|
|
9
|
+
| Party | Trusted for |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| The program's author | the program's logic: what it allows, to whom, for how much |
|
|
12
|
+
| TypeSOL (this repository) | that the binary does what the source and these guarantees say, and nothing an attacker can steer beyond it |
|
|
13
|
+
| The toolchain | the TypeScript compiler API (parsing, types), Agave platform-tools (clang, lld), Node.js |
|
|
14
|
+
| The Solana runtime | account ownership and signer flags as passed, the loader's account serialization, CPI privilege rules, sysvars |
|
|
15
|
+
|
|
16
|
+
Everything a transaction carries is attacker-controlled: the instruction data, which accounts are passed in which
|
|
17
|
+
slots (any account, repeated or not), the flags the runtime then enforces, the data of accounts the program does not
|
|
18
|
+
own, remaining accounts, and what another program returns or writes.
|
|
19
|
+
|
|
20
|
+
## Guarantees
|
|
21
|
+
|
|
22
|
+
Each holds for every instruction of every program the compiler accepts. A break of any of them is a security bug
|
|
23
|
+
(SECURITY.md).
|
|
24
|
+
|
|
25
|
+
### Before the body runs
|
|
26
|
+
|
|
27
|
+
1. **Account count.** Fewer accounts than parameters fail (3005); more than 32 in total fail before parsing (5113).
|
|
28
|
+
2. **Account types.** Each parameter is checked for what its type says: `Signer` signed (3010); `Mut<…>` writable
|
|
29
|
+
(2000); a program account (`Account<T>`, `Mut<T>`) owned by this program (3007), initialized (3012), with `T`'s
|
|
30
|
+
discriminator (3002) and at least its size (3003); `TokenAccount` / `Mint` owned by SPL Token or Token-2022 and an
|
|
31
|
+
initialized account of that kind (3007 / 3003); program parameters at their fixed address and executable
|
|
32
|
+
(3008 / 3009); `SystemAccount` owned by the System Program (3011); `Zeroed<T>` owned, all-zero discriminator and
|
|
33
|
+
rent-exempt.
|
|
34
|
+
3. **No aliasing.** Two writable parameters are never the same account (2040), whatever slots the transaction uses,
|
|
35
|
+
unless the program marks one `Dup<…>`, and then only for accounts read and written in place (see below).
|
|
36
|
+
4. **Arguments.** Every argument is length-checked before it is read; strings are valid UTF-8; booleans, enums and
|
|
37
|
+
options have valid tags (102).
|
|
38
|
+
5. **Constraints.** `Seeds` (2006, with the stored or the canonical bump), `Ata` (2015 / 2009 / 2014), `HasOne`
|
|
39
|
+
(2001), `Owner` (2004), `Executable` (2007), `Address` (2012), `TokenOf` / `MintOf` (2014–2017, 2021, 2022) hold
|
|
40
|
+
before the body, per account in Anchor's order ([errors.md](errors.md#which-error-a-transaction-gets)). A
|
|
41
|
+
constraint naming an optional account is refused at compile time, so leaving the account out cannot skip it.
|
|
42
|
+
|
|
43
|
+
### While it runs
|
|
44
|
+
|
|
45
|
+
6. **Arithmetic.** Every `+ - * / % **`, compound assignment, `++` / `--`, unary minus, shift and cast is checked:
|
|
46
|
+
overflow and lost bits fail (5100), division by zero fails (5101), a cast out of range fails (4102). There is no
|
|
47
|
+
unchecked integer operation in the language.
|
|
48
|
+
7. **Bounds.** Every index into an array, Vec, FixedVec or remaining-account list is checked (5104); a
|
|
49
|
+
growable Vec's stored length is checked against the account's size before it is used (3003).
|
|
50
|
+
8. **Account creation.** `create` / `createIfNeeded` is a top-level statement, and the account cannot be read or
|
|
51
|
+
written before it (compile error). Creation refuses an account that already exists (3000), and an address someone
|
|
52
|
+
pre-funded still gets created. `createIfNeeded` on an existing account re-derives the canonical PDA and checks
|
|
53
|
+
owner, discriminator and size (token accounts: mint and owner; mints: decimals, authorities and the extensions it
|
|
54
|
+
names). A `Migrate<From, To>` account is checked as an existing `From`; `migrate(…)` is a top-level statement every
|
|
55
|
+
path reaches, builds the new value before rewriting the bytes, and nothing reads the old data after it (compile
|
|
56
|
+
errors).
|
|
57
|
+
9. **Writes through references.** A place is never written after the code that computes its value may have moved it:
|
|
58
|
+
replacing or resizing what a live reference points into is a compile error, as is writing a Vec element with a
|
|
59
|
+
right side that may resize the Vec.
|
|
60
|
+
10. **Remaining accounts.** Writing one (`.lamports`, `loadAccountMut`) checks it is writable (3006) and not another
|
|
61
|
+
account of the instruction (2040); `loadAccountMut` loads a given account once per instruction (2040);
|
|
62
|
+
`loadAccount<T>` checks owner, discriminator and size, and validates the data as instruction arguments are.
|
|
63
|
+
11. **Growth.** An account grows by at most 10 KiB per instruction (3016), the payer is a `Mut<Signer>`, new bytes are
|
|
64
|
+
zero and the new rent is paid; shrinking keeps the lamports in the account.
|
|
65
|
+
12. **CPIs.** A program signs only for its own PDAs (the runtime enforces it; the seeds are those written in the
|
|
66
|
+
source). `invoke` marks an account writable or signer only when the parameter is `Mut` / `Signer` or the program
|
|
67
|
+
signs for it; remaining accounts keep the flags they were passed with, or are checked for the role requested.
|
|
68
|
+
Return data is read only from the program named, at the expected size (5106).
|
|
69
|
+
13. **Events.** `emitCpi` is accepted only from this program's event-authority PDA, signed (2002).
|
|
70
|
+
|
|
71
|
+
### When it ends
|
|
72
|
+
|
|
73
|
+
14. **Closing.** A `CloseTo<…>` or `close(…)` account has its lamports moved to the destination, its data zeroed and
|
|
74
|
+
its owner reset to the System Program on every successful return, so it cannot be revived as the same account.
|
|
75
|
+
15. **No silent success.** Every failed check returns its error code and the transaction fails: no code path continues
|
|
76
|
+
or returns success after a failed check or CPI.
|
|
77
|
+
|
|
78
|
+
## Left to the author
|
|
79
|
+
|
|
80
|
+
- **Logic.** Who may call what, limits, prices, fees. TypeSOL checks what the types and constraints say, nothing more.
|
|
81
|
+
- **`AccountInfo` / `UncheckedProgram`.** Unchecked by design, as in Anchor: check what you rely on.
|
|
82
|
+
- **`requirePda(account, seeds, bump)`.** Any valid bump passes (Anchor's `bump = expr`). Use a stored bump, never one
|
|
83
|
+
from the arguments.
|
|
84
|
+
- **`Dup<…>`.** A parameter marked `Dup` may be the same account as another writable one. Reads see every write
|
|
85
|
+
(both are the same bytes), so a value read before a write through the other slot is stale: read, then write.
|
|
86
|
+
- **Instruction introspection.** The Instructions sysvar holds what the transaction's author put there. Require things
|
|
87
|
+
of the transaction with it (that a repay instruction follows), never treat it as proof of who did what.
|
|
88
|
+
- **Oracles.** `pythPrice` checks verification, feed and age (pass the exponent you scale by); the raw `update.*`
|
|
89
|
+
fields are unchecked. `switchboardValue` needs the expected feed; set `minSamples`.
|
|
90
|
+
- **`merkleVerify`.** Leaves and inner nodes hash alike: build leaves as `keccak256(keccak256(data))` in the program.
|
|
91
|
+
- **Existing accounts in `createIfNeeded`.** An existing token account's delegate and close authority, and a mint's
|
|
92
|
+
extensions the call does not name, are not checked (as in Anchor).
|
|
93
|
+
- **Seeds of variable length.** Two adjacent string seeds can collide (`"ab" + "c"` and `"a" + "bc"`), as in Anchor.
|
|
94
|
+
|
|
95
|
+
## Where TypeSOL is stricter than Anchor
|
|
96
|
+
|
|
97
|
+
- Any two writable parameters naming one account are refused unless one is `Dup<…>` (Anchor compares only program
|
|
98
|
+
and token accounts, and `dup` lifts that).
|
|
99
|
+
- Arithmetic overflow returns an error code (5100) in every build; Anchor aborts the program, and only when
|
|
100
|
+
overflow checks are enabled.
|
|
101
|
+
|
|
102
|
+
## Status
|
|
103
|
+
|
|
104
|
+
The compiler and runtime have had internal security reviews; the findings of the latest are fixed in 0.25.0
|
|
105
|
+
(CHANGELOG). There has been no external audit yet: see [audit.md](audit.md).
|
package/docs/testing.md
CHANGED
|
@@ -5,10 +5,11 @@ run prints its seed; reproduce with `FUZZ_SEED=<n>`, widen with `FUZZ_RUNS` / `F
|
|
|
5
5
|
|
|
6
6
|
| Check | Oracle | What must hold |
|
|
7
7
|
| --- | --- | --- |
|
|
8
|
-
| `test/fuzz-arith.test.ts` | BigInt | `+ - * / % **`, `& \| ^ << >> ~`, unary minus, comparisons and casts at every width u8…u128 / i8…i128, plus `mulDiv`, `mulDivCeil`, `isqrt`, `min`, `max`, return the same value or abort with the same code (5100/5101/4102), for edge and random inputs |
|
|
9
|
-
| `test/fuzz-state.test.ts` | TypeScript state model | random honest + hostile actions on vault and escrow: every outcome as predicted, balances equal the model, SOL conserved up to fees, token supply exactly conserved; random filings/decisions/resets on ledgerbook: Vec contents, account size and rent exemption equal the model |
|
|
10
|
-
| `test/
|
|
11
|
-
| `test/
|
|
8
|
+
| `test/verification/fuzz-arith.test.ts` | BigInt | `+ - * / % **`, `& \| ^ << >> ~`, unary minus, comparisons and casts at every width u8…u128 / i8…i128, plus `mulDiv`, `mulDivCeil`, `isqrt`, `min`, `max`, return the same value or abort with the same code (5100/5101/4102), for edge and random inputs |
|
|
9
|
+
| `test/verification/fuzz-state.test.ts` | TypeScript state model | random honest + hostile actions on vault and escrow: every outcome as predicted, balances equal the model, SOL conserved up to fees, token supply exactly conserved; random filings/decisions/resets on ledgerbook: Vec contents, account size and rent exemption equal the model |
|
|
10
|
+
| `test/verification/fuzz-layout.test.ts` | TypeScript model | random pushes, pops, sets, in-place additions, moves and clears on an account with two growable Vecs (honest, out of range and by a stranger): the decoded account equals the model after every step; random `loadAccountMut` index pairs over remaining-account lists that repeat accounts: every alias refused (2040), balances conserved |
|
|
11
|
+
| `test/verification/differential.test.ts` | Anchor twins in `test/anchor-twins` | identical transactions to TypeSOL and Anchor programs: same outcomes, same account contents, same wallet and token balances after every step |
|
|
12
|
+
| `test/porting/ports-*.test.ts` | the Anchor programs of solana-developers/program-examples and their tests | TypeSOL rewrites under `test/ports/` run the originals' flows and failure paths with the same outcomes, error codes and balances ([test/ports/README.md](../test/ports/README.md)) |
|
|
12
13
|
|
|
13
14
|
Compute-unit and binary-size comparisons with Anchor, native Rust and Pinocchio are in the README
|
|
14
15
|
([Numbers](../README.md#numbers)).
|
package/lib/typesol.d.ts
CHANGED
|
@@ -186,10 +186,43 @@ export declare function keccak256(...parts: (string | Pubkey | Hash | bigint | b
|
|
|
186
186
|
*/
|
|
187
187
|
export declare function merkleVerify(leaf: Hash, proof: Vec<Hash>, root: Hash): boolean;
|
|
188
188
|
|
|
189
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* The Instructions sysvar (Sysvar1nstructions…): every instruction of the transaction being executed. Its address is
|
|
191
|
+
* checked. What it holds is the transaction author's: use it to require something of the transaction (a repay
|
|
192
|
+
* instruction after a flash loan, a fee instruction before), never as proof of anything else.
|
|
193
|
+
*/
|
|
190
194
|
export interface InstructionsSysvar extends AccountMeta {
|
|
191
195
|
readonly __instructions_sysvar: void;
|
|
196
|
+
/** How many instructions the transaction has. */
|
|
197
|
+
readonly length: u64;
|
|
198
|
+
/** The index of the top-level instruction being executed (this one, or the one that CPI'd into this program). */
|
|
199
|
+
currentIndex(): u64;
|
|
200
|
+
/** The transaction's instruction at `index` (5104 when there is none). */
|
|
201
|
+
get(index: u64): TransactionInstruction;
|
|
192
202
|
}
|
|
203
|
+
/** One instruction of the transaction, read from the Instructions sysvar. Reads past its end fail with 5104. */
|
|
204
|
+
export interface TransactionInstruction {
|
|
205
|
+
readonly __transaction_instruction: void;
|
|
206
|
+
readonly programId: Pubkey;
|
|
207
|
+
readonly accountsLength: u64;
|
|
208
|
+
readonly dataLength: u64;
|
|
209
|
+
accountKey(i: u64): Pubkey;
|
|
210
|
+
accountIsSigner(i: u64): boolean;
|
|
211
|
+
accountIsWritable(i: u64): boolean;
|
|
212
|
+
/** Little-endian integers and keys from the instruction data. */
|
|
213
|
+
readU8(offset: u64): u8;
|
|
214
|
+
readU16(offset: u64): u16;
|
|
215
|
+
readU32(offset: u64): u32;
|
|
216
|
+
readU64(offset: u64): u64;
|
|
217
|
+
readPubkey(offset: u64): Pubkey;
|
|
218
|
+
/** `program`'s instruction `name` (snake_case or camelCase): its program id and 8-byte Anchor discriminator match. */
|
|
219
|
+
is(program: Pubkey, name: string): boolean;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* How deep in cross-program calls this program runs: 1 when the transaction calls it directly, more when another
|
|
223
|
+
* program invoked it. `require(stackHeight() === 1n, …)` refuses CPI callers.
|
|
224
|
+
*/
|
|
225
|
+
export declare function stackHeight(): u64;
|
|
193
226
|
|
|
194
227
|
/**
|
|
195
228
|
* True when this transaction contains an Ed25519 precompile instruction that verified `message`
|
|
@@ -597,6 +630,27 @@ export type TokenOf<A, MintParam extends string, AuthorityParam extends string =
|
|
|
597
630
|
export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
|
|
598
631
|
readonly __mintOf?: [AuthorityParam, FreezeAuthorityParam, TokenProgramParam];
|
|
599
632
|
};
|
|
633
|
+
/**
|
|
634
|
+
* Anchor's `dup`: this writable account may be the same account as another writable parameter (TypeSOL refuses that
|
|
635
|
+
* otherwise, 2040). Both are read and written in place, so a write through one is seen through the other: read
|
|
636
|
+
* what you need before writing. Not for accounts that are created, closed, a close destination or `@layout borsh`.
|
|
637
|
+
*/
|
|
638
|
+
export type Dup<A> = A & { readonly __dup?: true };
|
|
639
|
+
/**
|
|
640
|
+
* Anchor 1.0's `Migration<From, To>`: an existing `From` account (owner, discriminator and size checked) that this
|
|
641
|
+
* instruction rewrites as `To` with `acct.migrate(payer, value)`, a top-level statement every path must reach. Read
|
|
642
|
+
* the old data through `acct.from` before migrating (the new value may use it: it is built in full before the account
|
|
643
|
+
* is rewritten), and the new data through `acct.to` after. The account is resized to `To`: the payer tops up the rent
|
|
644
|
+
* when it grows (at most 10 KiB), and shrinking keeps the lamports. `From` usually keeps the old type's discriminator
|
|
645
|
+
* under a new name: `/** @discriminator account:Counter *\/ interface CounterV1 { … }`. An account already migrated
|
|
646
|
+
* fails with 3002, as under Anchor.
|
|
647
|
+
*/
|
|
648
|
+
export interface Migrate<From, To> extends AccountMeta {
|
|
649
|
+
readonly __migrate: [From, To];
|
|
650
|
+
readonly from: Readonly<From>;
|
|
651
|
+
readonly to: To;
|
|
652
|
+
migrate(payer: Mut<Signer>, value: To): void;
|
|
653
|
+
}
|
|
600
654
|
export type Seeds<A, Names extends readonly string[], Bump extends string = never, ProgramId extends string = never> = A & {
|
|
601
655
|
readonly __seeds?: [Names, Bump, ProgramId];
|
|
602
656
|
/** The PDA's bump (Anchor `ctx.bumps.<account>`): the stored bump field when one is named, else the canonical bump found. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typesol/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"default": "./dist/workspace.js"
|
|
23
23
|
},
|
|
24
24
|
"./cpi": {
|
|
25
|
-
"types": "./dist/cpigen.d.ts",
|
|
26
|
-
"default": "./dist/cpigen.js"
|
|
25
|
+
"types": "./dist/tools/cpigen.d.ts",
|
|
26
|
+
"default": "./dist/tools/cpigen.js"
|
|
27
27
|
},
|
|
28
28
|
"./client": {
|
|
29
|
-
"types": "./dist/clientgen.d.ts",
|
|
30
|
-
"default": "./dist/clientgen.js"
|
|
29
|
+
"types": "./dist/tools/clientgen.d.ts",
|
|
30
|
+
"default": "./dist/tools/clientgen.js"
|
|
31
31
|
},
|
|
32
32
|
"./profile": {
|
|
33
|
-
"types": "./dist/profile.d.ts",
|
|
34
|
-
"default": "./dist/profile.js"
|
|
33
|
+
"types": "./dist/tools/profile.d.ts",
|
|
34
|
+
"default": "./dist/tools/profile.js"
|
|
35
35
|
},
|
|
36
36
|
"./testing": {
|
|
37
37
|
"types": "./dist/testing.d.ts",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"./ts-plugin": "./ts-plugin/index.js",
|
|
41
41
|
"./idl": {
|
|
42
|
-
"types": "./dist/idlpublish.d.ts",
|
|
43
|
-
"default": "./dist/idlpublish.js"
|
|
42
|
+
"types": "./dist/tools/idlpublish.d.ts",
|
|
43
|
+
"default": "./dist/tools/idlpublish.js"
|
|
44
44
|
},
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
"test": "vitest run",
|
|
64
64
|
"typesol": "node dist/cli.js",
|
|
65
65
|
"smoke": "node scripts/smoke.ts",
|
|
66
|
-
"fuzz": "FUZZ_RUNS=2000 FUZZ_STEPS=600 FUZZ_VERBOSE=1 vitest run test/fuzz-arith.test.ts test/fuzz-state.test.ts test/differential.test.ts --silent=false",
|
|
67
|
-
"
|
|
66
|
+
"fuzz": "FUZZ_RUNS=2000 FUZZ_STEPS=600 FUZZ_VERBOSE=1 vitest run test/verification/fuzz-arith.test.ts test/verification/fuzz-state.test.ts test/verification/fuzz-layout.test.ts test/verification/differential.test.ts --silent=false",
|
|
67
|
+
"build:twins": "cd test/anchor-twins && cargo-build-sbf",
|
|
68
68
|
"prepack": "npm run build",
|
|
69
69
|
"fetch:programs": "bash scripts/fetch-programs.sh",
|
|
70
|
-
"format": "prettier --write \"src/**/*.ts\" \"test/*.ts\" \"scripts/**/*.ts\" \"lib/**/*.ts\"",
|
|
71
|
-
"lint": "prettier --check \"src/**/*.ts\" \"test/*.ts\" \"scripts/**/*.ts\" \"lib/**/*.ts\" && pnpm typecheck",
|
|
70
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/*.ts\" \"test/*/*.test.ts\" \"scripts/**/*.ts\" \"lib/**/*.ts\"",
|
|
71
|
+
"lint": "prettier --check \"src/**/*.ts\" \"test/*.ts\" \"test/*/*.test.ts\" \"scripts/**/*.ts\" \"lib/**/*.ts\" && pnpm typecheck",
|
|
72
72
|
"prepublishOnly": "pnpm lint && pnpm test"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|