@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,324 @@
1
+ // Programs and tests that `typesol init --template <name>` writes. Each test runs its program in LiteSVM and covers its
2
+ // main flow and at least one refused call, so a new project starts from something that passes and shows how to test.
3
+ // The programs are copies of examples/ (test/workspace.test.ts checks they still match).
4
+ export const TEMPLATE_NAMES = ["counter", "vault", "escrow"];
5
+ /** Every test starts the same way: build the project's program into a LiteSVM with `typesol/testing`. */
6
+ const testFile = (name, o) => `// Runs the program in LiteSVM (an in-process Solana runtime): \`npm test\`.
7
+ ${o.imports ?? ""}import { testProgram, type TestProgram } from "typesol/testing";
8
+ import { beforeAll, describe, expect, it } from "vitest";
9
+
10
+ describe("${name}", () => {
11
+ let t: TestProgram;
12
+ beforeAll(async () => {
13
+ t = await testProgram("${name}", import.meta.url);
14
+ });
15
+ ${o.body}});
16
+ `;
17
+ export const TEMPLATES = {
18
+ counter: {
19
+ program: `import { type Account, type Init, type Mut, type Pubkey, type Signer, type SystemProgram, type u8, type u64, require } from "typesol";
20
+
21
+ export interface Counter {
22
+ authority: Pubkey;
23
+ count: u64;
24
+ bump: u8;
25
+ }
26
+
27
+ export enum CounterError {
28
+ Unauthorized = "Only the counter's authority can change it",
29
+ }
30
+
31
+ /** Create the caller's counter at PDA ["counter", authority]. */
32
+ export function initialize(authority: Mut<Signer>, counter: Init<Counter>, systemProgram: SystemProgram): void {
33
+ counter.bump = counter.create(authority, ["counter", authority.key]);
34
+ counter.authority = authority.key;
35
+ counter.count = 0n;
36
+ }
37
+
38
+ export function increment(authority: Signer, counter: Mut<Counter>, by: u64): void {
39
+ require(counter.authority === authority.key, CounterError.Unauthorized);
40
+ counter.count += by;
41
+ }
42
+
43
+ /** Read-only check that callers can simulate. */
44
+ export function assertAtLeast(counter: Account<Counter>, min: u64): void {
45
+ require(counter.count >= min, "counter is below the expected minimum");
46
+ }
47
+ `,
48
+ test: (name) => testFile(name, {
49
+ body: `
50
+ it("initializes and increments the caller's counter; refuses anyone else", async () => {
51
+ const alice = await t.wallet();
52
+ const mallory = await t.wallet();
53
+ const counter = await t.pda("counter", alice);
54
+ t.ok(await t.send(alice, t.ix("initialize", {}, { authority: alice, counter })));
55
+ t.ok(await t.send(alice, t.ix("increment", { by: 5n }, { authority: alice, counter })));
56
+ expect((await t.account<{ count: bigint }>("Counter", counter))?.count).toBe(5n);
57
+
58
+ // CounterError.Unauthorized is the enum's first member: 6000.
59
+ expect(t.errorCode(await t.send(mallory, t.ix("increment", { by: 1n }, { authority: mallory, counter })))).toBe(6000);
60
+ });
61
+ `,
62
+ }),
63
+ },
64
+ vault: {
65
+ program: `import {
66
+ type Init,
67
+ type Mut,
68
+ type Pubkey,
69
+ type Signer,
70
+ type SystemProgram,
71
+ type u8,
72
+ type u64,
73
+ close,
74
+ minimumBalance,
75
+ require,
76
+ } from "typesol";
77
+
78
+ /** A personal SOL vault at PDA ["vault", owner]. Lamports above rent are the balance. */
79
+ export interface Vault {
80
+ owner: Pubkey;
81
+ deposited: u64;
82
+ withdrawn: u64;
83
+ bump: u8;
84
+ }
85
+
86
+ export enum VaultError {
87
+ NotOwner = "Only the vault owner can do this",
88
+ ZeroAmount = "Amount must be greater than zero",
89
+ InsufficientFunds = "Withdrawal would dip below the rent-exempt minimum",
90
+ }
91
+
92
+ export function open(owner: Mut<Signer>, vault: Init<Vault>, systemProgram: SystemProgram): void {
93
+ vault.bump = vault.create(owner, ["vault", owner.key]);
94
+ vault.owner = owner.key;
95
+ }
96
+
97
+ export function deposit(owner: Mut<Signer>, vault: Mut<Vault>, systemProgram: SystemProgram, amount: u64): void {
98
+ require(amount > 0n, VaultError.ZeroAmount);
99
+ require(vault.owner === owner.key, VaultError.NotOwner);
100
+ systemProgram.transfer(owner, vault, amount);
101
+ vault.deposited += amount;
102
+ }
103
+
104
+ export function withdraw(owner: Mut<Signer>, vault: Mut<Vault>, amount: u64): void {
105
+ require(amount > 0n, VaultError.ZeroAmount);
106
+ require(vault.owner === owner.key, VaultError.NotOwner);
107
+ const floor = minimumBalance(vault);
108
+ require(vault.lamports - amount >= floor, VaultError.InsufficientFunds);
109
+ // The vault is owned by this program, so it can move lamports directly.
110
+ vault.lamports -= amount;
111
+ owner.lamports += amount;
112
+ vault.withdrawn += amount;
113
+ }
114
+
115
+ /** Close the vault and return everything, rent included, to the owner. */
116
+ export function shut(owner: Mut<Signer>, vault: Mut<Vault>): void {
117
+ require(vault.owner === owner.key, VaultError.NotOwner);
118
+ close(vault, owner);
119
+ }
120
+ `,
121
+ test: (name) => testFile(name, {
122
+ body: `
123
+ it("deposits and withdraws SOL, keeps the rent reserve, then closes", async () => {
124
+ const owner = await t.wallet();
125
+ const vault = await t.pda("vault", owner);
126
+ t.ok(await t.send(owner, t.ix("open", {}, { owner, vault })));
127
+ const rent = t.balance(vault);
128
+ t.ok(await t.send(owner, t.ix("deposit", { amount: 3_000_000_000n }, { owner, vault })));
129
+ t.ok(await t.send(owner, t.ix("withdraw", { amount: 1_000_000_000n }, { owner, vault })));
130
+ expect(t.balance(vault)).toBe(rent + 2_000_000_000n);
131
+
132
+ // VaultError.InsufficientFunds (third member, 6002): the rent reserve stays.
133
+ expect(t.errorCode(await t.send(owner, t.ix("withdraw", { amount: 2_000_000_001n }, { owner, vault })))).toBe(6002);
134
+
135
+ t.ok(await t.send(owner, t.ix("shut", {}, { owner, vault })));
136
+ expect(await t.account("Vault", vault)).toBeNull();
137
+ });
138
+
139
+ it("refuses a stranger", async () => {
140
+ const owner = await t.wallet();
141
+ const thief = await t.wallet();
142
+ const vault = await t.pda("vault", owner);
143
+ t.ok(await t.send(owner, t.ix("open", {}, { owner, vault })));
144
+ // VaultError.NotOwner: 6000.
145
+ expect(t.errorCode(await t.send(thief, t.ix("shut", {}, { owner: thief, vault })))).toBe(6000);
146
+ });
147
+ `,
148
+ }),
149
+ },
150
+ escrow: {
151
+ program: `import {
152
+ type AccountInfo,
153
+ type Init,
154
+ type Mut,
155
+ type Pubkey,
156
+ type Signer,
157
+ type SystemProgram,
158
+ type TokenAccount,
159
+ type TokenProgram,
160
+ type u8,
161
+ type u64,
162
+ close,
163
+ require,
164
+ } from "typesol";
165
+
166
+ /**
167
+ * A maker escrows \`amountA\` of mint A and asks for \`wantB\` of mint B.
168
+ * Anyone can take the offer atomically; the maker can refund while it is open.
169
+ *
170
+ * The client creates \`vault\` beforehand as a token account for mint A whose
171
+ * owner is the offer PDA ["offer", maker, seed].
172
+ */
173
+ export interface Offer {
174
+ maker: Pubkey;
175
+ mintA: Pubkey;
176
+ mintB: Pubkey;
177
+ vault: Pubkey;
178
+ amountA: u64;
179
+ wantB: u64;
180
+ seed: u64;
181
+ bump: u8;
182
+ }
183
+
184
+ export enum EscrowError {
185
+ ZeroAmount = "Amounts must be greater than zero",
186
+ BadVault = "Vault must be an empty token account owned by the offer",
187
+ WrongMaker = "Maker does not match the offer",
188
+ WrongVault = "Vault does not match the offer",
189
+ WrongMint = "Token account has the wrong mint",
190
+ WrongRecipient = "Maker's token account is not owned by the maker",
191
+ }
192
+
193
+ export function make(
194
+ maker: Mut<Signer>,
195
+ offer: Init<Offer>,
196
+ makerTokenA: Mut<TokenAccount>,
197
+ vault: Mut<TokenAccount>,
198
+ systemProgram: SystemProgram,
199
+ tokenProgram: TokenProgram,
200
+ seed: u64,
201
+ amountA: u64,
202
+ wantB: u64,
203
+ mintB: Pubkey,
204
+ ): void {
205
+ offer.bump = offer.create(maker, ["offer", maker.key, seed]);
206
+ require(amountA > 0n && wantB > 0n, EscrowError.ZeroAmount);
207
+ require(vault.owner === offer.key && vault.mint === makerTokenA.mint && vault.amount === 0n, EscrowError.BadVault);
208
+
209
+ tokenProgram.transfer(makerTokenA, vault, maker, amountA);
210
+
211
+ offer.maker = maker.key;
212
+ offer.mintA = makerTokenA.mint;
213
+ offer.mintB = mintB;
214
+ offer.vault = vault.key;
215
+ offer.amountA = amountA;
216
+ offer.wantB = wantB;
217
+ offer.seed = seed;
218
+ }
219
+
220
+ export function take(
221
+ taker: Mut<Signer>,
222
+ maker: Mut<AccountInfo>,
223
+ offer: Mut<Offer>,
224
+ takerTokenA: Mut<TokenAccount>,
225
+ takerTokenB: Mut<TokenAccount>,
226
+ makerTokenB: Mut<TokenAccount>,
227
+ vault: Mut<TokenAccount>,
228
+ tokenProgram: TokenProgram,
229
+ ): void {
230
+ require(offer.maker === maker.key, EscrowError.WrongMaker);
231
+ require(offer.vault === vault.key, EscrowError.WrongVault);
232
+ require(takerTokenA.mint === offer.mintA && takerTokenB.mint === offer.mintB, EscrowError.WrongMint);
233
+ require(makerTokenB.mint === offer.mintB, EscrowError.WrongMint);
234
+ require(makerTokenB.owner === offer.maker, EscrowError.WrongRecipient);
235
+
236
+ tokenProgram.transfer(takerTokenB, makerTokenB, taker, offer.wantB);
237
+ tokenProgram.transfer(vault, takerTokenA, offer, offer.amountA, ["offer", offer.maker, offer.seed, offer.bump]);
238
+ tokenProgram.closeAccount(vault, maker, offer, ["offer", offer.maker, offer.seed, offer.bump]);
239
+ close(offer, maker);
240
+ }
241
+
242
+ export function refund(
243
+ maker: Mut<Signer>,
244
+ offer: Mut<Offer>,
245
+ makerTokenA: Mut<TokenAccount>,
246
+ vault: Mut<TokenAccount>,
247
+ tokenProgram: TokenProgram,
248
+ ): void {
249
+ require(offer.maker === maker.key, EscrowError.WrongMaker);
250
+ require(offer.vault === vault.key, EscrowError.WrongVault);
251
+ require(makerTokenA.mint === offer.mintA, EscrowError.WrongMint);
252
+
253
+ tokenProgram.transfer(vault, makerTokenA, offer, offer.amountA, ["offer", offer.maker, offer.seed, offer.bump]);
254
+ tokenProgram.closeAccount(vault, maker, offer, ["offer", offer.maker, offer.seed, offer.bump]);
255
+ close(offer, maker);
256
+ }
257
+ `,
258
+ test: (name) => testFile(name, {
259
+ imports: `import { AccountState, getMintEncoder, getMintSize, getTokenDecoder, getTokenEncoder, getTokenSize } from "@solana-program/token";\nimport { type Address, type KeyPairSigner, generateKeyPairSigner, lamports } from "@solana/kit";\n`,
260
+ body: `
261
+ const TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;
262
+ /** A mint, written straight into the VM (a real client sends System CreateAccount + InitializeMint2). */
263
+ const mint = async (authority: Address) => {
264
+ const address = (await generateKeyPairSigner()).address;
265
+ const data = getMintEncoder().encode({ mintAuthority: authority, supply: 0n, decimals: 6, isInitialized: true, freezeAuthority: null });
266
+ t.svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(1_461_600n), programAddress: TOKEN, space: BigInt(getMintSize()) });
267
+ return address;
268
+ };
269
+ /** A token account of owner for mint holding amount, written straight into the VM. */
270
+ const tokenAccount = async (owner: Address, mintAddress: Address, amount: bigint) => {
271
+ const address = (await generateKeyPairSigner()).address;
272
+ const data = getTokenEncoder().encode({
273
+ mint: mintAddress,
274
+ owner,
275
+ amount,
276
+ delegate: null,
277
+ state: AccountState.Initialized,
278
+ isNative: null,
279
+ delegatedAmount: 0n,
280
+ closeAuthority: null,
281
+ });
282
+ t.svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(2_039_280n), programAddress: TOKEN, space: BigInt(getTokenSize()) });
283
+ return address;
284
+ };
285
+ const balance = (address: Address) => {
286
+ const account = t.svm.getAccount(address);
287
+ return account.exists ? getTokenDecoder().decode(account.data).amount : null;
288
+ };
289
+
290
+ it("escrows mint A for mint B; the taker swaps, the maker can refund", async () => {
291
+ const maker = await t.wallet();
292
+ const taker = await t.wallet();
293
+ const mintA = await mint(maker.address);
294
+ const mintB = await mint(taker.address);
295
+ const makerTokenA = await tokenAccount(maker.address, mintA, 1_000n);
296
+ const makerTokenB = await tokenAccount(maker.address, mintB, 0n);
297
+ const takerTokenA = await tokenAccount(taker.address, mintA, 0n);
298
+ const takerTokenB = await tokenAccount(taker.address, mintB, 500n);
299
+
300
+ const open = async (seed: bigint) => {
301
+ const offer = await t.pda("offer", maker, seed);
302
+ // The client creates the vault: an empty token account for mint A owned by the offer PDA.
303
+ const vault = await tokenAccount(offer, mintA, 0n);
304
+ t.ok(await t.send(maker, t.ix("make", { seed, amountA: 400n, wantB: 300n, mintB }, { maker, offer, makerTokenA, vault })));
305
+ return { offer, vault };
306
+ };
307
+
308
+ const first = await open(1n);
309
+ expect(balance(first.vault)).toBe(400n);
310
+ t.ok(await t.send(taker, t.ix("take", {}, { taker, maker: maker.address, offer: first.offer, takerTokenA, takerTokenB, makerTokenB, vault: first.vault })));
311
+ expect([balance(takerTokenA), balance(makerTokenB), balance(first.vault)]).toEqual([400n, 300n, null]);
312
+
313
+ const second = await open(2n);
314
+ const refund = (who: KeyPairSigner) => t.ix("refund", {}, { maker: who, offer: second.offer, makerTokenA, vault: second.vault });
315
+ // EscrowError.WrongMaker (third member, 6002): only the maker refunds.
316
+ expect(t.errorCode(await t.send(taker, refund(taker)))).toBe(6002);
317
+ t.ok(await t.send(maker, refund(maker)));
318
+ expect([balance(makerTokenA), await t.account("Offer", second.offer)]).toEqual([600n, null]);
319
+ });
320
+ `,
321
+ }),
322
+ devDependencies: { "@solana-program/token": "~0.18.0" },
323
+ },
324
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `typesol/testing`: test a project's program in LiteSVM (an in-process Solana runtime) without the boilerplate.
3
+ *
4
+ * const t = await testProgram("counter", import.meta.url);
5
+ * const alice = await t.wallet();
6
+ * const counter = await t.pda("counter", alice);
7
+ * t.ok(await t.send(alice, t.ix("initialize", {}, { authority: alice, counter })));
8
+ * t.ok(await t.send(alice, t.ix("increment", { by: 5n }, { authority: alice, counter })));
9
+ * expect((await t.account("Counter", counter))?.count).toBe(5n);
10
+ *
11
+ * Accounts are passed by name; their signer / writable flags and fixed addresses (system program, token programs…)
12
+ * come from the IDL, as Anchor's `.accounts({...})` does, and PDAs seeded by constants, arguments or other accounts'
13
+ * keys are derived when left out. Arguments take plain values: bigints or numbers for
14
+ * integers, address strings for Pubkeys, null for an empty Option. Decoded accounts and return data come back the
15
+ * same way (bigints, address strings).
16
+ *
17
+ * Needs `litesvm` and `@anchor-lang/core`, which `typesol init` projects have.
18
+ */
19
+ import anchor, { type Idl } from "@anchor-lang/core";
20
+ import { type Address, type Instruction, type KeyPairSigner } from "@solana/kit";
21
+ import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
22
+ declare const BorshCoder: typeof anchor.BorshCoder;
23
+ type BorshCoder = InstanceType<typeof BorshCoder>;
24
+ export type Result = TransactionMetadata | FailedTransactionMetadata;
25
+ /** An account for an instruction: an address, a keypair (which then signs), or null to leave an optional one out. */
26
+ export type AccountInput = Address | KeyPairSigner | null;
27
+ /**
28
+ * A PDA seed: text, an address (or keypair), raw bytes, or a bigint as a little-endian u64. A string that is a valid
29
+ * address is taken as one; pass `new TextEncoder().encode(s)` for text that happens to look like an address.
30
+ */
31
+ export type SeedInput = string | Address | KeyPairSigner | Uint8Array | bigint;
32
+ export interface TestProgram {
33
+ svm: LiteSVM;
34
+ programId: Address;
35
+ idl: Idl;
36
+ coder: BorshCoder;
37
+ /** A new keypair funded with `sol` SOL (default 10). */
38
+ wallet(sol?: number): Promise<KeyPairSigner>;
39
+ /** The program's PDA for these seeds. */
40
+ pda(...seeds: SeedInput[]): Promise<Address>;
41
+ /** The PDA and its bump. */
42
+ pdaWithBump(...seeds: SeedInput[]): Promise<[Address, number]>;
43
+ /** An instruction of this program, accounts by name (`authority` or `authority`'s snake_case), args as plain values. */
44
+ ix(name: string, args?: Record<string, unknown>, accounts?: Record<string, AccountInput>, remaining?: {
45
+ address: Address;
46
+ writable?: boolean;
47
+ signer?: KeyPairSigner;
48
+ }[]): Instruction;
49
+ /** Send instructions in one transaction paid by `payer` (keypairs passed as accounts sign too). */
50
+ send(payer: KeyPairSigner, ...ixs: Instruction[]): Promise<Result>;
51
+ /** The successful result, or a thrown error with the program's logs. */
52
+ ok(res: Result): TransactionMetadata;
53
+ /** The custom error code of a failed transaction (6000+ for the program's error enums); throws if it succeeded. */
54
+ errorCode(res: Result): number;
55
+ /** An account of this program decoded by its IDL type, or null when it does not exist. */
56
+ account<T = Record<string, unknown>>(type: string, address: Address): Promise<T | null>;
57
+ /** A successful instruction's return data, decoded by its IDL `returns` type. */
58
+ returned<T = unknown>(instruction: string, res: Result): T;
59
+ /** Lamports of any account (0 when it does not exist). */
60
+ balance(address: Address): bigint;
61
+ }
62
+ /** Build `program` of the project around `from` (a file URL such as import.meta.url, or a directory) and load it. */
63
+ export declare function testProgram(program: string, from: string | URL): Promise<TestProgram>;
64
+ /** The same helpers for a program already loaded into `svm` at `programId` with this IDL. */
65
+ export declare function programHarness(svm: LiteSVM, programId: Address, idl: Idl): TestProgram;
66
+ export {};