@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +35 -5
  3. package/dist/cli.js +161 -24
  4. package/dist/compiler/analyze.d.ts +4 -0
  5. package/dist/compiler/analyze.js +51 -9
  6. package/dist/compiler/emit/base.js +1 -1
  7. package/dist/compiler/emit/calls.js +7 -0
  8. package/dist/compiler/emit/cpi.js +188 -9
  9. package/dist/compiler/emit/expressions.d.ts +11 -0
  10. package/dist/compiler/emit/expressions.js +107 -2
  11. package/dist/compiler/emit/function.js +11 -1
  12. package/dist/compiler/emit/shared.js +3 -2
  13. package/dist/compiler/emit/statements.d.ts +13 -0
  14. package/dist/compiler/emit/statements.js +131 -5
  15. package/dist/compiler/emit/storage.d.ts +2 -1
  16. package/dist/compiler/emit/storage.js +24 -7
  17. package/dist/compiler/idl.js +28 -14
  18. package/dist/compiler/model.d.ts +16 -3
  19. package/dist/compiler/toolchain.d.ts +2 -0
  20. package/dist/compiler/toolchain.js +27 -1
  21. package/dist/idlpublish.js +43 -9
  22. package/dist/onchain.d.ts +30 -0
  23. package/dist/onchain.js +53 -0
  24. package/dist/portgen.d.ts +87 -0
  25. package/dist/portgen.js +311 -0
  26. package/dist/templates.d.ts +9 -0
  27. package/dist/templates.js +324 -0
  28. package/dist/testing.d.ts +66 -0
  29. package/dist/testing.js +337 -0
  30. package/dist/workspace.d.ts +3 -2
  31. package/dist/workspace.js +18 -106
  32. package/docs/architecture.md +3 -0
  33. package/docs/cli.md +17 -3
  34. package/docs/language.md +11 -10
  35. package/docs/mainnet.md +100 -0
  36. package/docs/migrate-from-anchor.md +104 -0
  37. package/docs/test-your-program.md +52 -0
  38. package/docs/testing.md +1 -1
  39. package/docs/troubleshooting.md +10 -2
  40. package/lib/typesol.d.ts +68 -12
  41. package/package.json +19 -1
  42. package/runtime/typesol_rt.h +25 -5
  43. package/ts-plugin/index.js +112 -0
  44. package/ts-plugin/package.json +4 -0
@@ -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 | `+ - * / %`, `& \| ^ << >> ~`, 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 |
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)) |
@@ -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"** — older validators reject a
27
- System instruction the current Program Metadata program uses; it works on clusters with current Agave versions.
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.14.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",
@@ -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 None, collection None, uses None }, is_mutable bool, collection_details None.
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 SolSignerSeed *seeds, int n) {
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
- TS_TRY(ts_alloc(1 + 12 + name.len + symbol.len + uri.len + 2 + 3 + 1 + 1, &data));
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
- data[at++] = 0; /* creators: None */
1231
- data[at++] = 0; /* collection: None */
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;
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "commonjs",
3
+ "main": "index.js"
4
+ }