@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/docs/cli.md
CHANGED
|
@@ -6,12 +6,13 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
|
|
|
6
6
|
|
|
7
7
|
| Command | What it does |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`, `programs/tsconfig.json` (with the editor plugin) and `.gitignore`. `<name>` must be snake_case; by default it is derived from `<dir>` (`my-app` → `my_app`). The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. |
|
|
9
|
+
| `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`, `programs/tsconfig.json` (with the editor plugin) and `.gitignore`. `<name>` must be snake_case; by default it is derived from `<dir>` (`my-app` → `my_app`). The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. It warns, with the install commands, when the platform-tools that building needs are missing. |
|
|
10
|
+
| `typesol doctor [--url <rpc>] [--config <cli config>]` | Check what building, testing and deploying need: Node.js, the platform-tools, the Solana CLI, its keypair, the RPC (`--url`, a cluster moniker, or the CLI config) and the keypair's balance there, and the project. Each problem is printed with the command that fixes it. Exit code 1 when building is impossible (Node.js or the platform-tools missing); deploy pieces are warnings. |
|
|
10
11
|
| `typesol build [<program>…] [--sbpf v0\|v1\|v2] [--profile] [--quiet]` | In a project: build every program (or those named) into `<out>/<name>/`: `<name>.so`, `<name>.json` (Anchor IDL), `<name>.client.ts` (TypeScript client, see [client.md](client.md)), `<name>.c`, `<name>.build.json`. The first build creates `<out>/deploy/<name>-keypair.json` and records the address in `typesol.json`; later builds refuse a missing or different keypair. |
|
|
11
12
|
| `typesol build <file.ts> [--out <dir>] [--program-id <keypair.json>] [--sbpf …] [--profile]` | Outside a project (or a file that is not one of its programs): build that file. A project program's `.ts` builds as that program. |
|
|
12
13
|
| `typesol check <file.ts> [--json] [--stdin]` | Type-check and validate without building. `--json` prints `{ ok, diagnostics: [{ file, line, column, message }] }`; `--stdin` checks the text given on stdin as that file (its imports come from disk). |
|
|
13
14
|
| `typesol emit-c <file.ts> [--out <file.c>] [--profile]` | Print or write the generated C. |
|
|
14
|
-
| `typesol deploy [<program>…] [--url <rpc>] [--keypair <payer.json>] [--config <cli config>] [--dry-run] [--sbpf …]` | Build, then `solana program deploy --use-rpc` at the program keypair's address. Deploying again upgrades (the payer must be the upgrade authority). `--dry-run` builds and prints the commands. Needs `solana` on PATH. |
|
|
15
|
+
| `typesol deploy [<program>…] [--url <rpc>] [--keypair <payer.json>] [--config <cli config>] [--dry-run] [--sbpf …]` | Build, then `solana program deploy --use-rpc` at the program keypair's address. Deploying again upgrades (the payer must be the upgrade authority). `--dry-run` builds and prints the commands. Before sending anything it checks that the RPC answers and that the payer holds enough for the rent (about twice the binary's on a first deploy, while the buffer exists), and says what to do if not (start `solana-test-validator`, `solana airdrop` on devnet or a local validator). Needs `solana` on PATH. |
|
|
15
16
|
| `typesol idl publish [<program>…] [--url <rpc>] [--keypair <authority.json>] [--config <cli config>] [--non-canonical]` | Compile (the binary is not rebuilt), then write each IDL to the Program Metadata program (seed `idl`). Canonical IDLs must be signed by the upgrade authority. Defaults come from the Solana CLI config (`~/.config/solana/cli/config.yml`). |
|
|
16
17
|
| `typesol cpi <idl.json> [--out <file.ts>] [--prefix <name>] [--address <base58>]` | Generate TypeSOL helpers that call another program from its IDL. Unsupported instructions are listed on stderr. |
|
|
17
18
|
| `typesol port <idl.json> [--out <file.ts>]` | A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program: account structs, enums, errors, events and one function per instruction with its accounts and arguments, each body `require(false, …NotPorted)` until written. Instruction discriminators (an explicit `@discriminator` when the name derives another), account names and error codes are kept, so existing clients keep working where the layouts match; notes on stderr list where they do not (Borsh `String`/`Option` in account data, a `Vec` that is not the last field, custom discriminators or error codes). |
|
package/docs/errors.md
CHANGED
|
@@ -5,6 +5,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
5
5
|
| Code | Meaning |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| 100 / 101 / 102 | instruction data missing / unknown instruction / malformed arguments (InstructionMissing / FallbackNotFound / DidNotDeserialize) |
|
|
8
|
+
| 2000 | a `Mut<…>` / `Init<…>` account passed read-only (ConstraintMut) |
|
|
8
9
|
| 2001 | HasOne constraint failed (ConstraintHasOne) |
|
|
9
10
|
| 2002 | event authority did not sign (ConstraintSigner) |
|
|
10
11
|
| 2004 | Owner constraint failed (ConstraintOwner) |
|
|
@@ -15,6 +16,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
15
16
|
| 2012 | not the expected address (ConstraintAddress) |
|
|
16
17
|
| 2014 / 2015 | token account mint / owner mismatch (ConstraintTokenMint / ConstraintTokenOwner) |
|
|
17
18
|
| 2016 / 2017 / 2018 | mint authority / freeze authority / decimals mismatch (2018: an existing mint `createIfNeeded` found) |
|
|
19
|
+
| 2021 / 2022 | token account / mint not owned by the token program `TokenOf` / `MintOf` names (ConstraintTokenTokenProgram / ConstraintMintTokenProgram) |
|
|
18
20
|
| 2024–2039 | an existing mint `createIfNeeded` found lacks a Token-2022 extension it was given, or holds other keys in it: group pointer 2024–2026, group member pointer 2027–2029, metadata pointer 2030–2032, close authority 2033–2034, permanent delegate 2035–2036, transfer hook 2037–2039 (the first code of each: the extension is missing; the next: the authority / address differs) (ConstraintMint…Extension…) |
|
|
19
21
|
| 2040 | the same account passed in two writable slots (ConstraintDuplicateMutableAccount) |
|
|
20
22
|
| 3000 | account already exists / initialized (AccountDiscriminatorAlreadySet) |
|
|
@@ -22,7 +24,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
22
24
|
| 3003 | data too small, not an initialized token account / mint, a read past the end, or a `@layout borsh` account whose bytes are not a valid Borsh value (AccountDidNotDeserialize) |
|
|
23
25
|
| 3004 | a `@layout borsh` account too small for the value it would now hold (AccountDidNotSerialize) |
|
|
24
26
|
| 3005 | not enough accounts (AccountNotEnoughKeys) |
|
|
25
|
-
| 3006 | account
|
|
27
|
+
| 3006 | a remaining account or CPI account the program needs writable was passed read-only (AccountNotMutable) |
|
|
26
28
|
| 3007 | wrong owner program (AccountOwnedByWrongProgram) |
|
|
27
29
|
| 3008 / 3009 | wrong program address / not executable (InvalidProgramId / InvalidProgramExecutable) |
|
|
28
30
|
| 3010 | account must sign (AccountNotSigner; also a new keypair account a `create(payer, null, …)` makes) |
|
|
@@ -47,3 +49,26 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
47
49
|
|
|
48
50
|
Any of Anchor's own codes can be raised directly: `require(cond, AnchorError.RequireKeysEqViolated)` fails with 2502,
|
|
49
51
|
as `require_keys_eq!` does.
|
|
52
|
+
|
|
53
|
+
## Which error a transaction gets
|
|
54
|
+
|
|
55
|
+
A transaction can break several checks; it fails with the first. Within each account's constraints the checks run in
|
|
56
|
+
the order Anchor's generated code runs them, so a transaction gets the same code under both:
|
|
57
|
+
|
|
58
|
+
1. The account count (3005), then each account in parameter order: its type, signer (3010), `Mut` (2000), owner,
|
|
59
|
+
discriminator and size of a program account (3007 / 3012 / 3002 / 3003), token account or mint, program address.
|
|
60
|
+
2. Two writable parameters naming one account (2040).
|
|
61
|
+
3. The instruction's arguments (102).
|
|
62
|
+
4. Each account in parameter order, its constraints in Anchor's order: seeds (2006), associated token account (owner
|
|
63
|
+
2015, then address 2009), `has_one` (2001), owner (2004), executable (2007), address (2012), then `TokenOf` /
|
|
64
|
+
`MintOf` (2015, 2014, 2021 / 2016, 2017, 2022).
|
|
65
|
+
|
|
66
|
+
Where TypeSOL's order differs, only a transaction that breaks two of these checks at once sees it:
|
|
67
|
+
|
|
68
|
+
- Anchor decodes the arguments first, and checks `mut` with the constraints (after seeds and the associated token
|
|
69
|
+
account). TypeSOL keeps both earlier and later as above, which keeps binaries smaller.
|
|
70
|
+
- Too few accounts fails with 3005 before any account is checked; Anchor fails on the first account it cannot find.
|
|
71
|
+
- Accounts the instruction creates (`Init<…>`, `create(…)`) are created in the body, after every other check, where
|
|
72
|
+
Anchor creates them before checking the other accounts.
|
|
73
|
+
- TypeSOL refuses any two writable parameters naming one account (2040), where Anchor only compares program and token
|
|
74
|
+
accounts: a signer passed as two writable parameters fails with 2040 here and later, or not at all, under Anchor.
|
package/docs/language.md
CHANGED
|
@@ -38,8 +38,8 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
38
38
|
| Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
|
|
39
39
|
| Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full): a template is joined in one allocation, text known at compile time (literals, string and Pubkey constants) is folded in, and `s += …` grows the newest heap string in place |
|
|
40
40
|
| `switch` | on integers (128-bit ones too), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
|
|
41
|
-
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
42
|
-
| Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds)` (fully verified, feed-matched, fresh); raw `update.price / conf / exponent / publishTime
|
|
41
|
+
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention; inner nodes and leaves are hashed alike, so hash leaves as OpenZeppelin does, `keccak256(keccak256(data))`, and never take the leaf itself from the arguments: an inner node would pass as a leaf); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
42
|
+
| Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds, exponent?)` (fully verified, feed-matched, fresh; with `exponent`, a feed at another scale is refused); raw `update.price / conf / exponent / publishTime` are unchecked (any feed, partially verified or stale: check `feedId`, `fullyVerified` and the age yourself, or use `pythPrice`); `switchboardValue(feed: SwitchboardFeed, expectedFeed, maxStaleSlots, decimals, minSamples?)` (the feed must be the expected one: anyone can create feeds; `minSamples`, default 1, the fewest oracle samples accepted) (mainnet program unless `SwitchboardFeed<"devnet">`) |
|
|
43
43
|
| Multiple files | `import { Side, min } from "./math"` — structs, enums, constants and helpers can live in other files; only the entry file's exported functions are instructions; names are program-wide, so imports from TypeSOL files cannot be renamed; error codes number imported files first |
|
|
44
44
|
| Loops | `for (let i = 0n; i < n; i++)`, `while`, `do … while`, `for (const x of list)` over a `Vec` or `FixedArray` (each `x` is what `list.get(i)` gives: a value, or a reference into account data for structs; the length is read again every step, as in JS), `break`, `continue` — bounded by the compute budget. Remaining accounts are iterated by index |
|
|
45
45
|
| Helpers | non-exported functions (and every function of an imported file): take values, structs, Vec arguments, `SignerSeeds` and accounts the instruction already checked (`Signer`, `AccountInfo`/`AccountMeta`, `Mut<T>`, token accounts, programs); return `void`, a scalar, a `string`, a function (see below) or a fixed-size struct (`function at(x: u64, y: u64): Point { return { x, y }; }`: the caller gets its own copy, to bind, store with `acct.home = at(…)` / `arr.set(i, at(…))` or read as `at(…).x`); no recursion. A helper that takes a struct account as `Mut<T>` can grow its Vec (`push(value, payer)`: the account's size at instruction entry is passed along) |
|
|
@@ -65,9 +65,9 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
65
65
|
| `Signer` / `Mut<Signer>` | signed (and writable) |
|
|
66
66
|
| `Account<T>` / `Mut<T>` | owned by this program, 8-byte discriminator matches, data large enough (and writable) |
|
|
67
67
|
| `Init<T>` | writable; `create(payer, seeds)` derives the PDA, verifies it, funds rent, writes the discriminator (up to 10 KiB); `create(payer, null)` creates a new keypair account that signs the transaction instead (3010 if it did not; a signer in the IDL); a third argument is Anchor's `space = …` (discriminator included, at least 8 + the struct's size) |
|
|
68
|
-
| `InitIfNeeded<T>` | `const created = acct.createIfNeeded(payer, seeds)` — creates once; afterwards re-derives the PDA and checks owner, discriminator and size (Anchor `init_if_needed`); `acct.pdaBump` after create |
|
|
68
|
+
| `InitIfNeeded<T>` | `const created = acct.createIfNeeded(payer, seeds)` — creates once; afterwards re-derives the PDA and checks owner, discriminator and size (Anchor `init_if_needed`); `acct.pdaBump` after create. An existing account is used as it is: check the fields you rely on |
|
|
69
69
|
| `HasOne<A, "owner">`, `Seeds<A, ["vault", "@owner"], "bump">`, `CloseTo<A, "owner">` | declarative constraints like Anchor's `has_one`, `seeds`/`bump` and `close`: checked before the body (2001 / 2006), close runs when the instruction succeeds; `"@name"` = account key or argument bytes, `"@acct.field"` = a Pubkey, integer or unit enum field of a struct account parameter (also the account's own: `["offer", "@maker", "@offer.id"]`), other strings are literals; recorded as `relations` / `pda` in the IDL. `acct.pdaBump` is the bump (Anchor `ctx.bumps.acct`): the stored field when one is named, else the canonical bump the check found — use it to sign as the PDA |
|
|
70
|
-
| `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name; `TokenOf<TokenAccount, "mintParam", "authorityParam">` / `MintOf<Mint, "authorityParam", "freezeParam">` name the parameters, as Anchor's `token::mint = …` / `mint::authority = …` do (`never` leaves one out) |
|
|
70
|
+
| `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name; `TokenOf<TokenAccount, "mintParam", "authorityParam", "tokenProgramParam">` / `MintOf<Mint, "authorityParam", "freezeParam", "tokenProgramParam">` name the parameters, as Anchor's `token::mint = …` / `mint::authority = …` / `…::token_program = …` do (`never` leaves one out; the token program one: owned by that token program, 2021 / 2022) |
|
|
71
71
|
| `Ata<TokenAccount, "owner", "mint">` | the account is the associated token account of those parameters (2009; ATA `pda` in the IDL) |
|
|
72
72
|
| `Ata<Init<TokenAccount>, "owner", "mint">`, `Ata<InitIfNeeded<TokenAccount>, "owner", "mint">` | Anchor `init` / `init_if_needed` with `associated_token::…`: `acct.create(payer)` creates the ATA (fails if it exists); `const created = acct.createIfNeeded(payer)` creates it only when missing, otherwise checks it is that ATA (2009). Then it is a `TokenAccount` of that mint and owner (2014 / 2015). Needs `AssociatedTokenProgram`, `SystemProgram` and a token program parameter; call it at the top level before using the account |
|
|
73
73
|
| `Address<A, "4Nd1…">` | exact address (2012; `address` in the IDL) |
|
|
@@ -77,8 +77,8 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
77
77
|
| `ProgramData` | this program's ProgramData (address derived from the program id, upgradeable-loader owner and state checked); `upgradeAuthority`, `deploySlot` — gate admin setup on `programData.upgradeAuthority === admin.key` |
|
|
78
78
|
| `EventAuthority`, `SelfProgram` | Anchor's `["__event_authority"]` PDA, and this program's own address, for `emitCpi` |
|
|
79
79
|
| `Zeroed<T>` | large accounts (up to 10 MiB) the client allocated with System `CreateAccount`: owned by this program, big enough, rent-exempt, discriminator still zero — then initialized; like Anchor's zero-copy `zero` |
|
|
80
|
-
| `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (its only token program parameter, or one passed as the last argument of `create` / `createIfNeeded`; plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. With `createIfNeeded` an existing mint must carry groupPointer / groupMemberPointer / metadataPointer / closeAuthority / permanentDelegate / transferHook with those keys (Anchor's 2024–2039). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
|
|
81
|
-
| `Init<TokenAccount>`, `InitIfNeeded<TokenAccount>` (no `Ata<…>`) | Anchor `init` / `init_if_needed` with `token::mint` / `token::authority`: `vault.create(payer, ["vault", mint.key], mint, authority)` (the authority may be `vault.key` itself) creates a token account at that PDA (or a keypair account with `null`), sized for the mint's extensions under Token-2022. `createIfNeeded` checks an existing one has that mint and owner (2014 / 2015) |
|
|
80
|
+
| `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (its only token program parameter, or one passed as the last argument of `create` / `createIfNeeded`; plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. With `createIfNeeded` an existing mint must carry groupPointer / groupMemberPointer / metadataPointer / closeAuthority / permanentDelegate / transferHook with those keys (Anchor's 2024–2039); extensions not listed are not looked at, so an existing mint may carry others (a permanent delegate, say). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
|
|
81
|
+
| `Init<TokenAccount>`, `InitIfNeeded<TokenAccount>` (no `Ata<…>`) | Anchor `init` / `init_if_needed` with `token::mint` / `token::authority`: `vault.create(payer, ["vault", mint.key], mint, authority)` (the authority may be `vault.key` itself) creates a token account at that PDA (or a keypair account with `null`), sized for the mint's extensions under Token-2022. `createIfNeeded` checks an existing one has that mint and owner (2014 / 2015), not its delegate or close authority (as Anchor) |
|
|
82
82
|
| `TokenAccount` / `Mut<TokenAccount>` | owned by SPL Token or Token-2022 (extensions allowed, multisig accounts refused), initialized; `mint`, `owner`, `amount`, `delegate`, `delegatedAmount`, `isFrozen`, `isNative`, `closeAuthority`, `tokenProgram` |
|
|
83
83
|
| `Mint` / `Mut<Mint>` | owned by SPL Token or Token-2022, initialized; `decimals`, `supply`, `mintAuthority`, `freezeAuthority`, `tokenProgram` |
|
|
84
84
|
| `SystemProgram`, `TokenProgram`, `Token2022Program` | exact program address |
|
|
@@ -94,7 +94,9 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
94
94
|
Every account exposes `key`, `lamports`, `dataLen`, `isSigner`, `isWritable`. At most 32 accounts per instruction (more is refused, 5113).
|
|
95
95
|
|
|
96
96
|
Two writable parameters can never be the same account: aliasing is refused with error 2040 (also when a remaining
|
|
97
|
-
account written through `.lamports` or `loadAccountMut` is another account of the instruction).
|
|
97
|
+
account written through `.lamports` or `loadAccountMut` is another account of the instruction). A remaining account
|
|
98
|
+
can be loaded with `loadAccountMut` once per instruction: loading the same one again (`rest.get(i)` and `rest.get(j)`
|
|
99
|
+
with `i == j`) is refused with 2040 too, since the two views would alias.
|
|
98
100
|
|
|
99
101
|
## Built-ins
|
|
100
102
|
|
|
@@ -106,7 +108,9 @@ account written through `.lamports` or `loadAccountMut` is another account of th
|
|
|
106
108
|
`freezeAccount` / `thawAccount(account, mint, freezeAuthority)`, `syncNative(account)`, `initializeMint2(mint, decimals, mintAuthority, freezeAuthority | null)`,
|
|
107
109
|
`initializeAccount3(account, mint, owner)`, and on Token-2022 `initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri)` (metadata extension) and the extension initializers to call between `createAccount` and `initializeMint2` / `initializeAccount3`: `initializeTransferFeeConfig(mint, configAuthority | null, withdrawAuthority | null, basisPoints, maximumFee)`, `initializeMintCloseAuthority(mint, authority | null)`, `initializeNonTransferableMint(mint)`, `initializePermanentDelegate(mint, delegate)`, `initializeDefaultAccountState(mint, frozen)`, `initializeMetadataPointer` / `initializeGroupPointer` / `initializeGroupMemberPointer` / `initializeTransferHook(mint, authority | null, address | null)`, `initializeInterestBearingMint(mint, rateAuthority | null, rate)`, `initializeImmutableOwner(account)`; the extension instructions `updateDefaultAccountState(mint, freezeAuthority, frozen)`, `updateTransferHook(mint, authority, programId | null)`, `enableRequiredMemoTransfers` / `disableRequiredMemoTransfers(account, owner)`, `updateInterestRate(mint, rateAuthority, rate)`, `transferCheckedWithFee(from, mint, to, authority, amount, fee)`, `harvestWithheldTokensToMint(mint, remainingAccounts())`, `withdrawWithheldTokensFromMint(mint, destination, authority)`, `setTransferFee(mint, authority, basisPoints, maximumFee)`; and the Token Metadata interface `updateMetadataField(mint, updateAuthority, field, value)` ("name" / "symbol" / "uri" or any other key), `removeMetadataKey(mint, updateAuthority, key, idempotent)`, `updateMetadataAuthority(mint, updateAuthority, next | null)`, `emitMetadata(mint)`. Each takes optional signerSeeds
|
|
108
110
|
- `account.lamports` read/write on `Mut` accounts, `close(account, destination)` for program accounts
|
|
109
|
-
- `requirePda(account, seeds, bump)`
|
|
111
|
+
- `requirePda(account, seeds, bump)`: any bump that gives a valid PDA passes, as with Anchor's `bump = expr`. Use a bump
|
|
112
|
+
stored when the account was created (or `Seeds<…>`); one taken from the instruction's arguments lets the caller pick
|
|
113
|
+
among up to 256 addresses for the same seeds
|
|
110
114
|
|
|
111
115
|
## Implementing another program's interface (transfer hooks)
|
|
112
116
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
TypeSOL programs speak Anchor's wire format: the same instruction and account discriminators, Borsh arguments, an
|
|
4
4
|
Anchor-format IDL, the same error numbering and event logs. A ported program can keep its clients. This page maps
|
|
5
|
-
Anchor's constructs to TypeSOL's and shows the porting workflow.
|
|
5
|
+
Anchor's constructs to TypeSOL's and shows the porting workflow. For one complete program ported this way, with the
|
|
6
|
+
original side by side, the checks run against it and its cost, see [Case study: porting Anchor's escrow](case-study-escrow.md).
|
|
6
7
|
|
|
7
8
|
## Start from the IDL
|
|
8
9
|
|
|
@@ -50,6 +51,7 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
50
51
|
| `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
|
|
51
52
|
| `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
|
|
52
53
|
| `token::mint = m, token::authority = a` | `TokenOf<TokenAccount, "m", "a">` (2014 / 2015), or `HasOne<TokenAccount, "mint" \| "owner">` when the parameters have those names; `mint::authority = a, mint::freeze_authority = f` on an existing mint: `MintOf<Mint, "a", "f">` (2016 / 2017) |
|
|
54
|
+
| `mint::token_program = tp` / `token::token_program = tp` | `MintOf<Mint, never, never, "tp">` / `TokenOf<TokenAccount, never, never, "tp">` (2022 / 2021) |
|
|
53
55
|
| `extensions::metadata_pointer::authority = a, …` (with `mint::…`) | `mint.create(p, …, { metadataPointer: { authority: a.key, metadataAddress: mint.key }, … })`: transferHook, closeAuthority, permanentDelegate, nonTransferable, groupPointer, groupMemberPointer and more |
|
|
54
56
|
| `#[account(init, payer = p, seeds = […], bump, mint::decimals = 6, mint::authority = a, mint::freeze_authority = f)]` | `Init<Mint>`, then `const bump = mint.create(p, […], 6n, a.key, f.key)` (`null` seeds: a keypair mint; `InitIfNeeded<Mint>` + `createIfNeeded`) |
|
|
55
57
|
| `#[account(init, payer = p, seeds = […], bump, token::mint = m, token::authority = a)]` | `Init<TokenAccount>`, then `vault.create(p, […], m, a.key)` (`InitIfNeeded<TokenAccount>` + `createIfNeeded`) |
|
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/lib/typesol.d.ts
CHANGED
|
@@ -162,7 +162,8 @@ export declare function remainingAccounts(): RemainingAccounts;
|
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* Read a struct of this program from a remaining account: owner, discriminator and size are checked.
|
|
165
|
-
* `loadAccountMut` also requires it to be writable
|
|
165
|
+
* `loadAccountMut` also requires it to be writable, not aliased by another writable account, and not already loaded
|
|
166
|
+
* with `loadAccountMut` in this instruction (2040).
|
|
166
167
|
*/
|
|
167
168
|
export declare function loadAccount<T>(account: RemainingAccount): Readonly<T>;
|
|
168
169
|
export declare function loadAccountMut<T>(account: RemainingAccount): T;
|
|
@@ -180,7 +181,8 @@ export declare function keccak256(...parts: (string | Pubkey | Hash | bigint | b
|
|
|
180
181
|
|
|
181
182
|
/**
|
|
182
183
|
* Verify a Merkle proof (keccak256, sorted pairs: the OpenZeppelin convention most airdrop tools use).
|
|
183
|
-
* `proof` must be a `Vec<Hash>` instruction argument.
|
|
184
|
+
* `proof` must be a `Vec<Hash>` instruction argument. Leaves and inner nodes hash alike: build the leaf in the program
|
|
185
|
+
* as OpenZeppelin does, `keccak256(keccak256(data))`, never take it from the arguments (an inner node would pass).
|
|
184
186
|
*/
|
|
185
187
|
export declare function merkleVerify(leaf: Hash, proof: Vec<Hash>, root: Hash): boolean;
|
|
186
188
|
|
|
@@ -211,7 +213,8 @@ export declare function ethMessageHash(message: string): Hash;
|
|
|
211
213
|
|
|
212
214
|
/**
|
|
213
215
|
* A Pyth `PriceUpdateV2` account (Pyth Solana Receiver, rec5EKMG…). Owner and discriminator are checked.
|
|
214
|
-
* The fields are the raw update
|
|
216
|
+
* The fields are the raw update and are NOT checked: it may be only partially verified, for another feed, or stale.
|
|
217
|
+
* Use pythPrice() for a verified, fresh, feed-matched price (with `exponent` to pin the scale).
|
|
215
218
|
*/
|
|
216
219
|
export interface PythPriceUpdate extends AccountMeta {
|
|
217
220
|
readonly __pyth: void;
|
|
@@ -223,8 +226,12 @@ export interface PythPriceUpdate extends AccountMeta {
|
|
|
223
226
|
readonly publishTime: i64;
|
|
224
227
|
readonly fullyVerified: boolean;
|
|
225
228
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Like Pyth's get_price_no_older_than: fully verified, `feedId` matches, published within `maxAgeSeconds` (5111
|
|
231
|
+
* otherwise). The price is in units of 10^exponent: pass the `exponent` your arithmetic assumes and a feed whose
|
|
232
|
+
* exponent differs is refused, instead of read at the wrong scale.
|
|
233
|
+
*/
|
|
234
|
+
export declare function pythPrice(update: PythPriceUpdate, feedId: Hash, maxAgeSeconds: u64, exponent?: i32): i64;
|
|
228
235
|
|
|
229
236
|
/**
|
|
230
237
|
* A Switchboard On-Demand pull feed (PullFeedAccountData). Mainnet program unless `SwitchboardFeed<"devnet">`
|
|
@@ -237,7 +244,14 @@ export interface SwitchboardFeed<Cluster extends "mainnet" | "devnet" = "mainnet
|
|
|
237
244
|
* The feed's current result scaled to `decimals` (from 18), if `feed` is `expectedFeed` (anyone can create feeds),
|
|
238
245
|
* it has samples and it is at most `maxStaleSlots` old.
|
|
239
246
|
*/
|
|
240
|
-
export declare function switchboardValue(
|
|
247
|
+
export declare function switchboardValue(
|
|
248
|
+
feed: SwitchboardFeed<"mainnet" | "devnet">,
|
|
249
|
+
expectedFeed: Pubkey,
|
|
250
|
+
maxStaleSlots: u64,
|
|
251
|
+
decimals: u8,
|
|
252
|
+
/** The fewest oracle samples the result may rest on (default 1). */
|
|
253
|
+
minSamples?: u8,
|
|
254
|
+
): i64;
|
|
241
255
|
|
|
242
256
|
/** The Associated Token Account program (ATokenGP…). Its address is checked. */
|
|
243
257
|
export interface AssociatedTokenProgram extends AccountMeta {
|
|
@@ -570,13 +584,18 @@ export type InitIfNeeded<T> = T extends TokenAccount
|
|
|
570
584
|
*/
|
|
571
585
|
export type HasOne<A, Field extends string> = A & { readonly __hasOne?: Field };
|
|
572
586
|
/**
|
|
573
|
-
* Anchor's `token::mint = mintParam, token::authority = authorityParam` on an
|
|
574
|
-
* parameters (2014 / 2015); `never` leaves one out.
|
|
587
|
+
* Anchor's `token::mint = mintParam, token::authority = authorityParam, token::token_program = tokenProgramParam` on an
|
|
588
|
+
* existing token account, naming the parameters (2014 / 2015 / 2021: owned by that token program); `never` leaves one out.
|
|
575
589
|
*/
|
|
576
|
-
export type TokenOf<A, MintParam extends string, AuthorityParam extends string = never> = A & {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
590
|
+
export type TokenOf<A, MintParam extends string, AuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
|
|
591
|
+
readonly __tokenOf?: [MintParam, AuthorityParam, TokenProgramParam];
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Anchor's `mint::authority = a, mint::freeze_authority = f, mint::token_program = tp` on an existing mint, naming the
|
|
595
|
+
* parameters (2016 / 2017 / 2022: owned by that token program); `never` leaves one out.
|
|
596
|
+
*/
|
|
597
|
+
export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
|
|
598
|
+
readonly __mintOf?: [AuthorityParam, FreezeAuthorityParam, TokenProgramParam];
|
|
580
599
|
};
|
|
581
600
|
export type Seeds<A, Names extends readonly string[], Bump extends string = never, ProgramId extends string = never> = A & {
|
|
582
601
|
readonly __seeds?: [Names, Bump, ProgramId];
|
|
@@ -1642,7 +1661,10 @@ export declare function emit<T>(event: T): void;
|
|
|
1642
1661
|
*/
|
|
1643
1662
|
export declare function returnData<T extends bigint | boolean | Pubkey | Hash>(program: AccountMeta): T;
|
|
1644
1663
|
|
|
1645
|
-
/**
|
|
1664
|
+
/**
|
|
1665
|
+
* Verify `account.key` is the PDA for `seeds` + `bump` under this program. Any bump that gives a PDA passes (Anchor's
|
|
1666
|
+
* `bump = expr`): use a stored bump, not one from the arguments, or the caller picks among up to 256 addresses.
|
|
1667
|
+
*/
|
|
1646
1668
|
export declare function requirePda(account: AccountMeta, seeds: Seed[], bump: u8): void;
|
|
1647
1669
|
|
|
1648
1670
|
/** Close a program-owned account: move all lamports to `destination` and wipe it. */
|
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": {
|
package/runtime/typesol_rt.h
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
#define TS_ERR_INSTRUCTION_NOT_FOUND 101 /* InstructionFallbackNotFound */
|
|
19
19
|
#define TS_ERR_INSTRUCTION_DATA 102 /* InstructionDidNotDeserialize */
|
|
20
20
|
#define TS_ERR_NOT_WRITABLE 3006 /* AccountNotMutable */
|
|
21
|
+
#define TS_ERR_CONSTRAINT_MUT 2000 /* ConstraintMut */
|
|
21
22
|
#define TS_ERR_CONSTRAINT 2001 /* ConstraintHasOne */
|
|
22
23
|
#define TS_ERR_NOT_RENT_EXEMPT 2005 /* ConstraintRentExempt */
|
|
23
24
|
#define TS_ERR_CONSTRAINT_SIGNER 2002 /* ConstraintSigner */
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
#define TS_ERR_MINT_AUTHORITY 2016 /* ConstraintMintMintAuthority */
|
|
32
33
|
#define TS_ERR_FREEZE_AUTHORITY 2017 /* ConstraintMintFreezeAuthority */
|
|
33
34
|
#define TS_ERR_MINT_DECIMALS 2018 /* ConstraintMintDecimals */
|
|
35
|
+
#define TS_ERR_TOKEN_TOKEN_PROGRAM 2021 /* ConstraintTokenTokenProgram */
|
|
36
|
+
#define TS_ERR_MINT_TOKEN_PROGRAM 2022 /* ConstraintMintTokenProgram */
|
|
34
37
|
#define TS_ERR_DUPLICATE_MUT 2040 /* ConstraintDuplicateMutableAccount */
|
|
35
38
|
#define TS_ERR_ALREADY_INITIALIZED 3000 /* AccountDiscriminatorAlreadySet */
|
|
36
39
|
#define TS_ERR_DISCRIMINATOR 3002 /* AccountDiscriminatorMismatch */
|
|
@@ -275,6 +278,7 @@ typedef struct {
|
|
|
275
278
|
const SolPubkey *program_id;
|
|
276
279
|
TsAccount *ka[TS_MAX_ACCOUNTS]; /* ka_num entries */
|
|
277
280
|
SolAccountInfo infos[TS_MAX_ACCOUNTS]; /* scratch for CPI account lists (ts_invoke) */
|
|
281
|
+
uint64_t loaded_mut; /* bit i: ka[i] loaded with loadAccountMut (zeroed by the entrypoint of a program that uses it) */
|
|
278
282
|
} TsParams;
|
|
279
283
|
|
|
280
284
|
/*
|
|
@@ -410,6 +414,12 @@ static inline uint64_t ts_check_writable(const TsAccount *a) {
|
|
|
410
414
|
if (!a->is_writable) TS_FAIL(TS_ERR_NOT_WRITABLE, "TypeSOL: account must be writable");
|
|
411
415
|
return SUCCESS;
|
|
412
416
|
}
|
|
417
|
+
/* A Mut<…> parameter passed read-only: Anchor's `#[account(mut)]` (ConstraintMut). ts_check_writable is for accounts
|
|
418
|
+
the program makes writable at runtime (remaining accounts, CPI metas). */
|
|
419
|
+
static inline uint64_t ts_check_mut(const TsAccount *a) {
|
|
420
|
+
if (!a->is_writable) TS_FAIL(TS_ERR_CONSTRAINT_MUT, "TypeSOL: account must be writable (mut)");
|
|
421
|
+
return SUCCESS;
|
|
422
|
+
}
|
|
413
423
|
|
|
414
424
|
static inline uint64_t ts_check_program_account(const TsAccount *a, const SolPubkey *program_id,
|
|
415
425
|
const uint8_t disc[8], uint64_t size) {
|
|
@@ -428,7 +438,7 @@ static inline uint64_t ts_check_vec_len(const TsAccount *a, uint64_t fixed, uint
|
|
|
428
438
|
return SUCCESS;
|
|
429
439
|
}
|
|
430
440
|
|
|
431
|
-
/* No other account in the instruction may be the same account (
|
|
441
|
+
/* No other account in the instruction may be the same account (remaining accounts written in place). */
|
|
432
442
|
static inline uint64_t ts_check_unaliased(const TsParams *params, const TsAccount *a) {
|
|
433
443
|
/* The loader passes each account once and marks repeats as duplicates, which share its record. */
|
|
434
444
|
uint64_t seen = 0;
|
|
@@ -437,6 +447,18 @@ static inline uint64_t ts_check_unaliased(const TsParams *params, const TsAccoun
|
|
|
437
447
|
return SUCCESS;
|
|
438
448
|
}
|
|
439
449
|
|
|
450
|
+
/* loadAccountMut: unaliased, and not already loaded mutably in this instruction. Two loads of the same slot would be
|
|
451
|
+
two writable views of one account, as two slots naming it would (read both, write both: the classic drain). */
|
|
452
|
+
static inline uint64_t ts_claim_mut(TsParams *params, const TsAccount *a) {
|
|
453
|
+
uint64_t seen = 0, at = 0;
|
|
454
|
+
for (uint64_t i = 0; i < params->ka_num; i++)
|
|
455
|
+
if (params->ka[i] == a) seen++, at = i;
|
|
456
|
+
if (seen > 1) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: account passed more than once");
|
|
457
|
+
if (params->loaded_mut >> at & 1) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: account already loaded with loadAccountMut");
|
|
458
|
+
params->loaded_mut |= (uint64_t)1 << at;
|
|
459
|
+
return SUCCESS;
|
|
460
|
+
}
|
|
461
|
+
|
|
440
462
|
static inline uint64_t ts_check_address(const TsAccount *a, const SolPubkey *expected) {
|
|
441
463
|
if (!ts_key_eq(&a->key, expected)) TS_FAIL(TS_ERR_WRONG_PROGRAM, "TypeSOL: unexpected program address");
|
|
442
464
|
return SUCCESS;
|
|
@@ -1466,7 +1488,8 @@ static inline int64_t ts_le_i64(const uint8_t *p) {
|
|
|
1466
1488
|
}
|
|
1467
1489
|
|
|
1468
1490
|
/* Like get_price_no_older_than: fully verified, matching feed id, publish_time + max_age >= now. */
|
|
1469
|
-
|
|
1491
|
+
/* expo: the exponent the caller scales by; checked when check_expo (a feed's exponent can change). */
|
|
1492
|
+
static inline uint64_t ts_pyth_price(const TsAccount *a, const SolPubkey *feed, uint64_t max_age, bool check_expo, int32_t expo, int64_t *out) {
|
|
1470
1493
|
uint64_t m;
|
|
1471
1494
|
bool full;
|
|
1472
1495
|
TS_TRY(ts_pyth_message(a, &m, &full));
|
|
@@ -1478,6 +1501,11 @@ static inline uint64_t ts_pyth_price(const TsAccount *a, const SolPubkey *feed,
|
|
|
1478
1501
|
int64_t deadline;
|
|
1479
1502
|
if (__builtin_add_overflow(published, (int64_t)(max_age > INT64_MAX ? INT64_MAX : max_age), &deadline)) deadline = INT64_MAX;
|
|
1480
1503
|
if (deadline < now) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: Pyth price is too old");
|
|
1504
|
+
if (check_expo) {
|
|
1505
|
+
int32_t e;
|
|
1506
|
+
ts_memcpy(&e, a->data + m + 48, 4);
|
|
1507
|
+
if (e != expo) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: Pyth exponent is not the expected one");
|
|
1508
|
+
}
|
|
1481
1509
|
*out = ts_le_i64(a->data + m + 32);
|
|
1482
1510
|
return SUCCESS;
|
|
1483
1511
|
}
|
|
@@ -1503,9 +1531,10 @@ static inline uint64_t ts_check_switchboard(const TsAccount *a, bool devnet) {
|
|
|
1503
1531
|
* The feed's current result (an 18-decimal fixed-point i128) scaled to `decimals`, if it has samples
|
|
1504
1532
|
* and was produced within `max_stale_slots` slots of now.
|
|
1505
1533
|
*/
|
|
1506
|
-
static inline uint64_t ts_switchboard_value(const TsAccount *a, uint64_t max_stale_slots, uint8_t decimals, int64_t *out) {
|
|
1534
|
+
static inline uint64_t ts_switchboard_value(const TsAccount *a, uint64_t max_stale_slots, uint8_t decimals, uint8_t min_samples, int64_t *out) {
|
|
1507
1535
|
if (decimals > 18) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: at most 18 decimals");
|
|
1508
|
-
if (a->data[TS_SB_RESULT_SAMPLES] == 0
|
|
1536
|
+
if (a->data[TS_SB_RESULT_SAMPLES] == 0 || a->data[TS_SB_RESULT_SAMPLES] < min_samples)
|
|
1537
|
+
TS_FAIL(TS_ERR_ORACLE, "TypeSOL: Switchboard feed has too few samples");
|
|
1509
1538
|
uint64_t clock[5] = {0};
|
|
1510
1539
|
if (sol_get_clock_sysvar(clock) != SUCCESS) TS_FAIL(TS_ERR_SYSVAR, "TypeSOL: clock sysvar unavailable");
|
|
1511
1540
|
uint64_t slot = (uint64_t)ts_le_i64(a->data + TS_SB_RESULT_SLOT);
|
|
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
|