@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
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ codes, and says so here.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.27.0 — 2026-10-29
|
|
10
|
+
|
|
11
|
+
Anchor 1.0 parity: instruction introspection, `Dup<…>` and account migration. Existing programs compile to the same
|
|
12
|
+
binaries as in 0.26.0.
|
|
13
|
+
|
|
14
|
+
- New: instruction introspection. `InstructionsSysvar` has `length`, `currentIndex()` and `get(i)`, a
|
|
15
|
+
`TransactionInstruction` with `programId`, its accounts (key, signer, writable), its data
|
|
16
|
+
(`readU8/U16/U32/U64/readPubkey`) and `is(program, "name")` (an instruction by its Anchor discriminator); every read
|
|
17
|
+
is bounds-checked (5104). `stackHeight()` is 1 at the top level, more under a CPI.
|
|
18
|
+
- New: `Dup<…>`, Anchor's `dup`: a writable parameter that may be the same account as another, for accounts read and
|
|
19
|
+
written in place (not created, closed, a close destination or `@layout borsh`, which are refused at compile time).
|
|
20
|
+
- New: `Migrate<From, To>`, Anchor 1.0's `Migration`: an existing account rewritten from one struct to another with
|
|
21
|
+
`acct.migrate(payer, value)`, the old data readable through `acct.from` until then (the new value is built before
|
|
22
|
+
the bytes are rewritten), the new through `acct.to` after; resized with the payer topping up rent. For now:
|
|
23
|
+
fixed-layout structs without growable Vecs, no constraints on the account, and it cannot be passed to a helper.
|
|
24
|
+
- New: `/** @discriminator … */` on an account struct (a sha256 preimage such as `account:Counter`, or 0x and 8
|
|
25
|
+
bytes), Anchor's `#[account(discriminator = …)]`: keeps an old type's on-chain discriminator under a new name.
|
|
26
|
+
|
|
27
|
+
## 0.26.0 — 2026-10-29
|
|
28
|
+
|
|
29
|
+
A first-run experience pass, the security model and audit package, and more fuzzing. The compiler's output is the
|
|
30
|
+
same as 0.25.0 for every program.
|
|
31
|
+
|
|
32
|
+
- New: `typesol doctor` checks Node.js, the platform-tools, the Solana CLI, its keypair, the RPC and the keypair's
|
|
33
|
+
balance there, and the project, and prints the command that fixes each problem (exit 1 when building is impossible).
|
|
34
|
+
- `typesol init` prints the files it creates relative to where it runs, warns with the install commands when the
|
|
35
|
+
platform-tools are missing, and lists the next steps.
|
|
36
|
+
- `typesol deploy` checks, before sending anything, that the RPC answers (else: start `solana-test-validator`, or
|
|
37
|
+
`--url devnet`) and that the payer can pay the rent (else: how much, and the `solana airdrop` command on devnet or a
|
|
38
|
+
local validator). RPC URLs in these messages show only the host.
|
|
39
|
+
- Docs: [security model](docs/security-model.md) (the guarantees a compiled program can rely on, trust boundaries,
|
|
40
|
+
what is left to the author) and [audit package](docs/audit.md) (scope, priorities, earlier reviews, evidence);
|
|
41
|
+
README: deploying on devnet and on a local validator.
|
|
42
|
+
- Tests: `test/verification/fuzz-layout.test.ts`, model-based fuzzing of an account with two growable Vecs and of
|
|
43
|
+
`loadAccountMut` aliasing through remaining accounts (also in `pnpm fuzz`).
|
|
44
|
+
- Repository: tests grouped by subject under `test/`, the Anchor twins in `test/anchor-twins` (`pnpm build:twins`),
|
|
45
|
+
the generators and chain tooling in `src/tools/` (the package's import paths are unchanged); Dependabot for npm and
|
|
46
|
+
GitHub Actions.
|
|
47
|
+
|
|
9
48
|
## 0.25.0 — 2026-10-29
|
|
10
49
|
|
|
11
50
|
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/compiler/analyze.js
CHANGED
|
@@ -85,6 +85,8 @@ const TYPE_EXPORTS = new Set([
|
|
|
85
85
|
"Ata",
|
|
86
86
|
"Owner",
|
|
87
87
|
"Executable",
|
|
88
|
+
"Dup",
|
|
89
|
+
"Migrate",
|
|
88
90
|
"Optional",
|
|
89
91
|
"EventAuthority",
|
|
90
92
|
"MplCoreProgram",
|
|
@@ -114,6 +116,22 @@ export function snakeCase(name) {
|
|
|
114
116
|
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
115
117
|
.toLowerCase();
|
|
116
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* An account struct's discriminator: Anchor's sha256("account:<Name>")[..8], or the one a `/** @discriminator … *\/` tag
|
|
121
|
+
* gives (a sha256 preimage such as `account:Counter`, or 0x and 8 bytes), e.g. for an old version of a struct kept under
|
|
122
|
+
* a new name for Migrate<From, To>.
|
|
123
|
+
*/
|
|
124
|
+
function accountDiscriminator(decl, name, reporter) {
|
|
125
|
+
const tag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "discriminator");
|
|
126
|
+
if (!tag)
|
|
127
|
+
return discriminator(`account:${name}`);
|
|
128
|
+
const text = (typeof tag.comment === "string" ? tag.comment : (ts.getTextOfJSDocComment(tag.comment) ?? "")).trim();
|
|
129
|
+
if (/^0x([0-9a-fA-F]{2}){8}$/.test(text))
|
|
130
|
+
return [...Buffer.from(text.slice(2), "hex")];
|
|
131
|
+
if (text && !text.startsWith("0x"))
|
|
132
|
+
return discriminator(text);
|
|
133
|
+
reporter.fail(decl, "@discriminator on an account struct needs a sha256 preimage (account:<Name>) or 0x and 8 bytes");
|
|
134
|
+
}
|
|
117
135
|
export function discriminator(preimage) {
|
|
118
136
|
return [...createHash("sha256").update(preimage).digest().subarray(0, 8)];
|
|
119
137
|
}
|
|
@@ -601,12 +619,17 @@ export function analyze(entry) {
|
|
|
601
619
|
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
602
620
|
if (ix.returns?.kind === "struct")
|
|
603
621
|
collectStructs(ix.returns, structs, dataEnums, argStructs);
|
|
604
|
-
for (const a of ix.accounts)
|
|
622
|
+
for (const a of ix.accounts) {
|
|
623
|
+
if (a.kind.kind === "migrate") {
|
|
624
|
+
accountStructs.add(a.kind.from.name);
|
|
625
|
+
accountStructs.add(a.kind.to.name);
|
|
626
|
+
}
|
|
605
627
|
if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded") {
|
|
606
628
|
accountStructs.add(a.kind.struct.name);
|
|
607
629
|
if (a.kind.struct.viewsOnly)
|
|
608
630
|
reporter.error(a.node, `${a.kind.struct.name} holds a string (or an enum carrying one), so it is an argument, return value or event; account data uses FixedString<N>`);
|
|
609
631
|
}
|
|
632
|
+
}
|
|
610
633
|
}
|
|
611
634
|
// Arguments are Borsh, where a FixedVec is a Vec (only what it holds). A struct passed as a whole argument and used
|
|
612
635
|
// nowhere else (not account data, not nested, not returned) is read as Anchor sends it: @layout borsh, implied.
|
|
@@ -966,7 +989,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
|
|
|
966
989
|
name,
|
|
967
990
|
fields: stored,
|
|
968
991
|
size: undefined,
|
|
969
|
-
accountDiscriminator:
|
|
992
|
+
accountDiscriminator: accountDiscriminator(decl, name, reporter),
|
|
970
993
|
eventDiscriminator: discriminator(`event:${name}`),
|
|
971
994
|
node: decl,
|
|
972
995
|
layout: layoutText === "borsh" ? "borsh" : "fixed",
|
|
@@ -1321,7 +1344,7 @@ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new S
|
|
|
1321
1344
|
wrapped === "Ata")
|
|
1322
1345
|
reporter.fail(p.type, "constraints belong on instruction parameters; helpers take the plain account type");
|
|
1323
1346
|
const account = resolveAccountKind(p.type, reporter, scope.imports, structs);
|
|
1324
|
-
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed")
|
|
1347
|
+
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed" || account.kind === "migrate")
|
|
1325
1348
|
reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
|
|
1326
1349
|
return { name: p.name.text, type: { kind: "account" }, account, node: p };
|
|
1327
1350
|
});
|
|
@@ -1445,7 +1468,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1445
1468
|
const inner = unwrapConstraints(typeNode, reporter, imports, constraints);
|
|
1446
1469
|
const kind = resolveAccountKind(inner, reporter, imports, structs);
|
|
1447
1470
|
if (optional &&
|
|
1448
|
-
(["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1471
|
+
(["init", "initIfNeeded", "zeroed", "migrate", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1449
1472
|
((kind.kind === "token" || kind.kind === "mint") && kind.init)))
|
|
1450
1473
|
reporter.fail(p, "Init, InitIfNeeded, Zeroed, PythPriceUpdate, EventAuthority, SelfProgram and ProgramData accounts cannot be optional");
|
|
1451
1474
|
accounts.push({ name: p.name.text, index: accounts.length, kind, node: p, constraints, ...(optional ? { optional } : {}) });
|
|
@@ -1566,6 +1589,18 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1566
1589
|
reporter.error(fn.name, "instructions that grow a Vec (push) need a SystemProgram parameter to top up rent");
|
|
1567
1590
|
if (needs("init") && !needs("system"))
|
|
1568
1591
|
reporter.error(fn.name, "instructions with Init<...> accounts need a SystemProgram parameter");
|
|
1592
|
+
if (accounts.some((a) => a.kind.kind === "migrate" && a.kind.to.size > a.kind.from.size) && !needs("system"))
|
|
1593
|
+
reporter.error(fn.name, "a Migrate<From, To> that grows the account needs a SystemProgram parameter (the payer tops up its rent)");
|
|
1594
|
+
for (const a of accounts)
|
|
1595
|
+
if (a.kind.kind === "migrate" &&
|
|
1596
|
+
(a.constraints.hasOne.length ||
|
|
1597
|
+
a.constraints.seeds ||
|
|
1598
|
+
a.constraints.closeTo ||
|
|
1599
|
+
a.constraints.ata ||
|
|
1600
|
+
a.constraints.address ||
|
|
1601
|
+
a.constraints.owner ||
|
|
1602
|
+
a.constraints.dup))
|
|
1603
|
+
reporter.error(a.node, "Migrate<From, To> takes no constraints yet: check the old fields with require(…, …) on acct.from");
|
|
1569
1604
|
const name = snakeCase(fn.name.text);
|
|
1570
1605
|
// `/** @discriminator spl-transfer-hook-interface:execute */` (sha256 preimage) or `@discriminator 0x...` (raw bytes)
|
|
1571
1606
|
// lets an instruction implement another program's interface.
|
|
@@ -1596,7 +1631,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1596
1631
|
/** Peel HasOne / Seeds / CloseTo wrappers off a parameter type, recording what they ask for. */
|
|
1597
1632
|
function unwrapConstraints(node, reporter, imports, out) {
|
|
1598
1633
|
const name = typesolName(node, imports);
|
|
1599
|
-
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
|
|
1634
|
+
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable", "Dup"].includes(name ?? ""))
|
|
1600
1635
|
return node;
|
|
1601
1636
|
const args = node.typeArguments ?? [];
|
|
1602
1637
|
const lit = (t) => {
|
|
@@ -1638,6 +1673,8 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1638
1673
|
}
|
|
1639
1674
|
if (name === "Executable")
|
|
1640
1675
|
out.executable = true;
|
|
1676
|
+
if (name === "Dup")
|
|
1677
|
+
out.dup = true;
|
|
1641
1678
|
if (name === "Address") {
|
|
1642
1679
|
const text = lit(args[1])[0];
|
|
1643
1680
|
const bytes = text === undefined ? undefined : decodeAddress(text);
|
|
@@ -1668,6 +1705,8 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1668
1705
|
reporter.fail(node, `${name}<…> wraps an account type`);
|
|
1669
1706
|
return unwrapConstraints(args[0], reporter, imports, out);
|
|
1670
1707
|
}
|
|
1708
|
+
/** Solana: an account grows at most this many bytes per instruction. */
|
|
1709
|
+
const MAX_PERMITTED_DATA_INCREASE = 10_240;
|
|
1671
1710
|
function resolveAccountKind(node, reporter, imports, structs) {
|
|
1672
1711
|
const outer = typesolName(node, imports);
|
|
1673
1712
|
const ref = node;
|
|
@@ -1769,6 +1808,24 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1769
1808
|
reporter.fail(node, `${s.name} is too large to create in a program; use Zeroed<${s.name}>`);
|
|
1770
1809
|
return { kind: "initIfNeeded", struct: s };
|
|
1771
1810
|
}
|
|
1811
|
+
case "Migrate": {
|
|
1812
|
+
const [fromNode, toNode] = ref.typeArguments ?? [];
|
|
1813
|
+
const from = fromNode && structOf(fromNode);
|
|
1814
|
+
const to = toNode && structOf(toNode);
|
|
1815
|
+
if (!from || !to || ref.typeArguments.length !== 2)
|
|
1816
|
+
reporter.fail(node, "Migrate<From, To> takes two account structs declared in this program");
|
|
1817
|
+
for (const s of [from, to]) {
|
|
1818
|
+
if (s.vec)
|
|
1819
|
+
reporter.fail(node, `${s.name} has a growable Vec; Migrate<From, To> takes fixed-size structs`);
|
|
1820
|
+
if (s.layout === "borsh")
|
|
1821
|
+
reporter.fail(node, `${s.name} is @layout borsh; Migrate<From, To> takes fixed layouts`);
|
|
1822
|
+
}
|
|
1823
|
+
if (from.accountDiscriminator.join() === to.accountDiscriminator.join())
|
|
1824
|
+
reporter.fail(node, `${from.name} and ${to.name} have the same discriminator: give the new struct another name (or the old one /** @discriminator account:<OldName> */)`);
|
|
1825
|
+
if (to.size - from.size > MAX_PERMITTED_DATA_INCREASE)
|
|
1826
|
+
reporter.fail(node, `${to.name} is more than 10 KiB larger than ${from.name}; an account grows at most 10 KiB per instruction`);
|
|
1827
|
+
return { kind: "migrate", from, to };
|
|
1828
|
+
}
|
|
1772
1829
|
case "Zeroed": {
|
|
1773
1830
|
const s = structOf(inner());
|
|
1774
1831
|
if (!s)
|
|
@@ -112,6 +112,8 @@ export declare abstract class EmitterBase {
|
|
|
112
112
|
protected abstract call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
|
|
113
113
|
protected abstract isRemaining(node: ts.Expression): boolean;
|
|
114
114
|
protected abstract handle(node: ts.Expression): string | undefined;
|
|
115
|
+
protected abstract instructionsParam(node: ts.Expression): AccountParam | undefined;
|
|
116
|
+
protected abstract isTxIx(node: ts.Expression): boolean;
|
|
115
117
|
protected abstract seeds(node: ts.Expression, extra: number): {
|
|
116
118
|
name: string;
|
|
117
119
|
count: number;
|
|
@@ -65,7 +65,10 @@ export class EmitterBase {
|
|
|
65
65
|
this.ix = target;
|
|
66
66
|
for (const a of target.accounts) {
|
|
67
67
|
this.accounts.set(a.name, a);
|
|
68
|
-
if (a.kind.kind === "init" ||
|
|
68
|
+
if (a.kind.kind === "init" ||
|
|
69
|
+
a.kind.kind === "initIfNeeded" ||
|
|
70
|
+
a.kind.kind === "migrate" ||
|
|
71
|
+
((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init))
|
|
69
72
|
this.pendingInit.add(a.name);
|
|
70
73
|
}
|
|
71
74
|
for (const a of target.args)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { type ErrorDef, type FieldType, type Helper } from "../model.js";
|
|
2
|
+
import { type AccountParam, type ErrorDef, type FieldType, type Helper } from "../model.js";
|
|
3
3
|
import { type Value, type Ref, type Closure } from "./shared.js";
|
|
4
4
|
import { ExpressionEmitter } from "./expressions.js";
|
|
5
5
|
export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
@@ -20,6 +20,15 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
|
20
20
|
/** `remainingAccounts()` or a const bound to it. */
|
|
21
21
|
protected isRemaining(node: ts.Expression): boolean;
|
|
22
22
|
/** C pointer (`TsAccount *`) for an expression naming a remaining account, or undefined. */
|
|
23
|
+
/** Whether `node` is an InstructionsSysvar parameter. */
|
|
24
|
+
protected instructionsParam(node: ts.Expression): AccountParam | undefined;
|
|
25
|
+
/** Whether `node` is a TransactionInstruction: a local holding one, or `ixs.get(i)`. */
|
|
26
|
+
protected isTxIx(node: ts.Expression): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* InstructionsSysvar.currentIndex() / .get(i) and the methods of a TransactionInstruction: bounds-checked reads of the
|
|
29
|
+
* sysvar (ts_ix_* in the runtime). Undefined when the call is something else.
|
|
30
|
+
*/
|
|
31
|
+
protected instructionsCall(node: ts.CallExpression, callee: ts.PropertyAccessExpression, method: string): Value | undefined;
|
|
23
32
|
protected handle(node: ts.Expression): string | undefined;
|
|
24
33
|
/** A declared account parameter or a remaining account, as a `TsAccount *` C expression. */
|
|
25
34
|
protected anyAccount(node: ts.Expression, what: string): string;
|
|
@@ -29,6 +38,12 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
|
29
38
|
protected loadForeign(node: ts.CallExpression): Value;
|
|
30
39
|
/** `loadAccount<T>(acct)` / `loadAccountMut<T>(acct)`: checked view of a program account. */
|
|
31
40
|
protected loadAccount(node: ts.CallExpression, mut: boolean): Value;
|
|
41
|
+
/**
|
|
42
|
+
* `acct.migrate(payer, value)` on a Migrate<From, To> account: the new value is built in full first (it may read
|
|
43
|
+
* acct.from, whose bytes the rewrite overwrites), then the account is resized (the payer tops up the rent when it
|
|
44
|
+
* grows; shrinking keeps the lamports) and rewritten with To's discriminator and the value.
|
|
45
|
+
*/
|
|
46
|
+
protected migrate(node: ts.CallExpression, a: AccountParam, topLevel: boolean): Value | undefined;
|
|
32
47
|
/** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
|
|
33
48
|
private helperGrows;
|
|
34
49
|
/**
|