@typesol/cli 0.14.0 → 0.15.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 CHANGED
@@ -5,6 +5,26 @@ until 1.0, a minor version may change the language, the IDL layout or error code
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.15.0 — 2026-10-27
9
+
10
+ Project templates for `typesol init`, a mainnet deployment guide, and clearer `typesol idl publish` failures. No
11
+ language, IDL or error-code changes. Not externally audited (see SECURITY.md).
12
+
13
+ ### Added
14
+ - `typesol init --template counter|vault|escrow`: start from a PDA counter (the default, as before), a SOL vault or an
15
+ SPL token escrow. Each template is the matching `examples/` program with a LiteSVM test of its main flow and of a
16
+ refused call; the escrow project also gets `@solana-program/token`.
17
+
18
+ ### Fixed
19
+ - `typesol idl publish` names the reason a write failed (the transaction's error and its last log lines) instead of
20
+ "The provided transaction plan failed to execute".
21
+
22
+ ### Docs
23
+ - docs/mainnet.md: from `typesol init` to a verified program on mainnet-beta (wallet, cost, deploy, IDL, `verify`,
24
+ freezing and closing).
25
+ - README: the official escrow on mainnet-beta in all four versions, with each transaction's compute units linked to
26
+ Solscan; the TypeSOL program's IDL is published on chain.
27
+
8
28
  ## 0.14.0 — 2026-10-27
9
29
 
10
30
  PDAs are derived without the PDA syscalls, so instructions that create, check or search for PDAs use up to a few
package/README.md CHANGED
@@ -152,6 +152,9 @@ npx typesol build # target/counter/{counter.so, coun
152
152
  npx typesol deploy --url http://127.0.0.1:8899 --keypair ~/.config/solana/id.json
153
153
  ```
154
154
 
155
+ `--template vault` or `--template escrow` starts from a SOL vault or an SPL token escrow instead of the counter, each
156
+ with a passing LiteSVM test.
157
+
155
158
  The npm package is [`@typesol/cli`](https://www.npmjs.com/package/@typesol/cli). Projects install it under the name
156
159
  `typesol`, which is what programs import (`import { … } from "typesol"`); `typesol init` sets that up. To add it to an
157
160
  existing project: `npm install -D typesol@npm:@typesol/cli`.
@@ -159,6 +162,8 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
159
162
  Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
160
163
  program's address.
161
164
 
165
+ To go to mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
166
+
162
167
  ## How it works
163
168
 
164
169
  ```
@@ -246,6 +251,27 @@ CPI account records only for the accounts a call lists. It uses fewer compute un
246
251
  instruction here, while also refusing duplicate writable accounts as Anchor does. Fewer compute
247
252
  units are not a safety claim. The differential tests also run the escrow against its Anchor twin ([docs/testing.md](docs/testing.md)).
248
253
 
254
+ ### On mainnet
255
+
256
+ The official escrow ([tokens/escrow](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow)),
257
+ deployed to Solana mainnet-beta in all four versions on 2026-10-27 and called with the same mints, amounts and accounts,
258
+ one instruction per transaction. Each figure is the compute units of that transaction, linked to Solscan; the lowest per row is in bold. The deployed
259
+ programs match these builds byte for byte (`typesol verify` for TypeSOL), and the TypeSOL program's IDL is published
260
+ on chain with `typesol idl publish`.
261
+
262
+ | Transaction | TypeSOL | Pinocchio | Native Rust | Anchor |
263
+ | --- | ---: | ---: | ---: | ---: |
264
+ | make_offer (offer 1) | [20,304](https://solscan.io/tx/32PVva77dT1rSWrMWX2sHTMPZiwztdEZYtpANaxrsuJxPcd3hnV6pDgJJaV4LkL3bX6ALDoHMNF8fjngtodYHBS1) | [**19,645**](https://solscan.io/tx/528BdMTrnLWauSXVh2vaFfy4XwKFpbyt95hQ6w3fwjtpkT85RPkRhGydunAizNDRqTTLPxcEVMrdcDvRer4H9LRE) | [30,698](https://solscan.io/tx/4MQgt75MvojaAkfxnvrE2yJWabGpE8bsZhNq2CeGJxHZg6uD6A64Kv71JmrsgiGnaEovvv8K34DYL49owLjhs4QJ) | [38,648](https://solscan.io/tx/9T1ZY9JaSMb1GMqv1i22AuZuJQN6B5XcgqeDv5tGqEm5Uox3MBTdXsECt59GwcXkFG9QKgXJyqndTEzpSXhbAVM) |
265
+ | take_offer (offer 1) | [**7,998**](https://solscan.io/tx/49EQwgnKShFdsAs37AEzYmv6tLY8DipcCjmRgqwj4fVTFEgVVWdEXw5AvuEufL5ymJYs4T4hVFdkENW9XK9een5m) | [25,206](https://solscan.io/tx/21MGkXpKRe4c7EeA1hC6JHevsk78xUdbcWmyqG47EbLeCNKSMTzTjkJQqtydnQmV8uHPmr6bJHnvQmmQTSZB7Am2) | [28,612](https://solscan.io/tx/B1hu1EL4Y5vWhBVWdyZEpzRNJb8kd6G9CgN7KtyNNGpQ84o9dfn1sdy9Z1UCHECNtp24Mw8bEH2iYz2YADdEUjx) | [32,835](https://solscan.io/tx/3ZLuQQz7Bx5YdgmFXpVQWHn5kyc8v39jr2BnfofH6sqenzfJpAdVm4h38uYcHwYygnfso92kJe7HRHeASsABoKPQ) |
266
+ | make_offer (offer 2) | [21,804](https://solscan.io/tx/xuu9KXbZvzGDpTEEcDE9LYvBqmPen81y6ty92e6crRxT8m4Mc4XyxhjWHbjVNsr9RE3QY4M1j1Sw9cH7PRMKKuc) | [**19,645**](https://solscan.io/tx/LLzS13AJrqACGoT1VHZN278ij39pNkttb1Z4Wh2AYXknWxwjaiaiMeD7WZomjP6TBv18RHfQHUfogQzApvXr2mC) | [35,198](https://solscan.io/tx/4xiKfzyccVatef1T52tZ3VAJwcx5R7iWgHWezBzZ3p2WpvoLzMp1PSUgr7x3sspE53mDE5JaghaNPKuUohVgPuwc) | [47,648](https://solscan.io/tx/4mxW8D5zpY8t4fL8JRbvhp6ejU6CUvB3iWmFjACipzexW6tZfFeatfnEnDTX3Y97BxsvAftoX9eQauiVu84xrLxh) |
267
+ | refund_offer (offer 2) | [**5,961**](https://solscan.io/tx/4kY7s3y95Lrd2WGCWGPWW3zFBVtEKH6EskdscuYmRCRRgPt2rJTZ1CmZMGUUkHs7aai9AygEiTSUG23EMUrFr7rg) | [13,387](https://solscan.io/tx/QUXWwdbKRcT6WhZvSe4U8nxr4GAMaPNV6pZ1fwrC28kRxdJm7HKXVr2DFRuKD9bEzoS6hFdiA2rLr1mQWyrN3oz) | [17,731](https://solscan.io/tx/5zj7E4c61j1JzUsUPC1fNs9ECFLVmCug9rsdYsVLn7YD1RSxdbAJUQDam1BX3epTzW6ed2uFGwTiNhwaYo7vqdwm) | [23,576](https://solscan.io/tx/5Kjwnq5tN8mzj5s67VGxSry4kRzZk6HLdgXW65FyTvpUeC1D7aQoDGTwzrA7kqyYEvV9y9znnZmLaqU98zFrJJo4) |
268
+ | program | [65ybkcg6…](https://solscan.io/account/65ybkcg6mQa9t5EuHtirRdvf2qAu12qRiK6wYBbGk16P) | [FrQwq3iU…](https://solscan.io/account/FrQwq3iUa8ZkWEne1y27webXTAyLgH4Hj6t986t6VS2G) | [5pnmi1Md…](https://solscan.io/account/5pnmi1MdxzXKmgX7ooRSE212rzZK8rb7nNpRTSMqP8V1) | [EG7M3sLL…](https://solscan.io/account/EG7M3sLLwB7MhmtzwYsaxbhZaFDbshL9hmVK79bQVyVq) |
269
+
270
+ Most of make_offer is the Associated Token Account program creating the vault (about 13,500 CU in every version); it
271
+ searches for the vault address's bump itself at 1,500 CU a try, which is why TypeSOL's offer 2 cost 1,500 more than
272
+ offer 1 (its own part was 6,719 CU both times). The Pinocchio version takes the offer PDA's bump from the client instead
273
+ of searching for it (its own part: 6,051 CU). Mainnet figures differ from the LiteSVM means above.
274
+
249
275
  ## How it is tested
250
276
 
251
277
  - **Execution tests.** Every feature is compiled and run in LiteSVM.
@@ -276,7 +302,7 @@ Details are in [docs/testing.md](docs/testing.md).
276
302
 
277
303
  ## Status
278
304
 
279
- **TypeSOL is pre-1.0 (0.14).** Releases follow the organization's
305
+ **TypeSOL is pre-1.0 (0.15).** Releases follow the organization's
280
306
  [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
281
307
  change.
282
308
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { generateClient, writeClient } from "./clientgen.js";
11
11
  import { buildIdl } from "./compiler/idl.js";
12
12
  import { decodeAddress, loadOrCreateKeypair, readKeypair } from "./keys.js";
13
13
  import { formatProfile, profileLogs } from "./profile.js";
14
+ import { TEMPLATE_NAMES } from "./templates.js";
14
15
  import { buildWorkspace, CONFIG, deployArgs, findWorkspace, programAddress, programForEntry, programPaths, scaffold } from "./workspace.js";
15
16
  const ASSETS = fileURLToPath(new URL("../assets/", import.meta.url));
16
17
  /** A usage problem: printed with the command's help, exit code 2. */
@@ -38,14 +39,23 @@ function banner() {
38
39
  const SBPF = `--sbpf v0|v1|v2 SBPF version (default v0, or "sbpf" in ${CONFIG}); v1/v2 need clusters that enabled them`;
39
40
  const COMMANDS = {
40
41
  init: {
41
- usage: "typesol init [<dir>] [--name <program>]",
42
- help: "Create a project: typesol.json, programs/<name>.ts, tests/<name>.test.ts (LiteSVM), package.json, tsconfig.json, .gitignore.\n Refuses to overwrite files. <name> must be snake_case (default: derived from <dir>).",
43
- options: { name: { type: "string" } },
42
+ usage: "typesol init [<dir>] [--name <program>] [--template counter|vault|escrow]",
43
+ help: [
44
+ "Create a project: typesol.json, programs/<name>.ts, tests/<name>.test.ts (LiteSVM), package.json, tsconfig.json, .gitignore.",
45
+ "Refuses to overwrite files. <name> must be snake_case (default: derived from <dir>).",
46
+ "--template counter a PDA counter per user (default)",
47
+ "--template vault a SOL vault at a PDA: deposit, withdraw above rent, close",
48
+ "--template escrow an SPL token escrow: make, take, refund, with the offer PDA signing for its vault",
49
+ ].join("\n "),
50
+ options: { name: { type: "string" }, template: { type: "string" } },
44
51
  run(v, args) {
45
52
  if (args.length > 1)
46
53
  throw new UsageError("init takes at most one directory");
47
54
  if (v.name !== undefined && !/^[a-z][a-z0-9_]*$/.test(v.name))
48
55
  throw new UsageError(`--name "${v.name}" must be snake_case (a-z, 0-9, _; starting with a letter)`);
56
+ const template = v.template ?? "counter";
57
+ if (!TEMPLATE_NAMES.includes(template))
58
+ throw new UsageError(`--template "${template}" is not one of ${TEMPLATE_NAMES.join(", ")}`);
49
59
  const root = path.resolve(args[0] ?? ".");
50
60
  const derived = path
51
61
  .basename(root)
@@ -53,7 +63,7 @@ const COMMANDS = {
53
63
  .replace(/^_+|_+$/g, "")
54
64
  .toLowerCase();
55
65
  const name = v.name ?? (/^[a-z]/.test(derived) ? derived : `p_${derived || "program"}`);
56
- for (const f of scaffold(root, name))
66
+ for (const f of scaffold(root, name, template))
57
67
  log(` created ${path.join(root, f)}`);
58
68
  log(`\n next: cd ${path.relative(process.cwd(), root) || "."} && npm install && npm test`);
59
69
  return 0;
@@ -62,17 +62,51 @@ export async function publishIdl(chain, opts) {
62
62
  seeds: [getAddressEncoder().encode(opts.program)],
63
63
  }))[0]
64
64
  : undefined;
65
- await writeMetadata(client, {
66
- payer: opts.payer,
67
- authority,
68
- program: opts.program,
69
- ...(programData ? { programData } : {}),
70
- seed: "idl",
71
- format: Format.Json,
72
- ...packed,
73
- });
65
+ try {
66
+ await writeMetadata(client, {
67
+ payer: opts.payer,
68
+ authority,
69
+ program: opts.program,
70
+ ...(programData ? { programData } : {}),
71
+ seed: "idl",
72
+ format: Format.Json,
73
+ ...packed,
74
+ });
75
+ }
76
+ catch (e) {
77
+ throw new Error(planFailure(e));
78
+ }
74
79
  return metadataAddress(opts.program, canonical ? null : authority.address);
75
80
  }
81
+ /**
82
+ * The transaction planner reports a failed write as "The provided transaction plan failed to execute" and keeps the
83
+ * reason inside: name the first failed transaction's own error instead.
84
+ */
85
+ function planFailure(e) {
86
+ const seen = new Set();
87
+ const messages = [];
88
+ let logs = [];
89
+ const walk = (v, depth) => {
90
+ if (!v || typeof v !== "object" || seen.has(v) || depth > 8)
91
+ return;
92
+ seen.add(v);
93
+ const o = v;
94
+ if (Array.isArray(o.logs) && !logs.length)
95
+ logs = o.logs.map(String);
96
+ if (v instanceof Error && v.message && !/transaction plan failed to execute/.test(v.message))
97
+ messages.push(v.message);
98
+ for (const k of ["transactionPlanResult", "plans", "error", "cause", "context"]) {
99
+ const c = o[k];
100
+ if (Array.isArray(c))
101
+ c.forEach((x) => walk(x, depth + 1));
102
+ else
103
+ walk(c, depth + 1);
104
+ }
105
+ };
106
+ walk(e, 0);
107
+ const tail = logs.length ? `\n ${logs.slice(-6).join("\n ")}` : "";
108
+ return messages.length ? `IDL write failed: ${messages[0]}${tail}` : e instanceof Error ? e.message : String(e);
109
+ }
76
110
  export async function metadataAddress(program, authority) {
77
111
  return (await findMetadataPda({ program, authority, seed: "idl" }))[0];
78
112
  }
@@ -0,0 +1,9 @@
1
+ export declare const TEMPLATE_NAMES: readonly ["counter", "vault", "escrow"];
2
+ export type TemplateName = (typeof TEMPLATE_NAMES)[number];
3
+ export interface Template {
4
+ program: string;
5
+ test: (name: string) => string;
6
+ /** devDependencies beyond the ones every project gets. */
7
+ devDependencies?: Record<string, string>;
8
+ }
9
+ export declare const TEMPLATES: Record<TemplateName, Template>;
@@ -0,0 +1,429 @@
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
+ /** The part every test shares: build the project's program into a LiteSVM, encode instructions with its IDL, send. */
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";
28
+ import { beforeAll, describe, expect, it } from "vitest";
29
+
30
+ const SYSTEM = "11111111111111111111111111111111" as Address;
31
+ const ws = findWorkspace(path.dirname(fileURLToPath(import.meta.url)))!;
32
+
33
+ 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)),
52
+ });
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
+ ${o.body}});
71
+ `;
72
+ export const TEMPLATES = {
73
+ counter: {
74
+ program: `import { type Account, type Init, type Mut, type Pubkey, type Signer, type SystemProgram, type u8, type u64, require } from "typesol";
75
+
76
+ export interface Counter {
77
+ authority: Pubkey;
78
+ count: u64;
79
+ bump: u8;
80
+ }
81
+
82
+ export enum CounterError {
83
+ Unauthorized = "Only the counter's authority can change it",
84
+ }
85
+
86
+ /** Create the caller's counter at PDA ["counter", authority]. */
87
+ export function initialize(authority: Mut<Signer>, counter: Init<Counter>, systemProgram: SystemProgram): void {
88
+ counter.bump = counter.create(authority, ["counter", authority.key]);
89
+ counter.authority = authority.key;
90
+ counter.count = 0n;
91
+ }
92
+
93
+ export function increment(authority: Signer, counter: Mut<Counter>, by: u64): void {
94
+ require(counter.authority === authority.key, CounterError.Unauthorized);
95
+ counter.count += by;
96
+ }
97
+
98
+ /** Read-only check that callers can simulate. */
99
+ export function assertAtLeast(counter: Account<Counter>, min: u64): void {
100
+ require(counter.count >= min, "counter is below the expected minimum");
101
+ }
102
+ `,
103
+ test: (name) => testFile(name, {
104
+ imports: "",
105
+ kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
106
+ body: `
107
+ 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");
119
+
120
+ // 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);
123
+ });
124
+ `,
125
+ }),
126
+ },
127
+ vault: {
128
+ program: `import {
129
+ type Init,
130
+ type Mut,
131
+ type Pubkey,
132
+ type Signer,
133
+ type SystemProgram,
134
+ type u8,
135
+ type u64,
136
+ close,
137
+ minimumBalance,
138
+ require,
139
+ } from "typesol";
140
+
141
+ /** A personal SOL vault at PDA ["vault", owner]. Lamports above rent are the balance. */
142
+ export interface Vault {
143
+ owner: Pubkey;
144
+ deposited: u64;
145
+ withdrawn: u64;
146
+ bump: u8;
147
+ }
148
+
149
+ export enum VaultError {
150
+ NotOwner = "Only the vault owner can do this",
151
+ ZeroAmount = "Amount must be greater than zero",
152
+ InsufficientFunds = "Withdrawal would dip below the rent-exempt minimum",
153
+ }
154
+
155
+ export function open(owner: Mut<Signer>, vault: Init<Vault>, systemProgram: SystemProgram): void {
156
+ vault.bump = vault.create(owner, ["vault", owner.key]);
157
+ vault.owner = owner.key;
158
+ }
159
+
160
+ export function deposit(owner: Mut<Signer>, vault: Mut<Vault>, systemProgram: SystemProgram, amount: u64): void {
161
+ require(amount > 0n, VaultError.ZeroAmount);
162
+ require(vault.owner === owner.key, VaultError.NotOwner);
163
+ systemProgram.transfer(owner, vault, amount);
164
+ vault.deposited += amount;
165
+ }
166
+
167
+ export function withdraw(owner: Mut<Signer>, vault: Mut<Vault>, amount: u64): void {
168
+ require(amount > 0n, VaultError.ZeroAmount);
169
+ require(vault.owner === owner.key, VaultError.NotOwner);
170
+ const floor = minimumBalance(vault);
171
+ require(vault.lamports - amount >= floor, VaultError.InsufficientFunds);
172
+ // The vault is owned by this program, so it can move lamports directly.
173
+ vault.lamports -= amount;
174
+ owner.lamports += amount;
175
+ vault.withdrawn += amount;
176
+ }
177
+
178
+ /** Close the vault and return everything, rent included, to the owner. */
179
+ export function shut(owner: Mut<Signer>, vault: Mut<Vault>): void {
180
+ require(vault.owner === owner.key, VaultError.NotOwner);
181
+ close(vault, owner);
182
+ }
183
+ `,
184
+ test: (name) => testFile(name, {
185
+ imports: "",
186
+ kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
187
+ body: `
188
+ 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);
200
+
201
+ // 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);
203
+
204
+ expect(await send(owner, [ix("shut", {}, [me, v])])).toBeInstanceOf(TransactionMetadata);
205
+ expect(svm.getAccount(vault).exists).toBe(false);
206
+ });
207
+
208
+ 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
+ );
218
+ // VaultError.NotOwner: 6000.
219
+ expect(errorCode(await send(thief, [ix("shut", {}, [{ address: thief.address, signer: thief, writable: true }, v])]))).toBe(6000);
220
+ });
221
+ `,
222
+ }),
223
+ },
224
+ escrow: {
225
+ program: `import {
226
+ type AccountInfo,
227
+ type Init,
228
+ type Mut,
229
+ type Pubkey,
230
+ type Signer,
231
+ type SystemProgram,
232
+ type TokenAccount,
233
+ type TokenProgram,
234
+ type u8,
235
+ type u64,
236
+ close,
237
+ require,
238
+ } from "typesol";
239
+
240
+ /**
241
+ * A maker escrows \`amountA\` of mint A and asks for \`wantB\` of mint B.
242
+ * Anyone can take the offer atomically; the maker can refund while it is open.
243
+ *
244
+ * The client creates \`vault\` beforehand as a token account for mint A whose
245
+ * owner is the offer PDA ["offer", maker, seed].
246
+ */
247
+ export interface Offer {
248
+ maker: Pubkey;
249
+ mintA: Pubkey;
250
+ mintB: Pubkey;
251
+ vault: Pubkey;
252
+ amountA: u64;
253
+ wantB: u64;
254
+ seed: u64;
255
+ bump: u8;
256
+ }
257
+
258
+ export enum EscrowError {
259
+ ZeroAmount = "Amounts must be greater than zero",
260
+ BadVault = "Vault must be an empty token account owned by the offer",
261
+ WrongMaker = "Maker does not match the offer",
262
+ WrongVault = "Vault does not match the offer",
263
+ WrongMint = "Token account has the wrong mint",
264
+ WrongRecipient = "Maker's token account is not owned by the maker",
265
+ }
266
+
267
+ export function make(
268
+ maker: Mut<Signer>,
269
+ offer: Init<Offer>,
270
+ makerTokenA: Mut<TokenAccount>,
271
+ vault: Mut<TokenAccount>,
272
+ systemProgram: SystemProgram,
273
+ tokenProgram: TokenProgram,
274
+ seed: u64,
275
+ amountA: u64,
276
+ wantB: u64,
277
+ mintB: Pubkey,
278
+ ): void {
279
+ offer.bump = offer.create(maker, ["offer", maker.key, seed]);
280
+ require(amountA > 0n && wantB > 0n, EscrowError.ZeroAmount);
281
+ require(vault.owner === offer.key && vault.mint === makerTokenA.mint && vault.amount === 0n, EscrowError.BadVault);
282
+
283
+ tokenProgram.transfer(makerTokenA, vault, maker, amountA);
284
+
285
+ offer.maker = maker.key;
286
+ offer.mintA = makerTokenA.mint;
287
+ offer.mintB = mintB;
288
+ offer.vault = vault.key;
289
+ offer.amountA = amountA;
290
+ offer.wantB = wantB;
291
+ offer.seed = seed;
292
+ }
293
+
294
+ export function take(
295
+ taker: Mut<Signer>,
296
+ maker: Mut<AccountInfo>,
297
+ offer: Mut<Offer>,
298
+ takerTokenA: Mut<TokenAccount>,
299
+ takerTokenB: Mut<TokenAccount>,
300
+ makerTokenB: Mut<TokenAccount>,
301
+ vault: Mut<TokenAccount>,
302
+ tokenProgram: TokenProgram,
303
+ ): void {
304
+ require(offer.maker === maker.key, EscrowError.WrongMaker);
305
+ require(offer.vault === vault.key, EscrowError.WrongVault);
306
+ require(takerTokenA.mint === offer.mintA && takerTokenB.mint === offer.mintB, EscrowError.WrongMint);
307
+ require(makerTokenB.mint === offer.mintB, EscrowError.WrongMint);
308
+ require(makerTokenB.owner === offer.maker, EscrowError.WrongRecipient);
309
+
310
+ tokenProgram.transfer(takerTokenB, makerTokenB, taker, offer.wantB);
311
+ tokenProgram.transfer(vault, takerTokenA, offer, offer.amountA, ["offer", offer.maker, offer.seed, offer.bump]);
312
+ tokenProgram.closeAccount(vault, maker, offer, ["offer", offer.maker, offer.seed, offer.bump]);
313
+ close(offer, maker);
314
+ }
315
+
316
+ export function refund(
317
+ maker: Mut<Signer>,
318
+ offer: Mut<Offer>,
319
+ makerTokenA: Mut<TokenAccount>,
320
+ vault: Mut<TokenAccount>,
321
+ tokenProgram: TokenProgram,
322
+ ): void {
323
+ require(offer.maker === maker.key, EscrowError.WrongMaker);
324
+ require(offer.vault === vault.key, EscrowError.WrongVault);
325
+ require(makerTokenA.mint === offer.mintA, EscrowError.WrongMint);
326
+
327
+ tokenProgram.transfer(vault, makerTokenA, offer, offer.amountA, ["offer", offer.maker, offer.seed, offer.bump]);
328
+ tokenProgram.closeAccount(vault, maker, offer, ["offer", offer.maker, offer.seed, offer.bump]);
329
+ close(offer, maker);
330
+ }
331
+ `,
332
+ test: (name) => testFile(name, {
333
+ imports: `import { AccountState, getMintEncoder, getMintSize, getTokenDecoder, getTokenEncoder, getTokenSize } from "@solana-program/token";\n`,
334
+ kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
335
+ body: `
336
+ const TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;
337
+ /** A mint, written straight into the VM (a real client sends System CreateAccount + InitializeMint2). */
338
+ const mint = async (authority: Address) => {
339
+ const address = (await generateKeyPairSigner()).address;
340
+ 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()) });
342
+ return address;
343
+ };
344
+ /** A token account of \`owner\` for \`mint\` holding \`amount\`, written straight into the VM. */
345
+ const tokenAccount = async (owner: Address, mintAddress: Address, amount: bigint) => {
346
+ const address = (await generateKeyPairSigner()).address;
347
+ const data = getTokenEncoder().encode({
348
+ mint: mintAddress,
349
+ owner,
350
+ amount,
351
+ delegate: null,
352
+ state: AccountState.Initialized,
353
+ isNative: null,
354
+ delegatedAmount: 0n,
355
+ closeAuthority: null,
356
+ });
357
+ svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(2_039_280n), programAddress: TOKEN, space: BigInt(getTokenSize()) });
358
+ return address;
359
+ };
360
+ const balance = (address: Address) => {
361
+ const account = svm.getAccount(address);
362
+ return account.exists ? getTokenDecoder().decode(account.data).amount : null;
363
+ };
364
+
365
+ 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));
370
+ const mintA = await mint(maker.address);
371
+ 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);
376
+
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
+ });
382
+ // The client creates the vault: an empty token account for mint A owned by the offer PDA.
383
+ 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);
393
+ return { offer, vault };
394
+ };
395
+
396
+ const first = await open(1);
397
+ 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]);
410
+
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
+ ]);
420
+ // 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]);
424
+ });
425
+ `,
426
+ }),
427
+ devDependencies: { "@solana-program/token": "~0.18.0" },
428
+ },
429
+ };
@@ -1,4 +1,5 @@
1
1
  import { type BuildResult, type Sbpf } from "./compiler/index.js";
2
+ import { type TemplateName } from "./templates.js";
2
3
  export declare const CONFIG = "typesol.json";
3
4
  /** A program entry in typesol.json: its entry file and (recorded on first build) its address. */
4
5
  export interface ProgramEntry {
@@ -47,5 +48,5 @@ export declare function deployArgs(soPath: string, keypair: string, opts: {
47
48
  payer?: string;
48
49
  config?: string;
49
50
  }): string[];
50
- /** Create a project: typesol.json, one program, a LiteSVM test, configs. Refuses to overwrite files. */
51
- export declare function scaffold(dir: string, name: string): string[];
51
+ /** Create a project: typesol.json, one program from a template, its LiteSVM test, configs. Refuses to overwrite files. */
52
+ export declare function scaffold(dir: string, name: string, template?: TemplateName): string[];
package/dist/workspace.js CHANGED
@@ -16,6 +16,7 @@ import path from "node:path";
16
16
  import { writeClient } from "./clientgen.js";
17
17
  import { build, COMPILER_VERSION, SBPF_VERSIONS } from "./compiler/index.js";
18
18
  import { loadOrCreateKeypair } from "./keys.js";
19
+ import { TEMPLATES } from "./templates.js";
19
20
  export const CONFIG = "typesol.json";
20
21
  export function findWorkspace(dir) {
21
22
  for (let d = path.resolve(dir);; d = path.dirname(d)) {
@@ -104,119 +105,19 @@ export function deployArgs(soPath, keypair, opts) {
104
105
  args.push("--keypair", opts.payer);
105
106
  return args;
106
107
  }
107
- const PROGRAM = `import { type Account, type Init, type Mut, type Pubkey, type Signer, type SystemProgram, type u8, type u64, require } from "typesol";
108
-
109
- export interface Counter {
110
- authority: Pubkey;
111
- count: u64;
112
- bump: u8;
113
- }
114
-
115
- export enum CounterError {
116
- Unauthorized = "Only the counter's authority can change it",
117
- }
118
-
119
- /** Create the caller's counter at PDA ["counter", authority]. */
120
- export function initialize(authority: Mut<Signer>, counter: Init<Counter>, systemProgram: SystemProgram): void {
121
- counter.bump = counter.create(authority, ["counter", authority.key]);
122
- counter.authority = authority.key;
123
- counter.count = 0n;
124
- }
125
-
126
- export function increment(authority: Signer, counter: Mut<Counter>, by: u64): void {
127
- require(counter.authority === authority.key, CounterError.Unauthorized);
128
- counter.count += by;
129
- }
130
-
131
- export function assertAtLeast(counter: Account<Counter>, min: u64): void {
132
- require(counter.count >= min, "counter is below the expected minimum");
133
- }
134
- `;
135
- const TEST = (name) => `// Runs the program in LiteSVM (an in-process Solana runtime): \`npm test\`.
136
- import { BN, BorshCoder, type Idl } from "@anchor-lang/core";
137
- import {
138
- AccountRole,
139
- appendTransactionMessageInstructions,
140
- createTransactionMessage,
141
- generateKeyPairSigner,
142
- getAddressEncoder,
143
- getProgramDerivedAddress,
144
- lamports,
145
- pipe,
146
- setTransactionMessageFeePayerSigner,
147
- signTransactionMessageWithSigners,
148
- type Address,
149
- type Instruction,
150
- type KeyPairSigner,
151
- } from "@solana/kit";
152
- import { FailedTransactionMetadata, LiteSVM } from "litesvm";
153
- import path from "node:path";
154
- import { fileURLToPath } from "node:url";
155
- import { buildWorkspace, findWorkspace } from "typesol/workspace";
156
- import { beforeAll, describe, expect, it } from "vitest";
157
-
158
- const SYSTEM = "11111111111111111111111111111111" as Address;
159
- const ws = findWorkspace(path.dirname(fileURLToPath(import.meta.url)))!;
160
-
161
- describe("${name}", () => {
162
- const svm = new LiteSVM();
163
- let programId: Address;
164
- let coder: BorshCoder;
165
- beforeAll(() => {
166
- const [built] = buildWorkspace(ws, ["${name}"]);
167
- programId = built.address as Address;
168
- svm.addProgramFromFile(programId, built.result.soPath);
169
- coder = new BorshCoder(built.result.idl as unknown as Idl);
170
- });
171
-
172
- const ix = (name: string, args: object, accounts: { address: Address; signer?: KeyPairSigner; writable?: boolean }[]): Instruction => ({
173
- programAddress: programId,
174
- accounts: accounts.map((a) => ({
175
- address: a.address,
176
- role: a.signer ? (a.writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER) : a.writable ? AccountRole.WRITABLE : AccountRole.READONLY,
177
- ...(a.signer ? { signer: a.signer } : {}),
178
- })) as Instruction["accounts"],
179
- data: new Uint8Array(coder.instruction.encode(name, args)),
180
- });
181
- const send = async (payer: KeyPairSigner, ixs: Instruction[]) => {
182
- const tx = await pipe(
183
- createTransactionMessage({ version: 0 }),
184
- (t) => setTransactionMessageFeePayerSigner(payer, t),
185
- (t) => svm.setTransactionMessageLifetimeUsingLatestBlockhash(t),
186
- (t) => appendTransactionMessageInstructions(ixs, t),
187
- (t) => signTransactionMessageWithSigners(t),
188
- );
189
- const res = svm.sendTransaction(tx);
190
- svm.expireBlockhash();
191
- return res;
192
- };
193
-
194
- it("initializes and increments the caller's counter", async () => {
195
- const alice = await generateKeyPairSigner();
196
- svm.airdrop(alice.address, lamports(1_000_000_000n));
197
- const [counter] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["counter", getAddressEncoder().encode(alice.address)] });
198
- const me = { address: alice.address, signer: alice, writable: true };
199
- expect(await send(alice, [ix("initialize", {}, [me, { address: counter, writable: true }, { address: SYSTEM }])])).not.toBeInstanceOf(FailedTransactionMetadata);
200
- expect(await send(alice, [ix("increment", { by: new BN(5) }, [{ address: alice.address, signer: alice }, { address: counter, writable: true }])])).not.toBeInstanceOf(FailedTransactionMetadata);
201
- const account = svm.getAccount(counter);
202
- if (!account.exists) throw new Error("counter was not created");
203
- const state = coder.accounts.decode("Counter", Buffer.from(account.data));
204
- expect(state.count.toString()).toBe("5");
205
- });
206
- });
207
- `;
208
- /** Create a project: typesol.json, one program, a LiteSVM test, configs. Refuses to overwrite files. */
209
- export function scaffold(dir, name) {
108
+ /** Create a project: typesol.json, one program from a template, its LiteSVM test, configs. Refuses to overwrite files. */
109
+ export function scaffold(dir, name, template = "counter") {
110
+ const t = TEMPLATES[template];
210
111
  if (!/^[a-z][a-z0-9_]*$/.test(name))
211
112
  throw new Error(`program name "${name}" must be snake_case`);
212
113
  const files = {
213
114
  [CONFIG]: JSON.stringify({ programs: { [name]: { path: `programs/${name}.ts` } }, out: "target", sbpf: "v0" }, null, 2) + "\n",
214
- [`programs/${name}.ts`]: PROGRAM,
115
+ [`programs/${name}.ts`]: t.program,
215
116
  "programs/tsconfig.json": JSON.stringify({
216
117
  compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", strict: true, noEmit: true, types: [], lib: ["ES2022"] },
217
118
  include: ["**/*.ts"],
218
119
  }, null, 2) + "\n",
219
- [`tests/${name}.test.ts`]: TEST(name),
120
+ [`tests/${name}.test.ts`]: t.test(name),
220
121
  "package.json": JSON.stringify({
221
122
  name,
222
123
  private: true,
@@ -232,6 +133,7 @@ export function scaffold(dir, name) {
232
133
  "@solana/kit": "^8.5.0",
233
134
  litesvm: "^1.6.1",
234
135
  vitest: "^5.0.5",
136
+ ...t.devDependencies,
235
137
  },
236
138
  }, null, 2) + "\n",
237
139
  // target/deploy/*-keypair.json are the programs' addresses: never commit them, but keep a backup.
package/docs/cli.md CHANGED
@@ -6,7 +6,7 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
6
6
 
7
7
  | Command | What it does |
8
8
  | --- | --- |
9
- | `typesol init [<dir>] [--name <program>]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`. Refuses to overwrite files. |
9
+ | `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`. The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. |
10
10
  | `typesol build [<program>…] [--sbpf v0\|v1\|v2] [--profile] [--quiet]` | In a project: build every program (or those named) into `<out>/<name>/`: `<name>.so`, `<name>.json` (Anchor IDL), `<name>.client.ts` (TypeScript client, see [client.md](client.md)), `<name>.c`, `<name>.build.json`. The first build creates `<out>/deploy/<name>-keypair.json` and records the address in `typesol.json`; later builds refuse a missing or different keypair. |
11
11
  | `typesol build <file.ts> [--out <dir>] [--program-id <keypair.json>] [--sbpf …] [--profile]` | Outside a project (or a file that is not one of its programs): build that file. A project program's `.ts` builds as that program. |
12
12
  | `typesol check <file.ts>` | Type-check and validate without building. |
@@ -0,0 +1,99 @@
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
+ solana program dump <program id> deployed.so
81
+ npx typesol verify my_app deployed.so
82
+ ```
83
+
84
+ `verify` rebuilds the program from `programs/my_app.ts` and compares it with the deployed bytes. `match` means anyone
85
+ with your source and the same TypeSOL version gets exactly the program on chain.
86
+
87
+ ## 7. Call it
88
+
89
+ `target/my_app/my_app.client.ts` builds the program's instructions on `@solana/kit`; see [client.md](client.md).
90
+
91
+ ## Later
92
+
93
+ - **Freeze it.** `solana program set-upgrade-authority <program id> --final` makes the program immutable. It cannot
94
+ be undone.
95
+ - **Close it.** `solana program close <program id> --bypass-warning` returns the rent to the authority. The program
96
+ id can never be used again.
97
+
98
+ A worked example: the official escrow, deployed to mainnet in TypeSOL, Pinocchio, native Rust and Anchor, with every
99
+ transaction linked, is in the README's [On mainnet](../README.md#on-mainnet) section.
@@ -23,5 +23,6 @@ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",