@typesol/cli 0.15.0 → 0.17.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 (50) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +11 -7
  3. package/dist/cli.js +147 -20
  4. package/dist/clientgen.js +37 -0
  5. package/dist/compiler/analyze.d.ts +11 -1
  6. package/dist/compiler/analyze.js +307 -37
  7. package/dist/compiler/emit/base.d.ts +7 -0
  8. package/dist/compiler/emit/base.js +62 -2
  9. package/dist/compiler/emit/borsh.d.ts +13 -0
  10. package/dist/compiler/emit/borsh.js +232 -0
  11. package/dist/compiler/emit/calls.js +50 -15
  12. package/dist/compiler/emit/cpi.d.ts +32 -8
  13. package/dist/compiler/emit/cpi.js +706 -25
  14. package/dist/compiler/emit/expressions.d.ts +11 -0
  15. package/dist/compiler/emit/expressions.js +126 -4
  16. package/dist/compiler/emit/function.js +45 -12
  17. package/dist/compiler/emit/program.js +24 -6
  18. package/dist/compiler/emit/shared.d.ts +12 -0
  19. package/dist/compiler/emit/shared.js +22 -4
  20. package/dist/compiler/emit/statements.d.ts +13 -0
  21. package/dist/compiler/emit/statements.js +141 -7
  22. package/dist/compiler/emit/storage.d.ts +6 -1
  23. package/dist/compiler/emit/storage.js +82 -17
  24. package/dist/compiler/idl.js +61 -18
  25. package/dist/compiler/model.d.ts +56 -4
  26. package/dist/compiler/model.js +8 -2
  27. package/dist/compiler/toolchain.d.ts +2 -0
  28. package/dist/compiler/toolchain.js +27 -1
  29. package/dist/onchain.d.ts +30 -0
  30. package/dist/onchain.js +53 -0
  31. package/dist/portgen.d.ts +87 -0
  32. package/dist/portgen.js +367 -0
  33. package/dist/templates.js +49 -154
  34. package/dist/testing.d.ts +66 -0
  35. package/dist/testing.js +337 -0
  36. package/dist/workspace.js +11 -1
  37. package/docs/architecture.md +3 -0
  38. package/docs/cli.md +16 -2
  39. package/docs/errors.md +3 -2
  40. package/docs/language.md +19 -14
  41. package/docs/mainnet.md +5 -4
  42. package/docs/migrate-from-anchor.md +113 -0
  43. package/docs/test-your-program.md +52 -0
  44. package/docs/testing.md +1 -1
  45. package/docs/troubleshooting.md +7 -0
  46. package/lib/typesol.d.ts +325 -13
  47. package/package.json +19 -1
  48. package/runtime/typesol_rt.h +54 -9
  49. package/ts-plugin/index.js +112 -0
  50. package/ts-plugin/package.json +4 -0
@@ -110,9 +110,11 @@ export type FieldType = ScalarType | {
110
110
  name: string;
111
111
  }
112
112
  /** `Vec<T>`: Borsh u32 length + elements. Only as the last field of an account struct. */
113
+ /** `Vec<T>` (last field, grows the account) or, with `cap`, `FixedVec<T, N>`: a u32 length and room for N elements. */
113
114
  | {
114
115
  kind: "vec";
115
116
  elem: FieldType;
117
+ cap?: number;
116
118
  };
117
119
  export interface StructField {
118
120
  name: string;
@@ -136,6 +138,11 @@ export interface StructDef {
136
138
  accountDiscriminator: number[];
137
139
  eventDiscriminator: number[];
138
140
  node: ts.InterfaceDeclaration;
141
+ /**
142
+ * How account data stores it: "fixed" (every field at a fixed offset, the default) or "borsh" (`@layout borsh`:
143
+ * Anchor's bytes, where strings, Vecs and Options take only what they hold).
144
+ */
145
+ layout: "fixed" | "borsh";
139
146
  }
140
147
  export type TokenProgramKind = "token" | "token2022" | "tokenInterface";
141
148
  export type ParamKind = {
@@ -278,7 +285,11 @@ export interface Instruction {
278
285
  args: ArgParam[];
279
286
  /** Fixed bytes of the arguments; strings add their length on top. */
280
287
  minArgsSize: number;
281
- returns: ScalarType | undefined;
288
+ /** Sent as return data: a scalar in Borsh, or a fixed-size struct in its fixed layout (as the IDL describes it). */
289
+ returns: ScalarType | {
290
+ kind: "struct";
291
+ name: string;
292
+ } | undefined;
282
293
  /** account name -> seeds, recorded when the body creates or verifies a PDA with describable seeds. */
283
294
  pdas: Map<string, IdlSeed[]>;
284
295
  /** account name -> the program its PDA is derived under, when not this program (e.g. associated token accounts). */
@@ -287,6 +298,14 @@ export interface Instruction {
287
298
  /** The declaring file's `typesol` imports (local name -> export name). */
288
299
  imports: Map<string, string>;
289
300
  }
301
+ /** A helper with type parameters, before it is instantiated. */
302
+ export interface GenericHelper {
303
+ name: string;
304
+ display: string;
305
+ node: HelperNode;
306
+ typeParams: string[];
307
+ imports: Map<string, string>;
308
+ }
290
309
  export interface HelperParam {
291
310
  name: string;
292
311
  /** `{ kind: "account" }` for account parameters (then `account` says which kind). */
@@ -294,12 +313,28 @@ export interface HelperParam {
294
313
  account?: ParamKind;
295
314
  node: ts.ParameterDeclaration;
296
315
  }
297
- /** A non-exported function (or any function of an imported file): values and accounts in, scalar or void out. */
316
+ /** A function written as `function f(…)`, `const f = (…) => …` or `const f = function (…) {…}`. */
317
+ export type HelperNode = ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression;
318
+ /**
319
+ * A non-exported function (or any function of an imported file), or an arrow function declared with const at the top
320
+ * level or inside a function (capturing nothing): values and accounts in; void, a scalar or a struct out.
321
+ */
298
322
  export interface Helper {
323
+ /** The C name (fn_<name>): the function's name, `outer__local` for a local arrow, `name__u64` for an instance. */
299
324
  name: string;
325
+ /** The name in messages: `local`, `max<u64>`. */
326
+ display: string;
300
327
  params: HelperParam[];
301
- returns: ScalarType | undefined;
302
- node: ts.FunctionDeclaration;
328
+ /** A struct result is written through an out pointer (`TSS_<name> *ret`) that the caller provides. */
329
+ returns: ScalarType | {
330
+ kind: "struct";
331
+ name: string;
332
+ } | undefined;
333
+ node: HelperNode;
334
+ /** The body; an arrow's expression body is one return (or expression) statement. */
335
+ statements: readonly ts.Statement[];
336
+ /** A generic helper's instance: type parameter -> the type it stands for. */
337
+ typeArgs?: Map<string, ScalarType>;
303
338
  /** The declaring file's `typesol` imports (local name -> export name). */
304
339
  imports: Map<string, string>;
305
340
  }
@@ -322,6 +357,11 @@ export interface DataEnumDef {
322
357
  }[];
323
358
  /** Largest variant size: the fixed payload in account data. */
324
359
  payload: number;
360
+ /**
361
+ * A variant holds a `string` (directly or in a nested enum): such an enum is Borsh in instruction arguments and
362
+ * events and a local value otherwise, never account data or part of a fixed layout. Its C payload holds TS_Str views.
363
+ */
364
+ variable?: boolean;
325
365
  node: ts.TypeAliasDeclaration;
326
366
  }
327
367
  /** A unit-variant enum: `export enum Status { Open, Closed }`. */
@@ -339,6 +379,10 @@ export interface ProgramModel {
339
379
  structs: Map<string, StructDef>;
340
380
  instructions: Instruction[];
341
381
  helpers: Map<string, Helper>;
382
+ /** Generic helpers by name: instantiated (into `helpers`) for the types each call uses. */
383
+ generics: Map<string, GenericHelper>;
384
+ /** Declaration (function, or the const of an arrow) -> helper or generic name, for calls to local arrows. */
385
+ helperDecls: Map<ts.Node, string>;
342
386
  errors: ErrorDef[];
343
387
  /** Enum name -> member name -> error */
344
388
  errorEnums: Map<string, Map<string, ErrorDef>>;
@@ -361,6 +405,10 @@ export interface ProgramModel {
361
405
  argStructs: Set<string>;
362
406
  /** FixedString capacities in use, for C typedefs and IDL types. */
363
407
  fstrings: Set<number>;
408
+ /** FixedVec<T, N> types in use (IDL name -> type), for IDL types. */
409
+ fixedVecs: Map<string, FieldType & {
410
+ kind: "vec";
411
+ }>;
364
412
  }
365
413
  export declare function isWritable(k: ParamKind): boolean;
366
414
  export declare function typeName(t: ValueType | FieldType): string;
@@ -373,3 +421,7 @@ export declare function optionKey(t: ScalarType | {
373
421
  name: string;
374
422
  }): string;
375
423
  export declare function scalarKey(t: ScalarType): string;
424
+ /** The IDL type name of a FixedVec<T, N>: `FixedVec<N>_<T>`. */
425
+ export declare function fixedVecName(t: FieldType & {
426
+ kind: "vec";
427
+ }): string;
@@ -56,7 +56,7 @@ export function typeName(t) {
56
56
  case "enumRef":
57
57
  return t.name;
58
58
  case "vec":
59
- return `Vec<${typeName(t.elem)}>`;
59
+ return t.cap === undefined ? `Vec<${typeName(t.elem)}>` : `FixedVec<${typeName(t.elem)}, ${t.cap}>`;
60
60
  case "vecArg":
61
61
  return `Vec<${typeName(t.elem)}>`;
62
62
  case "arrayArg":
@@ -92,7 +92,7 @@ export function sameType(a, b) {
92
92
  case "option":
93
93
  return sameType(a.inner, b.inner);
94
94
  case "vec":
95
- return sameType(a.elem, b.elem);
95
+ return a.cap === b.cap && sameType(a.elem, b.elem);
96
96
  case "vecArg":
97
97
  return sameType(a.elem, b.elem);
98
98
  case "arrayArg":
@@ -120,3 +120,9 @@ export function optionKey(t) {
120
120
  export function scalarKey(t) {
121
121
  return t.kind === "int" ? t.name : t.kind === "enum" ? `e_${t.name}` : t.kind === "pubkey" && t.hash ? "hash" : t.kind;
122
122
  }
123
+ /** The IDL type name of a FixedVec<T, N>: `FixedVec<N>_<T>`. */
124
+ export function fixedVecName(t) {
125
+ return `FixedVec${t.cap}_${typeName(t.elem)
126
+ .replace(/[^A-Za-z0-9]+/g, "_")
127
+ .replace(/_+$/, "")}`;
128
+ }
@@ -14,6 +14,8 @@ export interface Toolchain {
14
14
  /** Locate the Solana C SDK that ships with the Agave CLI's platform-tools. */
15
15
  export declare function findToolchain(sbpf?: Sbpf): Toolchain;
16
16
  /** Compile generated C to an SBF shared object (same flags as the SDK's sbf.mk for the chosen SBPF version). */
17
+ /** The SBF stack frame of one function. */
18
+ export declare const STACK_FRAME = 4096;
17
19
  export declare function buildSharedObject(cFile: string, outSo: string, sbpf?: Sbpf, tc?: Toolchain): void;
18
20
  /** The clang version string, for build manifests. */
19
21
  export declare function toolchainVersion(tc?: Toolchain): string;
@@ -51,6 +51,8 @@ export function findToolchain(sbpf = "v0") {
51
51
  };
52
52
  }
53
53
  /** Compile generated C to an SBF shared object (same flags as the SDK's sbf.mk for the chosen SBPF version). */
54
+ /** The SBF stack frame of one function. */
55
+ export const STACK_FRAME = 4096;
54
56
  export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(sbpf)) {
55
57
  const obj = outSo.replace(/\.so$/, ".o");
56
58
  const cflags = [
@@ -58,6 +60,9 @@ export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(
58
60
  "-Wno-parentheses-equality",
59
61
  // Generated checks may be always true/false (`a === a`, `enumByte >= 256` for a 256-variant enum); that is not an error.
60
62
  "-Wno-tautological-compare",
63
+ // SBPF v0 gives every function a fixed 4 KiB stack frame; past it the program corrupts memory instead of failing.
64
+ // v1 and v2 grow frames dynamically, so the limit does not apply there.
65
+ ...(sbpf === "v0" ? [`-Wframe-larger-than=${STACK_FRAME}`] : []),
61
66
  "-O2",
62
67
  "-fno-builtin",
63
68
  // One section per function and constant, so the linker's --gc-sections can drop what nothing reaches.
@@ -70,7 +75,12 @@ export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(
70
75
  ...(sbpf === "v0" ? [] : [`-mcpu=${sbpf}`]),
71
76
  ];
72
77
  const incs = tc.includes.flatMap((i, n) => (n < 2 ? ["-isystem", i] : ["-I", i]));
73
- run(tc.clang, [...cflags, ...incs, "-o", obj, "-c", cFile]);
78
+ try {
79
+ run(tc.clang, [...cflags, ...incs, "-o", obj, "-c", cFile]);
80
+ }
81
+ catch (e) {
82
+ throw stackError(e.message) ?? e;
83
+ }
74
84
  run(tc.lld, [
75
85
  "-z",
76
86
  "notext",
@@ -99,6 +109,22 @@ export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(
99
109
  export function toolchainVersion(tc = findToolchain()) {
100
110
  return execFileSync(tc.clang, ["--version"], { encoding: "utf8" }).split("\n")[0].trim();
101
111
  }
112
+ /** clang's frame-size errors, in the program's terms: which instruction or helper, how much, what to do. */
113
+ function stackError(message) {
114
+ const over = [...message.matchAll(/stack frame size \((\d+)\) exceeds limit \((\d+)\) in '(\w+)'/g)];
115
+ if (!over.length)
116
+ return undefined;
117
+ // clang inlines helpers into their callers, so an instruction's frame includes theirs.
118
+ const what = (fn) => fn.startsWith("ix_") ? `instruction ${fn.slice(3)} (with the helpers inlined into it)` : fn.startsWith("fn_") ? `helper ${fn.slice(3)}()` : fn;
119
+ // Anything else clang reported in the same run stays in the message.
120
+ const rest = message
121
+ .split("\n")
122
+ .filter((l) => / error: /.test(l) && !/-Wframe-larger-than/.test(l))
123
+ .join("\n");
124
+ return new Error(over.map((m) => `${what(m[3])} needs ${m[1]} bytes of stack; SBPF v0 gives each function ${m[2]}`).join("\n") +
125
+ "\nKeep large structs and arrays in account data: bind them with const (a reference) instead of a typed local copy, or split the work into helpers." +
126
+ (rest ? `\n${rest}` : ""));
127
+ }
102
128
  function run(cmd, args) {
103
129
  try {
104
130
  execFileSync(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A deployed program read straight from a cluster, for `typesol verify --address`: its ELF bytes and who can still
3
+ * change them. Handles the three loaders a program can live under:
4
+ *
5
+ * - upgradeable (loader v3, what `solana program deploy` uses): the program account points at a ProgramData account
6
+ * holding the deploy slot, the upgrade authority (none once frozen) and then the ELF;
7
+ * - loader v4: one account holding the slot, the authority, the status and then the ELF;
8
+ * - the old non-upgradeable loader (v2): the account data is the ELF itself.
9
+ */
10
+ import type { Rpc, SolanaRpcApi } from "@solana/kit";
11
+ export declare const LOADER_V2 = "BPFLoader2111111111111111111111111111111111";
12
+ export declare const LOADER_V3 = "BPFLoaderUpgradeab1e11111111111111111111111";
13
+ export declare const LOADER_V4 = "LoaderV411111111111111111111111111111111111";
14
+ export interface Deployed {
15
+ loader: "v2" | "v3" | "v4";
16
+ /** The ELF as stored (loader v3 keeps it zero-padded to the account's allocation). */
17
+ elf: Buffer;
18
+ /** Who can upgrade or close it; null when the program is immutable. */
19
+ authority: string | null;
20
+ /** The slot of the last deploy or upgrade (v3, v4). */
21
+ slot?: bigint;
22
+ /** Where the bytes live (the ProgramData account for v3). */
23
+ dataAccount: string;
24
+ }
25
+ type AccountRpc = Pick<Rpc<SolanaRpcApi>, "getAccountInfo">;
26
+ /** The deployed program at `programId`, or an error that says why there is none. */
27
+ export declare function fetchDeployed(rpc: AccountRpc, programId: string): Promise<Deployed>;
28
+ /** JSON with object keys sorted, so two IDLs compare by content and not by key order. */
29
+ export declare function canonicalJson(v: unknown): string;
30
+ export {};
@@ -0,0 +1,53 @@
1
+ import { getAddressDecoder } from "@solana/kit";
2
+ export const LOADER_V2 = "BPFLoader2111111111111111111111111111111111";
3
+ export const LOADER_V3 = "BPFLoaderUpgradeab1e11111111111111111111111";
4
+ export const LOADER_V4 = "LoaderV411111111111111111111111111111111111";
5
+ async function account(rpc, address) {
6
+ const { value } = await rpc.getAccountInfo(address, { encoding: "base64", commitment: "confirmed" }).send();
7
+ if (!value)
8
+ return null;
9
+ return { owner: value.owner, data: Buffer.from(value.data[0], "base64") };
10
+ }
11
+ const key = (data, offset) => getAddressDecoder().decode(data.subarray(offset, offset + 32));
12
+ /** The deployed program at `programId`, or an error that says why there is none. */
13
+ export async function fetchDeployed(rpc, programId) {
14
+ const program = await account(rpc, programId);
15
+ if (!program)
16
+ throw new Error(`no account at ${programId} on this cluster`);
17
+ if (program.owner === LOADER_V2)
18
+ return { loader: "v2", elf: program.data, authority: null, dataAccount: programId };
19
+ if (program.owner === LOADER_V4) {
20
+ // u64 slot, authority (or next version), u64 status (0 retracted, 1 deployed, 2 finalized), then the ELF.
21
+ if (program.data.length < 48)
22
+ throw new Error(`${programId} is not a loader-v4 program`);
23
+ const status = program.data.readBigUInt64LE(40);
24
+ if (status === 0n)
25
+ throw new Error(`${programId} is retracted (loader v4): it has no executable code`);
26
+ return {
27
+ loader: "v4",
28
+ elf: program.data.subarray(48),
29
+ authority: status === 2n ? null : key(program.data, 8),
30
+ slot: program.data.readBigUInt64LE(0),
31
+ dataAccount: programId,
32
+ };
33
+ }
34
+ if (program.owner !== LOADER_V3)
35
+ throw new Error(`${programId} is not a program (owner ${program.owner})`);
36
+ // Program account: u32 state = 2, ProgramData address.
37
+ if (program.data.length < 36 || program.data.readUInt32LE(0) !== 2)
38
+ throw new Error(`${programId} is an upgradeable-loader account but not a program`);
39
+ const dataAccount = key(program.data, 4);
40
+ const pd = await account(rpc, dataAccount);
41
+ // ProgramData: u32 state = 3, u64 slot, Option<Pubkey> authority (1 + 32 bytes), then the ELF.
42
+ if (!pd || pd.owner !== LOADER_V3 || pd.data.length < 45 || pd.data.readUInt32LE(0) !== 3)
43
+ throw new Error(`${programId} has no program data (closed or never deployed)`);
44
+ return { loader: "v3", elf: pd.data.subarray(45), authority: pd.data[12] ? key(pd.data, 13) : null, slot: pd.data.readBigUInt64LE(4), dataAccount };
45
+ }
46
+ /** JSON with object keys sorted, so two IDLs compare by content and not by key order. */
47
+ export function canonicalJson(v) {
48
+ return JSON.stringify(v, (_k, x) => x && typeof x === "object" && !Array.isArray(x)
49
+ ? Object.fromEntries(Object.keys(x)
50
+ .sort()
51
+ .map((k) => [k, x[k]]))
52
+ : x);
53
+ }
@@ -0,0 +1,87 @@
1
+ type IdlType = string | {
2
+ vec?: IdlType;
3
+ option?: IdlType;
4
+ coption?: IdlType;
5
+ array?: [IdlType, number | unknown];
6
+ defined?: string | {
7
+ name: string;
8
+ generics?: unknown[];
9
+ };
10
+ };
11
+ interface IdlField {
12
+ name: string;
13
+ type: IdlType;
14
+ docs?: string[];
15
+ }
16
+ interface IdlTypeDef {
17
+ name: string;
18
+ docs?: string[];
19
+ type: {
20
+ kind: string;
21
+ fields?: (IdlField | IdlType)[];
22
+ variants?: {
23
+ name: string;
24
+ fields?: (IdlField | IdlType)[];
25
+ }[];
26
+ };
27
+ generics?: unknown[];
28
+ }
29
+ interface IdlAccountItem {
30
+ name: string;
31
+ writable?: boolean;
32
+ signer?: boolean;
33
+ optional?: boolean;
34
+ address?: string;
35
+ docs?: string[];
36
+ pda?: {
37
+ seeds: {
38
+ kind: string;
39
+ value?: number[];
40
+ path?: string;
41
+ }[];
42
+ program?: unknown;
43
+ };
44
+ relations?: string[];
45
+ accounts?: IdlAccountItem[];
46
+ }
47
+ interface IdlInstruction {
48
+ name: string;
49
+ docs?: string[];
50
+ discriminator: number[];
51
+ accounts: IdlAccountItem[];
52
+ args: IdlField[];
53
+ returns?: IdlType;
54
+ }
55
+ export interface PortIdl {
56
+ address?: string;
57
+ metadata?: {
58
+ name?: string;
59
+ };
60
+ instructions: IdlInstruction[];
61
+ accounts?: {
62
+ name: string;
63
+ discriminator: number[];
64
+ }[];
65
+ events?: {
66
+ name: string;
67
+ discriminator: number[];
68
+ }[];
69
+ errors?: {
70
+ code: number;
71
+ name: string;
72
+ msg?: string;
73
+ }[];
74
+ types?: IdlTypeDef[];
75
+ constants?: {
76
+ name: string;
77
+ type: IdlType;
78
+ value: string;
79
+ }[];
80
+ }
81
+ export interface PortResult {
82
+ source: string;
83
+ /** What the skeleton cannot carry over as is, and where Anchor clients would see a different layout. */
84
+ notes: string[];
85
+ }
86
+ export declare function generatePort(idl: PortIdl): PortResult;
87
+ export {};