@typesol/cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,105 @@ until 1.0, a minor version may change the language, the IDL layout or error code
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.19.0 — 2026-10-28
9
+
10
+ Anchor's error codes where TypeSOL raised its own: `TokenOf` / `MintOf` constraints that name the parameters as
11
+ `token::mint = …` does, every Anchor `ErrorCode` for `require`, and `init_if_needed` mints with extensions checked as
12
+ Anchor checks them. Strings and logs cost up to a third fewer compute units. 30 of the 46 official examples are now
13
+ full ports. Existing programs keep their layouts and error codes. Not externally audited (see SECURITY.md).
14
+
15
+ ### Added
16
+ - `TokenOf<TokenAccount, "mintParam", "authorityParam">` and `MintOf<Mint, "authorityParam", "freezeParam">`: Anchor's
17
+ `token::mint = …` / `mint::authority = …` naming the parameters (2014 / 2015 / 2016 / 2017).
18
+ - `AnchorError`: Anchor's own error codes for `require(condition, AnchorError.X)` (`RequireKeysEqViolated`,
19
+ `AccountDidNotDeserialize`, the extension constraints …), as `require!` / `err!` with `ErrorCode` raise them.
20
+ - Extensions on `InitIfNeeded<Mint>`: an existing mint is checked as Anchor's `extensions::…` constraints check it
21
+ (2024–2039).
22
+ - Bubblegum `mintToCollection` (MintToCollectionV1 on V1 trees, Token Metadata collections).
23
+
24
+ ### Changed
25
+ - Strings are built with less work and heap: a template literal is joined in one allocation (it was copied piece by
26
+ piece), text known at compile time (literals, string and Pubkey constants) is folded in, `s += …` and `a + b` grow
27
+ the newest heap string in place, and base58 of a Pubkey is about 20% cheaper. Results are the same; official
28
+ examples that log addresses use 11–36% fewer compute units (set_favorites 10,244 → 6,601, transfer_tokens
29
+ 9,760 → 8,467, create_system_account 4,664 → 4,075, go_to_park 435 → 372); binaries of programs that build
30
+ strings grow by up to 2.4 KB.
31
+
32
+ ## 0.18.0 — 2026-10-28
33
+
34
+ The remaining Anchor differences, closed: Anchor's argument structs (Strings, Vecs, Options and enums in Borsh),
35
+ keypair `init` with `space`, `init` of mints with Token-2022 extensions and of token accounts, `Vec<String>`
36
+ arguments, `SystemAccount`, string return values, and the Token-2022 and Token Metadata instructions programs call.
37
+ Closures, list callbacks and more everyday TypeScript; the rest of Bubblegum and Core. 26 of the 46 official
38
+ examples are now full ports. Existing programs keep their layouts and error codes (2018 and 3011 are new, raised only
39
+ by the new `Init<Mint>` and `SystemAccount` checks). Not externally audited (see SECURITY.md).
40
+
41
+ ### Added
42
+ Anchor accounts and arguments:
43
+ - `@layout borsh` structs as whole instruction arguments, return values and events, in Anchor's Borsh bytes (a
44
+ FixedString is a String, a FixedVec a Vec, an Option one byte when empty). Arguments are checked as Borsh
45
+ deserialization does, UTF-8 included (102 otherwise). `return vault` returns a whole struct account.
46
+ - `@layout borsh` structs used as arguments, return values or events may hold `string` fields and data enums (with or
47
+ without strings), as Anchor argument structs do; Borsh accounts may hold data enums without strings.
48
+ - `acct.create(payer, null)` on `Init<T>`: Anchor's `init` of a keypair account (it must sign; 3010 otherwise), and an
49
+ optional third argument for Anchor's `space = …` on `create` / `createIfNeeded`.
50
+ - Mint extensions on `Init<Mint>`: `mint.create(payer, seeds | null, decimals, authority, freeze, { … })` with
51
+ transferFee, closeAuthority, defaultAccountState, nonTransferable, interestBearing, permanentDelegate, transferHook,
52
+ metadataPointer, groupPointer and groupMemberPointer (Anchor's `extensions::…`).
53
+ - `Init<Mint>` / `InitIfNeeded<Mint>` with `mint.create(payer, seeds | null, decimals, mintAuthority, freezeAuthority)`
54
+ and `Init<TokenAccount>` / `InitIfNeeded<TokenAccount>` (without `Ata<…>`) with `vault.create(payer, seeds | null,
55
+ mint, authority)`: Anchor's `init` with `mint::` / `token::` constraints, at a PDA of this program or (with `null`) a
56
+ new keypair account that signs, under SPL Token or Token-2022 (token accounts sized with GetAccountDataSize). The
57
+ `createIfNeeded` forms check an existing account like Anchor's `init_if_needed` (2014 / 2015 / 2016 / 2017 and the
58
+ new 2018 ConstraintMintDecimals). The IDL marks keypair-created accounts as signers.
59
+ - `SystemAccount` / `Mut<SystemAccount>`: Anchor's `SystemAccount`, refused with 3011 (AccountNotSystemOwned) when
60
+ another program owns it.
61
+ - `Vec<string>` instruction arguments (Anchor's `Vec<String>`): `.get(i)`, `.length`, `for … of`, list callbacks,
62
+ helper parameters, storing into `FixedVec<FixedString<N>, M>`, and re-encoding through `invoke`; each string is
63
+ checked as UTF-8 on entry (102).
64
+ - Helpers and instructions can return `string` (an instruction's is Borsh String return data).
65
+ - `acct.key` of an `Init<…>` account before it is created (the address is known; Anchor seeds use it that way).
66
+ - `mint.create(…, tokenProgram)` / `vault.create(…, tokenProgram)`: an explicit token program, for instructions that
67
+ take more than one.
68
+
69
+ Language:
70
+ - Closures: a local function that reads or writes the enclosing function's variables is expanded where it is called,
71
+ with the names it was declared with (a block at the call that declares the same name does not change it), struct
72
+ parameters by reference and `return` leaving the closure. Recursion and storing / returning closures are refused.
73
+ - List callbacks on Vec, FixedArray, FixedVec and Vec arguments: `forEach`, `some`, `every`, `find`, `findIndex`,
74
+ `reduce`, `includes`, `indexOf`, expanded into loops with the arrow written at the call.
75
+ - Helpers can take function parameters (`f: (x: u64) => u64`): compiled once per named function passed.
76
+ - `do … while` loops (`continue` goes to the test).
77
+ - `x **= n` (the target must not call a function: it is read and written).
78
+ - `switch` on 128-bit integers (the case is picked with comparisons, then switched on as usual, fallthrough included).
79
+ - `?.` on optional accounts (`acct?.field`) and on `Option<struct>` fields (`x.home?.field`): an Option, empty when the
80
+ account was left out or the option is empty; use it with `??`.
81
+ - `acct!` on optional accounts (fails with 5115 when it was left out), Pubkeys / Hashes in template strings (base58),
82
+ and data-enum arguments whose variants carry fixed-size structs or arrays.
83
+ - Type parameters standing for fixed-size structs (`pick<Point>(…)`, or taken from struct arguments), and `cond ? a : b`
84
+ for struct values (places, literals or calls).
85
+ - Growable `Vec<T>` `push` / `pop` / `clear` inside helpers that take the account as `Mut<T>` (with its payer and
86
+ SystemProgram, as in instructions).
87
+ - `mulDiv` / `mulDivCeil` on u128 through a 256-bit product (all three u128; literals take that type).
88
+
89
+ CPI:
90
+ - Token-2022 extension instructions: `updateDefaultAccountState`, `updateTransferHook`, `enableRequiredMemoTransfers` /
91
+ `disableRequiredMemoTransfers`, `updateInterestRate`, `transferCheckedWithFee`, `harvestWithheldTokensToMint`,
92
+ `withdrawWithheldTokensFromMint`, `setTransferFee`; the Token Metadata interface: `updateMetadataField`,
93
+ `removeMetadataKey`, `updateMetadataAuthority`, `emitMetadata`; `closeAccount` of a mint; `mintTo` /
94
+ `initializeMetadata` on a mint created earlier in the instruction; `isTransferring(tokenAccount)` and
95
+ `transferHookAuthority(mint)`.
96
+ - Metaplex Core plugin values: MasterEdition, VerifiedCreators, Autograph, FreezeExecute, PermanentFreezeExecute, and
97
+ royalty rule sets (`ruleSet: { programAllowList | programDenyList }`).
98
+ - Bubblegum: `delegateV2`, `freezeV2`, `thawV2`, `setNonTransferableV2`, `updateMetadataV2`, `verifyCreatorV2`,
99
+ `unverifyCreatorV2`, `setCollectionV2`, and the V1 `transfer`, `burn`, `delegate`. Checked on the real programs with
100
+ every leaf recomputed: verify → delegate → freeze (a frozen leaf is refused) → thaw → rename, unverify, moving a leaf
101
+ into a collection, soulbinding a collection's leaf, and a V1 transfer signed by a PDA owner, delegate and burn.
102
+
103
+ ### Fixed
104
+ - Bubblegum leaf fields that may be null at run time (`assetDataHash: Hash | null`, `flags`) are sent as Borsh Options;
105
+ a null aborted the instruction before.
106
+
8
107
  ## 0.17.0 — 2026-10-28
9
108
 
10
109
  Anchor's account bytes on request (`@layout borsh`, so a TypeSOL program can take over an Anchor program's accounts
package/README.md CHANGED
@@ -189,10 +189,10 @@ Runtime failures point back to your source: `Error 6000 Unauthorized (counter.ts
189
189
 
190
190
  | | |
191
191
  | --- | --- |
192
- | **Accounts** | `Signer`, `Account<T>`, `Mut<T>`, `Init<T>`, `InitIfNeeded<T>` (PDAs and associated token accounts), `Zeroed<T>` (up to 10 MiB), optional accounts (`A \| null`), token accounts and mints, programs, sysvars |
192
+ | **Accounts** | `Signer`, `Account<T>`, `Mut<T>`, `Init<T>`, `InitIfNeeded<T>` (PDAs or keypair accounts, with Anchor's `space`; mints, token accounts and associated token accounts), `Zeroed<T>` (up to 10 MiB), optional accounts (`A \| null`), token accounts and mints, `SystemAccount`, programs, sysvars |
193
193
  | **Constraints** | `HasOne`, `Seeds` (also from account fields and under another program; `pdaBump` like `ctx.bumps`), `CloseTo`, `Ata`, `Address`, `Owner`, `Executable`. They are checked before the body runs and recorded in the IDL |
194
- | **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account, `FixedVec` lists anywhere, and Anchor's own byte layout (`@layout borsh`) to take over existing Anchor accounts |
195
- | **Control flow** | `if`, `for`, `for … of`, `while`, `switch` (with narrowing on data enums), ternaries, `??`, destructuring, helper functions (returning structs too, arrow functions, generics) across multiple files |
194
+ | **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account, `FixedVec` lists anywhere, and Anchor's own byte layout (`@layout borsh`) to take over existing Anchor accounts and to pass, return and emit structs with String / Vec / Option fields as Anchor does |
195
+ | **Control flow** | `if`, `for`, `for … of`, `while`, `switch` (with narrowing on data enums), ternaries, `??`, destructuring, helper functions (returning structs too, arrow functions, closures, function arguments, generics), list callbacks (`some`, `find`, `reduce`, …) across multiple files |
196
196
  | **CPIs** | System, SPL Token, Token-2022 (including extension reads and metadata), ATA, Metaplex Token Metadata, Core, Bubblegum. Generic `invoke` with PDA signing, return data, and typed bindings generated from any Anchor IDL (`typesol cpi`) |
197
197
  | **Events** | `emit` (log-based) and `emitCpi` (Anchor `emit_cpi!`, through a self-CPI) |
198
198
  | **Crypto & oracles** | `sha256`, `keccak256`, Merkle proofs, Ed25519 and secp256k1 signature checks, Pyth pull oracle, Switchboard |
@@ -306,7 +306,7 @@ Details are in [docs/testing.md](docs/testing.md).
306
306
 
307
307
  ## Status
308
308
 
309
- **TypeSOL is pre-1.0 (0.17).** Releases follow the organization's
309
+ **TypeSOL is pre-1.0 (0.19).** Releases follow the organization's
310
310
  [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
311
311
  change.
312
312
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
package/dist/clientgen.js CHANGED
@@ -39,6 +39,7 @@ export const TYPESOL_ERRORS = {
39
39
  2015: "ConstraintTokenOwner",
40
40
  2016: "ConstraintMintMintAuthority",
41
41
  2017: "ConstraintMintFreezeAuthority",
42
+ 2018: "ConstraintMintDecimals",
42
43
  2040: "ConstraintDuplicateMutableAccount",
43
44
  3000: "AccountDiscriminatorAlreadySet",
44
45
  3002: "AccountDiscriminatorMismatch",
@@ -50,6 +51,7 @@ export const TYPESOL_ERRORS = {
50
51
  3008: "InvalidProgramId",
51
52
  3009: "InvalidProgramExecutable",
52
53
  3010: "AccountNotSigner",
54
+ 3011: "AccountNotSystemOwned",
53
55
  3012: "AccountNotInitialized",
54
56
  3016: "AccountReallocExceedsLimit",
55
57
  4102: "InvalidNumericConversion",
@@ -12,7 +12,7 @@ export declare const MAX_CPI_ALLOCATION: number;
12
12
  export interface Analysis {
13
13
  model: ProgramModel;
14
14
  /** A generic helper's instance for these type arguments (analyzed once, then kept in `model.helpers`). */
15
- instantiate(g: GenericHelper, typeArgs: ScalarType[]): Helper;
15
+ instantiate(g: GenericHelper, typeArgs: TypeArg[], fnArgs?: string[]): Helper;
16
16
  /** TypeScript's checker: used for narrowing (which variant a data-enum value is in a branch). */
17
17
  checker: ts.TypeChecker;
18
18
  sourceFile: ts.SourceFile;
@@ -22,11 +22,20 @@ export interface Analysis {
22
22
  imports: Map<string, string>;
23
23
  reporter: Reporter;
24
24
  }
25
+ /** What a generic helper's type parameter stands for: a scalar, or a fixed-size struct. */
26
+ export type TypeArg = ScalarType | {
27
+ kind: "struct";
28
+ name: string;
29
+ };
30
+ /** A struct named by a type reference, through the type parameters of a generic helper's instance. */
31
+ export declare function structNameOf(node: ts.TypeNode, scope: Pick<TypeScope, "structArgs">): string | undefined;
25
32
  /** What type resolution needs: imports, unit enums, and where to record Option<T> uses. */
26
33
  export interface TypeScope {
27
34
  imports: Map<string, string>;
28
35
  /** Inside a generic helper's instance: type parameter -> the type it stands for. */
29
36
  typeArgs?: Map<string, ScalarType>;
37
+ /** Inside a generic helper's instance: type parameter -> the struct it stands for. */
38
+ structArgs?: Map<string, string>;
30
39
  enums: Map<string, EnumDef>;
31
40
  dataEnums?: Map<string, DataEnumDef>;
32
41
  options: Map<string, ScalarType | {
@@ -77,6 +77,8 @@ const TYPE_EXPORTS = new Set([
77
77
  "TokenMetadataProgram",
78
78
  "SwitchboardFeed",
79
79
  "HasOne",
80
+ "TokenOf",
81
+ "MintOf",
80
82
  "Seeds",
81
83
  "CloseTo",
82
84
  "Address",
@@ -90,6 +92,7 @@ const TYPE_EXPORTS = new Set([
90
92
  "SelfProgram",
91
93
  "ProgramData",
92
94
  "SignerSeeds",
95
+ "SystemAccount",
93
96
  ]);
94
97
  /** Largest fixed array a struct may declare, and largest FixedString. */
95
98
  const MAX_ARRAY_LEN = 1_000_000;
@@ -99,6 +102,12 @@ export const MAX_ACCOUNT_SIZE = 10 * 1024 * 1024;
99
102
  /** sol_set_return_data's limit. */
100
103
  export const MAX_RETURN_DATA = 1024;
101
104
  export const MAX_CPI_ALLOCATION = 10 * 1024;
105
+ /** A struct named by a type reference, through the type parameters of a generic helper's instance. */
106
+ export function structNameOf(node, scope) {
107
+ if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName) || node.typeArguments?.length)
108
+ return undefined;
109
+ return scope.structArgs?.get(node.typeName.text) ?? node.typeName.text;
110
+ }
102
111
  export function snakeCase(name) {
103
112
  return name
104
113
  .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
@@ -424,23 +433,35 @@ export function analyze(entry) {
424
433
  if (h.node.body)
425
434
  collectLocals(h.node.body, h.name, h.imports, h.node);
426
435
  const localDecls = new Set(locals.map((l) => l.decl));
427
- for (const l of locals)
428
- reporter.guard(() => checkCaptures(l, localDecls, checker, reporter));
436
+ // A local function that reads the enclosing function's variables (or calls one that does) is a closure: it is
437
+ // expanded where it is called, in the enclosing function. The others become helpers of their own.
438
+ const inline = new Set(locals.filter((l) => captures(l, localDecls, checker)).map((l) => l.decl));
439
+ for (let changed = true; changed;) {
440
+ changed = false;
441
+ for (const l of locals)
442
+ if (!inline.has(l.decl) && references(l, inline, checker)) {
443
+ inline.add(l.decl);
444
+ changed = true;
445
+ }
446
+ }
447
+ const closureDecls = new Map(locals.filter((l) => inline.has(l.decl)).map((l) => [l.decl, l.display]));
429
448
  const helpers = new Map();
430
449
  const generics = new Map();
431
450
  const helperDecls = new Map();
432
- for (const fn of [...helperNodes, ...locals]) {
451
+ for (const fn of [...helperNodes, ...locals.filter((l) => !inline.has(l.decl))]) {
433
452
  helperDecls.set(fn.decl, fn.name);
434
- if (fn.node.typeParameters?.length) {
453
+ const fnParams = fn.node.parameters.map((p, i) => (p.type && ts.isFunctionTypeNode(p.type) ? i : -1)).filter((i) => i >= 0);
454
+ if (fn.node.typeParameters?.length || fnParams.length) {
435
455
  reporter.guard(() => {
436
- for (const tp of fn.node.typeParameters)
456
+ for (const tp of fn.node.typeParameters ?? [])
437
457
  if (tp.default)
438
458
  reporter.fail(tp, "type parameters of helpers take no default");
439
459
  generics.set(fn.name, {
440
460
  name: fn.name,
441
461
  display: fn.display,
442
462
  node: fn.node,
443
- typeParams: fn.node.typeParameters.map((t) => t.name.text),
463
+ typeParams: (fn.node.typeParameters ?? []).map((t) => t.name.text),
464
+ fnParams,
444
465
  imports: fn.imports,
445
466
  });
446
467
  });
@@ -451,20 +472,34 @@ export function analyze(entry) {
451
472
  helpers.set(h.name, h);
452
473
  }
453
474
  const instances = new Map();
454
- const instantiate = (g, typeArgs) => {
455
- const key = `${g.name}__${typeArgs.map(scalarKey).join("_")}`;
475
+ const instantiate = (g, typeArgs, fnArgs = []) => {
476
+ const argKey = (t) => (t.kind === "struct" ? `S${t.name}` : scalarKey(t));
477
+ const key = [g.name, ...typeArgs.map(argKey), ...fnArgs.map((f) => `fn_${f}`)].join("__");
456
478
  const done = instances.get(key);
457
479
  if (done)
458
480
  return done;
459
- const bound = new Map(g.typeParams.map((n, i) => [n, typeArgs[i]]));
460
- const display = `${g.display}<${typeArgs.map(typeName).join(", ")}>`;
461
- const h = analyzeHelper(g.node, key, display, reporter, { ...scopeFor(g.imports), typeArgs: bound }, structs);
481
+ const bound = new Map();
482
+ const boundStructs = new Map();
483
+ g.typeParams.forEach((n, i) => {
484
+ const t = typeArgs[i];
485
+ if (t.kind === "struct")
486
+ boundStructs.set(n, t.name);
487
+ else
488
+ bound.set(n, t);
489
+ });
490
+ const display = `${g.display}<${[...typeArgs.map((t) => (t.kind === "struct" ? t.name : typeName(t))), ...fnArgs].join(", ")}>`;
491
+ const h = analyzeHelper(g.node, key, display, reporter, { ...scopeFor(g.imports), typeArgs: bound, structArgs: boundStructs }, structs, new Set(g.fnParams));
462
492
  h.typeArgs = bound;
493
+ h.structArgs = boundStructs;
494
+ if (g.fnParams.length) {
495
+ h.fnArgs = new Map(g.fnParams.map((i, k) => [g.node.parameters[i].name.text, fnArgs[k]]));
496
+ h.fnArgIndexes = g.fnParams;
497
+ }
463
498
  instances.set(key, h);
464
499
  helpers.set(key, h);
465
500
  return h;
466
501
  };
467
- reporter.guard(() => checkRecursion([...helperNodes, ...locals], helperDecls, checker, reporter));
502
+ reporter.guard(() => checkRecursion([...helperNodes, ...locals.filter((l) => !inline.has(l.decl))], helperDecls, checker, reporter));
468
503
  const accountStructs = new Set();
469
504
  const argStructs = new Set();
470
505
  const instructions = [];
@@ -478,26 +513,29 @@ export function analyze(entry) {
478
513
  for (const a of ix.args) {
479
514
  const seen = new Set();
480
515
  collectStructs(a.type, structs, dataEnums, seen);
516
+ // A @layout borsh struct is decoded as a whole argument; inside lists or enums it would need a fixed size.
517
+ const whole = a.type.kind === "structRef" ? a.type.struct : undefined;
481
518
  for (const s of seen)
482
- if (structs.get(s).layout === "borsh")
483
- reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} is @layout borsh, an account layout only`);
519
+ if (structs.get(s).layout === "borsh" && s !== whole)
520
+ reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} is @layout borsh; pass it as a whole argument, not inside a list or enum`);
484
521
  }
485
- if (ix.returns?.kind === "struct" && structs.get(ix.returns.name).layout === "borsh")
486
- reporter.error(fn.node.type ?? fn.node, `${ix.returns.name} is @layout borsh, an account layout only; return another struct`);
487
- // Arguments are Borsh: a FixedVec's padded layout is account data only.
522
+ // Arguments are Borsh: a FixedVec's padded layout is account data only (a @layout borsh struct's FixedVec is a Borsh Vec).
488
523
  for (const a of ix.args) {
489
524
  const seen = new Set();
490
525
  collectStructs(a.type, structs, dataEnums, seen);
491
526
  for (const s of seen)
492
- if (structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
527
+ if (structs.get(s).layout !== "borsh" && structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
493
528
  reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; pass a Vec<T> argument instead`);
494
529
  }
495
530
  // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
496
531
  if (ix.returns?.kind === "struct")
497
532
  collectStructs(ix.returns, structs, dataEnums, argStructs);
498
533
  for (const a of ix.accounts)
499
- if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded")
534
+ if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded") {
500
535
  accountStructs.add(a.kind.struct.name);
536
+ if (a.kind.struct.viewsOnly)
537
+ reporter.error(a.node, `${a.kind.struct.name} holds a string (or an enum carrying one), so it is an argument, return value or event; account data uses FixedString<N>`);
538
+ }
501
539
  }
502
540
  checkSnakeDuplicates(instructions.map((ix) => ({ name: ix.node.name.text, node: ix.node.name })), "instructions", reporter);
503
541
  // emitCpi's event instruction (Anchor's EVENT_IX_TAG) is dispatched before every other instruction.
@@ -530,6 +568,7 @@ export function analyze(entry) {
530
568
  helpers,
531
569
  generics,
532
570
  helperDecls,
571
+ closureDecls,
533
572
  errors,
534
573
  errorEnums,
535
574
  enums,
@@ -585,7 +624,7 @@ export function resolveArgAggregate(node, scope, structs) {
585
624
  if (scalar)
586
625
  return scalar;
587
626
  if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && !t.typeArguments?.length) {
588
- const s = structs.get(t.typeName.text);
627
+ const s = structs.get(structNameOf(t, scope));
589
628
  if (s && s.size !== undefined && !s.vec)
590
629
  return { kind: "struct", name: s.name };
591
630
  }
@@ -668,6 +707,9 @@ export function resolveValueType(node, scope, allowVec = false) {
668
707
  return { kind: "dataEnumArg", name: node.typeName.text };
669
708
  if (allowVec && typesolName(node, scope.imports) === "Vec") {
670
709
  const elemNode = node.typeArguments?.[0];
710
+ // Vec<string>: Borsh Vec<String>, read as views of the instruction data.
711
+ if (elemNode?.kind === ts.SyntaxKind.StringKeyword)
712
+ return { kind: "vecArg", elem: { kind: "string" } };
671
713
  const elem = elemNode && resolveScalarType(elemNode, scope);
672
714
  if (elem)
673
715
  return { kind: "vecArg", elem };
@@ -935,7 +977,11 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
935
977
  state.set(s.name, "visiting");
936
978
  let total = 0;
937
979
  for (const f of s.fields) {
938
- const fsz = sizeOf(f.type, s.node);
980
+ // In a @layout borsh struct a string (or a data enum carrying one) is held as a TS_Str view: a C size, no layout.
981
+ const view = s.layout === "borsh" && (f.type.kind === "string" || (f.type.kind === "dataEnum" && sizeOf(f.type, s.node) === undefined));
982
+ if (view)
983
+ s.viewsOnly = true;
984
+ const fsz = view ? (f.type.kind === "string" ? 16 : 1 + dataEnums.get(f.type.name).payload) : sizeOf(f.type, s.node);
939
985
  total = total === undefined || fsz === undefined ? undefined : total + fsz;
940
986
  }
941
987
  s.size = total;
@@ -954,19 +1000,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
954
1000
  reporter.guard(() => visit(s));
955
1001
  for (const s of structs.values())
956
1002
  if (s.layout === "borsh")
957
- reporter.guard(() => checkBorshLayout(s, structs, reporter));
1003
+ reporter.guard(() => checkBorshLayout(s, structs, dataEnums, reporter));
958
1004
  for (const e of dataEnums.values())
959
1005
  reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
960
1006
  return structs;
961
1007
  }
962
1008
  const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
963
- /**
964
- * A local function becomes a helper of its own, so it can only use its parameters, its own locals, top-level names and
965
- * other local functions: reading a variable of the enclosing function (a closure) is refused.
966
- */
967
- function checkCaptures(l, localDecls, checker, reporter) {
1009
+ /** Whether a local function reads a variable or parameter of a function around it (a closure). */
1010
+ function captures(l, localDecls, checker) {
968
1011
  const fn = l.node;
1012
+ let found = false;
969
1013
  const visit = (n) => {
1014
+ if (found)
1015
+ return;
970
1016
  if (ts.isIdentifier(n)) {
971
1017
  const sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n);
972
1018
  for (const d of sym?.declarations ?? []) {
@@ -974,30 +1020,53 @@ function checkCaptures(l, localDecls, checker, reporter) {
974
1020
  continue;
975
1021
  if (d.getSourceFile() !== fn.getSourceFile() || (d.pos >= fn.pos && d.end <= fn.end))
976
1022
  continue;
977
- // Declared in some function around this one: a captured variable.
978
1023
  for (let p = d.parent; p; p = p.parent)
979
- if (isFunctionNode(p)) {
980
- reporter.fail(n, `${l.display} reads "${n.text}" of the function around it; pass it as a parameter (local functions capture nothing)`);
981
- }
1024
+ if (isFunctionNode(p))
1025
+ found = true;
982
1026
  }
983
1027
  }
984
1028
  ts.forEachChild(n, visit);
985
1029
  };
986
1030
  if (fn.body)
987
1031
  visit(fn.body);
1032
+ return found;
1033
+ }
1034
+ /** Whether a function's body names one of `decls` (a closure it calls). */
1035
+ function references(l, decls, checker) {
1036
+ let found = false;
1037
+ const visit = (n) => {
1038
+ if (found)
1039
+ return;
1040
+ if (ts.isIdentifier(n))
1041
+ for (const d of checker.getSymbolAtLocation(n)?.declarations ?? [])
1042
+ if (decls.has(d))
1043
+ found = true;
1044
+ ts.forEachChild(n, visit);
1045
+ };
1046
+ if (l.node.body)
1047
+ visit(l.node.body);
1048
+ return found;
988
1049
  }
989
1050
  /**
990
1051
  * A `@layout borsh` struct: its own fields may be FixedString (Borsh String), FixedVec (Borsh Vec) and Option; anything
991
1052
  * nested must already be Borsh-identical (fixed-size scalars, Pubkeys, enums, arrays and structs of them).
992
1053
  */
993
- function checkBorshLayout(s, structs, reporter) {
1054
+ function checkBorshLayout(s, structs, dataEnums, reporter) {
994
1055
  const model = { structs };
995
1056
  const fixed = (t) => borshIdentical(t, model);
996
1057
  for (const f of s.fields) {
997
1058
  const t = f.type;
998
1059
  const at = s.node.members.find((m) => m.name && ts.isIdentifier(m.name) && m.name.text === f.name) ?? s.node;
999
- if (fixed(t) || t.kind === "fstring")
1060
+ if (fixed(t) || t.kind === "fstring" || t.kind === "string")
1000
1061
  continue;
1062
+ if (t.kind === "dataEnum") {
1063
+ // Borsh enum: tag, then the variant's fields; each a fixed-size value or a string.
1064
+ for (const v of dataEnums.get(t.name)?.variants ?? [])
1065
+ for (const vf of v.fields)
1066
+ if (!fixed(vf.type) && vf.type.kind !== "string")
1067
+ reporter.fail(at, `${t.name}.${v.name}.${vf.name} in a @layout borsh struct: enum variants hold fixed-size values and strings`);
1068
+ continue;
1069
+ }
1001
1070
  if (t.kind === "vec" && t.cap === undefined)
1002
1071
  reporter.fail(at, "a @layout borsh struct cannot end in a growable Vec<T>; use FixedVec<T, N> (Anchor's #[max_len(N)] Vec)");
1003
1072
  if (t.kind === "vec" && (fixed(t.elem) || t.elem.kind === "fstring"))
@@ -1015,7 +1084,7 @@ function checkBorshLayout(s, structs, reporter) {
1015
1084
  reporter.error(s.node.name, `${s.name} is @layout borsh, so it is account data only; ${other.name}.${f.name} cannot hold one`);
1016
1085
  }
1017
1086
  }
1018
- function analyzeHelper(fn, name, display, reporter, scope, structs) {
1087
+ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new Set()) {
1019
1088
  if (!fn.body)
1020
1089
  reporter.fail(fn, "helper functions need a body");
1021
1090
  if (fn.asteriskToken)
@@ -1024,16 +1093,23 @@ function analyzeHelper(fn, name, display, reporter, scope, structs) {
1024
1093
  reporter.fail(fn, "helper functions cannot be async");
1025
1094
  if (!fn.type)
1026
1095
  reporter.fail(fn.name ?? fn, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
1027
- let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
1096
+ let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword
1097
+ ? undefined
1098
+ : fn.type.kind === ts.SyntaxKind.StringKeyword
1099
+ ? { kind: "string" }
1100
+ : resolveScalarType(fn.type, scope);
1028
1101
  if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
1029
- const s = ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments ? structs.get(fn.type.typeName.text) : undefined;
1102
+ const sn = structNameOf(fn.type, scope);
1103
+ const s = sn ? structs.get(sn) : undefined;
1030
1104
  if (s && (s.size === undefined || s.vec))
1031
1105
  reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
1032
1106
  if (!s)
1033
- reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, a struct or a type parameter");
1107
+ reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, string, a struct or a type parameter");
1034
1108
  returns = { kind: "struct", name: s.name };
1035
1109
  }
1036
- const params = fn.parameters.map((p) => {
1110
+ const params = fn.parameters
1111
+ .filter((_, i) => !skip.has(i))
1112
+ .map((p) => {
1037
1113
  if (!ts.isIdentifier(p.name) || !p.type || p.initializer || p.questionToken || p.dotDotDotToken)
1038
1114
  reporter.fail(p, "parameters need a plain name and an explicit type");
1039
1115
  // `p_<name>__n` carries a SignerSeeds count: "__" in a name could collide with it.
@@ -1046,7 +1122,13 @@ function analyzeHelper(fn, name, display, reporter, scope, structs) {
1046
1122
  return { name: p.name.text, type: { kind: "signerSeeds" }, node: p };
1047
1123
  // Accounts the caller already checked: Signer, AccountInfo, Account<T>/Mut<T>, token accounts, programs.
1048
1124
  const wrapped = typesolName(p.type, scope.imports);
1049
- if (wrapped === "HasOne" || wrapped === "Seeds" || wrapped === "CloseTo" || wrapped === "Address" || wrapped === "Ata")
1125
+ if (wrapped === "HasOne" ||
1126
+ wrapped === "TokenOf" ||
1127
+ wrapped === "MintOf" ||
1128
+ wrapped === "Seeds" ||
1129
+ wrapped === "CloseTo" ||
1130
+ wrapped === "Address" ||
1131
+ wrapped === "Ata")
1050
1132
  reporter.fail(p.type, "constraints belong on instruction parameters; helpers take the plain account type");
1051
1133
  const account = resolveAccountKind(p.type, reporter, scope.imports, structs);
1052
1134
  if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed")
@@ -1120,7 +1202,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1120
1202
  reporter.fail(fn, "instructions cannot be async or generators");
1121
1203
  let returns;
1122
1204
  if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword) {
1123
- returns = resolveScalarType(fn.type, scope);
1205
+ returns = fn.type.kind === ts.SyntaxKind.StringKeyword ? { kind: "string" } : resolveScalarType(fn.type, scope);
1124
1206
  const s = !returns && ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments
1125
1207
  ? structs.get(fn.type.typeName.text)
1126
1208
  : undefined;
@@ -1133,7 +1215,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1133
1215
  returns = { kind: "struct", name: s.name };
1134
1216
  }
1135
1217
  if (!returns)
1136
- reporter.fail(fn.type, "instructions return void, one integer, boolean or Pubkey, or a fixed-size struct (sent as return data)");
1218
+ reporter.fail(fn.type, "instructions return void, one integer, boolean, Pubkey or string, or a fixed-size struct (sent as return data)");
1137
1219
  }
1138
1220
  const accounts = [];
1139
1221
  const args = [];
@@ -1173,7 +1255,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1173
1255
  const inner = unwrapConstraints(typeNode, reporter, imports, constraints);
1174
1256
  const kind = resolveAccountKind(inner, reporter, imports, structs);
1175
1257
  if (optional &&
1176
- (["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) || (kind.kind === "token" && kind.init)))
1258
+ (["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
1259
+ ((kind.kind === "token" || kind.kind === "mint") && kind.init)))
1177
1260
  reporter.fail(p, "Init, InitIfNeeded, Zeroed, PythPriceUpdate, EventAuthority, SelfProgram and ProgramData accounts cannot be optional");
1178
1261
  accounts.push({ name: p.name.text, index: accounts.length, kind, node: p, constraints, ...(optional ? { optional } : {}) });
1179
1262
  });
@@ -1186,15 +1269,26 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1186
1269
  // Constraints refer to other parameters by name: check them now that all are known.
1187
1270
  for (const a of accounts) {
1188
1271
  const c = a.constraints;
1189
- if (a.kind.kind === "token" && a.kind.init && !c.ata)
1190
- reporter.fail(a.node, 'Init<TokenAccount> / InitIfNeeded<TokenAccount> create associated token accounts: wrap them in Ata<…, "owner", "mint">');
1191
- if (a.kind.kind === "token" && a.kind.init && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
1272
+ if (a.kind.kind === "token" && a.kind.init && c.ata && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
1192
1273
  reporter.fail(a.node, "an associated token account created here is checked against Ata<…>'s owner and mint; drop HasOne/Seeds/Address/Owner/Executable");
1274
+ // Token accounts and mints created in the body are checked there (seeds, mint, authorities).
1275
+ if ((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init && !c.ata && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
1276
+ reporter.fail(a.node, `${a.name}.create(...) takes the seeds, mint and authorities; drop HasOne/Seeds/Address/Owner/Executable`);
1193
1277
  if (!c.hasOne.length && !c.seeds && !c.closeTo && !c.ata)
1194
1278
  continue;
1195
1279
  const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
1196
1280
  reporter.guard(() => {
1197
- for (const f of c.hasOne) {
1281
+ for (const entry of c.hasOne) {
1282
+ const [f, param = f] = entry.split("=");
1283
+ if (entry.includes("=")) {
1284
+ if (!accounts.some((o) => o.name === param))
1285
+ reporter.fail(a.node, `add an account parameter named "${param}"`);
1286
+ if (a.kind.kind === "token" ? f !== "mint" && f !== "owner" : a.kind.kind === "mint" ? false : true)
1287
+ reporter.fail(a.node, a.kind.kind === "token" || a.kind.kind === "mint" ? "TokenOf<…> applies to a TokenAccount" : "TokenOf / MintOf apply to a TokenAccount / Mint");
1288
+ if (a.kind.kind === "mint" && f !== "mintAuthority" && f !== "freezeAuthority")
1289
+ reporter.fail(a.node, "MintOf<…> applies to a Mint");
1290
+ continue;
1291
+ }
1198
1292
  if (!accounts.some((o) => o.name === f))
1199
1293
  reporter.fail(a.node, `HasOne<…, "${f}">: add an account parameter named "${f}"`);
1200
1294
  if (a.kind.kind === "token") {
@@ -1245,7 +1339,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1245
1339
  for (const a of accounts) {
1246
1340
  const c = a.constraints;
1247
1341
  const refs = [
1248
- ...c.hasOne,
1342
+ ...c.hasOne.map((e) => e.split("=").at(-1)),
1249
1343
  ...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1).split(".")[0]),
1250
1344
  ...(c.seeds?.program?.startsWith("@") ? [c.seeds.program.slice(1)] : []),
1251
1345
  ...(c.closeTo ? [c.closeTo] : []),
@@ -1305,7 +1399,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1305
1399
  /** Peel HasOne / Seeds / CloseTo wrappers off a parameter type, recording what they ask for. */
1306
1400
  function unwrapConstraints(node, reporter, imports, out) {
1307
1401
  const name = typesolName(node, imports);
1308
- if (!["HasOne", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
1402
+ if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
1309
1403
  return node;
1310
1404
  const args = node.typeArguments ?? [];
1311
1405
  const lit = (t) => {
@@ -1319,6 +1413,20 @@ function unwrapConstraints(node, reporter, imports, out) {
1319
1413
  };
1320
1414
  if (name === "HasOne")
1321
1415
  out.hasOne.push(...lit(args[1]));
1416
+ // Anchor's `token::mint = m, token::authority = a` / `mint::authority = a, mint::freeze_authority = f` naming the
1417
+ // parameters: recorded as "field=param" (`never` leaves one out).
1418
+ if (name === "TokenOf" || name === "MintOf") {
1419
+ const [first, second] = name === "TokenOf" ? ["mint", "owner"] : ["mintAuthority", "freezeAuthority"];
1420
+ const one = (t) => (t && t.kind !== ts.SyntaxKind.NeverKeyword ? lit(t)[0] : undefined);
1421
+ const p1 = one(args[1]);
1422
+ const p2 = one(args[2]);
1423
+ if (!p1 && !p2)
1424
+ reporter.fail(node, `${name}<…> names at least one parameter`);
1425
+ if (p1)
1426
+ out.hasOne.push(`${first}=${p1}`);
1427
+ if (p2)
1428
+ out.hasOne.push(`${second}=${p2}`);
1429
+ }
1322
1430
  if (name === "CloseTo")
1323
1431
  out.closeTo = lit(args[1])[0];
1324
1432
  if (name === "Owner") {
@@ -1381,6 +1489,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1381
1489
  // Any account (helpers that accept every kind, e.g. generated CPI helpers).
1382
1490
  case "AccountMeta":
1383
1491
  return { kind: "info", mut: false };
1492
+ case "SystemAccount":
1493
+ return { kind: "info", mut: false, systemOwned: true };
1384
1494
  case "TokenAccount":
1385
1495
  return { kind: "token", mut: false };
1386
1496
  case "Mint":
@@ -1438,9 +1548,11 @@ function resolveAccountKind(node, reporter, imports, structs) {
1438
1548
  case "Init": {
1439
1549
  if (typesolName(inner(), imports) === "TokenAccount")
1440
1550
  return { kind: "token", mut: true, init: "create" };
1551
+ if (typesolName(inner(), imports) === "Mint")
1552
+ return { kind: "mint", mut: true, init: "create" };
1441
1553
  const s = structOf(inner());
1442
1554
  if (!s)
1443
- reporter.fail(node, "Init<T> needs a struct declared in this program");
1555
+ reporter.fail(node, "Init<T> needs a struct declared in this program, TokenAccount or Mint");
1444
1556
  if (s.size + 8 > MAX_CPI_ALLOCATION)
1445
1557
  reporter.fail(node, `${s.name} needs ${s.size + 8} bytes but a program can create at most ${MAX_CPI_ALLOCATION} through Init<T>; have the client allocate it and take it as Zeroed<${s.name}>`);
1446
1558
  return { kind: "init", struct: s };
@@ -1448,6 +1560,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1448
1560
  case "InitIfNeeded": {
1449
1561
  if (typesolName(inner(), imports) === "TokenAccount")
1450
1562
  return { kind: "token", mut: true, init: "ifNeeded" };
1563
+ if (typesolName(inner(), imports) === "Mint")
1564
+ return { kind: "mint", mut: true, init: "ifNeeded" };
1451
1565
  const s = structOf(inner());
1452
1566
  if (!s)
1453
1567
  reporter.fail(node, "InitIfNeeded<T> needs a struct declared in this program");
@@ -1475,6 +1589,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1475
1589
  return { kind: "signer", mut: true };
1476
1590
  if (innerName === "AccountInfo" || innerName === "AccountMeta")
1477
1591
  return { kind: "info", mut: true };
1592
+ if (innerName === "SystemAccount")
1593
+ return { kind: "info", mut: true, systemOwned: true };
1478
1594
  if (innerName === "TokenAccount")
1479
1595
  return { kind: "token", mut: true };
1480
1596
  if (innerName === "Mint")
@@ -0,0 +1 @@
1
+ export declare const ANCHOR_ERRORS: Record<string, number>;