@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,337 @@
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 from "@anchor-lang/core";
20
+ import { AccountRole, appendTransactionMessageInstructions, createTransactionMessage, generateKeyPairSigner, getAddressDecoder, getAddressEncoder, getProgramDerivedAddress, isAddress, isOffCurveAddress, lamports, pipe, setTransactionMessageFeePayerSigner, signTransactionMessageWithSigners, } from "@solana/kit";
21
+ import { FailedTransactionMetadata, LiteSVM } from "litesvm";
22
+ import { createHash } from "node:crypto";
23
+ import path from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { buildWorkspace, findWorkspace } from "./workspace.js";
26
+ // @anchor-lang/core is CommonJS: Node 22 cannot import its classes by name from an installed ES module.
27
+ const { BN, BorshCoder } = anchor;
28
+ const BIG = new Set(["u64", "i64", "u128", "i128"]);
29
+ const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
30
+ const camel = (s) => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
31
+ const isSigner = (v) => typeof v === "object" && v !== null && "address" in v && "signTransactions" in v;
32
+ /** Build `program` of the project around `from` (a file URL such as import.meta.url, or a directory) and load it. */
33
+ export async function testProgram(program, from) {
34
+ const start = typeof from === "string" && !from.startsWith("file:") ? from : path.dirname(fileURLToPath(from));
35
+ const ws = findWorkspace(start);
36
+ if (!ws)
37
+ throw new Error(`no typesol.json at or above ${start}`);
38
+ if (!ws.programs[program])
39
+ throw new Error(`typesol.json has no program "${program}" (it has ${Object.keys(ws.programs).join(", ")})`);
40
+ const [built] = buildWorkspace(ws, [program]);
41
+ const svm = new LiteSVM();
42
+ const programId = built.address;
43
+ svm.addProgramFromFile(programId, built.result.soPath);
44
+ return programHarness(svm, programId, built.result.idl);
45
+ }
46
+ /** The same helpers for a program already loaded into `svm` at `programId` with this IDL. */
47
+ export function programHarness(svm, programId, idl) {
48
+ const coder = new BorshCoder(idl);
49
+ const pidl = idl;
50
+ const types = new Map((pidl.types ?? []).map((t) => [t.name, t]));
51
+ const enc = getAddressEncoder();
52
+ /** A plain value → what the Anchor coder expects for `type`. */
53
+ const toCoder = (v, type, where) => {
54
+ if (typeof type === "string") {
55
+ if (BIG.has(type))
56
+ return new BN(String(v));
57
+ if (/^[ui](8|16|32)$/.test(type))
58
+ return Number(v);
59
+ if (type === "pubkey") {
60
+ const a = isSigner(v) ? v.address : v;
61
+ if (typeof a !== "string" || !isAddress(a))
62
+ throw new Error(`${where}: expected an address`);
63
+ return { toBuffer: () => Buffer.from(enc.encode(a)) };
64
+ }
65
+ return v;
66
+ }
67
+ if ("option" in type)
68
+ return v === null || v === undefined ? null : toCoder(v, type.option, where);
69
+ if ("vec" in type)
70
+ return v.map((x, i) => toCoder(x, type.vec, `${where}[${i}]`));
71
+ if ("array" in type)
72
+ return v.map((x, i) => toCoder(x, type.array[0], `${where}[${i}]`));
73
+ const def = types.get(type.defined.name);
74
+ if (def?.type.kind === "struct" && def.type.fields && typeof v === "object" && v !== null) {
75
+ const o = v;
76
+ return Object.fromEntries(def.type.fields.map((f) => [f.name, toCoder(o[f.name] ?? o[camel(f.name)], f.type, `${where}.${f.name}`)]));
77
+ }
78
+ // A unit enum as its index or variant name, as well as Anchor's { Variant: {} }.
79
+ const variants = def?.type?.variants;
80
+ const unitObject = typeof v === "object" && v !== null && Object.keys(v).length === 1 && !Object.values(v)[0];
81
+ const emptyObject = typeof v === "object" && v !== null && Object.keys(v).length === 1 && Object.keys(Object.values(v)[0]).length === 0;
82
+ if (def?.type.kind === "enum" && variants && (typeof v === "number" || typeof v === "bigint" || typeof v === "string" || unitObject || emptyObject)) {
83
+ const key = typeof v === "object" ? Object.keys(v)[0] : v;
84
+ const same = (x) => x.name === key || camel(x.name) === key || x.name.toLowerCase() === String(key).toLowerCase();
85
+ const i = typeof key === "string" ? variants.findIndex(same) : Number(key);
86
+ if (!variants[i])
87
+ throw new Error(`${where}: ${String(v)} is not a variant of ${type.defined.name}`);
88
+ return { [variants[i].name]: {} };
89
+ }
90
+ // A variant with fields, { Variant: { field: value } }: its fields converted like a struct's.
91
+ if (def?.type.kind === "enum" && variants && typeof v === "object" && v !== null && Object.keys(v).length === 1) {
92
+ const [key, body] = Object.entries(v)[0];
93
+ const variant = variants.find((x) => x.name === key || camel(x.name) === key || x.name.toLowerCase() === key.toLowerCase());
94
+ if (!variant)
95
+ throw new Error(`${where}: ${key} is not a variant of ${type.defined.name}`);
96
+ const fields = (variant.fields ?? []);
97
+ if (fields.length && fields.every((f) => typeof f === "object" && f?.name && f.type) && typeof body === "object" && body !== null) {
98
+ const o = body;
99
+ return {
100
+ [variant.name]: Object.fromEntries(fields.map((f) => [f.name, toCoder(o[f.name] ?? o[camel(f.name)], f.type, `${where}.${variant.name}.${f.name}`)])),
101
+ };
102
+ }
103
+ return { [variant.name]: body };
104
+ }
105
+ return v;
106
+ };
107
+ /** What the coder decodes → plain values (bigints, address strings), recursively. */
108
+ const plain = (v) => {
109
+ if (v === null || v === undefined)
110
+ return v;
111
+ if (BN.isBN(v))
112
+ return BigInt(v.toString());
113
+ if (typeof v === "object" && typeof v.toBase58 === "function")
114
+ return v.toBase58();
115
+ if (Array.isArray(v))
116
+ return v.map(plain);
117
+ if (v instanceof Uint8Array)
118
+ return v;
119
+ if (typeof v === "object") {
120
+ const o = v;
121
+ const keys = Object.keys(o).sort().join(",");
122
+ // Account data keeps Options and FixedStrings in a fixed layout; give them back as the plain value / string.
123
+ if (keys === "is_some,value")
124
+ return o.is_some ? plain(o.value) : null;
125
+ if (keys === "bytes,len" && Array.isArray(o.bytes))
126
+ return Buffer.from(o.bytes.slice(0, Number(o.len))).toString("utf8");
127
+ return Object.fromEntries(Object.entries(o).map(([k, x]) => [k, plain(x)]));
128
+ }
129
+ return v;
130
+ };
131
+ /** find_program_address, synchronously (sha256 + the curve check), so ix() can fill PDAs in. */
132
+ const findPda = (seeds, program) => {
133
+ for (let bump = 255; bump >= 0; bump--) {
134
+ const h = createHash("sha256");
135
+ for (const s of seeds)
136
+ h.update(s);
137
+ h.update(Uint8Array.of(bump));
138
+ h.update(new Uint8Array(enc.encode(program)));
139
+ h.update("ProgramDerivedAddress");
140
+ const a = getAddressDecoder().decode(h.digest());
141
+ if (isOffCurveAddress(a))
142
+ return a;
143
+ }
144
+ throw new Error("no off-curve bump for these seeds");
145
+ };
146
+ /** An argument's bytes as a seed: little-endian integers at their width, bool as one byte, Pubkey, UTF-8 text. */
147
+ const argSeed = (v, type) => {
148
+ if (typeof type !== "string" || v === undefined || v === null)
149
+ return undefined;
150
+ const int = /^([ui])(8|16|32|64|128)$/.exec(type);
151
+ if (int) {
152
+ const bytes = Number(int[2]) / 8;
153
+ let n = BigInt.asUintN(bytes * 8, BigInt(v));
154
+ const out = new Uint8Array(bytes);
155
+ for (let i = 0; i < bytes; i++, n >>= 8n)
156
+ out[i] = Number(n & 0xffn);
157
+ return out;
158
+ }
159
+ if (type === "bool")
160
+ return Uint8Array.of(v ? 1 : 0);
161
+ if (type === "pubkey")
162
+ return new Uint8Array(enc.encode((isSigner(v) ? v.address : v)));
163
+ if (type === "string")
164
+ return new TextEncoder().encode(v);
165
+ return undefined;
166
+ };
167
+ /** A unit-enum argument as a seed: one byte, its variant index (given as the index or as Anchor's { Variant: {} }). */
168
+ const enumSeed = (v, type) => {
169
+ if (typeof type !== "object" || !("defined" in type) || v === undefined || v === null)
170
+ return undefined;
171
+ const def = types.get(type.defined.name);
172
+ const variants = def?.type.kind === "enum" ? def.type.variants : undefined;
173
+ if (!variants || variants.some((x) => x.fields?.length))
174
+ return undefined;
175
+ if (typeof v === "number" || typeof v === "bigint")
176
+ return Uint8Array.of(Number(v));
177
+ const name = typeof v === "object" ? Object.keys(v)[0] : String(v);
178
+ const i = variants.findIndex((x) => x.name === name || camel(x.name) === name || x.name.toLowerCase() === String(name).toLowerCase());
179
+ return i < 0 ? undefined : Uint8Array.of(i);
180
+ };
181
+ const seedBytes = (s) => {
182
+ if (s instanceof Uint8Array)
183
+ return s;
184
+ if (typeof s === "bigint") {
185
+ const b = new Uint8Array(8);
186
+ new DataView(b.buffer).setBigUint64(0, s, true);
187
+ return b;
188
+ }
189
+ if (isSigner(s))
190
+ return new Uint8Array(enc.encode(s.address));
191
+ if (isAddress(s))
192
+ return new Uint8Array(enc.encode(s));
193
+ return new TextEncoder().encode(s);
194
+ };
195
+ const harness = {
196
+ svm,
197
+ programId,
198
+ idl,
199
+ coder,
200
+ async wallet(sol = 10) {
201
+ const k = await generateKeyPairSigner();
202
+ svm.airdrop(k.address, lamports(BigInt(Math.round(sol * 1e9))));
203
+ return k;
204
+ },
205
+ async pda(...seeds) {
206
+ return (await harness.pdaWithBump(...seeds))[0];
207
+ },
208
+ async pdaWithBump(...seeds) {
209
+ const [a, bump] = await getProgramDerivedAddress({ programAddress: programId, seeds: seeds.map(seedBytes) });
210
+ return [a, bump];
211
+ },
212
+ ix(name, args = {}, accounts = {}, remaining = []) {
213
+ const def = pidl.instructions.find((i) => i.name === name || i.name === snake(name));
214
+ if (!def)
215
+ throw new Error(`the program has no instruction "${name}" (it has ${pidl.instructions.map((i) => i.name).join(", ")})`);
216
+ const given = new Map(Object.entries(accounts).map(([k, v]) => [snake(k), v]));
217
+ for (const k of given.keys())
218
+ if (!def.accounts.some((a) => a.name === k))
219
+ throw new Error(`${def.name} has no account "${k}"`);
220
+ const argsIn = args;
221
+ // PDAs whose seeds are constants, arguments or other accounts' keys are filled in, as the generated client does.
222
+ const addressOf = (name) => {
223
+ const v = given.get(name);
224
+ if (v === undefined || v === null)
225
+ return def.accounts.find((x) => x.name === name)?.address; // fixed in the IDL
226
+ return isSigner(v) ? v.address : v;
227
+ };
228
+ for (let progress = true; progress;) {
229
+ progress = false;
230
+ for (const a of def.accounts) {
231
+ if (given.has(a.name) || !a.pda || a.address)
232
+ continue;
233
+ const seed = (s) => {
234
+ if (s.kind === "const")
235
+ return Uint8Array.from(s.value);
236
+ if (s.kind === "account") {
237
+ const at = s.path.includes(".") ? undefined : addressOf(s.path);
238
+ return at && new Uint8Array(enc.encode(at));
239
+ }
240
+ const arg = def.args.find((x) => x.name === s.path);
241
+ const value = arg && (argsIn[arg.name] ?? argsIn[camel(arg.name)]);
242
+ return arg && (argSeed(value, arg.type) ?? enumSeed(value, arg.type));
243
+ };
244
+ const seeds = a.pda.seeds.map(seed);
245
+ const program = a.pda.program ? seed(a.pda.program) : new Uint8Array(enc.encode(programId));
246
+ if (seeds.some((x) => x === undefined) || !program || program.length !== 32)
247
+ continue;
248
+ given.set(a.name, findPda(seeds, getAddressDecoder().decode(program)));
249
+ progress = true;
250
+ }
251
+ }
252
+ const metas = def.accounts.map((a) => {
253
+ let v = given.has(a.name) ? given.get(a.name) : undefined;
254
+ if (v === undefined && a.address)
255
+ v = a.address;
256
+ if (v === undefined || v === null) {
257
+ if (!a.optional)
258
+ throw new Error(`${def.name}: account "${camel(a.name)}" is required`);
259
+ return { address: programId, role: AccountRole.READONLY }; // left out: the program's own id stands in
260
+ }
261
+ if (a.signer && !isSigner(v))
262
+ throw new Error(`${def.name}: account "${camel(a.name)}" signs; pass its keypair, not its address`);
263
+ const address = isSigner(v) ? v.address : v;
264
+ const role = a.signer
265
+ ? a.writable
266
+ ? AccountRole.WRITABLE_SIGNER
267
+ : AccountRole.READONLY_SIGNER
268
+ : a.writable
269
+ ? AccountRole.WRITABLE
270
+ : AccountRole.READONLY;
271
+ return { address, role, ...(isSigner(v) && a.signer ? { signer: v } : {}) };
272
+ });
273
+ for (const r of remaining)
274
+ metas.push({
275
+ address: r.address,
276
+ role: r.signer ? (r.writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER) : r.writable ? AccountRole.WRITABLE : AccountRole.READONLY,
277
+ ...(r.signer ? { signer: r.signer } : {}),
278
+ });
279
+ const a = args;
280
+ const encoded = Object.fromEntries(def.args.map((x) => [x.name, toCoder(a[x.name] ?? a[camel(x.name)], x.type, `${def.name}(${camel(x.name)})`)]));
281
+ return { programAddress: programId, accounts: metas, data: new Uint8Array(coder.instruction.encode(def.name, encoded)) };
282
+ },
283
+ async send(payer, ...ixs) {
284
+ const tx = await pipe(createTransactionMessage({ version: 0 }), (t) => setTransactionMessageFeePayerSigner(payer, t), (t) => svm.setTransactionMessageLifetimeUsingLatestBlockhash(t), (t) => appendTransactionMessageInstructions(ixs, t), (t) => signTransactionMessageWithSigners(t));
285
+ const res = svm.sendTransaction(tx);
286
+ svm.expireBlockhash();
287
+ return res;
288
+ },
289
+ ok(res) {
290
+ if (res instanceof FailedTransactionMetadata)
291
+ throw new Error(`transaction failed: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
292
+ return res;
293
+ },
294
+ errorCode(res) {
295
+ if (!(res instanceof FailedTransactionMetadata))
296
+ throw new Error("expected the transaction to fail, but it succeeded");
297
+ const m = /Custom\D*(\d+)/.exec(String(res.err()));
298
+ if (!m)
299
+ throw new Error(`not a custom program error: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
300
+ return Number(m[1]);
301
+ },
302
+ async account(type, address) {
303
+ const a = svm.getAccount(address);
304
+ if (!a.exists)
305
+ return null;
306
+ return plain(coder.accounts.decode(type, Buffer.from(a.data)));
307
+ },
308
+ returned(instruction, res) {
309
+ const def = pidl.instructions.find((i) => i.name === instruction || i.name === snake(instruction));
310
+ if (!def?.returns)
311
+ throw new Error(`${instruction} returns nothing`);
312
+ const data = Buffer.from(harness.ok(res).returnData().data());
313
+ const t = def.returns;
314
+ if (typeof t === "string") {
315
+ if (t === "pubkey")
316
+ return getAddressDecoder().decode(data);
317
+ if (t === "bool")
318
+ return (data[0] !== 0);
319
+ const signed = t.startsWith("i");
320
+ const bytes = Number(t.slice(1)) / 8;
321
+ let n = 0n;
322
+ for (let i = bytes - 1; i >= 0; i--)
323
+ n = (n << 8n) | BigInt(data[i]);
324
+ if (signed && n >= 1n << BigInt(bytes * 8 - 1))
325
+ n -= 1n << BigInt(bytes * 8);
326
+ return (bytes <= 4 ? Number(n) : n);
327
+ }
328
+ if ("defined" in t)
329
+ return plain(coder.types.decode(t.defined.name, data));
330
+ throw new Error(`decode ${instruction}'s return data yourself (${JSON.stringify(t)})`);
331
+ },
332
+ balance(address) {
333
+ return svm.getBalance(address) ?? 0n;
334
+ },
335
+ };
336
+ return harness;
337
+ }
@@ -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,29 @@ 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
- compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", strict: true, noEmit: true, types: [], lib: ["ES2022"] },
117
+ compilerOptions: {
118
+ target: "ES2022",
119
+ module: "ESNext",
120
+ moduleResolution: "Bundler",
121
+ strict: true,
122
+ noEmit: true,
123
+ types: [],
124
+ lib: ["ES2022"],
125
+ // TypeSOL's own errors in the editor (with the workspace's TypeScript; see docs/cli.md).
126
+ plugins: [{ name: "typesol/ts-plugin" }],
127
+ },
217
128
  include: ["**/*.ts"],
218
129
  }, null, 2) + "\n",
219
- [`tests/${name}.test.ts`]: TEST(name),
130
+ [`tests/${name}.test.ts`]: t.test(name),
220
131
  "package.json": JSON.stringify({
221
132
  name,
222
133
  private: true,
@@ -232,6 +143,7 @@ export function scaffold(dir, name) {
232
143
  "@solana/kit": "^8.5.0",
233
144
  litesvm: "^1.6.1",
234
145
  vitest: "^5.0.5",
146
+ ...t.devDependencies,
235
147
  },
236
148
  }, null, 2) + "\n",
237
149
  // target/deploy/*-keypair.json are the programs' addresses: never commit them, but keep a backup.
@@ -39,6 +39,9 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
39
39
  | `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
40
40
  | `src/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
41
41
  | `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program. |
42
+ | `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
43
+ | `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
44
+ | `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
42
45
  | `src/profile.ts` | Turns `--profile` logs into compute units per source line. |
43
46
  | `src/cli.ts` | Command parsing. Every command has strict options, `--help`, and exit codes 0 (success), 1 (failure), 2 (usage error) and 3 (verify mismatch). |
44
47
 
package/docs/cli.md CHANGED
@@ -6,16 +6,17 @@ 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
- | `typesol check <file.ts>` | Type-check and validate without building. |
12
+ | `typesol check <file.ts> [--json] [--stdin]` | Type-check and validate without building. `--json` prints `{ ok, diagnostics: [{ file, line, column, message }] }`; `--stdin` checks the text given on stdin as that file (its imports come from disk). |
13
13
  | `typesol emit-c <file.ts> [--out <file.c>] [--profile]` | Print or write the generated C. |
14
14
  | `typesol deploy [<program>…] [--url <rpc>] [--keypair <payer.json>] [--config <cli config>] [--dry-run] [--sbpf …]` | Build, then `solana program deploy --use-rpc` at the program keypair's address. Deploying again upgrades (the payer must be the upgrade authority). `--dry-run` builds and prints the commands. Needs `solana` on PATH. |
15
15
  | `typesol idl publish [<program>…] [--url <rpc>] [--keypair <authority.json>] [--config <cli config>] [--non-canonical]` | Compile (the binary is not rebuilt), then write each IDL to the Program Metadata program (seed `idl`). Canonical IDLs must be signed by the upgrade authority. Defaults come from the Solana CLI config (`~/.config/solana/cli/config.yml`). |
16
16
  | `typesol cpi <idl.json> [--out <file.ts>] [--prefix <name>] [--address <base58>]` | Generate TypeSOL helpers that call another program from its IDL. Unsupported instructions are listed on stderr. |
17
+ | `typesol port <idl.json> [--out <file.ts>]` | A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program: account structs, enums, errors and one function per instruction with its accounts and arguments, each body `require(false, …NotPorted)` until written. Instruction discriminators (an explicit `@discriminator` when the name derives another), account names and error codes are kept, so existing clients keep working where the layouts match; notes on stderr list where they do not (Borsh `String`/`Option` in account data, a `Vec` that is not the last field, custom discriminators or error codes). |
17
18
  | `typesol client <idl.json> [--out <file.ts>] [--address <base58>]` | Generate a typed TypeScript client on `@solana/kit` from an Anchor IDL (0.30+/1.x or TypeSOL); the IDL is validated first, and what the client leaves out is printed as notes. See [client.md](client.md). |
18
- | `typesol verify <program\|file.ts> <deployed.so> [--sbpf …]` | Rebuild and compare with a deployed binary (`solana program dump`); trailing zero padding is ignored; the SBPF version is read from the deployed ELF unless `--sbpf` is given. |
19
+ | `typesol verify <program\|file.ts> [<deployed.so>] [--address <program id>] [--url <rpc>] [--config <cli config>] [--sbpf …]` | Rebuild and compare with what is deployed. Without a `.so`, reads the program from the cluster (`--url`, cluster monikers such as `mainnet-beta` included, else the Solana CLI config) at `--address` or the address `typesol.json` records, under any loader (v2, upgradeable v3, v4); prints the upgrade authority (or that the program is immutable) and the last deploy slot, and compares the IDL published with `typesol idl publish` (a published IDL that differs is a mismatch). With a `.so` (e.g. `solana program dump`), compares with that file. Trailing zero padding is ignored; the SBPF version is read from the deployed ELF unless `--sbpf` is given. |
19
20
  | `typesol profile <logs.txt\|-> [--program <id>]` | Compute units per TypeScript line from the logs of a `--profile` build's transaction (exit 1 when the logs have no markers). |
20
21
  | `typesol help <command>` | Same as `typesol <command> --help`. |
21
22
  | `typesol --version` | Print the version. |
@@ -32,3 +33,16 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
32
33
 
33
34
  `"programs": { "vault": "programs/vault.ts" }` (a plain path) is accepted too. Keep `target/deploy/*-keypair.json`
34
35
  safe and out of git: it is each program's address and its first upgrade authority must not lose it.
36
+
37
+ ## Editor
38
+
39
+ `typesol init` projects load `typesol/ts-plugin` from `programs/tsconfig.json`: besides TypeScript's errors, the editor
40
+ shows what the TypeSOL compiler refuses (an unsupported construct, a constraint that cannot hold…) at its line, for the
41
+ text being edited, once TypeScript itself has nothing to report. It checks the program entries listed in
42
+ `typesol.json`. Editors load tsconfig plugins from the project's own TypeScript: in VS Code, run "TypeScript: Select
43
+ TypeScript Version" → "Use Workspace Version". To add it to an existing project:
44
+
45
+ ```json
46
+ { "compilerOptions": { "plugins": [{ "name": "typesol/ts-plugin" }] } }
47
+ ```
48
+
package/docs/language.md CHANGED
@@ -6,9 +6,9 @@ The full reference. For a tour, start with the [README](../README.md).
6
6
 
7
7
  | Concept | TypeSOL |
8
8
  | --- | --- |
9
- | Instruction | an exported function returning `void`, or one integer / boolean / `Pubkey` (sent as return data, `returns` in the IDL) |
9
+ | Instruction | an exported function returning `void`, one integer / boolean / `Pubkey`, or a fixed-size struct (sent as return data, at most 1024 bytes; `returns` in the IDL, and the generated client decodes it) |
10
10
  | Account data | an exported `interface` with fixed-size fields; interfaces nest (`home: Point`, `trail: FixedArray<Point, 4>`) |
11
- | Integers | `u8` `u16` `u32` `u64` `u128` `i8` `i16` `i32` `i64` `i128` — bigint literals (`1n`), every `+ - * / %` checked; `& \| ^` on equal types, `~` within the width, `<<` / `>>` checked (amount below the width, `<<` may not lose bits) |
11
+ | Integers | `u8` `u16` `u32` `u64` `u128` `i8` `i16` `i32` `i64` `i128` — bigint literals (`1n`), every `+ - * / % **` checked (`**` aborts on a negative exponent, as JS throws); `& \| ^` on equal types, `~` within the width, `<<` / `>>` checked (amount below the width, `<<` may not lose bits) |
12
12
  | Integer helpers | `mulDiv(a, b, c)` / `mulDivCeil` (u64 through a 128-bit product), `isqrt(x)` (up to u128), `min(a, b)`, `max(a, b)` |
13
13
  | Other scalars | `boolean`, `Pubkey` (compare with `===` or `.equals`) |
14
14
  | Errors | `export enum MyError { Name = "message" }` + `require(cond, MyError.Name)` → codes 6000+ |
@@ -18,11 +18,12 @@ The full reference. For a tour, start with the [README](../README.md).
18
18
  | Local arrays | `const xs = fixedArray<i64, 5>(0n)` — up to 256 elements on the stack |
19
19
  | Strings | `string` arguments and literals; `name: FixedString<32>` in account data (u32 length + 32 bytes); `===`, `byteLength(s)`, `log(s)`, `+=`, as PDA seeds; too-long writes abort (5105). A local bound to account text keeps its own copy |
20
20
  | Copies | `a.home = b.home`, `a.trail.set(0n, a.home)` copy whole structs / arrays of the same type: assignment copies (like Rust/Anchor), binding with `const` references |
21
+ | Destructuring | `const { owner, pos: p } = acct` (or `let` for scalars to change) from a name or a field path — one declaration per name, read like `acct.owner`: scalars are copies, nested structs are references. No rest, defaults or nesting |
21
22
  | Struct values | `const p: Point = { x, y }` (or a copy of another Point) — a local struct; fields can be updated, store it with `acct.home = p` |
22
23
  | Pubkey constants | `const ADMIN = pubkey("4Nd1…")` (checked at compile time) or inline `pubkey("…")`; usable as seeds |
23
24
  | Events | `emit<Deposited>({ owner: owner.key, amount })` — Anchor-format `Program data:` logs, listed under `events` in the IDL; event structs may hold `string` and `Option` |
24
25
  | Enums | `export enum Status { Pending, Approved }` — unit variants stored as the Borsh u8 index; `Status.Approved`, `===` |
25
- | Options | `best: Option<u64>` (or `u64 \| null`) — `null`, `=== null`, TypeScript narrowing; `===` between options or with a value follows JS (null equals null, empty never equals a value); empty use aborts (5108); `${opt}` prints the value or `null`. Borsh `option` in arguments/events, fixed `{ is_some, value }` in account data |
26
+ | Options | `best: Option<u64>` (or `u64 \| null`) — `null`, `=== null`, TypeScript narrowing; `===` between options or with a value follows JS (null equals null, empty never equals a value); empty use aborts (5108); `a ?? b` takes b only when a is empty (b runs only then; the result is an Option when b can be null), `x!` is the value or aborts with 5108; `${opt}` prints the value or `null`. Borsh `option` in arguments/events, fixed `{ is_some, value }` in account data |
26
27
  | Vecs | `entries: Vec<Entry>` as the last account field — `get`, `set`, `push(v, payer)`, `pop()`, `clear()`, `.length`; the account is resized (realloc) and rent topped up by `payer`; `Vec<u64>` arguments are read-only views; `pop()` of a Vec of structs works as a statement |
27
28
  | Struct arguments | `order: Order`, `ids: FixedArray<u8, 4>`, `orders: Vec<Order>` — fixed layouts, Borsh-identical, validated on entry (bools, enums, option tags, string lengths, enum tags; 102) and read-only |
28
29
  | Remaining accounts | `remainingAccounts()` — `.length`, `.get(i)` → `key`, `owner`, `lamports` (writes check writability), `isSigner`, `isWritable`; `loadAccount<T>` / `loadAccountMut<T>` give checked struct views; pass them to `invoke`, one at a time or spread: `...remainingAccounts()` (each with the flags it was passed with), `...readonly(rest)` / `...writable(rest)` (all with that role), `rest.slice(a, b)` for part of them; a CPI lists at most 32 accounts (5113) |
@@ -31,15 +32,15 @@ The full reference. For a tour, start with the [README](../README.md).
31
32
  | Other programs' accounts | `const pool = loadForeign<Pool>(account, AMM_PROGRAM)` — owner, `account:Pool` discriminator and size checked (3007 / 3002 / 3003); `readData<u64>(account, offset)` for raw little-endian reads (bounds-checked, 3003) |
32
33
  | Ternaries | `const fee = vip ? 0n : 30n` — only the chosen branch runs |
33
34
  | Constants | top-level `const SEED = "vault"`, `const FEE: u16 = 30n`, `const ON = true` — inlined, usable as seeds (and described in the IDL) |
34
- | Data enums | `export type Action = { kind: "Deposit"; amount: u64 } \| { kind: "Pause" }` — `a.kind === "Deposit"`, `switch (a.kind)`, TypeScript narrowing picks the variant (checked again at runtime, 5112); Borsh enums in arguments/events, fixed `{ tag, payload }` in account data |
35
+ | Data enums | `export type Action = { kind: "Deposit"; amount: u64 } \| { kind: "Pause" }` — `a.kind === "Deposit"`, `switch (a.kind)`, TypeScript narrowing picks the variant (checked again at runtime, 5112); Borsh enums in arguments/events, fixed `{ tag, payload }` in account data. A variant may hold a `string` (`{ kind: "Say"; text: string }`): such an enum works in arguments, events and locals (Borsh, as Anchor encodes it) but not in account data |
35
36
  | Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
36
37
  | Strings at runtime | `"hello, " + name`, `` `#${n}` `` — built on the 32 KiB heap (5110 when full) |
37
38
  | `switch` | on integers (up to 64 bits), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
38
39
  | Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
39
40
  | Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds)` (fully verified, feed-matched, fresh); raw `update.price / conf / exponent / publishTime`; `switchboardValue(feed: SwitchboardFeed, expectedFeed, maxStaleSlots, decimals)` (the feed must be the expected one: anyone can create feeds) (mainnet program unless `SwitchboardFeed<"devnet">`) |
40
41
  | Multiple files | `import { Side, min } from "./math"` — structs, enums, constants and helpers can live in other files; only the entry file's exported functions are instructions; names are program-wide, so imports from TypeSOL files cannot be renamed; error codes number imported files first |
41
- | Loops | `for (let i = 0n; i < n; i++)`, `while`, `break`, `continue` — bounded by the compute budget |
42
- | Helpers | non-exported functions (and every function of an imported file): take values, structs, Vec arguments, `SignerSeeds` and accounts the instruction already checked (`Signer`, `AccountInfo`/`AccountMeta`, `Mut<T>`, token accounts, programs); return a scalar or `void`; no recursion. Helpers cannot grow Vecs (only instructions know the entry size) |
42
+ | Loops | `for (let i = 0n; i < n; i++)`, `while`, `for (const x of list)` over a `Vec` or `FixedArray` (each `x` is what `list.get(i)` gives: a value, or a reference into account data for structs; the length is read again every step, as in JS), `break`, `continue` — bounded by the compute budget. Remaining accounts are iterated by index |
43
+ | Helpers | non-exported functions (and every function of an imported file): take values, structs, Vec arguments, `SignerSeeds` and accounts the instruction already checked (`Signer`, `AccountInfo`/`AccountMeta`, `Mut<T>`, token accounts, programs); return `void`, a scalar or a fixed-size struct (`function at(x: u64, y: u64): Point { return { x, y }; }`: the caller gets its own copy, to bind, store with `acct.home = at(…)` / `arr.set(i, at(…))` or read as `at(…).x`); no recursion. Helpers cannot grow Vecs (only instructions know the entry size) |
43
44
  | Evaluation order | operands and arguments are read left to right, as in JS, even when a later one changes account data (`v.length + v.pop()`) |
44
45
  | `++` `--` `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` `<<=` `>>=` | all checked |
45
46
  | Sysvars | `now()`, `currentSlot()`, `currentEpoch()` (clock), `minimumBalance(account)`, `rentExempt(space)` (rent) |
@@ -75,7 +76,7 @@ The full reference. For a tour, start with the [README](../README.md).
75
76
  | `SystemProgram`, `TokenProgram`, `Token2022Program` | exact program address |
76
77
  | `TokenInterface` | either token program |
77
78
  | `AssociatedTokenProgram` | `create` / `createIdempotent` CPIs; `associatedTokenAddress(owner, mint, tokenProgram)` |
78
- | `TokenMetadataProgram` | Metaplex `createV3(...)` (CreateMetadataAccountV3); `metadataAddress(mint)` |
79
+ | `TokenMetadataProgram` | Metaplex `createV3(...)` (CreateMetadataAccountV3), optionally with `{ creators: [{ address, verified, share }], collection: mint }` after `isMutable` (1–5 creators, shares adding up to 100; only the update authority, signing, can be `verified: true`; the collection is written unverified); `metadataAddress(mint)` |
79
80
  | `InstructionsSysvar`, `PythPriceUpdate`, `SwitchboardFeed` | exact address / owner program + discriminator |
80
81
  | `Program<"Address…">` | exact program address + executable |
81
82
  | `UncheckedProgram` | executable only — check `.key` yourself |
@@ -94,7 +95,7 @@ account written through `.lamports` or `loadAccountMut` is another account of th
94
95
  `transfer`, `transferChecked(from, mint, to, authority, amount)` (required by Token-2022 mints with fees/hooks), `mintTo`, `burn`, `closeAccount`,
95
96
  `approve(source, delegate, owner, amount)`, `revoke`, `setAuthority(target, current, AuthorityType.MintTokens, next | null)`,
96
97
  `freezeAccount` / `thawAccount(account, mint, freezeAuthority)`, `syncNative(account)`, `initializeMint2(mint, decimals, mintAuthority, freezeAuthority | null)`,
97
- `initializeAccount3(account, mint, owner)`, and on Token-2022 `initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri)` (metadata extension)
98
+ `initializeAccount3(account, mint, owner)`, and on Token-2022 `initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri)` (metadata extension) and the extension initializers to call between `createAccount` and `initializeMint2` / `initializeAccount3`: `initializeTransferFeeConfig(mint, configAuthority | null, withdrawAuthority | null, basisPoints, maximumFee)`, `initializeMintCloseAuthority(mint, authority | null)`, `initializeNonTransferableMint(mint)`, `initializePermanentDelegate(mint, delegate)`, `initializeDefaultAccountState(mint, frozen)`, `initializeMetadataPointer` / `initializeGroupPointer` / `initializeGroupMemberPointer` / `initializeTransferHook(mint, authority | null, address | null)`, `initializeInterestBearingMint(mint, rateAuthority | null, rate)`, `initializeImmutableOwner(account)`
98
99
  - `account.lamports` read/write on `Mut` accounts, `close(account, destination)` for program accounts
99
100
  - `requirePda(account, seeds, bump)`
100
101
 
@@ -145,8 +146,8 @@ an integer literal, an argument, `acct.key` or a scalar field of an account, the
145
146
  ## Not supported yet
146
147
 
147
148
  Vecs anywhere but the last account field, growing an account by more than 10 KiB in one instruction, growing a Vec
148
- inside a helper, data-enum arguments carrying strings/structs, Pubkeys in templates, `?.` / `!` / `**`, `switch` on
149
- 128-bit integers, `mulDiv` beyond u64, Token Metadata creators/collections, Core plugins, Bubblegum v2 / collections /
149
+ inside a helper, data-enum arguments carrying structs, Pubkeys in templates, `?.`, `**=`, `!` on anything but an Option, `switch` on
150
+ 128-bit integers, `mulDiv` beyond u64, Token Metadata collection verification, Core plugins, Bubblegum v2 / collections /
150
151
  transfers, floating point (by design), `do … while`.
151
152
  They are rejected at compile time rather than half-supported.
152
153