@typesol/cli 0.15.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 (41) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +10 -6
  3. package/dist/cli.js +147 -20
  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/onchain.d.ts +30 -0
  22. package/dist/onchain.js +53 -0
  23. package/dist/portgen.d.ts +87 -0
  24. package/dist/portgen.js +311 -0
  25. package/dist/templates.js +49 -154
  26. package/dist/testing.d.ts +66 -0
  27. package/dist/testing.js +337 -0
  28. package/dist/workspace.js +11 -1
  29. package/docs/architecture.md +3 -0
  30. package/docs/cli.md +16 -2
  31. package/docs/language.md +11 -10
  32. package/docs/mainnet.md +5 -4
  33. package/docs/migrate-from-anchor.md +104 -0
  34. package/docs/test-your-program.md +52 -0
  35. package/docs/testing.md +1 -1
  36. package/docs/troubleshooting.md +7 -0
  37. package/lib/typesol.d.ts +68 -12
  38. package/package.json +19 -1
  39. package/runtime/typesol_rt.h +25 -5
  40. package/ts-plugin/index.js +112 -0
  41. package/ts-plugin/package.json +4 -0
package/dist/templates.js CHANGED
@@ -2,71 +2,16 @@
2
2
  // main flow and at least one refused call, so a new project starts from something that passes and shows how to test.
3
3
  // The programs are copies of examples/ (test/workspace.test.ts checks they still match).
4
4
  export const TEMPLATE_NAMES = ["counter", "vault", "escrow"];
5
- /** The part every test shares: build the project's program into a LiteSVM, encode instructions with its IDL, send. */
5
+ /** Every test starts the same way: build the project's program into a LiteSVM with `typesol/testing`. */
6
6
  const testFile = (name, o) => `// Runs the program in LiteSVM (an in-process Solana runtime): \`npm test\`.
7
- import { BN, BorshCoder, type Idl } from "@anchor-lang/core";
8
- ${o.imports}import {
9
- ${[
10
- "AccountRole",
11
- "appendTransactionMessageInstructions",
12
- "createTransactionMessage",
13
- ...o.kit,
14
- "pipe",
15
- "setTransactionMessageFeePayerSigner",
16
- "signTransactionMessageWithSigners",
17
- "type Address",
18
- "type Instruction",
19
- "type KeyPairSigner",
20
- ]
21
- .map((x) => ` ${x},`)
22
- .join("\n")}
23
- } from "@solana/kit";
24
- import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
25
- import path from "node:path";
26
- import { fileURLToPath } from "node:url";
27
- import { buildWorkspace, findWorkspace } from "typesol/workspace";
7
+ ${o.imports ?? ""}import { testProgram, type TestProgram } from "typesol/testing";
28
8
  import { beforeAll, describe, expect, it } from "vitest";
29
9
 
30
- const SYSTEM = "11111111111111111111111111111111" as Address;
31
- const ws = findWorkspace(path.dirname(fileURLToPath(import.meta.url)))!;
32
-
33
10
  describe("${name}", () => {
34
- const svm = new LiteSVM();
35
- let programId: Address;
36
- let coder: BorshCoder;
37
- beforeAll(() => {
38
- const [built] = buildWorkspace(ws, ["${name}"]);
39
- programId = built.address as Address;
40
- svm.addProgramFromFile(programId, built.result.soPath);
41
- coder = new BorshCoder(built.result.idl as unknown as Idl);
42
- });
43
-
44
- const ix = (name: string, args: object, accounts: { address: Address; signer?: KeyPairSigner; writable?: boolean }[]): Instruction => ({
45
- programAddress: programId,
46
- accounts: accounts.map((a) => ({
47
- address: a.address,
48
- role: a.signer ? (a.writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER) : a.writable ? AccountRole.WRITABLE : AccountRole.READONLY,
49
- ...(a.signer ? { signer: a.signer } : {}),
50
- })) as Instruction["accounts"],
51
- data: new Uint8Array(coder.instruction.encode(name, args)),
11
+ let t: TestProgram;
12
+ beforeAll(async () => {
13
+ t = await testProgram("${name}", import.meta.url);
52
14
  });
53
- const send = async (payer: KeyPairSigner, ixs: Instruction[]) => {
54
- const tx = await pipe(
55
- createTransactionMessage({ version: 0 }),
56
- (t) => setTransactionMessageFeePayerSigner(payer, t),
57
- (t) => svm.setTransactionMessageLifetimeUsingLatestBlockhash(t),
58
- (t) => appendTransactionMessageInstructions(ixs, t),
59
- (t) => signTransactionMessageWithSigners(t),
60
- );
61
- const res = svm.sendTransaction(tx);
62
- svm.expireBlockhash();
63
- return res;
64
- };
65
- /** The custom error code of a failed transaction: 6000 + the member's index in the program's error enum. */
66
- const errorCode = (res: TransactionMetadata | FailedTransactionMetadata) => {
67
- if (!(res instanceof FailedTransactionMetadata)) throw new Error("expected the transaction to fail");
68
- return Number(/Custom\\D*(\\d+)/.exec(String(res.err()))?.[1]);
69
- };
70
15
  ${o.body}});
71
16
  `;
72
17
  export const TEMPLATES = {
@@ -101,25 +46,17 @@ export function assertAtLeast(counter: Account<Counter>, min: u64): void {
101
46
  }
102
47
  `,
103
48
  test: (name) => testFile(name, {
104
- imports: "",
105
- kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
106
49
  body: `
107
50
  it("initializes and increments the caller's counter; refuses anyone else", async () => {
108
- const alice = await generateKeyPairSigner();
109
- const mallory = await generateKeyPairSigner();
110
- svm.airdrop(alice.address, lamports(1_000_000_000n));
111
- svm.airdrop(mallory.address, lamports(1_000_000_000n));
112
- const [counter] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["counter", getAddressEncoder().encode(alice.address)] });
113
- const me = { address: alice.address, signer: alice, writable: true };
114
- expect(await send(alice, [ix("initialize", {}, [me, { address: counter, writable: true }, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
115
- expect(await send(alice, [ix("increment", { by: new BN(5) }, [{ address: alice.address, signer: alice }, { address: counter, writable: true }])])).toBeInstanceOf(TransactionMetadata);
116
- const account = svm.getAccount(counter);
117
- if (!account.exists) throw new Error("counter was not created");
118
- expect(coder.accounts.decode("Counter", Buffer.from(account.data)).count.toString()).toBe("5");
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);
119
57
 
120
58
  // CounterError.Unauthorized is the enum's first member: 6000.
121
- const stranger = ix("increment", { by: new BN(1) }, [{ address: mallory.address, signer: mallory }, { address: counter, writable: true }]);
122
- expect(errorCode(await send(mallory, [stranger]))).toBe(6000);
59
+ expect(t.errorCode(await t.send(mallory, t.ix("increment", { by: 1n }, { authority: mallory, counter })))).toBe(6000);
123
60
  });
124
61
  `,
125
62
  }),
@@ -182,41 +119,30 @@ export function shut(owner: Mut<Signer>, vault: Mut<Vault>): void {
182
119
  }
183
120
  `,
184
121
  test: (name) => testFile(name, {
185
- imports: "",
186
- kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
187
122
  body: `
188
123
  it("deposits and withdraws SOL, keeps the rent reserve, then closes", async () => {
189
- const owner = await generateKeyPairSigner();
190
- svm.airdrop(owner.address, lamports(10_000_000_000n));
191
- const [vault] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["vault", getAddressEncoder().encode(owner.address)] });
192
- const me = { address: owner.address, signer: owner, writable: true };
193
- const v = { address: vault, writable: true };
194
-
195
- expect(await send(owner, [ix("open", {}, [me, v, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
196
- const rent = svm.getBalance(vault)!;
197
- expect(await send(owner, [ix("deposit", { amount: new BN(3_000_000_000) }, [me, v, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
198
- expect(await send(owner, [ix("withdraw", { amount: new BN(1_000_000_000) }, [me, v])])).toBeInstanceOf(TransactionMetadata);
199
- expect(svm.getBalance(vault)).toBe(rent + 2_000_000_000n);
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);
200
131
 
201
132
  // VaultError.InsufficientFunds (third member, 6002): the rent reserve stays.
202
- expect(errorCode(await send(owner, [ix("withdraw", { amount: new BN(2_000_000_001) }, [me, v])]))).toBe(6002);
133
+ expect(t.errorCode(await t.send(owner, t.ix("withdraw", { amount: 2_000_000_001n }, { owner, vault })))).toBe(6002);
203
134
 
204
- expect(await send(owner, [ix("shut", {}, [me, v])])).toBeInstanceOf(TransactionMetadata);
205
- expect(svm.getAccount(vault).exists).toBe(false);
135
+ t.ok(await t.send(owner, t.ix("shut", {}, { owner, vault })));
136
+ expect(await t.account("Vault", vault)).toBeNull();
206
137
  });
207
138
 
208
139
  it("refuses a stranger", async () => {
209
- const owner = await generateKeyPairSigner();
210
- const thief = await generateKeyPairSigner();
211
- svm.airdrop(owner.address, lamports(2_000_000_000n));
212
- svm.airdrop(thief.address, lamports(1_000_000_000n));
213
- const [vault] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["vault", getAddressEncoder().encode(owner.address)] });
214
- const v = { address: vault, writable: true };
215
- expect(await send(owner, [ix("open", {}, [{ address: owner.address, signer: owner, writable: true }, v, { address: SYSTEM }])])).toBeInstanceOf(
216
- TransactionMetadata,
217
- );
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 })));
218
144
  // VaultError.NotOwner: 6000.
219
- expect(errorCode(await send(thief, [ix("shut", {}, [{ address: thief.address, signer: thief, writable: true }, v])]))).toBe(6000);
145
+ expect(t.errorCode(await t.send(thief, t.ix("shut", {}, { owner: thief, vault })))).toBe(6000);
220
146
  });
221
147
  `,
222
148
  }),
@@ -330,18 +256,17 @@ export function refund(
330
256
  }
331
257
  `,
332
258
  test: (name) => testFile(name, {
333
- imports: `import { AccountState, getMintEncoder, getMintSize, getTokenDecoder, getTokenEncoder, getTokenSize } from "@solana-program/token";\n`,
334
- kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
259
+ imports: `import { AccountState, getMintEncoder, getMintSize, getTokenDecoder, getTokenEncoder, getTokenSize } from "@solana-program/token";\nimport { type Address, type KeyPairSigner, generateKeyPairSigner, lamports } from "@solana/kit";\n`,
335
260
  body: `
336
261
  const TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;
337
262
  /** A mint, written straight into the VM (a real client sends System CreateAccount + InitializeMint2). */
338
263
  const mint = async (authority: Address) => {
339
264
  const address = (await generateKeyPairSigner()).address;
340
265
  const data = getMintEncoder().encode({ mintAuthority: authority, supply: 0n, decimals: 6, isInitialized: true, freezeAuthority: null });
341
- svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(1_461_600n), programAddress: TOKEN, space: BigInt(getMintSize()) });
266
+ t.svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(1_461_600n), programAddress: TOKEN, space: BigInt(getMintSize()) });
342
267
  return address;
343
268
  };
344
- /** A token account of \`owner\` for \`mint\` holding \`amount\`, written straight into the VM. */
269
+ /** A token account of owner for mint holding amount, written straight into the VM. */
345
270
  const tokenAccount = async (owner: Address, mintAddress: Address, amount: bigint) => {
346
271
  const address = (await generateKeyPairSigner()).address;
347
272
  const data = getTokenEncoder().encode({
@@ -354,73 +279,43 @@ export function refund(
354
279
  delegatedAmount: 0n,
355
280
  closeAuthority: null,
356
281
  });
357
- svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(2_039_280n), programAddress: TOKEN, space: BigInt(getTokenSize()) });
282
+ t.svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(2_039_280n), programAddress: TOKEN, space: BigInt(getTokenSize()) });
358
283
  return address;
359
284
  };
360
285
  const balance = (address: Address) => {
361
- const account = svm.getAccount(address);
286
+ const account = t.svm.getAccount(address);
362
287
  return account.exists ? getTokenDecoder().decode(account.data).amount : null;
363
288
  };
364
289
 
365
290
  it("escrows mint A for mint B; the taker swaps, the maker can refund", async () => {
366
- const maker = await generateKeyPairSigner();
367
- const taker = await generateKeyPairSigner();
368
- svm.airdrop(maker.address, lamports(1_000_000_000n));
369
- svm.airdrop(taker.address, lamports(1_000_000_000n));
291
+ const maker = await t.wallet();
292
+ const taker = await t.wallet();
370
293
  const mintA = await mint(maker.address);
371
294
  const mintB = await mint(taker.address);
372
- const makerA = await tokenAccount(maker.address, mintA, 1_000n);
373
- const makerB = await tokenAccount(maker.address, mintB, 0n);
374
- const takerA = await tokenAccount(taker.address, mintA, 0n);
375
- const takerB = await tokenAccount(taker.address, mintB, 500n);
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);
376
299
 
377
- const open = async (seed: number) => {
378
- const [offer] = await getProgramDerivedAddress({
379
- programAddress: programId,
380
- seeds: ["offer", getAddressEncoder().encode(maker.address), new Uint8Array(new BN(seed).toArray("le", 8))],
381
- });
300
+ const open = async (seed: bigint) => {
301
+ const offer = await t.pda("offer", maker, seed);
382
302
  // The client creates the vault: an empty token account for mint A owned by the offer PDA.
383
303
  const vault = await tokenAccount(offer, mintA, 0n);
384
- const make = ix("make", { seed: new BN(seed), amount_a: new BN(400), want_b: new BN(300), mint_b: { toBuffer: () => Buffer.from(getAddressEncoder().encode(mintB)) } }, [
385
- { address: maker.address, signer: maker, writable: true },
386
- { address: offer, writable: true },
387
- { address: makerA, writable: true },
388
- { address: vault, writable: true },
389
- { address: SYSTEM },
390
- { address: TOKEN },
391
- ]);
392
- expect(await send(maker, [make])).toBeInstanceOf(TransactionMetadata);
304
+ t.ok(await t.send(maker, t.ix("make", { seed, amountA: 400n, wantB: 300n, mintB }, { maker, offer, makerTokenA, vault })));
393
305
  return { offer, vault };
394
306
  };
395
307
 
396
- const first = await open(1);
308
+ const first = await open(1n);
397
309
  expect(balance(first.vault)).toBe(400n);
398
- const take = ix("take", {}, [
399
- { address: taker.address, signer: taker, writable: true },
400
- { address: maker.address, writable: true },
401
- { address: first.offer, writable: true },
402
- { address: takerA, writable: true },
403
- { address: takerB, writable: true },
404
- { address: makerB, writable: true },
405
- { address: first.vault, writable: true },
406
- { address: TOKEN },
407
- ]);
408
- expect(await send(taker, [take])).toBeInstanceOf(TransactionMetadata);
409
- expect([balance(takerA), balance(makerB), balance(first.vault)]).toEqual([400n, 300n, null]);
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]);
410
312
 
411
- const second = await open(2);
412
- const refund = (signer: KeyPairSigner) =>
413
- ix("refund", {}, [
414
- { address: signer.address, signer, writable: true },
415
- { address: second.offer, writable: true },
416
- { address: makerA, writable: true },
417
- { address: second.vault, writable: true },
418
- { address: TOKEN },
419
- ]);
313
+ const second = await open(2n);
314
+ const refund = (who: KeyPairSigner) => t.ix("refund", {}, { maker: who, offer: second.offer, makerTokenA, vault: second.vault });
420
315
  // EscrowError.WrongMaker (third member, 6002): only the maker refunds.
421
- expect(errorCode(await send(taker, [refund(taker)]))).toBe(6002);
422
- expect(await send(maker, [refund(maker)])).toBeInstanceOf(TransactionMetadata);
423
- expect([balance(makerA), svm.getAccount(second.offer).exists]).toEqual([600n, false]);
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]);
424
319
  });
425
320
  `,
426
321
  }),
@@ -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 {};