@typesol/cli 0.16.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.
@@ -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 = {
@@ -291,6 +298,14 @@ export interface Instruction {
291
298
  /** The declaring file's `typesol` imports (local name -> export name). */
292
299
  imports: Map<string, string>;
293
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
+ }
294
309
  export interface HelperParam {
295
310
  name: string;
296
311
  /** `{ kind: "account" }` for account parameters (then `account` says which kind). */
@@ -298,16 +313,28 @@ export interface HelperParam {
298
313
  account?: ParamKind;
299
314
  node: ts.ParameterDeclaration;
300
315
  }
301
- /** A non-exported function (or any function of an imported file): values and accounts in; void, a scalar or a struct 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
+ */
302
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. */
303
324
  name: string;
325
+ /** The name in messages: `local`, `max<u64>`. */
326
+ display: string;
304
327
  params: HelperParam[];
305
328
  /** A struct result is written through an out pointer (`TSS_<name> *ret`) that the caller provides. */
306
329
  returns: ScalarType | {
307
330
  kind: "struct";
308
331
  name: string;
309
332
  } | undefined;
310
- node: ts.FunctionDeclaration;
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>;
311
338
  /** The declaring file's `typesol` imports (local name -> export name). */
312
339
  imports: Map<string, string>;
313
340
  }
@@ -352,6 +379,10 @@ export interface ProgramModel {
352
379
  structs: Map<string, StructDef>;
353
380
  instructions: Instruction[];
354
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>;
355
386
  errors: ErrorDef[];
356
387
  /** Enum name -> member name -> error */
357
388
  errorEnums: Map<string, Map<string, ErrorDef>>;
@@ -374,6 +405,10 @@ export interface ProgramModel {
374
405
  argStructs: Set<string>;
375
406
  /** FixedString capacities in use, for C typedefs and IDL types. */
376
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
+ }>;
377
412
  }
378
413
  export declare function isWritable(k: ParamKind): boolean;
379
414
  export declare function typeName(t: ValueType | FieldType): string;
@@ -386,3 +421,7 @@ export declare function optionKey(t: ScalarType | {
386
421
  name: string;
387
422
  }): string;
388
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
+ }
package/dist/portgen.js CHANGED
@@ -4,8 +4,9 @@
4
4
  * It writes the account structs, enums, errors, events and one function per instruction with its accounts and
5
5
  * arguments, bodies left to fill in. Instructions keep their discriminators (an explicit `@discriminator` when the
6
6
  * name would derive another), account structs keep their names (so their discriminators), errors keep their order —
7
- * so existing clients keep working when the layout allows it. Where TypeSOL stores data differently from Anchor's Borsh
8
- * layout (String, Option and a Vec that is not the last field in account data), it says so in a note and a comment.
7
+ * so existing clients keep working. Account structs holding a String, Vec or Option get `@layout borsh`, so they keep
8
+ * Anchor's bytes (existing accounts stay readable); where a layout still differs (a String or Vec nested in a struct, a
9
+ * data enum, a COption) it says so in a note and a comment.
9
10
  */
10
11
  import { createHash } from "node:crypto";
11
12
  import { cNameProblem } from "./compiler/analyze.js";
@@ -152,6 +153,57 @@ export function generatePort(idl) {
152
153
  notes.push(`${at}: unsupported IDL type ${JSON.stringify(t)}; left as u64`);
153
154
  return `${use("u64")} /* TODO */`;
154
155
  };
156
+ /** A String, Vec or Option at a struct's top level: Borsh-sized in Anchor. */
157
+ const borshOnly = (t) => t === "string" || t === "bytes" || (typeof t === "object" && (!!t.vec || !!t.option));
158
+ /** A field's type in a `@layout borsh` struct, or undefined when that layout cannot hold it. Notes the capacities. */
159
+ const borshField = (t, at) => {
160
+ const quiet = at === "";
161
+ const fixedOk = (u) => {
162
+ if (typeof u === "string")
163
+ return INTS.has(u) || u === "bool" || u === "pubkey" || u === "publicKey";
164
+ if (u.array)
165
+ return Number.isInteger(Number(u.array[1])) && fixedOk(u.array[0]);
166
+ if (u.defined) {
167
+ const name = definedName(u.defined);
168
+ const d = defs.get(name);
169
+ if (!d || skipped.has(name) || helperDefs.has(name))
170
+ return false;
171
+ if (d.type.kind === "enum")
172
+ return (d.type.variants ?? []).every((v) => !v.fields?.length);
173
+ return (d.type.fields ?? []).every((f) => fixedOk(f.type));
174
+ }
175
+ return false;
176
+ };
177
+ if (t === "string") {
178
+ if (!quiet)
179
+ notes.push(`${at}: set the String's capacity to Anchor's #[max_len] (Borsh bytes are kept: @layout borsh)`);
180
+ return `${use("FixedString")}<32> /* TODO: #[max_len] */`;
181
+ }
182
+ if (t === "bytes")
183
+ return borshField({ vec: "u8" }, at);
184
+ if (typeof t === "object" && t.vec) {
185
+ const elem = t.vec === "string"
186
+ ? `${use("FixedString")}<32> /* TODO: #[max_len] */`
187
+ : fixedOk(t.vec) && !quiet
188
+ ? tsType(t.vec, "account", at)
189
+ : fixedOk(t.vec)
190
+ ? "x"
191
+ : undefined;
192
+ if (elem === undefined)
193
+ return undefined;
194
+ if (!quiet)
195
+ notes.push(`${at}: set the Vec's capacity to Anchor's #[max_len] (Borsh bytes are kept: @layout borsh)`);
196
+ return `${use("FixedVec")}<${elem}, 8> /* TODO: #[max_len] */`;
197
+ }
198
+ if (typeof t === "object" && t.option) {
199
+ if (!fixedOk(t.option))
200
+ return undefined;
201
+ return quiet ? "x" : `${use("Option")}<${tsType(t.option, "account", at)}>`;
202
+ }
203
+ if (!fixedOk(t))
204
+ return undefined;
205
+ return quiet ? "x" : tsType(t, "account", at);
206
+ };
155
207
  const esc = (s) => s.replace(/\*\//g, "* /");
156
208
  const docs = (lines, pad = "") => (lines?.length ? `${pad}/** ${esc(lines.join(" "))} */\n` : "");
157
209
  const out = [];
@@ -180,8 +232,11 @@ export function generatePort(idl) {
180
232
  const where = used.has("account") ? "account" : used.has("arg") ? "struct" : used.has("event") ? "event" : "account";
181
233
  if (def.type.kind === "struct") {
182
234
  const fields = (def.type.fields ?? []);
235
+ // An account struct with a String, Vec or Option of its own: `@layout borsh` keeps Anchor's bytes.
236
+ const borsh = accountNames.has(def.name) && !isTuple(fields) && fields.some((f) => borshOnly(f.type)) && fields.every((f) => borshField(f.type, "") !== undefined);
183
237
  const tag = accountNames.has(def.name) ? " (account)" : eventNames.has(def.name) ? ` (event: emit<${typeName(def.name)}>({ … }))` : "";
184
- out.push(docs(def.docs).trimEnd() || `/** ${def.name}${tag} */`);
238
+ const doc = def.docs?.length ? esc(def.docs.join(" ")) : `${def.name}${tag}`;
239
+ out.push(borsh ? `/**\n * ${doc}\n * @layout borsh\n */` : `/** ${doc} */`);
185
240
  out.push(`export interface ${typeName(def.name)} {`);
186
241
  const seen = new Set();
187
242
  fields.forEach((f, i) => {
@@ -189,7 +244,8 @@ export function generatePort(idl) {
189
244
  while (seen.has(n))
190
245
  n = `${n}2`;
191
246
  seen.add(n);
192
- out.push(`${docs(f.docs, " ")} ${n}: ${tsType(f.type, where, `${def.name}.${f.name}`, i === fields.length - 1)};`);
247
+ const ty = borsh ? borshField(f.type, `${def.name}.${f.name}`) : tsType(f.type, where, `${def.name}.${f.name}`, i === fields.length - 1);
248
+ out.push(`${docs(f.docs, " ")} ${n}: ${ty};`);
193
249
  });
194
250
  out.push("}", "");
195
251
  }
package/docs/errors.md CHANGED
@@ -15,7 +15,8 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
15
15
  | 2040 | the same account passed in two writable slots (ConstraintDuplicateMutableAccount) |
16
16
  | 3000 | account already exists / initialized (AccountDiscriminatorAlreadySet) |
17
17
  | 3002 | discriminator mismatch (AccountDiscriminatorMismatch) |
18
- | 3003 | data too small, not an initialized token account / mint, or a read past the end (AccountDidNotDeserialize) |
18
+ | 3003 | data too small, not an initialized token account / mint, a read past the end, or a `@layout borsh` account whose bytes are not a valid Borsh value (AccountDidNotDeserialize) |
19
+ | 3004 | a `@layout borsh` account too small for the value it would now hold (AccountDidNotSerialize) |
19
20
  | 3005 | not enough accounts (AccountNotEnoughKeys) |
20
21
  | 3006 | account must be writable (AccountNotMutable) |
21
22
  | 3007 | wrong owner program (AccountOwnedByWrongProgram) |
@@ -26,7 +27,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
26
27
  | 4102 | cast overflow (InvalidNumericConversion) |
27
28
  | 5100 / 5101 | arithmetic overflow (including shifts) / division by zero |
28
29
  | 5103 | sysvar unavailable |
29
- | 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec) |
30
+ | 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec, `push` on a full FixedVec) |
30
31
  | 5105 | string longer than the FixedString capacity |
31
32
  | 5106 | missing or mismatched return data |
32
33
  | 5107 | event larger than 1024 bytes |
package/docs/language.md CHANGED
@@ -25,6 +25,8 @@ The full reference. For a tour, start with the [README](../README.md).
25
25
  | Enums | `export enum Status { Pending, Approved }` — unit variants stored as the Borsh u8 index; `Status.Approved`, `===` |
26
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 |
27
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 |
28
+ | Anchor's account bytes | `/** @layout borsh */` on an account struct stores it as Anchor does (Borsh: a `FixedString<N>` is a String, a `FixedVec<T, N>` a Vec, an `Option` one byte when empty, and the fields after them move), so a TypeSOL program can take over accounts an Anchor program wrote and the IDL lists Anchor's own types. The instruction unpacks it into the usual fixed layout when it starts (bad bytes fail with 3003) and packs it back when it returns (3004 when the account is too small for the new value), so everything else works on it unchanged. Programs invoked from the body (CPIs) see the account's bytes as they were when the instruction started. Costs more compute than the fixed layout (the whole account is read and written each time; see the benchmarks); nested structs and list elements must be fixed-size (FixedString elements allowed); not for arguments, events, `Zeroed<T>`, `loadAccount` or a growable `Vec<T>` |
29
+ | Fixed-capacity lists | `owners: FixedVec<Pubkey, 8>` — any field, any number of them, in nested structs too: a u32 length and room for N elements (always 4 + N × size bytes), the same `get`, `set`, `push(v)`, `pop()`, `clear()`, `.length` and `for … of`; `push` on a full one fails (5104); `acct.owners = fixedVec([a, b])` writes a whole one (unused slots zeroed). Account data and returned structs only: arguments use `Vec<T>` |
28
30
  | 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 |
29
31
  | 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) |
30
32
  | Struct literals | `book.entries.push({ who: who.key, amount, status: Status.Pending, note }, who)` — every field required |
@@ -41,6 +43,8 @@ The full reference. For a tour, start with the [README](../README.md).
41
43
  | 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 |
42
44
  | 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
45
  | 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) |
46
+ | Arrow functions | `const fee = (x: u64): u64 => x / 100n` (or `const f = function (…) {…}`) at the top level or inside a function is a helper (parameter and return types written out). A local one capturing nothing: it reads its parameters, top-level names and other functions, not the enclosing function's variables (pass them in). Functions are called, not stored or passed |
47
+ | Generic helpers | `function max<T extends bigint>(a: T, b: T): T` — compiled once per set of types it is called with (`max(a, b)` with u64 arguments, or `max<u128>(x, 1n)` when only literals tell); a type parameter stands for an integer, boolean, Pubkey or enum. Each instance is checked like any helper; one that is never called is not compiled (TypeScript still type-checks it). Arithmetic on `T` is a `bigint` to TypeScript: `return (a + b) as T` (`as` names a type the value already has; it never converts) |
44
48
  | 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()`) |
45
49
  | `++` `--` `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` `<<=` `>>=` | all checked |
46
50
  | Sysvars | `now()`, `currentSlot()`, `currentEpoch()` (clock), `minimumBalance(account)`, `rentExempt(space)` (rent) |
@@ -49,8 +53,8 @@ The full reference. For a tour, start with the [README](../README.md).
49
53
  | Events through CPI | `emitCpi<Paid>({ … }, eventAuthority, self)` with `eventAuthority: EventAuthority, self: SelfProgram` parameters — Anchor `emit_cpi!`: the event rides in a self-CPI (inner instruction), not in logs; the program accepts Anchor's event instruction (2002 / 2006 otherwise) |
50
54
  | Heap | 32 KiB by default; `const HEAP_SIZE_KIB = 64n` (32–256) lets the program use more, when clients add ComputeBudget `requestHeapFrame` |
51
55
  | Docs | JSDoc on instructions, `@param`, structs, fields and data enums becomes `docs` in the IDL |
52
- | Metaplex Core | `core: MplCoreProgram` — `createCollection`, `createAsset` (in a collection, PDA authorities via signerSeeds), `transferAsset`, `burnAsset` (no plugins; `null` leaves an optional account out); `coreAssetOwner(asset)`, `coreAssetCollection(asset)` |
53
- | Compressed NFTs | `bubblegum: BubblegumProgram` — `createTree` (client-allocated tree on SPL Account Compression), `mint` (MintV1, no collection/creators) |
56
+ | Metaplex Core | `core: MplCoreProgram` — `createCollection`, `createAsset` (in a collection, PDA authorities via signerSeeds), `transferAsset`, `burnAsset` (`null` leaves an optional account out); plugins as a literal list after the uri (`[{ kind: "Royalties", basisPoints: 500n, creators: [{ address, percentage: 100n }] }, { kind: "PermanentFreezeDelegate", frozen: false, authority: studio.key }]`), and `addPlugin` / `updatePlugin` / `removePlugin(…, "FreezeDelegate")` with their `…CollectionPlugin` forms. Plugins: Royalties (no rule set), FreezeDelegate, BurnDelegate, TransferDelegate, UpdateDelegate, PermanentFreezeDelegate, Attributes, PermanentTransferDelegate, PermanentBurnDelegate, Edition, AddBlocker, ImmutableMetadata; `authority` is `"owner"`, `"updateAuthority"`, `"none"` or a Pubkey; `coreAssetOwner(asset)`, `coreAssetCollection(asset)` |
57
+ | Compressed NFTs | `bubblegum: BubblegumProgram` — V1: `createTree` (client-allocated tree on SPL Account Compression), `mint` (MintV1, no collection/creators). V2 (trees on mpl-account-compression with mpl-noop): `createTreeV2`, `mintV2(…, { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable? }, { collection, authority, cpiSigner }?)` (into a Core collection that has the `BubblegumV2` plugin), `transferV2` and `burnV2` with the leaf `{ root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index }` and its proof as `remainingAccounts()` / `rest.slice(a)` |
54
58
 
55
59
  ## Account parameters and the checks they emit
56
60
 
@@ -76,7 +80,7 @@ The full reference. For a tour, start with the [README](../README.md).
76
80
  | `SystemProgram`, `TokenProgram`, `Token2022Program` | exact program address |
77
81
  | `TokenInterface` | either token program |
78
82
  | `AssociatedTokenProgram` | `create` / `createIdempotent` CPIs; `associatedTokenAddress(owner, mint, tokenProgram)` |
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)` |
83
+ | `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; `isCollection: true` makes the NFT a sized collection); `createMasterEditionV3(edition, mint, updateAuthority, mintAuthority, payer, metadata, tokenProgram, systemProgram, maxSupply \| null)`; `verifyCollection(authority, metadata, collectionMint, collectionMetadata, collectionMasterEdition, systemProgram, instructionsSysvar)` (Verify with CollectionV1); `metadataAddress(mint)`, `masterEditionAddress(mint)` |
80
84
  | `InstructionsSysvar`, `PythPriceUpdate`, `SwitchboardFeed` | exact address / owner program + discriminator |
81
85
  | `Program<"Address…">` | exact program address + executable |
82
86
  | `UncheckedProgram` | executable only — check `.key` yourself |
@@ -145,10 +149,10 @@ an integer literal, an argument, `acct.key` or a scalar field of an account, the
145
149
 
146
150
  ## Not supported yet
147
151
 
148
- Vecs anywhere but the last account field, growing an account by more than 10 KiB in one instruction, growing a Vec
152
+ Growable `Vec`s anywhere but the last account field (use `FixedVec<T, N>`), FixedVec in arguments and events, growing an account by more than 10 KiB in one instruction, growing a Vec
149
153
  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 /
151
- transfers, floating point (by design), `do … while`.
154
+ 128-bit integers, `mulDiv` beyond u64, Core's MasterEdition / VerifiedCreators / Autograph plugins, royalty rule sets and external plugin adapters, Bubblegum V2 delegates / freezes / metadata updates / V1 transfers, closures (local functions reading the enclosing function's variables), functions as values (callbacks,
155
+ `.map`), type parameters standing for structs, floating point (by design), `do … while`.
152
156
  They are rejected at compile time rather than half-supported.
153
157
 
154
158
  `Init.create` handles a PDA address someone pre-funded: it tops up rent, then allocates and assigns instead of
@@ -60,14 +60,21 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
60
60
  | `u8` … `u128`, `i8` … `i128`, `bool`, `Pubkey` | `u8` … `u128`, `i8` … `i128`, `boolean`, `Pubkey` |
61
61
  | `[T; N]` | `FixedArray<T, N>` |
62
62
  | `String` with `#[max_len(n)]` | `FixedString<N>`: a u32 length and N bytes, always N bytes long |
63
- | `Vec<T>` with `#[max_len(n)]` | `Vec<T>` as the account's last field; it grows with `push(v, payer)` |
63
+ | `Vec<T>` with `#[max_len(n)]` | `Vec<T>` as the account's last field (it grows with `push(v, payer)`), or `FixedVec<T, n>` anywhere: a u32 length and room for n, always 4 + n × size bytes |
64
64
  | `Option<T>` | `Option<T>`: fixed size (1 + size of T) |
65
65
  | `#[derive(AnchorSerialize…)] enum` | `export enum` (unit variants) or a data enum `type A = { kind: "X"; … } \| …` |
66
66
 
67
- TypeSOL keeps account data at a fixed layout, so three Anchor layouts differ: a Borsh `String` is variable-length (TypeSOL
68
- uses `FixedString<N>`), a Borsh `Option` takes 1 byte when empty (TypeSOL always reserves the value), and a `Vec` can be
69
- anywhere in an Anchor struct (TypeSOL keeps it last). Accounts using them need a migration (or a new program) rather
70
- than an in-place upgrade. Everything else, including instruction arguments (`String`, `Option`, `Vec` are Borsh there),
67
+ TypeSOL keeps account data at a fixed layout by default, so three Anchor layouts differ: a Borsh `String` is
68
+ variable-length (TypeSOL uses `FixedString<N>`), a Borsh `Option` takes 1 byte when empty (TypeSOL always reserves the
69
+ value), and a `Vec` can be anywhere in an Anchor struct (TypeSOL keeps a growable one last; a `FixedVec<T, N>` elsewhere
70
+ always takes its full capacity, where Anchor packs the elements and moves the fields after them).
71
+
72
+ To keep an Anchor program's existing accounts, mark the struct `/** @layout borsh */`: it is then stored exactly as
73
+ Anchor stores it (`FixedString<N>` / `FixedVec<T, N>` / `Option<T>` become Borsh String / Vec / Option, N being the
74
+ `#[max_len]`), its IDL type is Anchor's, and the upgraded program reads and writes the accounts in place. `typesol port`
75
+ adds it for you. It costs some compute (the account is unpacked and packed in every instruction that uses it) but stays
76
+ several times cheaper than Anchor. Accounts using them need a migration (or a new program) rather
77
+ than an in-place upgrade, unless they use `@layout borsh`. Everything else, including instruction arguments (`String`, `Option`, `Vec` are Borsh there),
71
78
  matches byte for byte.
72
79
 
73
80
  ## Code
@@ -90,6 +97,8 @@ matches byte for byte.
90
97
  | `declare_program!(other)` / CPI to another Anchor program | `typesol cpi other.json` generates typed helpers |
91
98
  | `for x in v.iter()` | `for (const x of v)` |
92
99
  | `x.unwrap_or(d)` | `x ?? d` |
100
+ | `fn max<T: PartialOrd>(a: T, b: T) -> T` | `function max<T extends bigint>(a: T, b: T): T` (one copy per type used) |
101
+ | `let fee = \|x: u64\| x / 100;` (no captures) | `const fee = (x: u64): u64 => x / 100n;` |
93
102
 
94
103
  ## Tooling
95
104
 
@@ -41,7 +41,7 @@ it("counts", async () => {
41
41
  | --- | --- |
42
42
  | `t.ok(res)` | the successful result, or throws with the program's logs |
43
43
  | `t.errorCode(res)` | the custom error code of a failed transaction: 6000 + the member's index in your error enum, or a framework code ([errors.md](errors.md)) |
44
- | `t.account(type, address)` | the account decoded by its IDL type (bigints, address strings), or `null` once closed |
44
+ | `t.account(type, address)` | the account decoded by its IDL type (bigints, address strings), or `null` once closed. TypeSOL fixed layouts come back as their IDL structs: a `FixedString<N>` as `{ len, bytes }`, a `FixedVec<T, N>` as `{ len, items }` (the generated client gives strings and arrays) |
45
45
  | `t.returned(instruction, res)` | the instruction's return data: an integer, boolean, Pubkey or struct |
46
46
  | `t.balance(address)` | lamports |
47
47