@typesol/cli 0.25.0 → 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.
- package/CHANGELOG.md +21 -0
- package/README.md +32 -14
- package/SECURITY.md +2 -0
- package/dist/cli.js +52 -37
- 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 +1 -1
- package/docs/cli.md +3 -2
- package/docs/runtime.md +1 -1
- package/docs/security-model.md +97 -0
- package/docs/testing.md +5 -4
- package/package.json +13 -13
- /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
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ 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
|
+
|
|
9
30
|
## 0.25.0 — 2026-10-29
|
|
10
31
|
|
|
11
32
|
Security review (October 2026) fixes, and error codes aligned with Anchor. Some refused transactions now report a
|
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
|
|
|
@@ -262,23 +273,28 @@ units are not a safety claim. The differential tests also run the escrow against
|
|
|
262
273
|
### On mainnet
|
|
263
274
|
|
|
264
275
|
The official escrow ([tokens/escrow](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow)),
|
|
265
|
-
deployed to Solana mainnet-beta in all four versions
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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`.
|
|
269
281
|
|
|
270
282
|
| Transaction | TypeSOL | Pinocchio | Native Rust | Anchor |
|
|
271
283
|
| --- | ---: | ---: | ---: | ---: |
|
|
272
|
-
| make_offer (offer 1) | [
|
|
273
|
-
| take_offer (offer 1) | [**
|
|
274
|
-
| make_offer (offer 2) | [
|
|
275
|
-
| refund_offer (offer 2) | [**5,
|
|
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 |
|
|
276
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) |
|
|
277
290
|
|
|
278
|
-
Most of make_offer is the Associated Token Account program creating the vault
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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/`).
|
|
282
298
|
|
|
283
299
|
## How it is tested
|
|
284
300
|
|
|
@@ -316,7 +332,9 @@ change.
|
|
|
316
332
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
|
317
333
|
- They have **not been audited by an independent firm**. Do not put significant value behind programs built with
|
|
318
334
|
TypeSOL until such an audit is published.
|
|
319
|
-
- 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.
|
|
320
338
|
- Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
|
|
321
339
|
|
|
322
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(
|
|
70
|
-
|
|
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(
|
|
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
|
|
512
|
-
if (!
|
|
513
|
-
|
|
514
|
-
|
|
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;
|
package/dist/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Programs and tests that `typesol init --template <name>` writes. Each test runs its program in LiteSVM and covers its
|
|
2
2
|
// main flow and at least one refused call, so a new project starts from something that passes and shows how to test.
|
|
3
|
-
// The programs are copies of examples/ (test/workspace.test.ts checks they still match).
|
|
3
|
+
// The programs are copies of examples/ (test/cli/workspace.test.ts checks they still match).
|
|
4
4
|
export const TEMPLATE_NAMES = ["counter", "vault", "escrow"];
|
|
5
5
|
/** Every test starts the same way: build the project's program into a LiteSVM with `typesol/testing`. */
|
|
6
6
|
const testFile = (name, o) => `// Runs the program in LiteSVM (an in-process Solana runtime): \`npm test\`.
|
package/dist/testing.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import anchor, { type Idl } from "@anchor-lang/core";
|
|
20
20
|
import { type Address, type Instruction, type KeyPairSigner } from "@solana/kit";
|
|
21
21
|
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
|
|
22
|
-
import { type SvmConnection, svmConnection } from "./svmrpc.js";
|
|
22
|
+
import { type SvmConnection, svmConnection } from "./tools/svmrpc.js";
|
|
23
23
|
export { type SvmConnection, svmConnection };
|
|
24
24
|
declare const BorshCoder: typeof anchor.BorshCoder;
|
|
25
25
|
type BorshCoder = InstanceType<typeof BorshCoder>;
|
package/dist/testing.js
CHANGED
|
@@ -22,7 +22,7 @@ import { FailedTransactionMetadata, LiteSVM } from "litesvm";
|
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { svmConnection } from "./svmrpc.js";
|
|
25
|
+
import { svmConnection } from "./tools/svmrpc.js";
|
|
26
26
|
import { buildWorkspace, findWorkspace } from "./workspace.js";
|
|
27
27
|
export { svmConnection };
|
|
28
28
|
// @anchor-lang/core is CommonJS: Node 22 cannot import its classes by name from an installed ES module.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* sanitized comments.
|
|
19
19
|
*/
|
|
20
20
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { ANCHOR_ERRORS } from "
|
|
21
|
+
import { ANCHOR_ERRORS } from "../compiler/anchor-errors.js";
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
/** An IDL the generator refuses: malformed, unsafe to embed, or older than the 0.30 IDL format. */
|
|
24
24
|
export class ClientIdlError extends Error {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const INSTALL_AGAVE = "sh -c \"$(curl -sSfL https://release.anza.xyz/stable/install)\"";
|
|
2
|
+
export interface Check {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
/** A missing optional piece (the Solana CLI is only needed to deploy). */
|
|
5
|
+
optional?: boolean;
|
|
6
|
+
what: string;
|
|
7
|
+
detail: string;
|
|
8
|
+
fix?: string[];
|
|
9
|
+
}
|
|
10
|
+
/** The platform-tools the compiler needs: found, or what to run. */
|
|
11
|
+
export declare function toolchainCheck(): Check;
|
|
12
|
+
/** json_rpc_url / keypair_path from a Solana CLI config (the given file, or the CLI's default one). */
|
|
13
|
+
export declare function solanaConfig(file?: string): {
|
|
14
|
+
url?: string;
|
|
15
|
+
keypair?: string;
|
|
16
|
+
file: string;
|
|
17
|
+
exists: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** The Solana CLI's cluster monikers, as `solana --url` accepts them; anything else is used as given. */
|
|
20
|
+
export declare function clusterUrl(url: string): string;
|
|
21
|
+
/** Only the host: an RPC URL can carry an API key in its path or query, and these messages may be shared. */
|
|
22
|
+
export declare function rpcHost(url: string): string;
|
|
23
|
+
export declare function rpc<T>(url: string, method: string, params?: unknown[]): Promise<T>;
|
|
24
|
+
/** fetch's "fetch failed" says little: the cause's code (ECONNREFUSED, ENOTFOUND, a timeout) says more. */
|
|
25
|
+
export declare const errText: (e: unknown) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Before `solana program deploy`: the RPC answers, and the payer holds enough for the program's rent (a first deploy
|
|
28
|
+
* holds about twice the binary's rent while the buffer exists; an upgrade, the buffer's). Throws with what to do.
|
|
29
|
+
*/
|
|
30
|
+
export declare function deployPreflight(opts: {
|
|
31
|
+
url: string;
|
|
32
|
+
payer?: string;
|
|
33
|
+
programs: {
|
|
34
|
+
soPath: string;
|
|
35
|
+
address: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}[];
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/** Everything `typesol doctor` checks, in order. */
|
|
40
|
+
export declare function doctorChecks(opts: {
|
|
41
|
+
cwd: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
config?: string;
|
|
44
|
+
}): Promise<Check[]>;
|
|
45
|
+
export declare function formatChecks(checks: Check[]): string;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// `typesol doctor` and the checks `init` and `deploy` run before they need something: Node, the platform-tools the
|
|
2
|
+
// compiler builds with, the Solana CLI, its keypair and RPC, the project, and (with an RPC) whether it answers and the
|
|
3
|
+
// payer can afford a deploy. Each problem comes with the command that fixes it.
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { findToolchain } from "../compiler/toolchain.js";
|
|
9
|
+
import { base58, readKeypair } from "../keys.js";
|
|
10
|
+
import { findWorkspace } from "../workspace.js";
|
|
11
|
+
export const INSTALL_AGAVE = 'sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"';
|
|
12
|
+
const MIN_NODE = [22, 12];
|
|
13
|
+
/** The platform-tools the compiler needs: found, or what to run. */
|
|
14
|
+
export function toolchainCheck() {
|
|
15
|
+
try {
|
|
16
|
+
const tc = findToolchain();
|
|
17
|
+
let version = "";
|
|
18
|
+
try {
|
|
19
|
+
version = execFileSync(tc.clang, ["--version"], { encoding: "utf8" })
|
|
20
|
+
.split("\n")[0]
|
|
21
|
+
.replace(/\s*\(.*$/, "");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* the path is enough */
|
|
25
|
+
}
|
|
26
|
+
return { ok: true, what: "platform-tools", detail: version || tc.sdk };
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
what: "platform-tools",
|
|
32
|
+
detail: e.message.split(". ")[0],
|
|
33
|
+
fix: [INSTALL_AGAVE, "cargo-build-sbf --version # once: downloads the platform-tools TypeSOL compiles with"],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** json_rpc_url / keypair_path from a Solana CLI config (the given file, or the CLI's default one). */
|
|
38
|
+
export function solanaConfig(file) {
|
|
39
|
+
const configPath = file ?? path.join(os.homedir(), ".config/solana/cli/config.yml");
|
|
40
|
+
if (!existsSync(configPath))
|
|
41
|
+
return { file: configPath, exists: false };
|
|
42
|
+
const text = readFileSync(configPath, "utf8");
|
|
43
|
+
const get = (k) => new RegExp(`^${k}:\\s*["']?([^"'\\n]+?)["']?\\s*$`, "m").exec(text)?.[1];
|
|
44
|
+
const expand = (p) => (p?.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p);
|
|
45
|
+
return { url: get("json_rpc_url"), keypair: expand(get("keypair_path")), file: configPath, exists: true };
|
|
46
|
+
}
|
|
47
|
+
/** The Solana CLI's cluster monikers, as `solana --url` accepts them; anything else is used as given. */
|
|
48
|
+
export function clusterUrl(url) {
|
|
49
|
+
const monikers = {
|
|
50
|
+
"mainnet-beta": "https://api.mainnet-beta.solana.com",
|
|
51
|
+
m: "https://api.mainnet-beta.solana.com",
|
|
52
|
+
devnet: "https://api.devnet.solana.com",
|
|
53
|
+
d: "https://api.devnet.solana.com",
|
|
54
|
+
testnet: "https://api.testnet.solana.com",
|
|
55
|
+
t: "https://api.testnet.solana.com",
|
|
56
|
+
localhost: "http://127.0.0.1:8899",
|
|
57
|
+
l: "http://127.0.0.1:8899",
|
|
58
|
+
};
|
|
59
|
+
return monikers[url] ?? url;
|
|
60
|
+
}
|
|
61
|
+
/** Only the host: an RPC URL can carry an API key in its path or query, and these messages may be shared. */
|
|
62
|
+
export function rpcHost(url) {
|
|
63
|
+
try {
|
|
64
|
+
return new URL(url).host;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return "the RPC";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The cluster an RPC belongs to, for the airdrop hint (mainnet has none). */
|
|
71
|
+
function clusterOf(url) {
|
|
72
|
+
const host = rpcHost(url);
|
|
73
|
+
if (/devnet/.test(host))
|
|
74
|
+
return "devnet";
|
|
75
|
+
if (/testnet/.test(host))
|
|
76
|
+
return "testnet";
|
|
77
|
+
if (/^(127\.0\.0\.1|localhost|0\.0\.0\.0)(:|$)/.test(host))
|
|
78
|
+
return "localhost";
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
export async function rpc(url, method, params = []) {
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
86
|
+
signal: AbortSignal.timeout(10_000),
|
|
87
|
+
});
|
|
88
|
+
const body = (await res.json());
|
|
89
|
+
if (body.error)
|
|
90
|
+
throw new Error(body.error.message);
|
|
91
|
+
return body.result;
|
|
92
|
+
}
|
|
93
|
+
/** fetch's "fetch failed" says little: the cause's code (ECONNREFUSED, ENOTFOUND, a timeout) says more. */
|
|
94
|
+
export const errText = (e) => {
|
|
95
|
+
const err = e;
|
|
96
|
+
return err.cause?.code ?? err.cause?.message ?? (err.name === "TimeoutError" ? "no answer in 10 s" : err.message);
|
|
97
|
+
};
|
|
98
|
+
const sol = (lamports) => lamports === 0 ? "0 SOL" : lamports < 100_000 ? `${lamports} lamports` : `${(lamports / 1e9).toFixed(lamports < 1e7 ? 4 : 2)} SOL`;
|
|
99
|
+
/** A cost, rounded up so the advice is enough. */
|
|
100
|
+
const solUp = (lamports) => `${(Math.ceil(lamports / 1e7) / 100).toFixed(2)} SOL`;
|
|
101
|
+
/**
|
|
102
|
+
* Before `solana program deploy`: the RPC answers, and the payer holds enough for the program's rent (a first deploy
|
|
103
|
+
* holds about twice the binary's rent while the buffer exists; an upgrade, the buffer's). Throws with what to do.
|
|
104
|
+
*/
|
|
105
|
+
export async function deployPreflight(opts) {
|
|
106
|
+
const host = rpcHost(opts.url);
|
|
107
|
+
try {
|
|
108
|
+
await rpc(opts.url, "getVersion");
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
const local = clusterOf(opts.url) === "localhost";
|
|
112
|
+
throw new Error(`cannot reach ${host} (${errText(e)}). ` +
|
|
113
|
+
(local
|
|
114
|
+
? "Start a local validator in another terminal: `solana-test-validator`, or deploy to devnet with --url devnet."
|
|
115
|
+
: "Check --url (or json_rpc_url in the Solana CLI config); for a test deploy use --url devnet or a local `solana-test-validator`."));
|
|
116
|
+
}
|
|
117
|
+
if (!opts.payer || !existsSync(opts.payer))
|
|
118
|
+
return; // the Solana CLI reports a missing keypair itself
|
|
119
|
+
const payer = base58(readKeypair(opts.payer).subarray(32));
|
|
120
|
+
let need = 0;
|
|
121
|
+
for (const p of opts.programs) {
|
|
122
|
+
const size = statSync(p.soPath).size;
|
|
123
|
+
const exists = (await rpc(opts.url, "getAccountInfo", [p.address, { encoding: "base64", dataSlice: { offset: 0, length: 0 } }])).value;
|
|
124
|
+
const rent = await rpc(opts.url, "getMinimumBalanceForRentExemption", [size + 45]);
|
|
125
|
+
need += exists ? rent : 2 * rent;
|
|
126
|
+
}
|
|
127
|
+
need += 5_000_000; // fees for the buffer writes
|
|
128
|
+
const balance = (await rpc(opts.url, "getBalance", [payer])).value;
|
|
129
|
+
if (balance >= need)
|
|
130
|
+
return;
|
|
131
|
+
const cluster = clusterOf(opts.url);
|
|
132
|
+
const airdrop = cluster ? ` Get test SOL: \`solana airdrop ${Math.max(2, Math.ceil((need - balance) / 1e9))} ${payer} --url ${cluster}\`.` : "";
|
|
133
|
+
throw new Error(`the payer ${payer} has ${sol(balance)} on ${host}; deploying needs about ${solUp(need)} (rent, mostly returned when a buffer closes).${airdrop}`);
|
|
134
|
+
}
|
|
135
|
+
/** Everything `typesol doctor` checks, in order. */
|
|
136
|
+
export async function doctorChecks(opts) {
|
|
137
|
+
const checks = [];
|
|
138
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
139
|
+
const nodeOk = major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
|
|
140
|
+
checks.push({
|
|
141
|
+
ok: nodeOk,
|
|
142
|
+
what: "Node.js",
|
|
143
|
+
detail: process.versions.node,
|
|
144
|
+
fix: nodeOk ? undefined : [`install Node.js ${MIN_NODE.join(".")} or later (https://nodejs.org)`],
|
|
145
|
+
});
|
|
146
|
+
checks.push(toolchainCheck());
|
|
147
|
+
let solanaVersion;
|
|
148
|
+
try {
|
|
149
|
+
solanaVersion = execFileSync("solana", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
150
|
+
.split(" (")[0]
|
|
151
|
+
.trim();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* not installed */
|
|
155
|
+
}
|
|
156
|
+
checks.push(solanaVersion
|
|
157
|
+
? { ok: true, what: "Solana CLI", detail: solanaVersion }
|
|
158
|
+
: { ok: false, optional: true, what: "Solana CLI", detail: "not on PATH (needed to deploy, not to build or test)", fix: [INSTALL_AGAVE] });
|
|
159
|
+
const cfg = solanaConfig(opts.config);
|
|
160
|
+
const keypair = cfg.keypair ?? path.join(os.homedir(), ".config/solana/id.json");
|
|
161
|
+
if (existsSync(keypair)) {
|
|
162
|
+
let address = "";
|
|
163
|
+
try {
|
|
164
|
+
address = base58(readKeypair(keypair).subarray(32));
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
checks.push({ ok: false, optional: true, what: "keypair", detail: e.message });
|
|
168
|
+
}
|
|
169
|
+
if (address)
|
|
170
|
+
checks.push({ ok: true, what: "keypair", detail: `${address} (${keypair.replace(os.homedir(), "~")})` });
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
checks.push({
|
|
174
|
+
ok: false,
|
|
175
|
+
optional: true,
|
|
176
|
+
what: "keypair",
|
|
177
|
+
detail: `none at ${keypair.replace(os.homedir(), "~")} (the deploy payer)`,
|
|
178
|
+
fix: ["solana-keygen new"],
|
|
179
|
+
});
|
|
180
|
+
const url = opts.url ? clusterUrl(opts.url) : cfg.url;
|
|
181
|
+
if (!url)
|
|
182
|
+
checks.push({ ok: false, optional: true, what: "RPC", detail: "none set (deploy takes --url)", fix: ["solana config set --url devnet"] });
|
|
183
|
+
else {
|
|
184
|
+
const cluster = clusterOf(url);
|
|
185
|
+
try {
|
|
186
|
+
const v = await rpc(url, "getVersion");
|
|
187
|
+
checks.push({ ok: true, what: "RPC", detail: `${rpcHost(url)} (solana-core ${v["solana-core"]})` });
|
|
188
|
+
if (existsSync(keypair)) {
|
|
189
|
+
const balance = (await rpc(url, "getBalance", [base58(readKeypair(keypair).subarray(32))])).value;
|
|
190
|
+
checks.push(balance > 0
|
|
191
|
+
? { ok: true, what: "balance", detail: sol(balance) }
|
|
192
|
+
: {
|
|
193
|
+
ok: false,
|
|
194
|
+
optional: true,
|
|
195
|
+
what: "balance",
|
|
196
|
+
detail: `0 SOL on ${rpcHost(url)}`,
|
|
197
|
+
fix: [
|
|
198
|
+
cluster && cluster !== "localhost"
|
|
199
|
+
? `solana airdrop 2 --url ${cluster}`
|
|
200
|
+
: cluster
|
|
201
|
+
? "solana airdrop 2 --url localhost"
|
|
202
|
+
: "fund the keypair above",
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
checks.push({
|
|
209
|
+
ok: false,
|
|
210
|
+
optional: true,
|
|
211
|
+
what: "RPC",
|
|
212
|
+
detail: `${rpcHost(url)} does not answer (${errText(e)})`,
|
|
213
|
+
fix: cluster === "localhost" ? ["solana-test-validator # in another terminal"] : ["pass --url devnet, or fix json_rpc_url in the Solana CLI config"],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const ws = findWorkspace(opts.cwd);
|
|
218
|
+
checks.push(ws
|
|
219
|
+
? { ok: true, what: "project", detail: `${path.relative(opts.cwd, ws.file) || ws.file}: ${Object.keys(ws.programs).join(", ")}` }
|
|
220
|
+
: { ok: true, optional: true, what: "project", detail: "none here (typesol init <dir> creates one)" });
|
|
221
|
+
return checks;
|
|
222
|
+
}
|
|
223
|
+
export function formatChecks(checks) {
|
|
224
|
+
const mark = (c) => (c.ok ? "ok " : c.optional ? " ! " : "ERR");
|
|
225
|
+
const width = Math.max(...checks.map((c) => c.what.length));
|
|
226
|
+
return checks
|
|
227
|
+
.map((c) => ` ${mark(c)} ${c.what.padEnd(width)} ${c.detail}${(c.fix ?? []).map((f) => `\n${" ".repeat(width + 9)}${f}`).join("")}`)
|
|
228
|
+
.join("\n");
|
|
229
|
+
}
|
|
@@ -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.
|
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/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,97 @@
|
|
|
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
|
+
4. **Arguments.** Every argument is length-checked before it is read; strings are valid UTF-8; booleans, enums and
|
|
36
|
+
options have valid tags (102).
|
|
37
|
+
5. **Constraints.** `Seeds` (2006, with the stored or the canonical bump), `Ata` (2015 / 2009 / 2014), `HasOne`
|
|
38
|
+
(2001), `Owner` (2004), `Executable` (2007), `Address` (2012), `TokenOf` / `MintOf` (2014–2017, 2021, 2022) hold
|
|
39
|
+
before the body, per account in Anchor's order ([errors.md](errors.md#which-error-a-transaction-gets)). A
|
|
40
|
+
constraint naming an optional account is refused at compile time, so leaving the account out cannot skip it.
|
|
41
|
+
|
|
42
|
+
### While it runs
|
|
43
|
+
|
|
44
|
+
6. **Arithmetic.** Every `+ - * / % **`, compound assignment, `++` / `--`, unary minus, shift and cast is checked:
|
|
45
|
+
overflow and lost bits fail (5100), division by zero fails (5101), a cast out of range fails (4102). There is no
|
|
46
|
+
unchecked integer operation in the language.
|
|
47
|
+
7. **Bounds.** Every index into an array, Vec, FixedVec or remaining-account list is checked (5104); a
|
|
48
|
+
growable Vec's stored length is checked against the account's size before it is used (3003).
|
|
49
|
+
8. **Account creation.** `create` / `createIfNeeded` is a top-level statement, and the account cannot be read or
|
|
50
|
+
written before it (compile error). Creation refuses an account that already exists (3000), and an address someone
|
|
51
|
+
pre-funded still gets created. `createIfNeeded` on an existing account re-derives the canonical PDA and checks
|
|
52
|
+
owner, discriminator and size (token accounts: mint and owner; mints: decimals, authorities and the extensions it
|
|
53
|
+
names).
|
|
54
|
+
9. **Writes through references.** A place is never written after the code that computes its value may have moved it:
|
|
55
|
+
replacing or resizing what a live reference points into is a compile error, as is writing a Vec element with a
|
|
56
|
+
right side that may resize the Vec.
|
|
57
|
+
10. **Remaining accounts.** Writing one (`.lamports`, `loadAccountMut`) checks it is writable (3006) and not another
|
|
58
|
+
account of the instruction (2040); `loadAccountMut` loads a given account once per instruction (2040);
|
|
59
|
+
`loadAccount<T>` checks owner, discriminator and size, and validates the data as instruction arguments are.
|
|
60
|
+
11. **Growth.** An account grows by at most 10 KiB per instruction (3016), the payer is a `Mut<Signer>`, new bytes are
|
|
61
|
+
zero and the new rent is paid; shrinking keeps the lamports in the account.
|
|
62
|
+
12. **CPIs.** A program signs only for its own PDAs (the runtime enforces it; the seeds are those written in the
|
|
63
|
+
source). `invoke` marks an account writable or signer only when the parameter is `Mut` / `Signer` or the program
|
|
64
|
+
signs for it; remaining accounts keep the flags they were passed with, or are checked for the role requested.
|
|
65
|
+
Return data is read only from the program named, at the expected size (5106).
|
|
66
|
+
13. **Events.** `emitCpi` is accepted only from this program's event-authority PDA, signed (2002).
|
|
67
|
+
|
|
68
|
+
### When it ends
|
|
69
|
+
|
|
70
|
+
14. **Closing.** A `CloseTo<…>` or `close(…)` account has its lamports moved to the destination, its data zeroed and
|
|
71
|
+
its owner reset to the System Program on every successful return, so it cannot be revived as the same account.
|
|
72
|
+
15. **No silent success.** Every failed check returns its error code and the transaction fails: no code path continues
|
|
73
|
+
or returns success after a failed check or CPI.
|
|
74
|
+
|
|
75
|
+
## Left to the author
|
|
76
|
+
|
|
77
|
+
- **Logic.** Who may call what, limits, prices, fees. TypeSOL checks what the types and constraints say, nothing more.
|
|
78
|
+
- **`AccountInfo` / `UncheckedProgram`.** Unchecked by design, as in Anchor: check what you rely on.
|
|
79
|
+
- **`requirePda(account, seeds, bump)`.** Any valid bump passes (Anchor's `bump = expr`). Use a stored bump, never one
|
|
80
|
+
from the arguments.
|
|
81
|
+
- **Oracles.** `pythPrice` checks verification, feed and age (pass the exponent you scale by); the raw `update.*`
|
|
82
|
+
fields are unchecked. `switchboardValue` needs the expected feed; set `minSamples`.
|
|
83
|
+
- **`merkleVerify`.** Leaves and inner nodes hash alike: build leaves as `keccak256(keccak256(data))` in the program.
|
|
84
|
+
- **Existing accounts in `createIfNeeded`.** An existing token account's delegate and close authority, and a mint's
|
|
85
|
+
extensions the call does not name, are not checked (as in Anchor).
|
|
86
|
+
- **Seeds of variable length.** Two adjacent string seeds can collide (`"ab" + "c"` and `"a" + "bc"`), as in Anchor.
|
|
87
|
+
|
|
88
|
+
## Where TypeSOL is stricter than Anchor
|
|
89
|
+
|
|
90
|
+
- Any two writable parameters naming one account are refused (Anchor compares only program and token accounts).
|
|
91
|
+
- Arithmetic overflow returns an error code (5100) in every build; Anchor aborts the program, and only when
|
|
92
|
+
overflow checks are enabled.
|
|
93
|
+
|
|
94
|
+
## Status
|
|
95
|
+
|
|
96
|
+
The compiler and runtime have had internal security reviews; the findings of the latest are fixed in 0.25.0
|
|
97
|
+
(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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typesol/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|