@typesol/cli 0.12.1 → 0.13.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,53 @@ 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.13.0 — 2026-10-27
9
+
10
+ Every program in solana-developers/program-examples now ports to TypeSOL: PDA seeds from account fields, remaining
11
+ accounts passed on in CPIs, transfer hooks with extra accounts, `findProgramAddress` and marker accounts. Not externally
12
+ audited (see SECURITY.md).
13
+
14
+ ### Added
15
+ - Transfer hooks with extra accounts: `initExtraAccountMetas(payer, account, mint, systemProgram, extraAccounts)` writes
16
+ an ExtraAccountMetaList (fixed addresses, PDAs of this program or of another program's account index, with literal,
17
+ account-key, account-data and instruction-data seeds), byte for byte what spl-tlv-account-resolution writes; Token-2022
18
+ checks the extra accounts against it on every transfer. `updateExtraAccountMetas(...)` rewrites and resizes a list.
19
+ - Remaining accounts in a CPI: spread them into `invoke`'s account list with the flags they were passed with
20
+ (`...remainingAccounts()`, Rust's `extend_from_slice`) or all with one role (`...readonly(rest)`,
21
+ `...writable(rest)`), and take part of them with `rest.slice(a, b)`. A CPI lists at most 32 accounts (5113); a slice
22
+ out of range fails with 5104.
23
+ - `findProgramAddress(seeds, program)`: the canonical PDA of another program at runtime.
24
+ - PDA seeds from account fields: `Seeds<…, ["@pool.amm", "@mintA", "authority"]>` takes a Pubkey, integer or unit
25
+ enum field of a struct account parameter, including the account's own (`Seeds<Mut<Offer>, ["offer", "@maker",
26
+ "@offer.id"], "bump">`), like Anchor's `seeds = [pool.amm.as_ref(), …]`. Recorded in the IDL as account paths
27
+ (`pool.amm`).
28
+ - Structs with no fields, for marker accounts that are only a discriminator (Anchor `#[account] pub struct
29
+ NftAuthority {}`).
30
+
31
+ ### Fixed
32
+ - An associated token account created in the instruction (`Ata<Init|InitIfNeeded<TokenAccount>>`) refuses optional
33
+ TokenProgram / AssociatedTokenProgram / SystemProgram parameters at compile time (they used to fail at runtime with an
34
+ access violation when left out), and refuses `Owner<…>` / `Executable<…>` (which could never hold).
35
+ - Unit enum values work as seeds in `create`, `signer([...])`, `findProgramAddress` and the other seed lists, one byte
36
+ each, as they already did in `Seeds<…>`.
37
+ - `rest.slice(a, b)` is typed as something to spread into a CPI only (`RemainingSlice`), with a clear message elsewhere.
38
+
39
+ ### Tooling
40
+ - Ports: the five transfer-hook examples with extra accounts (account-data-as-seed, allow-block-list-token, counter,
41
+ transfer-cost, transfer-switch) are ported; their tests transfer through Token-2022, which checks the extra accounts.
42
+ All 46 program-examples are ported now: 9 PORTED, 37 PARTIAL, 0 BLOCKED. The blocked-construct probes
43
+ (test/ports-blocked.test.ts) are gone with the last blocker.
44
+ - Ports: compression/cnft-burn, cnft-vault, cutils and token-2022/transfer-fee, blocked until now, are ported (PARTIAL;
45
+ test/ports/README.md). The compression ports run against Bubblegum, SPL Account Compression and Token Metadata, with
46
+ the leaf hashes, roots and proofs computed in the tests.
47
+ - Ports: the eight remaining program-examples that had not been attempted are ported: basics/repository-layout and
48
+ token-2022 cpi-guard, group, immutable-owner, interest-bearing, memo-transfer, metadata and nft-meta-data-pointer
49
+ (all PARTIAL; test/ports/README.md lists the differences). Every one of the 46 programs is now ported or blocked
50
+ by a named construct.
51
+ - Ports: token-swap is now PORTED (its `amm`, `pool`, authority and liquidity-mint PDAs are all checked
52
+ declaratively, with no bump arguments); escrow and token-fundraiser check their PDAs declaratively instead of with
53
+ `requirePda` in the body.
54
+
8
55
  ## 0.12.1 — 2026-10-27
9
56
 
10
57
  No change to the compiler, the runtime or the generated code.
package/README.md CHANGED
@@ -183,13 +183,13 @@ Runtime failures point back to your source: `Error 6000 Unauthorized (counter.ts
183
183
  | | |
184
184
  | --- | --- |
185
185
  | **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 |
186
- | **Constraints** | `HasOne`, `Seeds` (also under another program; `pdaBump` like `ctx.bumps`), `CloseTo`, `Ata`, `Address`, `Owner`, `Executable`. They are checked before the body runs and recorded in the IDL |
186
+ | **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 |
187
187
  | **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account |
188
188
  | **Control flow** | `if`, `for`, `while`, `switch` (with narrowing on data enums), ternaries, helper functions across multiple files |
189
189
  | **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`) |
190
190
  | **Events** | `emit` (log-based) and `emitCpi` (Anchor `emit_cpi!`, through a self-CPI) |
191
191
  | **Crypto & oracles** | `sha256`, `keccak256`, Merkle proofs, Ed25519 and secp256k1 signature checks, Pyth pull oracle, Switchboard |
192
- | **Interfaces** | custom discriminators (`@discriminator`), so a TypeSOL program can be a Token-2022 transfer hook |
192
+ | **Interfaces** | custom discriminators (`@discriminator`), so a TypeSOL program can be a Token-2022 transfer hook, with extra accounts (ExtraAccountMetaList) |
193
193
 
194
194
  The full reference, with every type, check and built-in, is in [docs/language.md](docs/language.md).
195
195
 
@@ -275,7 +275,7 @@ Details are in [docs/testing.md](docs/testing.md).
275
275
 
276
276
  ## Status
277
277
 
278
- **TypeSOL is pre-1.0 (0.12).** Releases follow the organization's
278
+ **TypeSOL is pre-1.0 (0.13).** Releases follow the organization's
279
279
  [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
280
280
  change.
281
281
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
@@ -657,8 +657,6 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
657
657
  reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size)");
658
658
  fields.push({ name, type });
659
659
  }
660
- if (fields.length === 0)
661
- reporter.fail(decl, "structs need at least one field");
662
660
  checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
663
661
  const name = decl.name.text;
664
662
  structs.set(name, {
@@ -923,8 +921,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
923
921
  const c = a.constraints;
924
922
  if (a.kind.kind === "token" && a.kind.init && !c.ata)
925
923
  reporter.fail(a.node, 'Init<TokenAccount> / InitIfNeeded<TokenAccount> create associated token accounts: wrap them in Ata<…, "owner", "mint">');
926
- if (a.kind.kind === "token" && a.kind.init && (c.hasOne.length || c.seeds || c.address))
927
- reporter.fail(a.node, "an associated token account created here is checked against Ata<…>'s owner and mint; drop HasOne/Seeds/Address");
924
+ if (a.kind.kind === "token" && a.kind.init && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
925
+ reporter.fail(a.node, "an associated token account created here is checked against Ata<…>'s owner and mint; drop HasOne/Seeds/Address/Owner/Executable");
928
926
  if (!c.hasOne.length && !c.seeds && !c.closeTo && !c.ata)
929
927
  continue;
930
928
  const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
@@ -981,7 +979,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
981
979
  const c = a.constraints;
982
980
  const refs = [
983
981
  ...c.hasOne,
984
- ...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1)),
982
+ ...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1).split(".")[0]),
985
983
  ...(c.seeds?.program?.startsWith("@") ? [c.seeds.program.slice(1)] : []),
986
984
  ...(c.closeTo ? [c.closeTo] : []),
987
985
  ...(c.ata
@@ -10,6 +10,16 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
10
10
  protected vecMethod(node: ts.CallExpression, method: string, v: Ref): Value | undefined;
11
11
  /** sha256(...) / keccak256(...) over strings, Pubkeys, Hashes, integers (LE) and booleans. */
12
12
  protected hashCall(node: ts.CallExpression, fn: "sha256" | "keccak256"): Value;
13
+ /**
14
+ * The extra accounts of a transfer hook, as ExtraAccountMeta records (35 bytes each): `{ address, isSigner?, isWritable? }`
15
+ * for a fixed address, `{ seeds: [...], program?, isSigner?, isWritable? }` for a PDA of this program (or, with
16
+ * `program: index`, of the program at that account index). Seeds: string literals, seedAccountKey(i),
17
+ * seedAccountData(i, offset, length), seedInstructionData(offset, length); packed into 32 bytes.
18
+ */
19
+ protected extraAccountMetas(node: ts.Expression): {
20
+ name: string;
21
+ count: number;
22
+ };
13
23
  /** `remainingAccounts()` or a const bound to it. */
14
24
  protected isRemaining(node: ts.Expression): boolean;
15
25
  /** C pointer (`TsAccount *`) for an expression naming a remaining account, or undefined. */
@@ -352,6 +352,18 @@ export class CallEmitter extends ExpressionEmitter {
352
352
  this.line(`TS_TRY(ts_ata_address(${o.c}, ${m.c}, ${tp.c}, &${t}));`);
353
353
  return { c: `(&${t})`, type: PUBKEY };
354
354
  }
355
+ case "findProgramAddress": {
356
+ // Pubkey::find_program_address(seeds, program).0: the canonical PDA of any program.
357
+ if (args.length !== 2)
358
+ this.fail(node, "findProgramAddress(seeds, program) takes the seeds and the program's address");
359
+ const seeds = this.seeds(args[0], 0);
360
+ const program = this.expect(this.expr(args[1], PUBKEY), PUBKEY, args[1]);
361
+ const t = this.tmp("pda");
362
+ const b = this.tmp("bump");
363
+ this.line(`SolPubkey ${t}; uint8_t ${b};`);
364
+ this.line(`if (sol_try_find_program_address(${seeds.name}, ${seeds.count}, ${program.c}, &${t}, &${b}) != SUCCESS) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: no program address for these seeds");`);
365
+ return { c: `(&${t})`, type: PUBKEY };
366
+ }
355
367
  case "metadataAddress": {
356
368
  if (args.length !== 1)
357
369
  this.fail(node, "metadataAddress(mint) takes the mint");
@@ -361,9 +373,10 @@ export class CallEmitter extends ExpressionEmitter {
361
373
  this.line(`TS_TRY(ts_metadata_address(${m.c}, &${t}));`);
362
374
  return { c: `(&${t})`, type: PUBKEY };
363
375
  }
364
- case "initExtraAccountMetas": {
365
- if (args.length !== 4)
366
- this.fail(node, "initExtraAccountMetas(payer, account, mint, systemProgram) takes four accounts");
376
+ case "initExtraAccountMetas":
377
+ case "updateExtraAccountMetas": {
378
+ if (args.length !== 4 && args.length !== 5)
379
+ this.fail(node, `${builtin}(payer, account, mint, systemProgram, extraAccounts?)`);
367
380
  const payer = this.accountParam(args[0], "the payer");
368
381
  if (payer.kind.kind !== "signer" || !payer.kind.mut)
369
382
  this.fail(args[0], "the payer must be a Mut<Signer>");
@@ -374,7 +387,9 @@ export class CallEmitter extends ExpressionEmitter {
374
387
  const sys = this.accountParam(args[3], "the system program");
375
388
  if (sys.kind.kind !== "system")
376
389
  this.fail(args[3], "pass the SystemProgram parameter");
377
- this.line(`TS_TRY(ts_init_extra_metas(params, a_${payer.name}, a_${acct.name}, &a_${mint.name}->key));`);
390
+ const { name: entries, count } = args[4] ? this.extraAccountMetas(args[4]) : { name: "NULL", count: 0 };
391
+ const fn = builtin === "initExtraAccountMetas" ? "ts_init_extra_metas" : "ts_update_extra_metas";
392
+ this.line(`TS_TRY(${fn}(params, a_${payer.name}, a_${acct.name}, &a_${mint.name}->key, ${entries}, ${count}));`);
378
393
  return undefined;
379
394
  }
380
395
  case "remainingAccounts":
@@ -513,6 +528,8 @@ export class CallEmitter extends ExpressionEmitter {
513
528
  }
514
529
  if (ts.isPropertyAccessExpression(callee) && ts.isCallExpression(callee.expression) && this.exportName(callee.expression.expression) === "fixedArray")
515
530
  this.fail(callee.expression, "fixedArray<T, N>(fill) is only allowed as a local declaration: const xs = fixedArray<u64, 4>(0n)");
531
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === "slice" && this.isRemaining(callee.expression))
532
+ this.fail(node, "rest.slice(a, b) is only for spreading into a CPI's accounts: invoke(p, [..., ...readonly(rest.slice(a, b))], data)");
516
533
  this.fail(node, "unsupported call");
517
534
  }
518
535
  vecMethod(node, method, v) {
@@ -658,6 +675,97 @@ export class CallEmitter extends ExpressionEmitter {
658
675
  this.line(`sol_${fn}(${arr}, ${parts.length}, (uint8_t *)&${out});`);
659
676
  return { c: `(&${out})`, type: HASH };
660
677
  }
678
+ /**
679
+ * The extra accounts of a transfer hook, as ExtraAccountMeta records (35 bytes each): `{ address, isSigner?, isWritable? }`
680
+ * for a fixed address, `{ seeds: [...], program?, isSigner?, isWritable? }` for a PDA of this program (or, with
681
+ * `program: index`, of the program at that account index). Seeds: string literals, seedAccountKey(i),
682
+ * seedAccountData(i, offset, length), seedInstructionData(offset, length); packed into 32 bytes.
683
+ */
684
+ extraAccountMetas(node) {
685
+ if (!ts.isArrayLiteralExpression(node))
686
+ this.fail(node, "the extra accounts must be an array literal of { address } / { seeds } objects");
687
+ const bytes = [];
688
+ const fixups = [];
689
+ const small = (e, what, max = 255) => {
690
+ if (!ts.isBigIntLiteral(e))
691
+ this.fail(e, `${what} must be a bigint literal`);
692
+ const v = Number(BigInt(e.text.replace(/n$/, "")));
693
+ if (v < 0 || v > max)
694
+ this.fail(e, `${what} must be 0..${max}`);
695
+ return v;
696
+ };
697
+ for (const el of node.elements) {
698
+ if (!ts.isObjectLiteralExpression(el))
699
+ this.fail(el, "each extra account is { address, … } or { seeds: [...], … }");
700
+ const props = new Map();
701
+ for (const p of el.properties) {
702
+ if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name))
703
+ this.fail(p, "use plain properties: address, seeds, program, isSigner, isWritable");
704
+ if (!["address", "seeds", "program", "isSigner", "isWritable"].includes(p.name.text))
705
+ this.fail(p.name, `unknown property "${p.name.text}"`);
706
+ props.set(p.name.text, p.initializer);
707
+ }
708
+ const flag = (k) => {
709
+ const v = props.get(k);
710
+ if (!v)
711
+ return 0;
712
+ if (v.kind === ts.SyntaxKind.TrueKeyword)
713
+ return 1;
714
+ if (v.kind === ts.SyntaxKind.FalseKeyword)
715
+ return 0;
716
+ this.fail(v, `${k} must be true or false`);
717
+ };
718
+ const start = bytes.length;
719
+ const address = props.get("address");
720
+ const seeds = props.get("seeds");
721
+ if (!!address === !!seeds)
722
+ this.fail(el, "give either address or seeds");
723
+ if (address) {
724
+ if (props.has("program"))
725
+ this.fail(props.get("program"), "program goes with seeds, not with address");
726
+ const v = this.expect(this.expr(address, PUBKEY), PUBKEY, address);
727
+ bytes.push(0, ...new Array(32).fill(0));
728
+ fixups.push({ offset: start + 1, c: v.c });
729
+ }
730
+ else {
731
+ if (!ts.isArrayLiteralExpression(seeds))
732
+ this.fail(seeds, "seeds must be an array literal");
733
+ const packed = [];
734
+ for (const s of seeds.elements) {
735
+ if (ts.isStringLiteral(s)) {
736
+ const b = [...Buffer.from(s.text, "utf8")];
737
+ if (b.length > 32)
738
+ this.fail(s, "a literal seed can be at most 32 bytes");
739
+ packed.push(1, b.length, ...b);
740
+ continue;
741
+ }
742
+ const fn = ts.isCallExpression(s) ? this.exportName(s.expression) : undefined;
743
+ const a = ts.isCallExpression(s) ? s.arguments : [];
744
+ if (fn === "seedInstructionData" && a.length === 2)
745
+ packed.push(2, small(a[0], "the data index"), small(a[1], "the length"));
746
+ else if (fn === "seedAccountKey" && a.length === 1)
747
+ packed.push(3, small(a[0], "the account index"));
748
+ else if (fn === "seedAccountData" && a.length === 3)
749
+ packed.push(4, small(a[0], "the account index"), small(a[1], "the data index"), small(a[2], "the length"));
750
+ else
751
+ this.fail(s, "a seed is a string literal, seedAccountKey(i), seedAccountData(i, offset, length) or seedInstructionData(offset, length)");
752
+ }
753
+ if (packed.length > 32)
754
+ this.fail(seeds, `the packed seeds take ${packed.length} bytes; at most 32 fit`);
755
+ const program = props.get("program");
756
+ bytes.push(program ? 128 + small(program, "program (an account index)", 127) : 1, ...packed, ...new Array(32 - packed.length).fill(0));
757
+ }
758
+ bytes.push(flag("isSigner"), flag("isWritable"));
759
+ }
760
+ const count = node.elements.length;
761
+ if (!count)
762
+ return { name: "NULL", count: 0 };
763
+ const name = this.tmp("extra_metas");
764
+ this.line(`${fixups.length ? "" : "static const "}uint8_t ${name}[${bytes.length}] = ${cBytes(bytes)};`);
765
+ for (const f of fixups)
766
+ this.line(`ts_memcpy(${name} + ${f.offset}, ${f.c}, 32);`);
767
+ return { name, count };
768
+ }
661
769
  /** `remainingAccounts()` or a const bound to it. */
662
770
  isRemaining(node) {
663
771
  if (ts.isIdentifier(node))
@@ -66,6 +66,14 @@ export declare abstract class CpiEmitter extends CallEmitter {
66
66
  protected coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
67
67
  /** Bubblegum (compressed NFTs on SPL account compression): createTree, mint (MintV1, no collection, no creators). */
68
68
  protected bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
69
+ /**
70
+ * `remainingAccounts()` (or a const bound to it): every remaining account; `rest.slice(a, b?)`: indexes a..b of them
71
+ * (both checked against the count, 5104). C expressions for the range, relative to the first remaining account.
72
+ */
73
+ protected remainingRange(node: ts.Expression): {
74
+ from: string;
75
+ to: string;
76
+ } | undefined;
69
77
  protected invoke(node: ts.CallExpression): undefined;
70
78
  /**
71
79
  * A byte buffer written front to back. Its C declaration is patched in afterwards so it is
@@ -63,8 +63,14 @@ export class CpiEmitter extends CallEmitter {
63
63
  this.line(`${info.c} ${t} = ${typed.c};`);
64
64
  parts.push(`{(const uint8_t *)&${t}, ${info.bytes}}`);
65
65
  }
66
+ else if (v.type.kind === "enum") {
67
+ // A unit enum value is its variant index, one byte (as in Seeds<…> and Borsh).
68
+ const t = this.tmp("seed");
69
+ this.line(`uint8_t ${t} = (uint8_t)(${v.c});`);
70
+ parts.push(`{(const uint8_t *)&${t}, 1}`);
71
+ }
66
72
  else
67
- this.fail(el, "seeds can be strings, Pubkeys or integers");
73
+ this.fail(el, "seeds can be strings, Pubkeys, integers or unit enum values");
68
74
  }
69
75
  const name = this.tmp("seeds");
70
76
  const slots = parts.length + extra;
@@ -168,6 +174,8 @@ export class CpiEmitter extends CallEmitter {
168
174
  const p = this.ix.accounts.find((o) => o.kind.kind === kind);
169
175
  if (!p)
170
176
  this.fail(node, `${call}(...) needs ${what} parameter`);
177
+ if (p.optional)
178
+ this.fail(p.node, `${call}(...) needs "${p.name}": it cannot be optional`);
171
179
  return p;
172
180
  };
173
181
  const ataProgram = need("ataProgram", "an AssociatedTokenProgram");
@@ -647,6 +655,27 @@ export class CpiEmitter extends CallEmitter {
647
655
  }
648
656
  this.fail(node.expression, `Bubblegum supports createTree and mint, not ${method}`);
649
657
  }
658
+ /**
659
+ * `remainingAccounts()` (or a const bound to it): every remaining account; `rest.slice(a, b?)`: indexes a..b of them
660
+ * (both checked against the count, 5104). C expressions for the range, relative to the first remaining account.
661
+ */
662
+ remainingRange(node) {
663
+ const n = this.ix.accounts.length;
664
+ const count = `(params->ka_num - ${n})`;
665
+ if (this.isRemaining(node))
666
+ return { from: "0", to: count };
667
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "slice")
668
+ return undefined;
669
+ if (!this.isRemaining(node.expression.expression))
670
+ return undefined;
671
+ if (node.arguments.length < 1 || node.arguments.length > 2)
672
+ this.fail(node, "slice(start, end?) takes one or two indexes");
673
+ // Both ends may equal the count (an empty slice at the end).
674
+ const from = this.index(node.arguments[0], `${count} + 1`);
675
+ const to = node.arguments[1] ? this.index(node.arguments[1], `${count} + 1`) : count;
676
+ this.line(`if (${from} > ${to}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: remaining accounts slice out of range");`);
677
+ return { from, to };
678
+ }
650
679
  invoke(node) {
651
680
  const a = node.arguments;
652
681
  if (a.length !== 3 && a.length !== 4)
@@ -665,11 +694,26 @@ export class CpiEmitter extends CallEmitter {
665
694
  programKey = `&a_${program.name}->key`;
666
695
  }
667
696
  const hasSeeds = a.length === 4;
668
- // Accounts: readonly(x) / writable(x) / readonlySigner(x) / writableSigner(x)
697
+ // Accounts: readonly(x) / writable(x) / readonlySigner(x) / writableSigner(x), and the remaining accounts spread in:
698
+ // `...remainingAccounts()` (each with the flags it was passed with, Rust's `extend_from_slice`) or
699
+ // `...readonly(rest)` / `...writable(rest)` (all with that role).
669
700
  if (!ts.isArrayLiteralExpression(a[1]))
670
701
  this.fail(a[1], "accounts must be an array literal");
671
702
  const metas = [];
703
+ const parts = [];
672
704
  for (const el of a[1].elements) {
705
+ if (ts.isSpreadElement(el)) {
706
+ if (!this.ix)
707
+ this.fail(el, "remainingAccounts() is only available in instructions");
708
+ const inner = el.expression;
709
+ const fnName = ts.isCallExpression(inner) ? this.exportName(inner.expression) : undefined;
710
+ const wrapped = ts.isCallExpression(inner) && (fnName === "readonly" || fnName === "writable") && inner.arguments.length === 1;
711
+ const range = this.remainingRange(wrapped ? inner.arguments[0] : inner);
712
+ if (!range)
713
+ this.fail(el, "spread the remaining accounts: ...remainingAccounts(), ...readonly(rest), ...writable(rest) or rest.slice(a, b)");
714
+ parts.push({ kind: "spread", mode: wrapped ? fnName : "asPassed", ...range });
715
+ continue;
716
+ }
673
717
  const fn = ts.isCallExpression(el) ? this.exportName(el.expression) : undefined;
674
718
  if (!ts.isCallExpression(el) || !fn || !["readonly", "writable", "readonlySigner", "writableSigner"].includes(fn) || el.arguments.length !== 1)
675
719
  this.fail(el, "each account must be readonly(x), writable(x), readonlySigner(x) or writableSigner(x)");
@@ -683,6 +727,7 @@ export class CpiEmitter extends CallEmitter {
683
727
  if (isS && !hasSeeds)
684
728
  this.line(`TS_TRY(ts_check_signer(${h}));`);
685
729
  metas.push(`{&${h}->key, ${isW}, ${isS}}`);
730
+ parts.push({ kind: "meta", c: metas[metas.length - 1] });
686
731
  continue;
687
732
  }
688
733
  const acct = this.accountParam(el.arguments[0], `${fn}()'s argument`);
@@ -691,6 +736,7 @@ export class CpiEmitter extends CallEmitter {
691
736
  if (isS && acct.kind.kind !== "signer" && !hasSeeds)
692
737
  this.fail(el, `"${acct.name}" did not sign; pass signerSeeds to sign for a PDA, or declare it Signer`);
693
738
  metas.push(`{&a_${acct.name}->key, ${isW}, ${isS}}`);
739
+ parts.push({ kind: "meta", c: metas[metas.length - 1] });
694
740
  }
695
741
  // The CPI's account list is built in a buffer with room for one entry per instruction account.
696
742
  if (metas.length > MAX_ACCOUNTS)
@@ -746,9 +792,38 @@ export class CpiEmitter extends CallEmitter {
746
792
  finish(a[2]);
747
793
  const seeds = hasSeeds ? this.signingSeeds(a[3]) : { name: "NULL", count: 0 };
748
794
  const metasName = this.tmp("metas");
749
- this.line(`SolAccountMeta ${metasName}[${Math.max(metas.length, 1)}] = {${metas.join(", ")}};`);
795
+ let count = String(metas.length);
796
+ if (parts.some((p) => p.kind === "spread")) {
797
+ // Room for every account of the instruction (ts_invoke's scratch holds TS_MAX_ACCOUNTS records).
798
+ const n = this.ix.accounts.length;
799
+ const cnt = this.tmp("nmetas");
800
+ this.line(`SolAccountMeta ${metasName}[TS_MAX_ACCOUNTS];`);
801
+ this.line(`uint64_t ${cnt} = 0;`);
802
+ const spreads = parts.filter((p) => p.kind === "spread");
803
+ const total = [String(metas.length), ...spreads.map((p) => `(${p.to} - ${p.from})`)].join(" + ");
804
+ this.line(`if (${total} > TS_MAX_ACCOUNTS) TS_FAIL(TS_ERR_TOO_MANY_ACCOUNTS, "TypeSOL: the CPI lists more than ${MAX_ACCOUNTS} accounts");`);
805
+ for (const p of parts) {
806
+ if (p.kind === "meta") {
807
+ this.line(`${metasName}[${cnt}++] = (SolAccountMeta)${p.c};`);
808
+ continue;
809
+ }
810
+ const i = this.tmp("ri");
811
+ const r = this.tmp("racct");
812
+ this.line(`for (uint64_t ${i} = ${n} + ${p.from}; ${i} < ${n} + ${p.to}; ${i}++) {`);
813
+ this.line(` TsAccount *${r} = params->ka[${i}];`);
814
+ if (p.mode === "writable")
815
+ this.line(` TS_TRY(ts_check_writable(${r}));`);
816
+ const w = p.mode === "asPassed" ? `${r}->is_writable` : p.mode === "writable" ? "1" : "0";
817
+ const sg = p.mode === "asPassed" ? `${r}->is_signer` : "0";
818
+ this.line(` ${metasName}[${cnt}++] = (SolAccountMeta){&${r}->key, ${w}, ${sg}};`);
819
+ this.line(`}`);
820
+ }
821
+ count = cnt;
822
+ }
823
+ else
824
+ this.line(`SolAccountMeta ${metasName}[${Math.max(metas.length, 1)}] = {${metas.join(", ")}};`);
750
825
  const ixName = this.tmp("cpi");
751
- this.line(`SolInstruction ${ixName} = {${programKey}, ${metasName}, ${metas.length}, ${buf}, ${len}};`);
826
+ this.line(`SolInstruction ${ixName} = {${programKey}, ${metasName}, ${count}, ${buf}, ${len}};`);
752
827
  if (hasSeeds) {
753
828
  const signer = this.tmp("signer");
754
829
  this.line(`SolSignerSeeds ${signer} = {${seeds.name}, ${seeds.count}};`);
@@ -368,6 +368,27 @@ export class FunctionEmitter extends StatementEmitter {
368
368
  // "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
369
369
  const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
370
370
  const n = ref ?? raw;
371
+ // "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
372
+ // `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
373
+ if (ref?.includes(".")) {
374
+ const [owner, field, ...rest] = ref.split(".");
375
+ const o = ix.accounts.find((x) => x.name === owner);
376
+ if (!o || o.kind.kind !== "account" || rest.length)
377
+ this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
378
+ const st = o.kind.struct;
379
+ const f = st.fields.find((x) => x.name === field);
380
+ if (!f)
381
+ this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
382
+ const t = f.type;
383
+ if (t.kind === "pubkey")
384
+ parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
385
+ else if (t.kind === "int" || t.kind === "enum")
386
+ parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
387
+ else
388
+ this.reporter.fail(a.node, `Seeds: field "${ref}" of type ${typeName(t)} cannot be a seed (use a Pubkey, an integer or a unit enum)`);
389
+ idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
390
+ continue;
391
+ }
371
392
  const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
372
393
  const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
373
394
  if (ref && !acct && !arg)
@@ -82,6 +82,8 @@ export function emitProgram(analysis, opts = {}) {
82
82
  out.push(`typedef struct __attribute__((packed)) {`);
83
83
  for (const f of s.fields)
84
84
  out.push(` ${storageDecl(f.type, f.name)};`);
85
+ if (!s.fields.length)
86
+ out.push(` uint8_t __empty[0];`); // a marker account (Anchor `pub struct NftAuthority {}`): discriminator only
85
87
  out.push(`} TSS_${s.name};`);
86
88
  out.push(`_Static_assert(sizeof(TSS_${s.name}) == ${s.size}, "TSS_${s.name} layout");`);
87
89
  if (s.vec)
package/docs/language.md CHANGED
@@ -25,7 +25,7 @@ The full reference. For a tour, start with the [README](../README.md).
25
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
26
  | 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
27
  | 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
- | 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` |
28
+ | 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) |
29
29
  | Struct literals | `book.entries.push({ who: who.key, amount, status: Status.Pending, note }, who)` — every field required |
30
30
  | Struct references | `const o = side === Side.Bid ? book.bids.get(i) : book.asks.get(i)` — aliases account data like a JS object reference (no copy); replacing the referenced place (`book.bids.set(i, …)`, `pop()`, `clear()`, `acct.p = …`) while the reference is still used afterwards is a compile error, because JS would keep the old object |
31
31
  | 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) |
@@ -35,7 +35,7 @@ The full reference. For a tour, start with the [README](../README.md).
35
35
  | Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
36
36
  | Strings at runtime | `"hello, " + name`, `` `#${n}` `` — built on the 32 KiB heap (5110 when full) |
37
37
  | `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
- | Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `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) |
38
+ | 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
39
  | 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
40
  | 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
41
  | Loops | `for (let i = 0n; i < n; i++)`, `while`, `break`, `continue` — bounded by the compute budget |
@@ -59,7 +59,7 @@ The full reference. For a tour, start with the [README](../README.md).
59
59
  | `Account<T>` / `Mut<T>` | owned by this program, 8-byte discriminator matches, data large enough (and writable) |
60
60
  | `Init<T>` | writable; `create(payer, seeds)` derives the PDA, verifies it, funds rent, writes the discriminator (up to 10 KiB) |
61
61
  | `InitIfNeeded<T>` | `const created = acct.createIfNeeded(payer, seeds)` — creates once; afterwards re-derives the PDA and checks owner, discriminator and size (Anchor `init_if_needed`); `acct.pdaBump` after create |
62
- | `HasOne<A, "owner">`, `Seeds<A, ["vault", "@owner"], "bump">`, `CloseTo<A, "owner">` | declarative constraints like Anchor's `has_one`, `seeds`/`bump` and `close`: checked before the body (2001 / 2006), close runs when the instruction succeeds; `"@name"` = account key or argument bytes, other strings are literals; recorded as `relations` / `pda` in the IDL. `acct.pdaBump` is the bump (Anchor `ctx.bumps.acct`): the stored field when one is named, else the canonical bump the check found — use it to sign as the PDA |
62
+ | `HasOne<A, "owner">`, `Seeds<A, ["vault", "@owner"], "bump">`, `CloseTo<A, "owner">` | declarative constraints like Anchor's `has_one`, `seeds`/`bump` and `close`: checked before the body (2001 / 2006), close runs when the instruction succeeds; `"@name"` = account key or argument bytes, `"@acct.field"` = a Pubkey, integer or unit enum field of a struct account parameter (also the account's own: `["offer", "@maker", "@offer.id"]`), other strings are literals; recorded as `relations` / `pda` in the IDL. `acct.pdaBump` is the bump (Anchor `ctx.bumps.acct`): the stored field when one is named, else the canonical bump the check found — use it to sign as the PDA |
63
63
  | `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name |
64
64
  | `Ata<TokenAccount, "owner", "mint">` | the account is the associated token account of those parameters (2009; ATA `pda` in the IDL) |
65
65
  | `Ata<Init<TokenAccount>, "owner", "mint">`, `Ata<InitIfNeeded<TokenAccount>, "owner", "mint">` | Anchor `init` / `init_if_needed` with `associated_token::…`: `acct.create(payer)` creates the ATA (fails if it exists); `const created = acct.createIfNeeded(payer)` creates it only when missing, otherwise checks it is that ATA (2009). Then it is a `TokenAccount` of that mint and owner (2014 / 2015). Needs `AssociatedTokenProgram`, `SystemProgram` and a token program parameter; call it at the top level before using the account |
@@ -102,8 +102,14 @@ account written through `.lamports` or `loadAccountMut` is another account of th
102
102
 
103
103
  `/** @discriminator spl-transfer-hook-interface:execute */` above an instruction replaces its Anchor discriminator with
104
104
  sha256(preimage)[..8] (or `@discriminator 0x…` for raw bytes), so a TypeSOL program can be a Token-2022 transfer hook:
105
- `execute(source, mint, destination, owner, extraMetas, amount)` plus `initExtraAccountMetas(payer, account, mint, systemProgram)`
106
- for the validation account. See `test/fixtures/hook.ts`.
105
+ `execute(source, mint, destination, owner, extraMetas, ...extraAccounts, amount)` plus
106
+ `initExtraAccountMetas(payer, account, mint, systemProgram, extraAccounts?)` for the validation account (an
107
+ ExtraAccountMetaList). Extra accounts are listed as `{ address, isSigner?, isWritable? }` (a fixed address) or
108
+ `{ seeds: [...], program?, isSigner?, isWritable? }` (a PDA of this program, or with `program: i` of the program at account
109
+ index i), with seeds from string literals, `seedAccountKey(i)`, `seedAccountData(i, offset, length)` and
110
+ `seedInstructionData(offset, length)`; Token-2022 resolves and checks them on every transfer. The first extra account has
111
+ index 5. `updateExtraAccountMetas(...)` rewrites and resizes an existing list. See `test/fixtures/hook.ts` and the
112
+ transfer-hook ports.
107
113
 
108
114
  ## Calling other programs
109
115
 
@@ -140,7 +146,7 @@ an integer literal, an argument, `acct.key` or a scalar field of an account, the
140
146
 
141
147
  Vecs anywhere but the last account field, growing an account by more than 10 KiB in one instruction, growing a Vec
142
148
  inside a helper, data-enum arguments carrying strings/structs, Pubkeys in templates, `?.` / `!` / `**`, `switch` on
143
- 128-bit integers, `mulDiv` beyond u64, transfer hooks with extra accounts, Token Metadata creators/collections, Core plugins, Bubblegum v2 / collections /
149
+ 128-bit integers, `mulDiv` beyond u64, Token Metadata creators/collections, Core plugins, Bubblegum v2 / collections /
144
150
  transfers, floating point (by design), `do … while`.
145
151
  They are rejected at compile time rather than half-supported.
146
152
 
package/lib/typesol.d.ts CHANGED
@@ -84,10 +84,20 @@ export interface RemainingAccount {
84
84
  readonly isWritable: boolean;
85
85
  }
86
86
 
87
- /** The accounts a client passed beyond the instruction's declared parameters. */
88
- export interface RemainingAccounts {
87
+ /**
88
+ * The accounts a client passed beyond the instruction's declared parameters. Spread them into a CPI's account list:
89
+ * `...remainingAccounts()` passes each with the flags it was passed with (Rust's `extend_from_slice`),
90
+ * `...readonly(rest)` / `...writable(rest)` pass them all with that role (a cNFT proof, a harvest's sources).
91
+ */
92
+ export interface RemainingAccounts extends Iterable<CpiAccount> {
89
93
  readonly length: u64;
90
94
  get(index: bigint): RemainingAccount;
95
+ /** Indexes start..end (end: all) of them, only to spread into a CPI: `...readonly(rest.slice(0n, proofLength))`. */
96
+ slice(start: bigint, end?: bigint): RemainingSlice;
97
+ }
98
+ /** Part of the remaining accounts (`rest.slice(a, b)`): spread it into a CPI's account list, directly or via readonly/writable. */
99
+ export interface RemainingSlice extends Iterable<CpiAccount> {
100
+ readonly __remainingSlice: void;
91
101
  }
92
102
  export declare function remainingAccounts(): RemainingAccounts;
93
103
 
@@ -213,15 +223,53 @@ export interface TokenMetadataProgram extends AccountMeta {
213
223
  signerSeeds?: Seed[] | SignerSeeds,
214
224
  ): void;
215
225
  }
226
+ /** The canonical program address of `seeds` under `program` (Rust `Pubkey::find_program_address(seeds, program).0`). */
227
+ export declare function findProgramAddress(seeds: Seed[], program: Pubkey): Pubkey;
216
228
  /** The Metaplex metadata account address of a mint. */
217
229
  export declare function metadataAddress(mint: Pubkey): Pubkey;
218
230
 
219
231
  /**
220
- * Create the transfer-hook validation account (an empty ExtraAccountMetaList) at PDA ["extra-account-metas", mint].
221
- * Pair it with instructions tagged `@discriminator spl-transfer-hook-interface:execute` and
232
+ * Create the transfer-hook validation account (an ExtraAccountMetaList) at PDA ["extra-account-metas", mint], listing
233
+ * the extra accounts Token-2022 passes to the hook after the standard four and this account (so the first extra account
234
+ * has index 5). Pair it with instructions tagged `@discriminator spl-transfer-hook-interface:execute` and
222
235
  * `@discriminator spl-transfer-hook-interface:initialize-extra-account-metas`.
236
+ *
237
+ * Each extra account (a literal list; Rust `ExtraAccountMeta::new_with_pubkey` / `new_with_seeds` /
238
+ * `new_external_pda_with_seeds`):
239
+ * - `{ address: WSOL_MINT, isSigner: false, isWritable: false }`: a fixed address;
240
+ * - `{ seeds: ["counter", seedAccountKey(3n)], isWritable: true }`: a PDA of this program;
241
+ * - `{ seeds: [...], program: 6n }`: a PDA of the program at account index 6.
242
+ */
243
+ export declare function initExtraAccountMetas(
244
+ payer: Mut<Signer>,
245
+ account: AccountMeta,
246
+ mint: AccountMeta,
247
+ systemProgram: SystemProgram,
248
+ extraAccounts?: ExtraAccountMeta[],
249
+ ): void;
250
+ /**
251
+ * Rewrite this program's existing ExtraAccountMetaList for `mint` (Rust `ExtraAccountMetaList::update`), resizing it to
252
+ * the new list; `payer` tops it up to the rent of the new size when needed.
223
253
  */
224
- export declare function initExtraAccountMetas(payer: Mut<Signer>, account: AccountMeta, mint: AccountMeta, systemProgram: SystemProgram): void;
254
+ export declare function updateExtraAccountMetas(
255
+ payer: Mut<Signer>,
256
+ account: AccountMeta,
257
+ mint: AccountMeta,
258
+ systemProgram: SystemProgram,
259
+ extraAccounts?: ExtraAccountMeta[],
260
+ ): void;
261
+ export type ExtraAccountMeta =
262
+ | { address: Pubkey; isSigner?: boolean; isWritable?: boolean }
263
+ | { seeds: (string | ExtraAccountSeed)[]; program?: bigint; isSigner?: boolean; isWritable?: boolean };
264
+ export interface ExtraAccountSeed {
265
+ readonly __extraSeed: void;
266
+ }
267
+ /** Seed::AccountKey: the key of the account at this index of the execute instruction. */
268
+ export declare function seedAccountKey(index: bigint): ExtraAccountSeed;
269
+ /** Seed::AccountData: `length` bytes at `offset` of the data of the account at `index`. */
270
+ export declare function seedAccountData(index: bigint, offset: bigint, length: bigint): ExtraAccountSeed;
271
+ /** Seed::InstructionData: `length` bytes at `offset` of the execute instruction's data. */
272
+ export declare function seedInstructionData(offset: bigint, length: bigint): ExtraAccountSeed;
225
273
 
226
274
  /** Fields every account parameter exposes. */
227
275
  export interface AccountMeta {
@@ -307,7 +355,8 @@ export type Account<T> = Readonly<T> & AccountMeta;
307
355
  /** Like `Account<T>` but writable. Also usable as `Mut<Signer>`, `Mut<AccountInfo>`, `Mut<TokenAccount>`. */
308
356
  export type Mut<T> = T & AccountMeta;
309
357
 
310
- export type Seed = string | Pubkey | bigint;
358
+ /** A PDA seed: a string, a Pubkey, an integer (at its width) or a unit enum value (one byte). */
359
+ export type Seed = string | Pubkey | bigint | number;
311
360
 
312
361
  /**
313
362
  * The seeds (with the bump) of one PDA signature, for helpers that sign CPIs: declare a `seeds: SignerSeeds`
@@ -343,7 +392,8 @@ export type InitIfNeeded<T> = T extends TokenAccount
343
392
  * Declarative constraints, checked before your code runs (like Anchor's account attributes):
344
393
  * - `HasOne<Mut<Vault>, "owner">`: vault.owner must be the key of the parameter named `owner` (union for several).
345
394
  * - `Seeds<Mut<Vault>, ["vault", "@owner"], "bump">`: the account must be the PDA of these seeds; "@name" is an
346
- * account parameter (its key) or an argument (its bytes), anything else a literal string; the bump is an optional u8 field.
395
+ * account parameter (its key) or an argument (its bytes), "@acct.field" a Pubkey, integer or unit enum field of a struct
396
+ * account parameter (this account's own fields too), anything else a literal string; the bump is an optional u8 field.
347
397
  * A fourth argument derives the PDA under another program: `Seeds<AccountInfo, ["metadata", "@mint"], never, "@tokenMetadata">`
348
398
  * ("@param" names a program parameter, or give a base58 address).
349
399
  * - `CloseTo<Mut<Vault>, "owner">`: close the account into `owner` when the instruction succeeds.
@@ -610,7 +660,9 @@ export interface CpiAccount {
610
660
  readonly __cpi_account: void;
611
661
  }
612
662
  export declare function readonly(account: AccountMeta | RemainingAccount): CpiAccount;
663
+ export declare function readonly(accounts: RemainingAccounts | RemainingSlice): Iterable<CpiAccount>;
613
664
  export declare function writable(account: AccountMeta | RemainingAccount): CpiAccount;
665
+ export declare function writable(accounts: RemainingAccounts | RemainingSlice): Iterable<CpiAccount>;
614
666
  export declare function readonlySigner(account: AccountMeta | RemainingAccount): CpiAccount;
615
667
  export declare function writableSigner(account: AccountMeta | RemainingAccount): CpiAccount;
616
668
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1197,13 +1197,50 @@ static inline uint64_t ts_mpl_create_v3(TsParams *params, const TsAccount *progr
1197
1197
  * ["extra-account-metas", mint]: TLV = execute discriminator | u32 length 4 | u32 count 0.
1198
1198
  */
1199
1199
  static const uint8_t TS_TRANSFER_HOOK_EXECUTE_DISC[8] = {105, 37, 101, 197, 75, 251, 102, 26};
1200
- static inline uint64_t ts_init_extra_metas(TsParams *params, TsAccount *payer, TsAccount *acct, const SolPubkey *mint) {
1200
+ /*
1201
+ * The ExtraAccountMetaList (spl-tlv-account-resolution) at PDA ["extra-account-metas", mint]: the execute
1202
+ * discriminator, a u32 length, then a PodSlice of `n` 35-byte ExtraAccountMeta records (discriminator, 32-byte address
1203
+ * or packed seeds, is_signer, is_writable), which the compiler encodes into `entries`.
1204
+ */
1205
+ static inline uint64_t ts_init_extra_metas(TsParams *params, TsAccount *payer, TsAccount *acct, const SolPubkey *mint, const uint8_t *entries,
1206
+ uint32_t n) {
1201
1207
  SolSignerSeed seeds[3] = {{(const uint8_t *)"extra-account-metas", 19}, {(const uint8_t *)mint, 32}};
1202
1208
  uint8_t bump;
1203
- /* 16 bytes in total: the 8-byte discriminator slot holds the execute discriminator. */
1204
- TS_TRY(ts_create_pda(params, payer, acct, seeds, 2, TS_TRANSFER_HOOK_EXECUTE_DISC, 16, &bump));
1205
- uint32_t len = 4;
1209
+ uint64_t space = 16 + 35 * (uint64_t)n;
1210
+ TS_TRY(ts_create_pda(params, payer, acct, seeds, 2, TS_TRANSFER_HOOK_EXECUTE_DISC, space, &bump));
1211
+ uint32_t len = 4 + 35 * n;
1212
+ ts_memcpy(acct->data + 8, &len, 4);
1213
+ ts_memcpy(acct->data + 12, &n, 4);
1214
+ if (n) ts_memcpy(acct->data + 16, entries, 35 * (uint64_t)n);
1215
+ return SUCCESS;
1216
+ }
1217
+
1218
+ /*
1219
+ * ExtraAccountMetaList::update: rewrite an existing list (this program's PDA ["extra-account-metas", mint], holding the
1220
+ * execute discriminator), resizing it to `n` records. The payer tops it up to the rent of the new size when its
1221
+ * lamports fall short, as the program-examples do before `resize`.
1222
+ */
1223
+ static inline uint64_t ts_update_extra_metas(TsParams *params, TsAccount *payer, TsAccount *acct, const SolPubkey *mint, const uint8_t *entries,
1224
+ uint32_t n) {
1225
+ SolSignerSeed seeds[2] = {{(const uint8_t *)"extra-account-metas", 19}, {(const uint8_t *)mint, 32}};
1226
+ SolPubkey pda;
1227
+ uint8_t bump;
1228
+ if (sol_try_find_program_address(seeds, 2, params->program_id, &pda, &bump) != SUCCESS || !ts_key_eq(&pda, &acct->key))
1229
+ TS_FAIL(TS_ERR_SEEDS, "TypeSOL: not the mint's extra-account-metas PDA");
1230
+ if (!ts_key_eq(&acct->owner, params->program_id)) TS_FAIL(TS_ERR_WRONG_OWNER, "TypeSOL: the extra-account-metas account is not this program's");
1231
+ if (acct->data_len < 16 || ts_ld64(acct->data) != ts_ld64(TS_TRANSFER_HOOK_EXECUTE_DISC))
1232
+ TS_FAIL(TS_ERR_DISCRIMINATOR, "TypeSOL: not an ExtraAccountMetaList");
1233
+ uint64_t new_len = 16 + 35 * (uint64_t)n;
1234
+ uint64_t rent;
1235
+ TS_TRY(ts_minimum_balance(new_len, &rent));
1236
+ if (rent > acct->lamports) TS_TRY(ts_system_transfer(params, payer, acct, rent - acct->lamports));
1237
+ if (new_len > acct->data_len + MAX_PERMITTED_DATA_INCREASE) TS_FAIL(TS_ERR_REALLOC, "TypeSOL: account would grow more than 10 KiB in one instruction");
1238
+ if (new_len > acct->data_len) ts_memzero(acct->data + acct->data_len, new_len - acct->data_len);
1239
+ acct->data_len = new_len;
1240
+ uint32_t len = 4 + 35 * n;
1206
1241
  ts_memcpy(acct->data + 8, &len, 4);
1242
+ ts_memcpy(acct->data + 12, &n, 4);
1243
+ if (n) ts_memcpy(acct->data + 16, entries, 35 * (uint64_t)n);
1207
1244
  return SUCCESS;
1208
1245
  }
1209
1246