@typesol/cli 0.24.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +35 -16
- package/SECURITY.md +2 -0
- package/dist/cli.js +52 -37
- package/dist/compiler/analyze.js +11 -1
- package/dist/compiler/emit/calls.js +10 -10
- package/dist/compiler/emit/cpi.js +4 -0
- package/dist/compiler/emit/function.d.ts +9 -0
- package/dist/compiler/emit/function.js +169 -139
- package/dist/compiler/emit/program.js +3 -0
- package/dist/compiler/emit/shared.d.ts +1 -0
- package/dist/compiler/emit/shared.js +1 -0
- package/dist/compiler/emit/statements.d.ts +6 -0
- package/dist/compiler/emit/statements.js +35 -5
- 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} +4 -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 +224 -0
- package/docs/cli.md +3 -2
- package/docs/errors.md +26 -1
- package/docs/language.md +12 -8
- package/docs/migrate-from-anchor.md +3 -1
- package/docs/runtime.md +1 -1
- package/docs/security-model.md +97 -0
- package/docs/testing.md +5 -4
- package/lib/typesol.d.ts +35 -13
- package/package.json +13 -13
- package/runtime/typesol_rt.h +33 -4
- /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,57 @@ codes, and says so here.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.26.0 — 2026-10-29
|
|
10
|
+
|
|
11
|
+
A first-run experience pass, the security model and audit package, and more fuzzing. The compiler's output is the
|
|
12
|
+
same as 0.25.0 for every program.
|
|
13
|
+
|
|
14
|
+
- New: `typesol doctor` checks Node.js, the platform-tools, the Solana CLI, its keypair, the RPC and the keypair's
|
|
15
|
+
balance there, and the project, and prints the command that fixes each problem (exit 1 when building is impossible).
|
|
16
|
+
- `typesol init` prints the files it creates relative to where it runs, warns with the install commands when the
|
|
17
|
+
platform-tools are missing, and lists the next steps.
|
|
18
|
+
- `typesol deploy` checks, before sending anything, that the RPC answers (else: start `solana-test-validator`, or
|
|
19
|
+
`--url devnet`) and that the payer can pay the rent (else: how much, and the `solana airdrop` command on devnet or a
|
|
20
|
+
local validator). RPC URLs in these messages show only the host.
|
|
21
|
+
- Docs: [security model](docs/security-model.md) (the guarantees a compiled program can rely on, trust boundaries,
|
|
22
|
+
what is left to the author) and [audit package](docs/audit.md) (scope, priorities, earlier reviews, evidence);
|
|
23
|
+
README: deploying on devnet and on a local validator.
|
|
24
|
+
- Tests: `test/verification/fuzz-layout.test.ts`, model-based fuzzing of an account with two growable Vecs and of
|
|
25
|
+
`loadAccountMut` aliasing through remaining accounts (also in `pnpm fuzz`).
|
|
26
|
+
- Repository: tests grouped by subject under `test/`, the Anchor twins in `test/anchor-twins` (`pnpm build:twins`),
|
|
27
|
+
the generators and chain tooling in `src/tools/` (the package's import paths are unchanged); Dependabot for npm and
|
|
28
|
+
GitHub Actions.
|
|
29
|
+
|
|
30
|
+
## 0.25.0 — 2026-10-29
|
|
31
|
+
|
|
32
|
+
Security review (October 2026) fixes, and error codes aligned with Anchor. Some refused transactions now report a
|
|
33
|
+
different error code (below): a minor version.
|
|
34
|
+
|
|
35
|
+
- Fixed: `loadAccountMut` on the same remaining account twice (`rest.get(i)`, `rest.get(j)` with `i == j`) gave two
|
|
36
|
+
writable views of one account; a program reading both before writing both could be made to create value. The second
|
|
37
|
+
load is now refused with 2040.
|
|
38
|
+
- Fixed: a Vec element written with a right side that resizes the Vec (`book.bids.get(i).qty += settle(book)` where the
|
|
39
|
+
helper pops, a closure, or a `push` / `pop` / `clear` in the expression) wrote through the element's old position,
|
|
40
|
+
into the bytes of the next Vec. Such an assignment is now a compile error ("store its result in a local first"),
|
|
41
|
+
as it already was for a plain `=` with a helper.
|
|
42
|
+
- Fixed: `Ata<Init<…>>` / `Ata<InitIfNeeded<…>>` creation and `Ata<AccountInfo, …>` with two token program parameters
|
|
43
|
+
used the first one; they now need exactly one (use `TokenInterface`).
|
|
44
|
+
- New: `TokenOf<…, …, …, "tokenProgram">` / `MintOf<…, …, …, "tokenProgram">`, Anchor's `token::token_program` /
|
|
45
|
+
`mint::token_program` (2021 / 2022). The escrow port uses it.
|
|
46
|
+
- New: `pythPrice(update, feedId, maxAge, exponent)` refuses a feed whose exponent is not the one given;
|
|
47
|
+
`switchboardValue(…, minSamples)` refuses a result resting on fewer oracle samples.
|
|
48
|
+
- Changed: each account's constraints are checked in Anchor's order (seeds, associated token account, has_one, owner,
|
|
49
|
+
executable, address, token::, mint::), so a transaction breaking several fails with Anchor's code: e.g. a stranger's
|
|
50
|
+
call on a `Seeds<HasOne<…>>` account now fails with 2006 (ConstraintSeeds) instead of 2001, and an `Ata<…>` of
|
|
51
|
+
another wallet with 2015 instead of 2009. A `Mut<…>` account passed read-only fails with 2000 (ConstraintMut), as
|
|
52
|
+
Anchor's `#[account(mut)]`, instead of 3006. docs/errors.md lists the order and the differences left.
|
|
53
|
+
- Docs: unchecked raw Pyth fields, `requirePda` with a bump from the arguments, Merkle leaves, and what
|
|
54
|
+
`createIfNeeded` leaves unchecked on an existing account.
|
|
55
|
+
- Docs: [Case study: porting Anchor's escrow](docs/case-study-escrow.md), one Anchor program ported end to end, with the
|
|
56
|
+
differences, the checks run against the original and the cost.
|
|
57
|
+
- Releases: the release workflow checks that CHANGELOG.md has the version, and after publishing installs the package
|
|
58
|
+
from npm, scaffolds a project from each template and runs its typecheck, build and test.
|
|
59
|
+
|
|
9
60
|
## 0.24.1 — 2026-10-28
|
|
10
61
|
|
|
11
62
|
Smaller programs and fewer compute units, with the same results; no change to the language, IDLs or error codes. Also
|
package/README.md
CHANGED
|
@@ -149,9 +149,20 @@ npx @typesol/cli init my-app --name counter # typesol.json, programs/counter.t
|
|
|
149
149
|
cd my-app && npm install
|
|
150
150
|
npm test # builds the program and runs it in LiteSVM, an in-process Solana runtime
|
|
151
151
|
npx typesol build # target/counter/{counter.so, counter.json (IDL), counter.client.ts, counter.build.json}
|
|
152
|
-
npx typesol deploy --url http://127.0.0.1:8899 --keypair ~/.config/solana/id.json
|
|
153
152
|
```
|
|
154
153
|
|
|
154
|
+
`npx typesol doctor` checks all of this (Node.js, platform-tools, Solana CLI, keypair, RPC) and prints the command
|
|
155
|
+
that fixes anything missing. To deploy, on devnet:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
solana-keygen new # once, if you have no keypair (~/.config/solana/id.json)
|
|
159
|
+
solana airdrop 2 --url devnet # test SOL for the rent (rate-limited: https://faucet.solana.com also works)
|
|
160
|
+
npx typesol deploy --url devnet
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
or on a local validator: `solana-test-validator` in another terminal, `solana airdrop 5 --url localhost`, then
|
|
164
|
+
`npx typesol deploy --url localhost`. `deploy` checks the RPC and the payer's balance before sending anything.
|
|
165
|
+
|
|
155
166
|
`--template vault` or `--template escrow` starts from a SOL vault or an SPL token escrow instead of the counter, each
|
|
156
167
|
with a passing LiteSVM test.
|
|
157
168
|
|
|
@@ -162,8 +173,9 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
|
|
|
162
173
|
Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
|
|
163
174
|
program's address.
|
|
164
175
|
|
|
165
|
-
Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct,
|
|
166
|
-
starts from your program's IDL
|
|
176
|
+
Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct, `typesol port`
|
|
177
|
+
starts from your program's IDL, and [docs/case-study-escrow.md](docs/case-study-escrow.md) follows one complete Anchor
|
|
178
|
+
program through the port. Tests use `typesol/testing` (accounts by name, plain values; [docs/test-your-program.md](docs/test-your-program.md)). To go to
|
|
167
179
|
mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
|
|
168
180
|
|
|
169
181
|
## How it works
|
|
@@ -261,23 +273,28 @@ units are not a safety claim. The differential tests also run the escrow against
|
|
|
261
273
|
### On mainnet
|
|
262
274
|
|
|
263
275
|
The official escrow ([tokens/escrow](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow)),
|
|
264
|
-
deployed to Solana mainnet-beta in all four versions
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
deployed to Solana mainnet-beta in all four versions and called with the same mints, amounts and accounts, one
|
|
277
|
+
instruction per transaction. The TypeSOL program was upgraded in place to 0.25.0 on 2026-10-29; the others are as first
|
|
278
|
+
deployed on 2026-10-27. Each figure is the compute units of that transaction, linked to Solscan; the lowest per row is
|
|
279
|
+
in bold. The deployed programs match these builds byte for byte (`typesol verify` for TypeSOL), and the TypeSOL
|
|
280
|
+
program's IDL is published on chain with `typesol idl publish`.
|
|
268
281
|
|
|
269
282
|
| Transaction | TypeSOL | Pinocchio | Native Rust | Anchor |
|
|
270
283
|
| --- | ---: | ---: | ---: | ---: |
|
|
271
|
-
| make_offer (offer 1) | [
|
|
272
|
-
| take_offer (offer 1) | [**
|
|
273
|
-
| make_offer (offer 2) | [
|
|
274
|
-
| 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 |
|
|
275
289
|
| program | [65ybkcg6…](https://solscan.io/account/65ybkcg6mQa9t5EuHtirRdvf2qAu12qRiK6wYBbGk16P) | [FrQwq3iU…](https://solscan.io/account/FrQwq3iUa8ZkWEne1y27webXTAyLgH4Hj6t986t6VS2G) | [5pnmi1Md…](https://solscan.io/account/5pnmi1MdxzXKmgX7ooRSE212rzZK8rb7nNpRTSMqP8V1) | [EG7M3sLL…](https://solscan.io/account/EG7M3sLLwB7MhmtzwYsaxbhZaFDbshL9hmVK79bQVyVq) |
|
|
276
290
|
|
|
277
|
-
Most of make_offer is the Associated Token Account program creating the vault
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
291
|
+
Most of make_offer is the Associated Token Account program creating the vault. It searches for the vault address's
|
|
292
|
+
bump itself at 1,500 CU a try, and every offer has its own vault, so that part varies from offer to offer in every
|
|
293
|
+
version (13,585 to 22,585 CU of the make_offers above); programs that check the vault's address re-derive it, so
|
|
294
|
+
take_offer and refund_offer vary with it too. The escrow program's own part is steadier: TypeSOL's make_offer spent
|
|
295
|
+
6,620 CU of its own on both offers. The Pinocchio version takes the offer PDA's bump from the client instead of
|
|
296
|
+
searching for it. Mainnet figures differ from the LiteSVM means above. Every run, including the one before the
|
|
297
|
+
upgrade, is in [typesol/benchmarks](https://github.com/typesol/benchmarks) (`results/`).
|
|
281
298
|
|
|
282
299
|
## How it is tested
|
|
283
300
|
|
|
@@ -315,7 +332,9 @@ change.
|
|
|
315
332
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
|
316
333
|
- They have **not been audited by an independent firm**. Do not put significant value behind programs built with
|
|
317
334
|
TypeSOL until such an audit is published.
|
|
318
|
-
- Audit your own programs as you would any Solana program.
|
|
335
|
+
- Audit your own programs as you would any Solana program. What a compiled program can rely on, and what is left to
|
|
336
|
+
you, is in [docs/security-model.md](docs/security-model.md); [docs/audit.md](docs/audit.md) is the package for an
|
|
337
|
+
external audit.
|
|
319
338
|
- Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
|
|
320
339
|
|
|
321
340
|
Not supported yet: see the end of [docs/language.md](docs/language.md#not-supported-yet).
|
package/SECURITY.md
CHANGED
|
@@ -9,6 +9,8 @@ TypeSOL generates programs that hold funds. Treat it accordingly.
|
|
|
9
9
|
firm.** Do not put significant value behind programs built with it until an external audit of the compiler and
|
|
10
10
|
runtime is published, and audit your own programs as you would any Solana program.
|
|
11
11
|
- Only the latest 0.x release receives security fixes.
|
|
12
|
+
- What a compiled program can rely on is specified in [docs/security-model.md](docs/security-model.md); the package
|
|
13
|
+
for an external audit is [docs/audit.md](docs/audit.md).
|
|
12
14
|
|
|
13
15
|
## Reporting a vulnerability
|
|
14
16
|
|
package/dist/cli.js
CHANGED
|
@@ -7,14 +7,15 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
import ts from "typescript";
|
|
9
9
|
import { build, CompileError, COMPILER_VERSION, generate, SBPF_VERSIONS, verifyBinary } from "./compiler/index.js";
|
|
10
|
-
import { generateCpi } from "./cpigen.js";
|
|
11
|
-
import { generatePort } from "./portgen.js";
|
|
12
|
-
import { generateClient, writeClient } from "./clientgen.js";
|
|
10
|
+
import { generateCpi } from "./tools/cpigen.js";
|
|
11
|
+
import { generatePort } from "./tools/portgen.js";
|
|
12
|
+
import { generateClient, writeClient } from "./tools/clientgen.js";
|
|
13
13
|
import { buildIdl } from "./compiler/idl.js";
|
|
14
14
|
import { decodeAddress, loadOrCreateKeypair, readKeypair } from "./keys.js";
|
|
15
|
-
import { formatProfile, profileLogs } from "./profile.js";
|
|
15
|
+
import { formatProfile, profileLogs } from "./tools/profile.js";
|
|
16
16
|
import { TEMPLATE_NAMES } from "./templates.js";
|
|
17
17
|
import { buildWorkspace, CONFIG, deployArgs, findWorkspace, programAddress, programForEntry, programPaths, scaffold } from "./workspace.js";
|
|
18
|
+
import { clusterUrl, deployPreflight, doctorChecks, formatChecks, INSTALL_AGAVE, solanaConfig as readSolanaConfig, toolchainCheck } from "./tools/doctor.js";
|
|
18
19
|
const ASSETS = fileURLToPath(new URL("../assets/", import.meta.url));
|
|
19
20
|
/** A usage problem: printed with the command's help, exit code 2. */
|
|
20
21
|
class UsageError extends Error {
|
|
@@ -65,9 +66,18 @@ const COMMANDS = {
|
|
|
65
66
|
.replace(/^_+|_+$/g, "")
|
|
66
67
|
.toLowerCase();
|
|
67
68
|
const name = v.name ?? (/^[a-z]/.test(derived) ? derived : `p_${derived || "program"}`);
|
|
69
|
+
const rel = path.relative(process.cwd(), root) || ".";
|
|
68
70
|
for (const f of scaffold(root, name, template))
|
|
69
|
-
log(` created ${path.join(
|
|
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
|
@@ -1473,6 +1473,13 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1473
1473
|
if (entry.includes("=")) {
|
|
1474
1474
|
if (!accounts.some((o) => o.name === param))
|
|
1475
1475
|
reporter.fail(a.node, `add an account parameter named "${param}"`);
|
|
1476
|
+
if (f === "tokenProgram") {
|
|
1477
|
+
if (a.kind.kind !== "token" && a.kind.kind !== "mint")
|
|
1478
|
+
reporter.fail(a.node, "TokenOf / MintOf apply to a TokenAccount / Mint");
|
|
1479
|
+
if (accounts.find((o) => o.name === param).kind.kind !== "tokenProgram")
|
|
1480
|
+
reporter.fail(a.node, `"${param}" must be a TokenProgram, Token2022Program or TokenInterface parameter`);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1476
1483
|
if (a.kind.kind === "token" ? f !== "mint" && f !== "owner" : a.kind.kind === "mint" ? false : true)
|
|
1477
1484
|
reporter.fail(a.node, a.kind.kind === "token" || a.kind.kind === "mint" ? "TokenOf<…> applies to a TokenAccount" : "TokenOf / MintOf apply to a TokenAccount / Mint");
|
|
1478
1485
|
if (a.kind.kind === "mint" && f !== "mintAuthority" && f !== "freezeAuthority")
|
|
@@ -1610,12 +1617,15 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1610
1617
|
const one = (t) => (t && t.kind !== ts.SyntaxKind.NeverKeyword ? lit(t)[0] : undefined);
|
|
1611
1618
|
const p1 = one(args[1]);
|
|
1612
1619
|
const p2 = one(args[2]);
|
|
1613
|
-
|
|
1620
|
+
const p3 = one(args[3]);
|
|
1621
|
+
if (!p1 && !p2 && !p3)
|
|
1614
1622
|
reporter.fail(node, `${name}<…> names at least one parameter`);
|
|
1615
1623
|
if (p1)
|
|
1616
1624
|
out.hasOne.push(`${first}=${p1}`);
|
|
1617
1625
|
if (p2)
|
|
1618
1626
|
out.hasOne.push(`${second}=${p2}`);
|
|
1627
|
+
if (p3)
|
|
1628
|
+
out.hasOne.push(`tokenProgram=${p3}`);
|
|
1619
1629
|
}
|
|
1620
1630
|
if (name === "CloseTo")
|
|
1621
1631
|
out.closeTo = lit(args[1])[0];
|
|
@@ -3,7 +3,7 @@ import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
|
|
|
3
3
|
import { ANCHOR_ERRORS } from "../anchor-errors.js";
|
|
4
4
|
import { decodeAddress } from "../../keys.js";
|
|
5
5
|
import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
|
|
6
|
-
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
|
|
6
|
+
import { U64, U8, I64, I32, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
|
|
7
7
|
import { ExpressionEmitter } from "./expressions.js";
|
|
8
8
|
/**
|
|
9
9
|
* Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
|
|
@@ -344,29 +344,29 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
344
344
|
return { c: `ts_secp256k1_verify(${h.c}, (uint64_t)${id.c}, ${r.c}, ${sv.c}, ${eth.c})`, type: BOOL };
|
|
345
345
|
}
|
|
346
346
|
case "pythPrice": {
|
|
347
|
-
if (args.length !== 3)
|
|
348
|
-
this.fail(node, "pythPrice(update, feedId, maxAgeSeconds) takes three arguments");
|
|
347
|
+
if (args.length !== 3 && args.length !== 4)
|
|
348
|
+
this.fail(node, "pythPrice(update, feedId, maxAgeSeconds, exponent?) takes three or four arguments");
|
|
349
349
|
const u = this.accountParam(args[0], "the price update");
|
|
350
350
|
if (u.kind.kind !== "pyth")
|
|
351
351
|
this.fail(args[0], "pass a PythPriceUpdate parameter");
|
|
352
|
-
const [feed, age] = this.lowerInOrder(args.slice(1), [HASH, U64]);
|
|
352
|
+
const [feed, age, expo] = this.lowerInOrder(args.slice(1), [HASH, U64, I32]);
|
|
353
353
|
const t = this.tmp("px");
|
|
354
354
|
this.line(`int64_t ${t};`);
|
|
355
|
-
this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, &${t}));`);
|
|
355
|
+
this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, ${expo ? `true, ${expo.c}` : "false, 0"}, &${t}));`);
|
|
356
356
|
return { c: t, type: I64 };
|
|
357
357
|
}
|
|
358
358
|
case "switchboardValue": {
|
|
359
359
|
// Anyone can create a Switchboard feed: like pythPrice's feed id, the expected feed address is required.
|
|
360
|
-
if (args.length !== 4)
|
|
361
|
-
this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals) takes four arguments");
|
|
360
|
+
if (args.length !== 4 && args.length !== 5)
|
|
361
|
+
this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals, minSamples?) takes four or five arguments");
|
|
362
362
|
const f = this.accountParam(args[0], "the feed");
|
|
363
363
|
if (f.kind.kind !== "switchboard")
|
|
364
364
|
this.fail(args[0], "pass a SwitchboardFeed parameter");
|
|
365
|
-
const [expected, stale, dec] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8]);
|
|
365
|
+
const [expected, stale, dec, min] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8, U8]);
|
|
366
366
|
const t = this.tmp("sb");
|
|
367
367
|
this.line(`int64_t ${t};`);
|
|
368
368
|
this.line(`if (!ts_key_eq(&a_${f.name}->key, ${expected.c})) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: not the expected Switchboard feed");`);
|
|
369
|
-
this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, &${t}));`);
|
|
369
|
+
this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, ${min ? min.c : "1"}, &${t}));`);
|
|
370
370
|
return { c: t, type: I64 };
|
|
371
371
|
}
|
|
372
372
|
case "associatedTokenAddress": {
|
|
@@ -980,7 +980,7 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
980
980
|
this.line(l);
|
|
981
981
|
if (mut) {
|
|
982
982
|
this.line(`TS_TRY(ts_check_writable(${h}));`);
|
|
983
|
-
this.line(`TS_TRY(
|
|
983
|
+
this.line(`TS_TRY(ts_claim_mut(params, ${h}));`);
|
|
984
984
|
}
|
|
985
985
|
return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
|
|
986
986
|
}
|
|
@@ -774,6 +774,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
774
774
|
const ataProgram = need("ataProgram", "an AssociatedTokenProgram");
|
|
775
775
|
const system = need("system", "a SystemProgram");
|
|
776
776
|
const tokenProgram = need("tokenProgram", "a token program (TokenProgram, Token2022Program or TokenInterface)");
|
|
777
|
+
// The address is derived under the token program: with two, which one the mint uses is not known here.
|
|
778
|
+
const tokenPrograms = this.ix.accounts.filter((o) => o.kind.kind === "tokenProgram");
|
|
779
|
+
if (tokenPrograms.length > 1)
|
|
780
|
+
this.fail(node, `${call}(...) needs exactly one token program parameter (found ${tokenPrograms.map((o) => o.name).join(", ")}); use TokenInterface`);
|
|
777
781
|
const { owner, mint } = target.constraints.ata;
|
|
778
782
|
const a = `a_${target.name}`;
|
|
779
783
|
const create = `TS_TRY(ts_ata_create(params, a_${ataProgram.name}, a_${payer.name}, ${a}, a_${owner}, a_${mint}, a_${system.name}, a_${tokenProgram.name}, false));`;
|
|
@@ -9,7 +9,16 @@ export declare class FunctionEmitter extends StatementEmitter {
|
|
|
9
9
|
emitInstruction(): string;
|
|
10
10
|
/** CloseTo<…> accounts are closed when the instruction succeeds, at every return. */
|
|
11
11
|
protected epilogue(): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Each account's constraints, account by account in parameter order and, within one account, in Anchor's order
|
|
14
|
+
* (anchor-syn's `linearize`): seeds, associated token, has_one, owner, executable, address, token::, mint::.
|
|
15
|
+
* A transaction that breaks several checks then fails with the same error code as under Anchor.
|
|
16
|
+
*/
|
|
12
17
|
protected constraintChecks(ix: Instruction): string[];
|
|
18
|
+
/** Ata<…>: the account's owner (2015) and address (2009) as Anchor checks them, then its mint. */
|
|
19
|
+
private ataCheck;
|
|
20
|
+
/** Seeds<…>: the account is the PDA of its seeds (with the stored bump, or the canonical one found here). */
|
|
21
|
+
private seedsCheck;
|
|
13
22
|
/** Optional account parameters a piece of generated C refers to (a_name / d_name). */
|
|
14
23
|
protected optionalIn(code: string): string[];
|
|
15
24
|
}
|