@typesol/cli 0.24.0 → 0.25.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.
@@ -38,8 +38,10 @@ export class FunctionEmitter extends StatementEmitter {
38
38
  const k = a.kind;
39
39
  if (k.kind === "signer")
40
40
  head.push(` TS_TRY(ts_check_signer(${v}));`);
41
+ // Mut<…>: Anchor's `mut` (2000). Anchor checks it with the constraints, after seeds and associated token; here it
42
+ // sits with the type checks, which keeps binaries smaller (only a transaction breaking both sees the difference).
41
43
  if (isWritable(k))
42
- head.push(` TS_TRY(ts_check_writable(${v}));`);
44
+ head.push(` TS_TRY(ts_check_mut(${v}));`);
43
45
  // A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
44
46
  if (k.kind === "account")
45
47
  head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : emptySizeC(k.struct)}));`);
@@ -160,7 +162,9 @@ export class FunctionEmitter extends StatementEmitter {
160
162
  pairs.push(`if (${present}a_${writable[i].name} == a_${writable[j].name}) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: ${writable[i].name} and ${writable[j].name} must be different accounts");`);
161
163
  }
162
164
  head.push(...pairs.map((p) => ` ${p}`));
163
- // Arguments: Borsh, read sequentially (strings make later offsets dynamic).
165
+ // Arguments: Borsh, read sequentially (strings make later offsets dynamic). Anchor decodes them before the accounts;
166
+ // here they come after the account types (decoding first keeps them live across the checks: larger binaries), so
167
+ // only a transaction wrong in both ways sees a different error.
164
168
  if (ix.args.length)
165
169
  head.push(` uint64_t off = ${ix.discriminator.length};`);
166
170
  const need = (bytes) => head.push(` if (params->data_len < off + ${bytes}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
@@ -365,6 +369,11 @@ export class FunctionEmitter extends StatementEmitter {
365
369
  }),
366
370
  ];
367
371
  }
372
+ /**
373
+ * Each account's constraints, account by account in parameter order and, within one account, in Anchor's order
374
+ * (anchor-syn's `linearize`): seeds, associated token, has_one, owner, executable, address, token::, mint::.
375
+ * A transaction that breaks several checks then fails with the same error code as under Anchor.
376
+ */
368
377
  constraintChecks(ix) {
369
378
  const out = [];
370
379
  // Bumps found by Seeds<…> checks, declared ahead of the (possibly optional-guarded) checks for `acct.pdaBump`.
@@ -375,21 +384,40 @@ export class FunctionEmitter extends StatementEmitter {
375
384
  const k = a.kind.kind;
376
385
  if (c.hasOne.length && k !== "account" && k !== "token" && k !== "mint")
377
386
  this.reporter.fail(a.node, "HasOne applies to existing accounts (Account<T> / Mut<T>), TokenAccount and Mint");
378
- for (const entry of c.hasOne) {
387
+ if (c.closeTo) {
388
+ const dest = ix.accounts.find((o) => o.name === c.closeTo);
389
+ if (!isWritable(dest.kind))
390
+ this.reporter.fail(a.node, `CloseTo<…, "${c.closeTo}">: ${c.closeTo} must be writable (Mut<…>)`);
391
+ }
392
+ if (c.seeds)
393
+ out.push(...this.seedsCheck(ix, a, bumpDecls));
394
+ if (c.ata)
395
+ out.push(...this.ataCheck(ix, a));
396
+ // has_one on a program account; on a token account or mint the same entries are Anchor's token:: / mint::
397
+ // constraints, which come last.
398
+ const hasOne = [];
399
+ // Anchor's order inside token:: (authority, mint, token program) and mint:: (authorities, token program).
400
+ const rank = (e) => ["owner=", "mint=", "mintAuthority=", "freezeAuthority=", "tokenProgram="].findIndex((x) => e.startsWith(x));
401
+ const entries = k === "token" || k === "mint" ? [...c.hasOne].sort((x, y) => rank(x) - rank(y)) : c.hasOne;
402
+ for (const entry of entries) {
379
403
  // "field=param" (TokenOf / MintOf) or a field checked against the parameter of the same name (HasOne).
380
404
  const [f, p = f] = entry.split("=");
381
- if (k === "token")
382
- out.push(f === "mint"
405
+ if (f === "tokenProgram")
406
+ hasOne.push(` if (!ts_key_eq(&a_${a.name}->owner, &a_${p}->key)) TS_FAIL(${k === "mint" ? "TS_ERR_MINT_TOKEN_PROGRAM" : "TS_ERR_TOKEN_TOKEN_PROGRAM"}, "TypeSOL: ${a.name} is not owned by ${p}");`);
407
+ else if (k === "token")
408
+ hasOne.push(f === "mint"
383
409
  ? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${p}");`
384
410
  : ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${p}");`);
385
411
  else if (k === "mint") {
386
412
  const at = f === "mintAuthority" ? 0 : 46;
387
413
  const code = f === "mintAuthority" ? "TS_ERR_MINT_AUTHORITY" : "TS_ERR_FREEZE_AUTHORITY";
388
- out.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${p}->key)) TS_FAIL(${code}, "TypeSOL: ${p} is not the ${f} of ${a.name}");`);
414
+ hasOne.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${p}->key)) TS_FAIL(${code}, "TypeSOL: ${p} is not the ${f} of ${a.name}");`);
389
415
  }
390
416
  else
391
- out.push(` if (!ts_key_eq(&d_${a.name}->${f}, &a_${f}->key)) TS_FAIL(TS_ERR_CONSTRAINT, "TypeSOL: ${a.name}.${f} must be the ${f} account");`);
417
+ hasOne.push(` if (!ts_key_eq(&d_${a.name}->${f}, &a_${f}->key)) TS_FAIL(TS_ERR_CONSTRAINT, "TypeSOL: ${a.name}.${f} must be the ${f} account");`);
392
418
  }
419
+ if (k === "account")
420
+ out.push(...hasOne);
393
421
  if (c.owner) {
394
422
  out.push(` static const SolPubkey co_${a.name} = {${cBytes(c.owner.bytes)}};`);
395
423
  out.push(` if (!ts_key_eq(&a_${a.name}->owner, &co_${a.name})) TS_FAIL(TS_ERR_CONSTRAINT_OWNER, "TypeSOL: ${a.name} is not owned by the expected program");`);
@@ -400,138 +428,8 @@ export class FunctionEmitter extends StatementEmitter {
400
428
  out.push(` static const SolPubkey ca_${a.name} = {${cBytes(c.address.bytes)}};`);
401
429
  out.push(` if (!ts_key_eq(&a_${a.name}->key, &ca_${a.name})) TS_FAIL(TS_ERR_ADDRESS, "TypeSOL: ${a.name} is not the expected address");`);
402
430
  }
403
- if (c.ata && a.kind.kind === "token" && a.kind.init) {
404
- // Created in the body: the ATA program checks the address when it creates it, and createIfNeeded checks an
405
- // existing one there. Only the IDL entry is recorded here.
406
- const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
407
- if (tp && !ix.pdas.has(a.name)) {
408
- ix.pdas.set(a.name, [
409
- { kind: "account", path: snakeCase(c.ata.owner) },
410
- { kind: "account", path: snakeCase(tp.name) },
411
- { kind: "account", path: snakeCase(c.ata.mint) },
412
- ]);
413
- ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
414
- }
415
- }
416
- else if (c.ata) {
417
- // The token program is the account's owner; an AccountInfo not created yet takes the instruction's token program.
418
- let program = `&a_${a.name}->owner`;
419
- if (k !== "token") {
420
- const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
421
- if (!tp)
422
- this.reporter.fail(a.node, "Ata<AccountInfo, …> needs a token program parameter to derive the address");
423
- program = `&a_${tp.name}->key`;
424
- }
425
- out.push(` { SolPubkey ata_${a.name};`);
426
- out.push(` TS_TRY(ts_ata_address(&a_${c.ata.owner}->key, &a_${c.ata.mint}->key, ${program}, &ata_${a.name}));`);
427
- out.push(` if (!ts_key_eq(&ata_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_ASSOCIATED, "TypeSOL: ${a.name} is not the associated token account of ${c.ata.owner} and ${c.ata.mint}"); }`);
428
- // The address alone is not enough: legacy SPL Token lets an ATA's owner be reassigned.
429
- if (k === "token") {
430
- out.push(` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${c.ata.mint}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${c.ata.mint}");`);
431
- out.push(` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${c.ata.owner}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${c.ata.owner}");`);
432
- }
433
- const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
434
- if (tp && !ix.pdas.has(a.name)) {
435
- ix.pdas.set(a.name, [
436
- { kind: "account", path: snakeCase(c.ata.owner) },
437
- { kind: "account", path: snakeCase(tp.name) },
438
- { kind: "account", path: snakeCase(c.ata.mint) },
439
- ]);
440
- ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
441
- }
442
- }
443
- if (c.closeTo) {
444
- const dest = ix.accounts.find((o) => o.name === c.closeTo);
445
- if (!isWritable(dest.kind))
446
- this.reporter.fail(a.node, `CloseTo<…, "${c.closeTo}">: ${c.closeTo} must be writable (Mut<…>)`);
447
- }
448
- if (c.seeds) {
449
- const parts = [];
450
- const idl = [];
451
- for (const raw of c.seeds.names) {
452
- // "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
453
- const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
454
- const n = ref ?? raw;
455
- // "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
456
- // `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
457
- if (ref?.includes(".")) {
458
- const [owner, field, ...rest] = ref.split(".");
459
- const o = ix.accounts.find((x) => x.name === owner);
460
- if (!o || o.kind.kind !== "account" || rest.length)
461
- this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
462
- const st = o.kind.struct;
463
- const f = st.fields.find((x) => x.name === field);
464
- if (!f)
465
- this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
466
- const t = f.type;
467
- if (t.kind === "pubkey")
468
- parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
469
- else if (t.kind === "int" || t.kind === "enum")
470
- parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
471
- else
472
- 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)`);
473
- idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
474
- continue;
475
- }
476
- const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
477
- const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
478
- if (ref && !acct && !arg)
479
- this.reporter.fail(a.node, `Seeds: "@${ref}" is neither an account parameter nor an argument`);
480
- if (acct) {
481
- parts.push(`{(const uint8_t *)&a_${n}->key, 32}`);
482
- idl.push({ kind: "account", path: snakeCase(n) });
483
- }
484
- else if (arg) {
485
- const t = arg.type;
486
- if (t.kind === "string")
487
- parts.push(`{arg_${n}.ptr, arg_${n}.len}`);
488
- else if (t.kind === "pubkey")
489
- parts.push(`{(const uint8_t *)&arg_${n}, 32}`);
490
- else if (t.kind === "int" || t.kind === "enum")
491
- parts.push(`{(const uint8_t *)&arg_${n}, ${scalarSize(t)}}`);
492
- else
493
- this.reporter.fail(a.node, `Seeds: argument "${n}" of type ${typeName(t)} cannot be a seed`);
494
- idl.push({ kind: "arg", path: snakeCase(n) });
495
- }
496
- else {
497
- if (Buffer.byteLength(n, "utf8") > 32)
498
- this.reporter.fail(a.node, "a seed can be at most 32 bytes");
499
- parts.push(`{(const uint8_t *)${cString(n)}, ${Buffer.byteLength(n, "utf8")}}`);
500
- idl.push({ kind: "const", value: [...Buffer.from(n, "utf8")] });
501
- }
502
- }
503
- if (!ix.pdas.has(a.name))
504
- ix.pdas.set(a.name, idl);
505
- // The deriving program: this one, another account parameter ("@name"), or a fixed address.
506
- let program = "params->program_id";
507
- const p = c.seeds.program;
508
- if (p?.startsWith("@")) {
509
- const pa = ix.accounts.find((o) => o.name === p.slice(1));
510
- if (!pa)
511
- this.reporter.fail(a.node, `Seeds program: "${p}" is not an account parameter`);
512
- program = `&a_${pa.name}->key`;
513
- ix.pdaPrograms.set(a.name, { kind: "account", path: snakeCase(pa.name) });
514
- }
515
- else if (p) {
516
- const bytes = [...decodeAddress(p)];
517
- out.push(` static const SolPubkey sp_${a.name} = {${cBytes(bytes)}};`);
518
- program = `&sp_${a.name}`;
519
- ix.pdaPrograms.set(a.name, { kind: "const", value: bytes });
520
- }
521
- const seeds = `cs_${a.name}`;
522
- out.push(` SolSignerSeed ${seeds}[${parts.length + 1}] = {${parts.join(", ")}};`);
523
- if (c.seeds.bump) {
524
- out.push(` ${seeds}[${parts.length}] = (SolSignerSeed){&d_${a.name}->${c.seeds.bump}, 1};`);
525
- out.push(` TS_TRY(ts_require_pda(a_${a.name}, ${seeds}, ${parts.length + 1}, ${program}));`);
526
- this.bumps.set(a.name, `d_${a.name}->${c.seeds.bump}`);
527
- }
528
- else {
529
- bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
530
- out.push(` { SolPubkey pda_${a.name};`);
531
- out.push(` if (ts_find_pda(${seeds}, ${parts.length}, ${program}, &pda_${a.name}, &sb_${a.name}) != SUCCESS || !ts_key_eq(&pda_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: ${a.name} is not the expected PDA"); }`);
532
- this.bumps.set(a.name, `sb_${a.name}`);
533
- }
534
- }
431
+ if (k !== "account")
432
+ out.push(...hasOne);
535
433
  // Constraints run only when every optional account they mention was passed.
536
434
  const lines = out.splice(start);
537
435
  const optional = this.optionalIn(lines.join("\n"));
@@ -542,6 +440,138 @@ export class FunctionEmitter extends StatementEmitter {
542
440
  }
543
441
  return [...bumpDecls, ...out];
544
442
  }
443
+ /** Ata<…>: the account's owner (2015) and address (2009) as Anchor checks them, then its mint. */
444
+ ataCheck(ix, a) {
445
+ const out = [];
446
+ const c = a.constraints;
447
+ const k = a.kind.kind;
448
+ const ata = c.ata;
449
+ // With two token programs the address depends on which the mint uses: no IDL derivation, and an Ata<AccountInfo>
450
+ // (derived here) is refused.
451
+ const tps = ix.accounts.filter((o) => o.kind.kind === "tokenProgram");
452
+ const tp = tps.length === 1 ? tps[0] : undefined;
453
+ if (tp && !ix.pdas.has(a.name)) {
454
+ ix.pdas.set(a.name, [
455
+ { kind: "account", path: snakeCase(ata.owner) },
456
+ { kind: "account", path: snakeCase(tp.name) },
457
+ { kind: "account", path: snakeCase(ata.mint) },
458
+ ]);
459
+ ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
460
+ }
461
+ // Created in the body: the ATA program checks the address when it creates it, and createIfNeeded checks an
462
+ // existing one there. Only the IDL entry is recorded.
463
+ if (k === "token" && a.kind.init)
464
+ return out;
465
+ // The address alone is not enough: legacy SPL Token lets an ATA's owner be reassigned.
466
+ if (k === "token")
467
+ out.push(` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${ata.owner}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${ata.owner}");`);
468
+ // The token program is the account's owner; an AccountInfo not created yet takes the instruction's token program.
469
+ let program = `&a_${a.name}->owner`;
470
+ if (k !== "token") {
471
+ if (!tp)
472
+ this.reporter.fail(a.node, tps.length
473
+ ? "Ata<AccountInfo, …> needs exactly one token program parameter to derive the address; use TokenInterface"
474
+ : "Ata<AccountInfo, …> needs a token program parameter to derive the address");
475
+ program = `&a_${tp.name}->key`;
476
+ }
477
+ out.push(` { SolPubkey ata_${a.name};`);
478
+ out.push(` TS_TRY(ts_ata_address(&a_${ata.owner}->key, &a_${ata.mint}->key, ${program}, &ata_${a.name}));`);
479
+ out.push(` if (!ts_key_eq(&ata_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_ASSOCIATED, "TypeSOL: ${a.name} is not the associated token account of ${ata.owner} and ${ata.mint}"); }`);
480
+ if (k === "token")
481
+ out.push(` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${ata.mint}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${ata.mint}");`);
482
+ return out;
483
+ }
484
+ /** Seeds<…>: the account is the PDA of its seeds (with the stored bump, or the canonical one found here). */
485
+ seedsCheck(ix, a, bumpDecls) {
486
+ const out = [];
487
+ const c = a.constraints;
488
+ const parts = [];
489
+ const idl = [];
490
+ for (const raw of c.seeds.names) {
491
+ // "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
492
+ const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
493
+ const n = ref ?? raw;
494
+ // "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
495
+ // `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
496
+ if (ref?.includes(".")) {
497
+ const [owner, field, ...rest] = ref.split(".");
498
+ const o = ix.accounts.find((x) => x.name === owner);
499
+ if (!o || o.kind.kind !== "account" || rest.length)
500
+ this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
501
+ const st = o.kind.struct;
502
+ const f = st.fields.find((x) => x.name === field);
503
+ if (!f)
504
+ this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
505
+ const t = f.type;
506
+ if (t.kind === "pubkey")
507
+ parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
508
+ else if (t.kind === "int" || t.kind === "enum")
509
+ parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
510
+ else
511
+ 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)`);
512
+ idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
513
+ continue;
514
+ }
515
+ const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
516
+ const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
517
+ if (ref && !acct && !arg)
518
+ this.reporter.fail(a.node, `Seeds: "@${ref}" is neither an account parameter nor an argument`);
519
+ if (acct) {
520
+ parts.push(`{(const uint8_t *)&a_${n}->key, 32}`);
521
+ idl.push({ kind: "account", path: snakeCase(n) });
522
+ }
523
+ else if (arg) {
524
+ const t = arg.type;
525
+ if (t.kind === "string")
526
+ parts.push(`{arg_${n}.ptr, arg_${n}.len}`);
527
+ else if (t.kind === "pubkey")
528
+ parts.push(`{(const uint8_t *)&arg_${n}, 32}`);
529
+ else if (t.kind === "int" || t.kind === "enum")
530
+ parts.push(`{(const uint8_t *)&arg_${n}, ${scalarSize(t)}}`);
531
+ else
532
+ this.reporter.fail(a.node, `Seeds: argument "${n}" of type ${typeName(t)} cannot be a seed`);
533
+ idl.push({ kind: "arg", path: snakeCase(n) });
534
+ }
535
+ else {
536
+ if (Buffer.byteLength(n, "utf8") > 32)
537
+ this.reporter.fail(a.node, "a seed can be at most 32 bytes");
538
+ parts.push(`{(const uint8_t *)${cString(n)}, ${Buffer.byteLength(n, "utf8")}}`);
539
+ idl.push({ kind: "const", value: [...Buffer.from(n, "utf8")] });
540
+ }
541
+ }
542
+ if (!ix.pdas.has(a.name))
543
+ ix.pdas.set(a.name, idl);
544
+ // The deriving program: this one, another account parameter ("@name"), or a fixed address.
545
+ let program = "params->program_id";
546
+ const p = c.seeds.program;
547
+ if (p?.startsWith("@")) {
548
+ const pa = ix.accounts.find((o) => o.name === p.slice(1));
549
+ if (!pa)
550
+ this.reporter.fail(a.node, `Seeds program: "${p}" is not an account parameter`);
551
+ program = `&a_${pa.name}->key`;
552
+ ix.pdaPrograms.set(a.name, { kind: "account", path: snakeCase(pa.name) });
553
+ }
554
+ else if (p) {
555
+ const bytes = [...decodeAddress(p)];
556
+ out.push(` static const SolPubkey sp_${a.name} = {${cBytes(bytes)}};`);
557
+ program = `&sp_${a.name}`;
558
+ ix.pdaPrograms.set(a.name, { kind: "const", value: bytes });
559
+ }
560
+ const seeds = `cs_${a.name}`;
561
+ out.push(` SolSignerSeed ${seeds}[${parts.length + 1}] = {${parts.join(", ")}};`);
562
+ if (c.seeds.bump) {
563
+ out.push(` ${seeds}[${parts.length}] = (SolSignerSeed){&d_${a.name}->${c.seeds.bump}, 1};`);
564
+ out.push(` TS_TRY(ts_require_pda(a_${a.name}, ${seeds}, ${parts.length + 1}, ${program}));`);
565
+ this.bumps.set(a.name, `d_${a.name}->${c.seeds.bump}`);
566
+ }
567
+ else {
568
+ bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
569
+ out.push(` { SolPubkey pda_${a.name};`);
570
+ out.push(` if (ts_find_pda(${seeds}, ${parts.length}, ${program}, &pda_${a.name}, &sb_${a.name}) != SUCCESS || !ts_key_eq(&pda_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: ${a.name} is not the expected PDA"); }`);
571
+ this.bumps.set(a.name, `sb_${a.name}`);
572
+ }
573
+ return out;
574
+ }
545
575
  /** Optional account parameters a piece of generated C refers to (a_name / d_name). */
546
576
  optionalIn(code) {
547
577
  return (this.ix?.accounts ?? []).filter((a) => a.optional && new RegExp(`\\b[ad]_${a.name}\\b`).test(code)).map((a) => a.name);
@@ -137,6 +137,9 @@ export function emitProgram(analysis, opts = {}) {
137
137
  // refuse more than the array holds instead (ts_deserialize relies on this bound).
138
138
  out.push(` if (*(const uint64_t *)input > TS_MAX_ACCOUNTS) TS_FAIL(TS_ERR_TOO_MANY_ACCOUNTS, "TypeSOL: at most ${MAX_ACCOUNTS} accounts per instruction");`);
139
139
  out.push(` ts_deserialize(input, &params);`);
140
+ // loadAccountMut's record of the accounts it loaded: set up only in programs that use it (one store otherwise wasted).
141
+ if ([...helperBodies, ...ixBodies].some((b) => b.includes("ts_claim_mut(")))
142
+ out.push(` params.loaded_mut = 0;`);
140
143
  const minDisc = Math.min(8, ...model.instructions.map((ix) => ix.discriminator.length));
141
144
  out.push(` if (params.data_len < ${minDisc}) TS_FAIL(TS_ERR_INSTRUCTION_MISSING, "TypeSOL: instruction data too short");`);
142
145
  // Self-CPI events (emitCpi): accept the event instruction when its authority PDA signed it, like Anchor.
@@ -11,6 +11,7 @@ export declare const ATA_PROGRAM_BYTES: number[];
11
11
  export declare const U64: ScalarType;
12
12
  export declare const U8: ScalarType;
13
13
  export declare const I64: ScalarType;
14
+ export declare const I32: ScalarType;
14
15
  export declare const BOOL: ScalarType;
15
16
  export declare const PUBKEY: ScalarType;
16
17
  export declare const STRING: ValueType;
@@ -13,6 +13,7 @@ export const ATA_PROGRAM_BYTES = [
13
13
  export const U64 = { kind: "int", name: "u64" };
14
14
  export const U8 = { kind: "int", name: "u8" };
15
15
  export const I64 = { kind: "int", name: "i64" };
16
+ export const I32 = { kind: "int", name: "i32" };
16
17
  export const BOOL = { kind: "bool" };
17
18
  export const PUBKEY = { kind: "pubkey" };
18
19
  export const STRING = { kind: "string" };
@@ -93,4 +93,10 @@ export declare abstract class StatementEmitter extends CpiEmitter {
93
93
  /** Whether a place goes through a Vec element (`v.get(i).x`, `v.get(i)`). */
94
94
  protected writesThroughVec(node: ts.Expression): boolean;
95
95
  protected helperTakesMutAccounts(node: ts.CallExpression): boolean;
96
+ /**
97
+ * Whether evaluating `node` may resize a growable Vec (and so move the Vecs after it): a push / pop / clear, a
98
+ * realloc or close, a helper taking a writable account or a function argument, or a closure (its body is not
99
+ * looked into: it may capture the account). Conservative: it only decides whether a Vec element may be the target.
100
+ */
101
+ protected mayResizeVecs(node: ts.Node): boolean;
96
102
  }
@@ -1085,6 +1085,10 @@ export class StatementEmitter extends CpiEmitter {
1085
1085
  this.fail(e, "this expression has no effect; only assignments and calls can be statements");
1086
1086
  }
1087
1087
  assign(target, valueNode, op, topLevel = false) {
1088
+ // The place is resolved before the right side runs (JS order). A right side that can resize a Vec of the account
1089
+ // would move the element out from under it: the write would land in the bytes that follow.
1090
+ if (this.writesThroughVec(target) && this.mayResizeVecs(valueNode))
1091
+ this.fail(target, "the right side may change this Vec (a helper, closure or push/pop/clear); store its result in a local first");
1088
1092
  if (op !== undefined) {
1089
1093
  const place = this.place(target);
1090
1094
  let current = { c: place.read, type: place.type };
@@ -1143,8 +1147,6 @@ export class StatementEmitter extends CpiEmitter {
1143
1147
  const structCall = isCall && ts.isIdentifier(valueNode.expression)
1144
1148
  ? this.helperOf(valueNode)?.returns?.kind === "struct"
1145
1149
  : false;
1146
- if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
1147
- this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
1148
1150
  // Whole arrays / structs: copy between identical layouts.
1149
1151
  const targetRef = isCall && !structCall ? undefined : this.ref(target);
1150
1152
  if (targetRef &&
@@ -1161,9 +1163,6 @@ export class StatementEmitter extends CpiEmitter {
1161
1163
  }
1162
1164
  const place = this.place(target);
1163
1165
  if (isCall && !createsFirst) {
1164
- // A helper that may replace a Vec element would leave `place` pointing at the new one (JS writes the old object).
1165
- if (this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
1166
- this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
1167
1166
  raw = this.call(valueNode, topLevel);
1168
1167
  if (!raw)
1169
1168
  this.fail(valueNode, "this call does not produce a value");
@@ -1194,4 +1193,35 @@ export class StatementEmitter extends CpiEmitter {
1194
1193
  const h = this.helperOf(node);
1195
1194
  return !!h?.params.some((p) => p.account && isWritable(p.account));
1196
1195
  }
1196
+ /**
1197
+ * Whether evaluating `node` may resize a growable Vec (and so move the Vecs after it): a push / pop / clear, a
1198
+ * realloc or close, a helper taking a writable account or a function argument, or a closure (its body is not
1199
+ * looked into: it may capture the account). Conservative: it only decides whether a Vec element may be the target.
1200
+ */
1201
+ mayResizeVecs(node) {
1202
+ let found = false;
1203
+ const isClosure = (e) => ts.isArrowFunction(e) || ts.isFunctionExpression(e) || (ts.isIdentifier(e) && !!this.lookup(e.text)?.closure);
1204
+ const visit = (n) => {
1205
+ if (found)
1206
+ return;
1207
+ if (ts.isCallExpression(n)) {
1208
+ const callee = n.expression;
1209
+ if (ts.isPropertyAccessExpression(callee) && ["push", "pop", "clear"].includes(callee.name.text))
1210
+ found = true;
1211
+ else if (ts.isCallExpression(callee))
1212
+ found = true; // a function a function returns
1213
+ else if (ts.isIdentifier(callee)) {
1214
+ if (this.lookup(callee.text)?.closure)
1215
+ found = true;
1216
+ else if (["realloc", "close"].includes(this.exportName(callee) ?? ""))
1217
+ found = true;
1218
+ else if (this.helperOf(n) && (this.helperTakesMutAccounts(n) || n.arguments.some(isClosure)))
1219
+ found = true;
1220
+ }
1221
+ }
1222
+ ts.forEachChild(n, visit);
1223
+ };
1224
+ visit(node);
1225
+ return found;
1226
+ }
1197
1227
  }
@@ -22,6 +22,21 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
22
22
  `compiler/index.ts` wires the stages together. `generate()` runs analyze, emit and IDL generation in memory, and
23
23
  `build()` also compiles and writes the output files.
24
24
 
25
+ ## Repository layout
26
+
27
+ | Path | Contents |
28
+ | --- | --- |
29
+ | `src/` | The compiler (`src/compiler/`) and the tools around it: CLI, workspace, client, CPI and port generators, testing, IDL publishing (source map below) |
30
+ | `runtime/` | `typesol_rt.h`, the C runtime every program includes ([runtime.md](runtime.md)) |
31
+ | `lib/` | `typesol.d.ts`, the language surface programs typecheck against |
32
+ | `ts-plugin/` | The editor plugin (`typesol/ts-plugin`) |
33
+ | `examples/` | Example programs; the `typesol init` templates are copies of three of them |
34
+ | `test/` | The test suite: `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated clients), `programs/` (third-party program binaries, see its SOURCES.md) |
35
+ | `bench/anchor/` | Anchor twins of the counter, vault and escrow examples, for the differential tests (`pnpm bench:anchor`) and the zero-copy client test; the benchmarks themselves are in [typesol/benchmarks](https://github.com/typesol/benchmarks) |
36
+ | `scripts/` | `smoke.ts` and `localnet.sh` (a local validator run), `fetch-programs.sh` (third-party binaries), `readme-bench.ts` (the README numbers) |
37
+ | `docs/` | User and contributor documentation (also published on typesol.com) |
38
+ | `assets/` | The CLI's logo and the README images (`readme/`, generated) |
39
+
25
40
  ## Source map
26
41
 
27
42
  | Path | Role |
@@ -34,11 +49,17 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
34
49
  | `src/compiler/idl.ts` | Turns the model into an Anchor IDL: accounts with `pda`, `relations`, `address` and `optional`, plus types, events, errors and docs. |
35
50
  | `src/compiler/toolchain.ts` | Finds the Agave SDK and runs clang and lld with the SDK's flags, plus per-function sections, `--gc-sections` and `--strip-all` so unused code and symbol tables stay out of the binary. |
36
51
  | `src/compiler/diagnostics.ts` | `Reporter` and `CompileError`: diagnostics with file, line and column. |
52
+ | `src/compiler/anchor-errors.ts` | Anchor's own error codes (`AnchorError.X`), shared by `require` and the generated clients. |
53
+ | `src/compiler/index.ts` | The compiler's API (`generate`, `build`, `verifyBinary`, the build manifest). |
37
54
  | `runtime/typesol_rt.h` | The C runtime: error codes, word-wise memory helpers, the entry point's input parser (`ts_deserialize`), account checks, checked math, Borsh helpers, CPIs (System, Token, Token-2022, ATA, Metaplex), oracles and the heap allocator. Header-only and `static inline`, so unused code is dropped. |
38
55
  | `src/workspace.ts` | `typesol.json` projects, program keypairs, `typesol init` templates and deploy arguments. |
39
56
  | `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
40
57
  | `src/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
41
- | `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program. |
58
+ | `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program, and reading a program's IDL back from the chain (Program Metadata or Anchor's IDL account) for `typesol client --address`. |
59
+ | `src/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
60
+ | `src/templates.ts` | The programs and tests `typesol init --template` writes (copies of `examples/`; a test checks they match). |
61
+ | `src/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
62
+ | `src/keys.ts` | Base58 and program keypair helpers. |
42
63
  | `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
43
64
  | `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
44
65
  | `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
@@ -54,7 +75,7 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
54
75
 
55
76
  | File | Layer | Contents |
56
77
  | --- | --- | --- |
57
- | `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`) |
78
+ | `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`), growable Vec placement (`vecPlace`, `vecLenCheck`) |
58
79
  | `base.ts` | `EmitterBase` | function state; `line()`, `tmp()`, `settle()`, scopes, `fail()`, account lookup |
59
80
  | `storage.ts` | `StorageEmitter` | references into account data and locals, reads and writes, indexing, aliasing, argument validation |
60
81
  | `expressions.ts` | `ExpressionEmitter` | literals, operators and checked arithmetic, property reads, conditionals, text, casts |
@@ -63,6 +84,9 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
63
84
  | `statements.ts` | `StatementEmitter` | variables, assignment, `if`/loops/`switch`, `return` |
64
85
  | `function.ts` | `FunctionEmitter` | the instruction prologue (account checks, constraints, argument decoding) and epilogue |
65
86
 
87
+ `borsh.ts` writes the Borsh pack/unpack codecs of `@layout borsh` structs, and `program.ts` assembles the C file
88
+ (typedefs, codecs, helpers, instructions, the entry point). `index.ts` is the emitter's export.
89
+
66
90
  ## Rules the emitter keeps
67
91
 
68
92
  These invariants are easy to break. Each one has tests.