@typesol/cli 0.14.0 → 0.16.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 +81 -0
- package/README.md +35 -5
- package/dist/cli.js +161 -24
- package/dist/compiler/analyze.d.ts +4 -0
- package/dist/compiler/analyze.js +51 -9
- package/dist/compiler/emit/base.js +1 -1
- package/dist/compiler/emit/calls.js +7 -0
- package/dist/compiler/emit/cpi.js +188 -9
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +107 -2
- package/dist/compiler/emit/function.js +11 -1
- package/dist/compiler/emit/shared.js +3 -2
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +131 -5
- package/dist/compiler/emit/storage.d.ts +2 -1
- package/dist/compiler/emit/storage.js +24 -7
- package/dist/compiler/idl.js +28 -14
- package/dist/compiler/model.d.ts +16 -3
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/idlpublish.js +43 -9
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +311 -0
- package/dist/templates.d.ts +9 -0
- package/dist/templates.js +324 -0
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.d.ts +3 -2
- package/dist/workspace.js +18 -106
- package/docs/architecture.md +3 -0
- package/docs/cli.md +17 -3
- package/docs/language.md +11 -10
- package/docs/mainnet.md +100 -0
- package/docs/migrate-from-anchor.md +104 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +10 -2
- package/lib/typesol.d.ts +68 -12
- package/package.json +19 -1
- package/runtime/typesol_rt.h +25 -5
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
package/docs/mainnet.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Deploy to mainnet
|
|
2
|
+
|
|
3
|
+
From an empty directory to a program on Solana mainnet-beta: create, test, deploy, publish the IDL, then check that
|
|
4
|
+
the deployed binary is the one your source builds. The program here is the counter that `typesol init` creates
|
|
5
|
+
(13.5 KB); any TypeSOL project works the same way.
|
|
6
|
+
|
|
7
|
+
You need Node.js 22.12 or later and the Agave CLI with platform-tools (see the README's Requirements).
|
|
8
|
+
|
|
9
|
+
## 1. Create and test
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx @typesol/cli init my-app
|
|
13
|
+
cd my-app
|
|
14
|
+
npm install
|
|
15
|
+
npm test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`npm test` runs `tests/my_app.test.ts` in LiteSVM: it compiles the program and calls it, with no validator or SOL.
|
|
19
|
+
Write your program in `programs/my_app.ts` and keep the tests passing before you spend anything.
|
|
20
|
+
|
|
21
|
+
## 2. Build
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx typesol build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This writes `target/my_app/`: the program (`my_app.so`), its Anchor IDL (`my_app.json`), a TypeScript client
|
|
28
|
+
(`my_app.client.ts`) and the generated C. The first build also creates `target/deploy/my_app-keypair.json`, the
|
|
29
|
+
program's address, and records the address in `typesol.json`.
|
|
30
|
+
|
|
31
|
+
**Back up `target/deploy/my_app-keypair.json` now.** It is not committed (`.gitignore`), and without it you cannot
|
|
32
|
+
redeploy to that address.
|
|
33
|
+
|
|
34
|
+
## 3. A wallet with SOL
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
solana-keygen new -o ~/.config/solana/deployer.json
|
|
38
|
+
solana config set --url mainnet-beta --keypair ~/.config/solana/deployer.json
|
|
39
|
+
solana address
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Send SOL to that address. Most of what a deploy costs is rent for the program's bytes, which you get back if you
|
|
43
|
+
ever close the program:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
solana rent $(wc -c < target/my_app/my_app.so)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The counter needs 0.069 SOL of rent. During the deploy the program is first written to a buffer account that holds
|
|
50
|
+
the same amount again; it is returned when the deploy finishes. Fund about twice the rent plus 0.01 SOL for fees.
|
|
51
|
+
|
|
52
|
+
The public mainnet RPC is rate-limited; for deploys, set `--url` (or `solana config set --url`) to your RPC
|
|
53
|
+
provider's endpoint.
|
|
54
|
+
|
|
55
|
+
## 4. Deploy
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx typesol deploy --dry-run # builds and prints the solana command it will run
|
|
59
|
+
npx typesol deploy
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It runs `solana program deploy` at the address in `target/deploy/my_app-keypair.json`, with your wallet as payer and
|
|
63
|
+
upgrade authority, and prints the program id. Deploying again upgrades the program in place.
|
|
64
|
+
|
|
65
|
+
If a deploy stops part-way, the buffer stays behind with its SOL. The error prints the buffer's address; recover it
|
|
66
|
+
with `solana program close <buffer address>`, or list yours with `solana program show --buffers`.
|
|
67
|
+
|
|
68
|
+
## 5. Publish the IDL
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npx typesol idl publish
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It writes the IDL to the Program Metadata program, signed by the upgrade authority, where Anchor clients and
|
|
75
|
+
explorers look for it. It costs a few thousandths of a SOL of rent.
|
|
76
|
+
|
|
77
|
+
## 6. Verify
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npx typesol verify my_app
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`verify` rebuilds the program from `programs/my_app.ts`, reads the deployed one at the address in `typesol.json` from
|
|
84
|
+
the cluster in your Solana CLI config, and compares the bytes. `match` means anyone with your source and the same
|
|
85
|
+
TypeSOL version gets exactly the program on chain. It also prints the upgrade authority and checks the IDL you
|
|
86
|
+
published in step 5. Anyone can run it against your program with `--address <program id> --url mainnet-beta`.
|
|
87
|
+
|
|
88
|
+
## 7. Call it
|
|
89
|
+
|
|
90
|
+
`target/my_app/my_app.client.ts` builds the program's instructions on `@solana/kit`; see [client.md](client.md).
|
|
91
|
+
|
|
92
|
+
## Later
|
|
93
|
+
|
|
94
|
+
- **Freeze it.** `solana program set-upgrade-authority <program id> --final` makes the program immutable. It cannot
|
|
95
|
+
be undone.
|
|
96
|
+
- **Close it.** `solana program close <program id> --bypass-warning` returns the rent to the authority. The program
|
|
97
|
+
id can never be used again.
|
|
98
|
+
|
|
99
|
+
A worked example: the official escrow, deployed to mainnet in TypeSOL, Pinocchio, native Rust and Anchor, with every
|
|
100
|
+
transaction linked, is in the README's [On mainnet](../README.md#on-mainnet) section.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Migrate from Anchor
|
|
2
|
+
|
|
3
|
+
TypeSOL programs speak Anchor's wire format: the same instruction and account discriminators, Borsh arguments, an
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Start from the IDL
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
anchor build # in the Anchor project: target/idl/my_program.json
|
|
11
|
+
npx @typesol/cli init my-program && cd my-program
|
|
12
|
+
rm programs/my_program.ts tests/my_program.test.ts # the counter template's program and test
|
|
13
|
+
npx typesol port ../anchor/target/idl/my_program.json --out programs/my_program.ts
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`typesol port` writes the account structs, enums, errors and one function per instruction with its accounts and
|
|
17
|
+
arguments, each body failing with `NotPorted` until you write it. It keeps instruction discriminators, account names
|
|
18
|
+
and error codes. Its notes (on stderr) list what an existing Anchor client would see differently; see
|
|
19
|
+
[Account data](#account-data) below. Then, for each instruction:
|
|
20
|
+
|
|
21
|
+
1. Give each account its real type: `Account<T>` / `Mut<T>` for your structs, `Init<T>` for one the instruction creates,
|
|
22
|
+
token types for token accounts (the table below).
|
|
23
|
+
2. Move the constraints from `#[account(...)]` into the parameter types or the body.
|
|
24
|
+
3. Port the body. Integers are `bigint`s with `n` literals, and every `+ - * / %` is checked.
|
|
25
|
+
4. Test with `typesol/testing` ([test your program](test-your-program.md)), then compare the IDLs.
|
|
26
|
+
|
|
27
|
+
## Accounts and constraints
|
|
28
|
+
|
|
29
|
+
| Anchor | TypeSOL |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `#[derive(Accounts)] struct Ctx` + `ctx.accounts.x` | the instruction's parameters, in the same order |
|
|
32
|
+
| `Signer<'info>` / `#[account(mut)] Signer` | `Signer` / `Mut<Signer>` |
|
|
33
|
+
| `Account<'info, T>` / `#[account(mut)]` | `Account<T>` / `Mut<T>` |
|
|
34
|
+
| `#[account(init, payer = p, space = …, seeds = […], bump)]` | `Init<T>`, then `const bump = acct.create(p, ["seed", other.key])` (space is computed) |
|
|
35
|
+
| `init_if_needed` | `InitIfNeeded<T>`, then `const created = acct.createIfNeeded(p, seeds)` |
|
|
36
|
+
| `seeds = [b"vault", owner.key().as_ref()], bump = vault.bump` | `Seeds<Mut<Vault>, ["vault", "@owner"], "bump">` |
|
|
37
|
+
| `seeds = [b"offer", offer.maker.as_ref(), offer.id.to_le_bytes().as_ref()]` | `Seeds<…, ["offer", "@offer.maker", "@offer.id"], "bump">` |
|
|
38
|
+
| `has_one = owner` | `HasOne<Mut<T>, "owner">` |
|
|
39
|
+
| `close = destination` | `CloseTo<Mut<T>, "destination">`, or `close(acct, destination)` in the body |
|
|
40
|
+
| `address = ADMIN` | `Address<AccountInfo, "Admin1111…">` |
|
|
41
|
+
| `owner = program`, `executable` | `Owner<A, "…">`, `Executable<A>` |
|
|
42
|
+
| `constraint = expr @ MyError::X` | `require(expr, MyError.X)` at the start of the body |
|
|
43
|
+
| `realloc = n, realloc::payer = p` | `realloc(acct, n, p)` |
|
|
44
|
+
| `Option<Account<'info, T>>` | `Mut<T> \| null` (the client passes the program id to leave it out) |
|
|
45
|
+
| `UncheckedAccount` / `AccountInfo` | `AccountInfo` / `Mut<AccountInfo>`: check what you rely on yourself |
|
|
46
|
+
| `AccountLoader<T>` with `#[account(zero)]` | `Zeroed<T>` (TypeSOL reads every account in place: no copy to avoid) |
|
|
47
|
+
| `Program<'info, System>` | `SystemProgram` |
|
|
48
|
+
| `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
|
|
49
|
+
| `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
|
|
50
|
+
| `token::mint = m, token::authority = a` | `HasOne<TokenAccount, "mint" \| "owner">` against parameters with those names |
|
|
51
|
+
| `associated_token::mint = m, associated_token::authority = a` | `Ata<TokenAccount, "a", "m">`; `Ata<Init<TokenAccount>, "a", "m">` creates it |
|
|
52
|
+
| `ctx.remaining_accounts` | `remainingAccounts()` |
|
|
53
|
+
| `ctx.bumps.vault` | the value `create` returns, or `acct.pdaBump` |
|
|
54
|
+
|
|
55
|
+
## Account data
|
|
56
|
+
|
|
57
|
+
| Anchor | TypeSOL |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `#[account] pub struct T { … }` | `export interface T { … }` |
|
|
60
|
+
| `u8` … `u128`, `i8` … `i128`, `bool`, `Pubkey` | `u8` … `u128`, `i8` … `i128`, `boolean`, `Pubkey` |
|
|
61
|
+
| `[T; N]` | `FixedArray<T, N>` |
|
|
62
|
+
| `String` with `#[max_len(n)]` | `FixedString<N>`: a u32 length and N bytes, always N bytes long |
|
|
63
|
+
| `Vec<T>` with `#[max_len(n)]` | `Vec<T>` as the account's last field; it grows with `push(v, payer)` |
|
|
64
|
+
| `Option<T>` | `Option<T>`: fixed size (1 + size of T) |
|
|
65
|
+
| `#[derive(AnchorSerialize…)] enum` | `export enum` (unit variants) or a data enum `type A = { kind: "X"; … } \| …` |
|
|
66
|
+
|
|
67
|
+
TypeSOL keeps account data at a fixed layout, so three Anchor layouts differ: a Borsh `String` is variable-length (TypeSOL
|
|
68
|
+
uses `FixedString<N>`), a Borsh `Option` takes 1 byte when empty (TypeSOL always reserves the value), and a `Vec` can be
|
|
69
|
+
anywhere in an Anchor struct (TypeSOL keeps it last). Accounts using them need a migration (or a new program) rather
|
|
70
|
+
than an in-place upgrade. Everything else, including instruction arguments (`String`, `Option`, `Vec` are Borsh there),
|
|
71
|
+
matches byte for byte.
|
|
72
|
+
|
|
73
|
+
## Code
|
|
74
|
+
|
|
75
|
+
| Anchor | TypeSOL |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()>` | `export function deposit(owner: Mut<Signer>, vault: Mut<Vault>, …, amount: u64): void` |
|
|
78
|
+
| `-> Result<u64>` / a struct | `: u64` / a struct: sent as return data, listed in the IDL |
|
|
79
|
+
| `#[error_code] enum MyError { #[msg("…")] X }` | `export enum MyError { X = "…" }` (codes from 6000, in order) |
|
|
80
|
+
| `require!(c, MyError::X)`, `err!(MyError::X)` | `require(c, MyError.X)`, `require(false, MyError.X)` |
|
|
81
|
+
| `require_keys_eq!(a, b)` | `require(a === b, …)` |
|
|
82
|
+
| `a.checked_add(b).ok_or(…)?` | `a + b` (every operation is checked; overflow aborts with 5100) |
|
|
83
|
+
| `emit!(Ev { … })` / `emit_cpi!` | `emit<Ev>({ … })` / `emitCpi<Ev>({ … }, eventAuthority, self)` |
|
|
84
|
+
| `msg!("…")` | `log("…")` |
|
|
85
|
+
| `Clock::get()?.unix_timestamp`, `.slot` | `now()`, `currentSlot()` |
|
|
86
|
+
| `Rent::get()?.minimum_balance(n)` | `rentExempt(n)`, `minimumBalance(acct)` |
|
|
87
|
+
| `system_program::transfer(CpiContext::new(…), lamports)` | `systemProgram.transfer(from, to, lamports)` |
|
|
88
|
+
| `token::transfer_checked(CpiContext::new_with_signer(…, seeds), amount, decimals)` | `tokenProgram.transferChecked(from, mint, to, authority, amount, ["seed", key, bump])` |
|
|
89
|
+
| `invoke_signed(&ix, &accounts, &[seeds])` | `invoke(program, [writable(a), readonly(b)], [data…], seeds)` |
|
|
90
|
+
| `declare_program!(other)` / CPI to another Anchor program | `typesol cpi other.json` generates typed helpers |
|
|
91
|
+
| `for x in v.iter()` | `for (const x of v)` |
|
|
92
|
+
| `x.unwrap_or(d)` | `x ?? d` |
|
|
93
|
+
|
|
94
|
+
## Tooling
|
|
95
|
+
|
|
96
|
+
| Anchor | TypeSOL |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `anchor init` | `typesol init` (`--template vault` / `escrow`) |
|
|
99
|
+
| `anchor build` | `typesol build` (writes the IDL and a TypeScript client next to the binary) |
|
|
100
|
+
| `anchor test` (mocha, bankrun) | `npm test` with `typesol/testing` in LiteSVM |
|
|
101
|
+
| `anchor deploy` | `typesol deploy` ([deploy to mainnet](mainnet.md)) |
|
|
102
|
+
| `anchor idl init` / `upgrade` | `typesol idl publish` |
|
|
103
|
+
| `anchor verify` | `typesol verify` (reads the deployed program and IDL from the cluster) |
|
|
104
|
+
| `@coral-xyz/anchor` `Program` client | the generated `<name>.client.ts` on `@solana/kit`, or Anchor's client with the IDL |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Test your program
|
|
2
|
+
|
|
3
|
+
`typesol/testing` runs your program in LiteSVM, an in-process Solana runtime: no validator, no SOL, a few
|
|
4
|
+
milliseconds per transaction. Projects from `typesol init` already use it in `tests/<name>.test.ts`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { testProgram, type TestProgram } from "typesol/testing";
|
|
8
|
+
import { beforeAll, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
let t: TestProgram;
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
t = await testProgram("counter", import.meta.url); // builds the program of typesol.json and loads it
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("counts", async () => {
|
|
16
|
+
const alice = await t.wallet(); // a funded keypair (10 SOL)
|
|
17
|
+
const counter = await t.pda("counter", alice); // seeds: text, addresses or keypairs, bytes, bigints (u64)
|
|
18
|
+
t.ok(await t.send(alice, t.ix("initialize", {}, { authority: alice, counter })));
|
|
19
|
+
t.ok(await t.send(alice, t.ix("increment", { by: 5n }, { authority: alice, counter })));
|
|
20
|
+
expect((await t.account<{ count: bigint }>("Counter", counter))?.count).toBe(5n);
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Instructions
|
|
25
|
+
|
|
26
|
+
`t.ix(name, args, accounts, remaining?)` builds one instruction.
|
|
27
|
+
|
|
28
|
+
- **Accounts by name**, in camelCase or snake_case. Their signer and writable flags come from the IDL, and accounts
|
|
29
|
+
with a fixed address (the System Program, token programs, sysvars) are filled in, as are PDAs whose seeds are
|
|
30
|
+
constants, arguments or other accounts' keys (`t.ix("initialize", {}, { authority: alice })` derives `counter`). Pass a keypair for an account that
|
|
31
|
+
signs, an address otherwise, and `null` to leave an optional account out.
|
|
32
|
+
- **Arguments as plain values:** bigints or numbers for integers, address strings for Pubkeys, `null` for an empty
|
|
33
|
+
Option, objects for structs (camelCase or snake_case fields), arrays for Vecs.
|
|
34
|
+
- **Remaining accounts:** `[{ address, writable?, signer? }]`.
|
|
35
|
+
|
|
36
|
+
`t.send(payer, ...ixs)` sends them in one transaction; keypairs given as accounts sign too.
|
|
37
|
+
|
|
38
|
+
## Results
|
|
39
|
+
|
|
40
|
+
| Helper | Gives |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `t.ok(res)` | the successful result, or throws with the program's logs |
|
|
43
|
+
| `t.errorCode(res)` | the custom error code of a failed transaction: 6000 + the member's index in your error enum, or a framework code ([errors.md](errors.md)) |
|
|
44
|
+
| `t.account(type, address)` | the account decoded by its IDL type (bigints, address strings), or `null` once closed |
|
|
45
|
+
| `t.returned(instruction, res)` | the instruction's return data: an integer, boolean, Pubkey or struct |
|
|
46
|
+
| `t.balance(address)` | lamports |
|
|
47
|
+
|
|
48
|
+
`t.svm`, `t.programId`, `t.idl` and `t.coder` are there for anything else, e.g. writing a mint straight into the VM
|
|
49
|
+
with `t.svm.setAccount` (see the escrow template). `programHarness(svm, programId, idl)` gives the same helpers for a
|
|
50
|
+
program you loaded yourself.
|
|
51
|
+
|
|
52
|
+
The module needs `litesvm` and `@anchor-lang/core`, which `typesol init` adds to `devDependencies`.
|
package/docs/testing.md
CHANGED
|
@@ -5,7 +5,7 @@ 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 | `+ - * /
|
|
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
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
10
|
| `test/differential.test.ts` | Anchor twins in `bench/anchor` | identical transactions to TypeSOL and Anchor programs: same outcomes, same account contents, same wallet and token balances after every step |
|
|
11
11
|
| `test/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; each BLOCKED probe fails once its construct starts compiling, meaning that program can then be ported ([test/ports/README.md](../test/ports/README.md)) |
|
package/docs/troubleshooting.md
CHANGED
|
@@ -23,5 +23,13 @@ compiler's checks log their TypeScript location, e.g. `TypeSOL: arithmetic overf
|
|
|
23
23
|
**Heap errors (5110) building strings** — strings are allocated on a 32 KiB heap and never freed. Build less text,
|
|
24
24
|
or declare `const HEAP_SIZE_KIB = 64n` and add a ComputeBudget `requestHeapFrame` instruction to the transaction.
|
|
25
25
|
|
|
26
|
-
**`typesol idl publish` fails on a local validator with "invalid instruction data"** —
|
|
27
|
-
System instruction the current Program Metadata program
|
|
26
|
+
**`typesol idl publish` fails on a local validator with "invalid instruction data"** — the System Program of
|
|
27
|
+
`solana-test-validator` (Agave 3.1 included) rejects a System instruction that the current Program Metadata program
|
|
28
|
+
uses. Publishing works on mainnet-beta. The error shows the failing transaction's last log lines.
|
|
29
|
+
|
|
30
|
+
**"instruction X needs N bytes of stack; SBF gives each function 4096"** — every function on Solana gets a 4 KiB
|
|
31
|
+
stack frame, and going past it corrupts memory, so the build refuses it. Large structs and arrays copied into typed
|
|
32
|
+
locals (`const q: Big = acct.big`) are the usual cause: bind them without a type (`const q = acct.big`, a reference
|
|
33
|
+
into the account data) or move the work into helpers. Helpers are often inlined into their instruction, so their
|
|
34
|
+
locals count toward its frame.
|
|
35
|
+
|
package/lib/typesol.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface FixedArray<T, N extends number> {
|
|
|
36
36
|
get(index: bigint): T;
|
|
37
37
|
set(index: bigint, value: T): void;
|
|
38
38
|
readonly __length?: N;
|
|
39
|
+
/** `for (const x of arr)`: each element in order, as `arr.get(i)` gives it. */
|
|
40
|
+
[Symbol.iterator](): Iterator<T>;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/** Create a local fixed-length array filled with `fill`: `const xs = fixedArray<u64, 4>(0n)`. */
|
|
@@ -71,6 +73,8 @@ export interface Vec<T> {
|
|
|
71
73
|
pop(): T;
|
|
72
74
|
/** Remove every element, shrinking the account. */
|
|
73
75
|
clear(): void;
|
|
76
|
+
/** `for (const x of vec)`: each element in order, as `vec.get(i)` gives it; the length is read again every step. */
|
|
77
|
+
[Symbol.iterator](): Iterator<T>;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
/** One extra account passed after the declared ones. Nothing about it is checked until you check it. */
|
|
@@ -204,6 +208,13 @@ export interface AssociatedTokenProgram extends AccountMeta {
|
|
|
204
208
|
/** The associated token account address of `owner` for `mint` under `tokenProgram`. */
|
|
205
209
|
export declare function associatedTokenAddress(owner: Pubkey, mint: Pubkey, tokenProgram: Pubkey): Pubkey;
|
|
206
210
|
|
|
211
|
+
/** A creator in Metaplex metadata. `verified` can only be true for the update authority when it signs. */
|
|
212
|
+
export interface MetadataCreator {
|
|
213
|
+
address: Pubkey;
|
|
214
|
+
verified: boolean;
|
|
215
|
+
share: u8;
|
|
216
|
+
}
|
|
217
|
+
|
|
207
218
|
/** Metaplex Token Metadata (metaqbxx…). Its address is checked. */
|
|
208
219
|
export interface TokenMetadataProgram extends AccountMeta {
|
|
209
220
|
readonly __mpl_metadata: void;
|
|
@@ -222,6 +233,22 @@ export interface TokenMetadataProgram extends AccountMeta {
|
|
|
222
233
|
isMutable: boolean,
|
|
223
234
|
signerSeeds?: Seed[] | SignerSeeds,
|
|
224
235
|
): void;
|
|
236
|
+
/** With creators (1–5, shares adding up to 100) and/or an unverified collection mint. */
|
|
237
|
+
createV3(
|
|
238
|
+
metadata: AccountMeta,
|
|
239
|
+
mint: AccountMeta,
|
|
240
|
+
mintAuthority: AccountMeta,
|
|
241
|
+
payer: Mut<Signer>,
|
|
242
|
+
updateAuthority: AccountMeta,
|
|
243
|
+
systemProgram: SystemProgram,
|
|
244
|
+
name: string,
|
|
245
|
+
symbol: string,
|
|
246
|
+
uri: string,
|
|
247
|
+
sellerFeeBasisPoints: u16,
|
|
248
|
+
isMutable: boolean,
|
|
249
|
+
extras: { creators?: MetadataCreator[]; collection?: Pubkey },
|
|
250
|
+
signerSeeds?: Seed[] | SignerSeeds,
|
|
251
|
+
): void;
|
|
225
252
|
}
|
|
226
253
|
/** The canonical program address of `seeds` under `program` (Rust `Pubkey::find_program_address(seeds, program).0`). */
|
|
227
254
|
export declare function findProgramAddress(seeds: Seed[], program: Pubkey): Pubkey;
|
|
@@ -602,16 +629,6 @@ interface TokenOps {
|
|
|
602
629
|
mintTo(mint: Mint, to: TokenAccount, authority: AccountMeta, amount: u64, signerSeeds?: Seed[] | SignerSeeds): void;
|
|
603
630
|
burn(from: TokenAccount, mint: Mint, authority: AccountMeta, amount: u64, signerSeeds?: Seed[] | SignerSeeds): void;
|
|
604
631
|
closeAccount(account: TokenAccount, destination: AccountMeta, authority: AccountMeta, signerSeeds?: Seed[] | SignerSeeds): void;
|
|
605
|
-
/** Token-2022 only: write name/symbol/uri into a mint whose MetadataPointer points at itself (the mint needs lamports for the larger size). */
|
|
606
|
-
initializeMetadata(
|
|
607
|
-
mint: Mint,
|
|
608
|
-
updateAuthority: AccountMeta,
|
|
609
|
-
mintAuthority: AccountMeta,
|
|
610
|
-
name: string,
|
|
611
|
-
symbol: string,
|
|
612
|
-
uri: string,
|
|
613
|
-
signerSeeds?: Seed[] | SignerSeeds,
|
|
614
|
-
): void;
|
|
615
632
|
/** Let `delegate` move up to `amount` from `source`. */
|
|
616
633
|
approve(source: TokenAccount, delegate: AccountMeta, owner: AccountMeta, amount: u64, signerSeeds?: Seed[] | SignerSeeds): void;
|
|
617
634
|
revoke(source: TokenAccount, owner: AccountMeta, signerSeeds?: Seed[] | SignerSeeds): void;
|
|
@@ -627,18 +644,57 @@ interface TokenOps {
|
|
|
627
644
|
initializeAccount3(account: AccountMeta, mint: AccountMeta, owner: Pubkey): void;
|
|
628
645
|
}
|
|
629
646
|
|
|
647
|
+
/** Operations only Token-2022 has: on Token2022Program and TokenInterface, not on TokenProgram. */
|
|
648
|
+
interface Token2022Ops {
|
|
649
|
+
/** Token-2022 only: write name/symbol/uri into a mint whose MetadataPointer points at itself (the mint needs lamports for the larger size). */
|
|
650
|
+
initializeMetadata(
|
|
651
|
+
mint: Mint,
|
|
652
|
+
updateAuthority: AccountMeta,
|
|
653
|
+
mintAuthority: AccountMeta,
|
|
654
|
+
name: string,
|
|
655
|
+
symbol: string,
|
|
656
|
+
uri: string,
|
|
657
|
+
signerSeeds?: Seed[] | SignerSeeds,
|
|
658
|
+
): void;
|
|
659
|
+
|
|
660
|
+
// Token-2022 extensions, set up on a mint created with room for them, before initializeMint2 (or, for the token
|
|
661
|
+
// account ones, before initializeAccount3). Token-2022 only: refused at compile time on TokenProgram.
|
|
662
|
+
/** TransferFeeConfig: `basisPoints` (at most 10000) of each transfer, capped at `maximumFee`. */
|
|
663
|
+
initializeTransferFeeConfig(mint: AccountMeta, configAuthority: Pubkey | null, withdrawAuthority: Pubkey | null, basisPoints: u16, maximumFee: u64): void;
|
|
664
|
+
/** MintCloseAuthority: who may close the mint once its supply is 0. */
|
|
665
|
+
initializeMintCloseAuthority(mint: AccountMeta, closeAuthority: Pubkey | null): void;
|
|
666
|
+
/** NonTransferable: tokens of this mint cannot be transferred (soul-bound). */
|
|
667
|
+
initializeNonTransferableMint(mint: AccountMeta): void;
|
|
668
|
+
/** PermanentDelegate: `delegate` may transfer or burn from any account of this mint. */
|
|
669
|
+
initializePermanentDelegate(mint: AccountMeta, delegate: Pubkey): void;
|
|
670
|
+
/** DefaultAccountState: new token accounts start frozen (true) or initialized (false). */
|
|
671
|
+
initializeDefaultAccountState(mint: AccountMeta, frozen: boolean): void;
|
|
672
|
+
/** MetadataPointer: where the mint's metadata lives (the mint itself for initializeMetadata). */
|
|
673
|
+
initializeMetadataPointer(mint: AccountMeta, authority: Pubkey | null, metadataAddress: Pubkey | null): void;
|
|
674
|
+
/** GroupPointer: where the mint's token-group data lives. */
|
|
675
|
+
initializeGroupPointer(mint: AccountMeta, authority: Pubkey | null, groupAddress: Pubkey | null): void;
|
|
676
|
+
/** GroupMemberPointer: where the mint's group-member data lives. */
|
|
677
|
+
initializeGroupMemberPointer(mint: AccountMeta, authority: Pubkey | null, memberAddress: Pubkey | null): void;
|
|
678
|
+
/** TransferHook: `programId` is invoked on every transfer of this mint. */
|
|
679
|
+
initializeTransferHook(mint: AccountMeta, authority: Pubkey | null, programId: Pubkey | null): void;
|
|
680
|
+
/** InterestBearingConfig: `rate` in basis points per year (UI amounts only; balances do not change). */
|
|
681
|
+
initializeInterestBearingMint(mint: AccountMeta, rateAuthority: Pubkey | null, rate: i16): void;
|
|
682
|
+
/** ImmutableOwner on a token account (before initializeAccount3): its owner can never change. */
|
|
683
|
+
initializeImmutableOwner(account: AccountMeta): void;
|
|
684
|
+
}
|
|
685
|
+
|
|
630
686
|
/** The SPL Token program (Tokenkeg). Its address is checked. */
|
|
631
687
|
export interface TokenProgram extends AccountMeta, TokenOps {
|
|
632
688
|
readonly __token: void;
|
|
633
689
|
}
|
|
634
690
|
|
|
635
691
|
/** The Token-2022 program (TokenzQdB...). Its address is checked. */
|
|
636
|
-
export interface Token2022Program extends AccountMeta, TokenOps {
|
|
692
|
+
export interface Token2022Program extends AccountMeta, TokenOps, Token2022Ops {
|
|
637
693
|
readonly __token2022: void;
|
|
638
694
|
}
|
|
639
695
|
|
|
640
696
|
/** Either token program. Token accounts and mints passed alongside must belong to the program given. */
|
|
641
|
-
export interface TokenInterface extends AccountMeta, TokenOps {
|
|
697
|
+
export interface TokenInterface extends AccountMeta, TokenOps, Token2022Ops {
|
|
642
698
|
readonly __token_interface: void;
|
|
643
699
|
}
|
|
644
700
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typesol/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"types": "./dist/profile.d.ts",
|
|
34
34
|
"default": "./dist/profile.js"
|
|
35
35
|
},
|
|
36
|
+
"./testing": {
|
|
37
|
+
"types": "./dist/testing.d.ts",
|
|
38
|
+
"default": "./dist/testing.js"
|
|
39
|
+
},
|
|
40
|
+
"./ts-plugin": "./ts-plugin/index.js",
|
|
36
41
|
"./idl": {
|
|
37
42
|
"types": "./dist/idlpublish.d.ts",
|
|
38
43
|
"default": "./dist/idlpublish.js"
|
|
@@ -43,6 +48,7 @@
|
|
|
43
48
|
"dist",
|
|
44
49
|
"lib",
|
|
45
50
|
"runtime",
|
|
51
|
+
"ts-plugin",
|
|
46
52
|
"assets",
|
|
47
53
|
"!assets/readme",
|
|
48
54
|
"LICENSE",
|
|
@@ -73,6 +79,18 @@
|
|
|
73
79
|
"@solana/kit": "^8.5.0",
|
|
74
80
|
"typescript": "6.0.3"
|
|
75
81
|
},
|
|
82
|
+
"peerDependencies": {
|
|
83
|
+
"@anchor-lang/core": "^1.2.1",
|
|
84
|
+
"litesvm": "^1.6.1"
|
|
85
|
+
},
|
|
86
|
+
"peerDependenciesMeta": {
|
|
87
|
+
"@anchor-lang/core": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"litesvm": {
|
|
91
|
+
"optional": true
|
|
92
|
+
}
|
|
93
|
+
},
|
|
76
94
|
"devDependencies": {
|
|
77
95
|
"@anchor-lang/core": "1.2.1",
|
|
78
96
|
"@noble/curves": "^2.4.0",
|
package/runtime/typesol_rt.h
CHANGED
|
@@ -1211,15 +1211,19 @@ static inline uint64_t ts_metadata_address(const SolPubkey *mint, SolPubkey *out
|
|
|
1211
1211
|
|
|
1212
1212
|
/*
|
|
1213
1213
|
* Metaplex CreateMetadataAccountV3 (instruction 33): DataV2 { name, symbol, uri, seller_fee_basis_points u16,
|
|
1214
|
-
* creators
|
|
1214
|
+
* creators Option<Vec<Creator>>, collection Option<Collection>, uses None }, is_mutable bool, collection_details None.
|
|
1215
|
+
* `creators` holds `ncreators` packed Creator records (address, verified, share: 34 bytes); ncreators < 0 is None.
|
|
1216
|
+
* `collection` (NULL for None) is written unverified, as Metaplex requires at creation.
|
|
1215
1217
|
* Accounts: metadata (w), mint, mint authority (s), payer (ws), update authority, system program.
|
|
1216
1218
|
*/
|
|
1217
1219
|
static inline uint64_t ts_mpl_create_v3(TsParams *params, const TsAccount *program, TsAccount *metadata,
|
|
1218
1220
|
TsAccount *mint, TsAccount *mint_authority, TsAccount *payer,
|
|
1219
1221
|
TsAccount *update_authority, TsAccount *system, TS_Str name, TS_Str symbol,
|
|
1220
|
-
TS_Str uri, uint16_t fee_bps, bool is_mutable, const
|
|
1222
|
+
TS_Str uri, uint16_t fee_bps, bool is_mutable, const uint8_t *creators, int ncreators,
|
|
1223
|
+
const SolPubkey *collection, const SolSignerSeed *seeds, int n) {
|
|
1221
1224
|
uint8_t *data;
|
|
1222
|
-
|
|
1225
|
+
uint64_t ncr = ncreators > 0 ? (uint64_t)ncreators : 0;
|
|
1226
|
+
TS_TRY(ts_alloc(1 + 12 + name.len + symbol.len + uri.len + 2 + (1 + 4 + 34 * ncr) + (1 + 33) + 1 + 1 + 1, &data));
|
|
1223
1227
|
uint64_t at = 0;
|
|
1224
1228
|
data[at++] = 33;
|
|
1225
1229
|
ts_put_str(data, &at, name);
|
|
@@ -1227,8 +1231,24 @@ static inline uint64_t ts_mpl_create_v3(TsParams *params, const TsAccount *progr
|
|
|
1227
1231
|
ts_put_str(data, &at, uri);
|
|
1228
1232
|
ts_memcpy(data + at, &fee_bps, 2);
|
|
1229
1233
|
at += 2;
|
|
1230
|
-
|
|
1231
|
-
|
|
1234
|
+
if (ncreators < 0) {
|
|
1235
|
+
data[at++] = 0; /* creators: None */
|
|
1236
|
+
} else {
|
|
1237
|
+
data[at++] = 1;
|
|
1238
|
+
uint32_t count = (uint32_t)ncreators;
|
|
1239
|
+
ts_memcpy(data + at, &count, 4);
|
|
1240
|
+
at += 4;
|
|
1241
|
+
ts_memcpy(data + at, creators, 34 * ncr);
|
|
1242
|
+
at += 34 * ncr;
|
|
1243
|
+
}
|
|
1244
|
+
if (collection) {
|
|
1245
|
+
data[at++] = 1;
|
|
1246
|
+
data[at++] = 0; /* verified: false */
|
|
1247
|
+
ts_memcpy(data + at, collection, 32);
|
|
1248
|
+
at += 32;
|
|
1249
|
+
} else {
|
|
1250
|
+
data[at++] = 0; /* collection: None */
|
|
1251
|
+
}
|
|
1232
1252
|
data[at++] = 0; /* uses: None */
|
|
1233
1253
|
data[at++] = is_mutable ? 1 : 0;
|
|
1234
1254
|
data[at++] = 0; /* collection_details: None */
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// TypeScript language-service plugin: TypeSOL's own errors in the editor, next to TypeScript's.
|
|
2
|
+
//
|
|
3
|
+
// programs/tsconfig.json: { "compilerOptions": { …, "plugins": [{ "name": "typesol/ts-plugin" }] } }
|
|
4
|
+
//
|
|
5
|
+
// For each program entry of the project (typesol.json), once TypeScript itself reports no errors, it runs
|
|
6
|
+
// `typesol check --json --stdin` on the editor's text (unsaved changes included) and shows what the compiler refuses
|
|
7
|
+
// (an unsupported construct, a missing account constraint…) at its line and column. Editors load tsconfig plugins
|
|
8
|
+
// from the workspace's TypeScript (VS Code: "TypeScript: Select TypeScript Version" → "Use Workspace Version").
|
|
9
|
+
// CommonJS on purpose: tsserver loads plugins with require().
|
|
10
|
+
"use strict";
|
|
11
|
+
const { execFileSync } = require("node:child_process");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
|
|
15
|
+
const CLI = path.join(__dirname, "..", "dist", "cli.js");
|
|
16
|
+
const CODE = 9900;
|
|
17
|
+
|
|
18
|
+
/** The program entry files of the typesol.json at or above `dir` (absolute), or an empty set. */
|
|
19
|
+
function entriesNear(dir) {
|
|
20
|
+
for (let d = dir; ; d = path.dirname(d)) {
|
|
21
|
+
const config = path.join(d, "typesol.json");
|
|
22
|
+
if (fs.existsSync(config)) {
|
|
23
|
+
try {
|
|
24
|
+
const programs = JSON.parse(fs.readFileSync(config, "utf8")).programs || {};
|
|
25
|
+
return new Set(Object.values(programs).map((p) => path.resolve(d, typeof p === "string" ? p : p.path)));
|
|
26
|
+
} catch {
|
|
27
|
+
return new Set();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (path.dirname(d) === d) return new Set();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function init({ typescript: ts }) {
|
|
35
|
+
function create(info) {
|
|
36
|
+
const ls = info.languageService;
|
|
37
|
+
const log = (m) => info.project.projectService.logger.info(`[typesol] ${m}`);
|
|
38
|
+
const cache = new Map(); // fileName -> { text, found }
|
|
39
|
+
|
|
40
|
+
/** The entry's text plus the modification times of the .ts files beside it (its imports usually live there). */
|
|
41
|
+
function key(fileName, sf) {
|
|
42
|
+
const dir = path.dirname(path.resolve(fileName));
|
|
43
|
+
let stamps = "";
|
|
44
|
+
try {
|
|
45
|
+
for (const f of fs.readdirSync(dir).sort())
|
|
46
|
+
if (f.endsWith(".ts") && path.join(dir, f) !== path.resolve(fileName)) stamps += `${f}:${fs.statSync(path.join(dir, f)).mtimeMs};`;
|
|
47
|
+
} catch {
|
|
48
|
+
stamps = "";
|
|
49
|
+
}
|
|
50
|
+
return `${stamps}\n${sf.text}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function typesolDiagnostics(fileName, sf) {
|
|
54
|
+
const k = key(fileName, sf);
|
|
55
|
+
const hit = cache.get(fileName);
|
|
56
|
+
if (hit && hit.key === k) return hit.found;
|
|
57
|
+
let found = [];
|
|
58
|
+
try {
|
|
59
|
+
const out = execFileSync(process.execPath, [CLI, "check", fileName, "--json", "--stdin"], {
|
|
60
|
+
input: sf.text,
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
timeout: 15000,
|
|
63
|
+
// tsserver may run inside an editor's Electron: make it behave as plain Node for the child.
|
|
64
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
});
|
|
67
|
+
found = JSON.parse(out).diagnostics;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Exit 1 with JSON on stdout means diagnostics; anything else is a crash worth logging, not showing.
|
|
70
|
+
try {
|
|
71
|
+
found = JSON.parse(String(e.stdout)).diagnostics;
|
|
72
|
+
} catch {
|
|
73
|
+
log(`check failed for ${fileName}: ${e.message}`);
|
|
74
|
+
found = [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
cache.set(fileName, { key: k, found });
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const proxy = Object.create(null);
|
|
82
|
+
for (const k of Object.keys(ls)) proxy[k] = (...args) => ls[k](...args);
|
|
83
|
+
proxy.getSemanticDiagnostics = (fileName) => {
|
|
84
|
+
const prior = ls.getSemanticDiagnostics(fileName);
|
|
85
|
+
if (prior.some((d) => d.category === ts.DiagnosticCategory.Error)) return prior; // TypeScript's errors first
|
|
86
|
+
if (!entriesNear(path.dirname(path.resolve(fileName))).has(path.resolve(fileName))) return prior;
|
|
87
|
+
const sf = ls.getProgram() && ls.getProgram().getSourceFile(fileName);
|
|
88
|
+
if (!sf) return prior;
|
|
89
|
+
const own = typesolDiagnostics(fileName, sf)
|
|
90
|
+
.filter((d) => path.resolve(d.file) === path.resolve(fileName))
|
|
91
|
+
.map((d) => {
|
|
92
|
+
const line = Math.max(0, Math.min(d.line - 1, sf.getLineStarts().length - 1));
|
|
93
|
+
const start = Math.min(sf.getPositionOfLineAndCharacter(line, 0) + Math.max(0, d.column - 1), sf.end);
|
|
94
|
+
const word = /^[\w$]+/.exec(sf.text.slice(start));
|
|
95
|
+
return {
|
|
96
|
+
file: sf,
|
|
97
|
+
start,
|
|
98
|
+
length: word ? word[0].length : 1,
|
|
99
|
+
messageText: d.message,
|
|
100
|
+
category: ts.DiagnosticCategory.Error,
|
|
101
|
+
code: CODE,
|
|
102
|
+
source: "typesol",
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
return [...prior, ...own];
|
|
106
|
+
};
|
|
107
|
+
return proxy;
|
|
108
|
+
}
|
|
109
|
+
return { create };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = init;
|