@typesol/cli 0.18.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,30 @@ 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
+
8
32
  ## 0.18.0 — 2026-10-28
9
33
 
10
34
  The remaining Anchor differences, closed: Anchor's argument structs (Strings, Vecs, Options and enums in Borsh),
package/README.md CHANGED
@@ -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.18).** 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.
@@ -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",
@@ -1120,7 +1122,13 @@ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new S
1120
1122
  return { name: p.name.text, type: { kind: "signerSeeds" }, node: p };
1121
1123
  // Accounts the caller already checked: Signer, AccountInfo, Account<T>/Mut<T>, token accounts, programs.
1122
1124
  const wrapped = typesolName(p.type, scope.imports);
1123
- 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")
1124
1132
  reporter.fail(p.type, "constraints belong on instruction parameters; helpers take the plain account type");
1125
1133
  const account = resolveAccountKind(p.type, reporter, scope.imports, structs);
1126
1134
  if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed")
@@ -1270,7 +1278,17 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1270
1278
  continue;
1271
1279
  const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
1272
1280
  reporter.guard(() => {
1273
- 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
+ }
1274
1292
  if (!accounts.some((o) => o.name === f))
1275
1293
  reporter.fail(a.node, `HasOne<…, "${f}">: add an account parameter named "${f}"`);
1276
1294
  if (a.kind.kind === "token") {
@@ -1321,7 +1339,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1321
1339
  for (const a of accounts) {
1322
1340
  const c = a.constraints;
1323
1341
  const refs = [
1324
- ...c.hasOne,
1342
+ ...c.hasOne.map((e) => e.split("=").at(-1)),
1325
1343
  ...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1).split(".")[0]),
1326
1344
  ...(c.seeds?.program?.startsWith("@") ? [c.seeds.program.slice(1)] : []),
1327
1345
  ...(c.closeTo ? [c.closeTo] : []),
@@ -1381,7 +1399,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1381
1399
  /** Peel HasOne / Seeds / CloseTo wrappers off a parameter type, recording what they ask for. */
1382
1400
  function unwrapConstraints(node, reporter, imports, out) {
1383
1401
  const name = typesolName(node, imports);
1384
- if (!["HasOne", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
1402
+ if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
1385
1403
  return node;
1386
1404
  const args = node.typeArguments ?? [];
1387
1405
  const lit = (t) => {
@@ -1395,6 +1413,20 @@ function unwrapConstraints(node, reporter, imports, out) {
1395
1413
  };
1396
1414
  if (name === "HasOne")
1397
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
+ }
1398
1430
  if (name === "CloseTo")
1399
1431
  out.closeTo = lit(args[1])[0];
1400
1432
  if (name === "Owner") {
@@ -0,0 +1 @@
1
+ export declare const ANCHOR_ERRORS: Record<string, number>;
@@ -0,0 +1,80 @@
1
+ // Anchor's own error codes (anchor-lang ErrorCode), for `require(condition, AnchorError.X)`.
2
+ export const ANCHOR_ERRORS = {
3
+ InstructionMissing: 100,
4
+ InstructionFallbackNotFound: 101,
5
+ InstructionDidNotDeserialize: 102,
6
+ InstructionDidNotSerialize: 103,
7
+ IdlInstructionStub: 1000,
8
+ IdlInstructionInvalidProgram: 1001,
9
+ IdlAccountNotEmpty: 1002,
10
+ EventInstructionStub: 1500,
11
+ ConstraintMut: 2000,
12
+ ConstraintHasOne: 2001,
13
+ ConstraintSigner: 2002,
14
+ ConstraintRaw: 2003,
15
+ ConstraintOwner: 2004,
16
+ ConstraintRentExempt: 2005,
17
+ ConstraintSeeds: 2006,
18
+ ConstraintExecutable: 2007,
19
+ ConstraintState: 2008,
20
+ ConstraintAssociated: 2009,
21
+ ConstraintAssociatedInit: 2010,
22
+ ConstraintClose: 2011,
23
+ ConstraintAddress: 2012,
24
+ ConstraintZero: 2013,
25
+ ConstraintTokenMint: 2014,
26
+ ConstraintTokenOwner: 2015,
27
+ ConstraintMintMintAuthority: 2016,
28
+ ConstraintMintFreezeAuthority: 2017,
29
+ ConstraintMintDecimals: 2018,
30
+ ConstraintSpace: 2019,
31
+ ConstraintAccountIsNone: 2020,
32
+ ConstraintTokenTokenProgram: 2021,
33
+ ConstraintMintTokenProgram: 2022,
34
+ ConstraintAssociatedTokenTokenProgram: 2023,
35
+ ConstraintMintGroupPointerExtension: 2024,
36
+ ConstraintMintGroupPointerExtensionAuthority: 2025,
37
+ ConstraintMintGroupPointerExtensionGroupAddress: 2026,
38
+ ConstraintMintGroupMemberPointerExtension: 2027,
39
+ ConstraintMintGroupMemberPointerExtensionAuthority: 2028,
40
+ ConstraintMintGroupMemberPointerExtensionMemberAddress: 2029,
41
+ ConstraintMintMetadataPointerExtension: 2030,
42
+ ConstraintMintMetadataPointerExtensionAuthority: 2031,
43
+ ConstraintMintMetadataPointerExtensionMetadataAddress: 2032,
44
+ ConstraintMintCloseAuthorityExtension: 2033,
45
+ ConstraintMintCloseAuthorityExtensionAuthority: 2034,
46
+ ConstraintMintPermanentDelegateExtension: 2035,
47
+ ConstraintMintPermanentDelegateExtensionDelegate: 2036,
48
+ ConstraintMintTransferHookExtension: 2037,
49
+ ConstraintMintTransferHookExtensionAuthority: 2038,
50
+ ConstraintMintTransferHookExtensionProgramId: 2039,
51
+ RequireViolated: 2500,
52
+ RequireEqViolated: 2501,
53
+ RequireKeysEqViolated: 2502,
54
+ RequireNeqViolated: 2503,
55
+ RequireKeysNeqViolated: 2504,
56
+ RequireGtViolated: 2505,
57
+ RequireGteViolated: 2506,
58
+ AccountDiscriminatorAlreadySet: 3000,
59
+ AccountDiscriminatorNotFound: 3001,
60
+ AccountDiscriminatorMismatch: 3002,
61
+ AccountDidNotDeserialize: 3003,
62
+ AccountDidNotSerialize: 3004,
63
+ AccountNotEnoughKeys: 3005,
64
+ AccountNotMutable: 3006,
65
+ AccountOwnedByWrongProgram: 3007,
66
+ InvalidProgramId: 3008,
67
+ InvalidProgramExecutable: 3009,
68
+ AccountNotSigner: 3010,
69
+ AccountNotSystemOwned: 3011,
70
+ AccountNotInitialized: 3012,
71
+ AccountNotProgramData: 3013,
72
+ AccountNotAssociatedTokenAccount: 3014,
73
+ AccountSysvarMismatch: 3015,
74
+ AccountReallocExceedsLimit: 3016,
75
+ AccountDuplicateReallocs: 3017,
76
+ DeclaredProgramIdMismatch: 4100,
77
+ TryingToInitPayerAsProgramAccount: 4101,
78
+ InvalidNumericConversion: 4102,
79
+ Deprecated: 5000,
80
+ };
@@ -1,5 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import { calleeName, resolveScalarType } from "../analyze.js";
3
+ import { ANCHOR_ERRORS } from "../anchor-errors.js";
3
4
  import { decodeAddress } from "../../keys.js";
4
5
  import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
5
6
  import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
@@ -1091,6 +1092,13 @@ export class CallEmitter extends ExpressionEmitter {
1091
1092
  this.fail(n, "pass signer([...seeds with the bump]) or a SignerSeeds parameter");
1092
1093
  }
1093
1094
  errorRef(node) {
1095
+ // `AnchorError.X`: one of Anchor's own codes (not one of this program's errors, so not in the IDL).
1096
+ if (ts.isPropertyAccessExpression(node) && this.exportName(node.expression) === "AnchorError") {
1097
+ const code = ANCHOR_ERRORS[node.name.text];
1098
+ if (code === undefined)
1099
+ this.fail(node.name, `AnchorError has no ${node.name.text}`);
1100
+ return { code, name: node.name.text, msg: node.name.text };
1101
+ }
1094
1102
  if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
1095
1103
  const def = this.model.errorEnums.get(node.expression.text)?.get(node.name.text);
1096
1104
  if (def)
@@ -41,9 +41,10 @@ export declare abstract class CpiEmitter extends CallEmitter {
41
41
  * in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
42
42
  * InitializeMint2) run after the account is created, sized for exactly these extensions.
43
43
  */
44
- protected mintExtensions(lit: ts.ObjectLiteralExpression, tp: string, acct: string): {
44
+ protected mintExtensions(lit: ts.ObjectLiteralExpression, tp: string, acct: string, ifNeeded?: boolean): {
45
45
  space: number;
46
46
  lines: string[];
47
+ checks: string[];
47
48
  };
48
49
  /**
49
50
  * `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
@@ -396,8 +396,6 @@ export class CpiEmitter extends CallEmitter {
396
396
  const fixedArgs = isMint ? (extArg ? 6 : 5) : 4;
397
397
  if (a.length !== fixedArgs && a.length !== fixedArgs + 1)
398
398
  this.fail(node, usage);
399
- if (extArg && ifNeeded)
400
- this.fail(extArg, "createIfNeeded does not take extensions yet: create the mint with create(…)");
401
399
  const payer = this.accountParam(a[0], "the payer");
402
400
  if (payer.kind.kind !== "signer" || !payer.kind.mut)
403
401
  this.fail(a[0], "the payer must be a Mut<Signer>");
@@ -438,6 +436,7 @@ export class CpiEmitter extends CallEmitter {
438
436
  };
439
437
  let mint;
440
438
  const extLines = [];
439
+ const extChecks = [];
441
440
  let mintSpace = "TS_MINT_LEN";
442
441
  if (isMint) {
443
442
  take(a[2], U8);
@@ -447,9 +446,10 @@ export class CpiEmitter extends CallEmitter {
447
446
  if (extArg) {
448
447
  if (programs[0].kind.kind === "tokenProgram" && programs[0].kind.flavor === "token")
449
448
  this.fail(extArg, "mint extensions are Token-2022's; use Token2022Program or TokenInterface");
450
- const ext = this.mintExtensions(extArg, tp, acct);
449
+ const ext = this.mintExtensions(extArg, tp, acct, ifNeeded);
451
450
  mintSpace = String(ext.space);
452
451
  extLines.push(...ext.lines);
452
+ extChecks.push(...ext.checks);
453
453
  }
454
454
  }
455
455
  else {
@@ -474,6 +474,8 @@ export class CpiEmitter extends CallEmitter {
474
474
  this.line(` TS_TRY(ts_token_existing(params, ${acct}, ${tp}, ${seeds.name}, ${seeds.count}, &${b}));`);
475
475
  this.line(` TS_TRY(ts_check_mint(${acct}));`);
476
476
  this.line(` TS_TRY(ts_check_mint_is(${acct}, (uint8_t)(${decimals.c}), ${authority.c}, ${freeze ? freeze.c : "NULL"}));`);
477
+ for (const l of extChecks)
478
+ this.line(` ${l}`);
477
479
  this.line(`}`);
478
480
  }
479
481
  else
@@ -510,7 +512,7 @@ export class CpiEmitter extends CallEmitter {
510
512
  * in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
511
513
  * InitializeMint2) run after the account is created, sized for exactly these extensions.
512
514
  */
513
- mintExtensions(lit, tp, acct) {
515
+ mintExtensions(lit, tp, acct, ifNeeded = false) {
514
516
  const optKey = { kind: "option", inner: PUBKEY };
515
517
  this.model.options.set("pubkey", PUBKEY);
516
518
  // name: [extension type length, initializer tag, the fields an object value takes ([] for a plain value)]
@@ -676,8 +678,60 @@ export class CpiEmitter extends CallEmitter {
676
678
  }
677
679
  }
678
680
  }
681
+ // An existing mint (createIfNeeded) is checked as Anchor's `extensions::…` constraints check it: the extension must
682
+ // be there (its Anchor code) and each key must match (the next codes). Anchor has constraints for these six only.
683
+ const CHECKS = {
684
+ groupPointer: {
685
+ type: 20,
686
+ code: 2024,
687
+ fields: [
688
+ ["authority", 0],
689
+ ["groupAddress", 32],
690
+ ],
691
+ },
692
+ groupMemberPointer: {
693
+ type: 21,
694
+ code: 2027,
695
+ fields: [
696
+ ["authority", 0],
697
+ ["memberAddress", 32],
698
+ ],
699
+ },
700
+ metadataPointer: {
701
+ type: 18,
702
+ code: 2030,
703
+ fields: [
704
+ ["authority", 0],
705
+ ["metadataAddress", 32],
706
+ ],
707
+ },
708
+ closeAuthority: { type: 3, code: 2033, fields: [["v", 0]] },
709
+ permanentDelegate: { type: 12, code: 2035, fields: [["v", 0]] },
710
+ transferHook: {
711
+ type: 14,
712
+ code: 2037,
713
+ fields: [
714
+ ["authority", 0],
715
+ ["programId", 32],
716
+ ],
717
+ },
718
+ };
719
+ const checks = [];
720
+ if (ifNeeded)
721
+ for (const [name, v] of held) {
722
+ const c = CHECKS[name];
723
+ if (!c)
724
+ this.fail(lit, `${name} cannot be checked on an existing mint (Anchor has no constraint for it): create the mint with create(…)`);
725
+ checks.push(`{ uint64_t eo, el; if (!ts_t22_ext(${acct}, ${c.type}, &eo, &el)) TS_FAIL(${c.code}, "TypeSOL: the mint has no ${name} extension"); }`);
726
+ c.fields.forEach(([f, at], i) => {
727
+ const held = v.get(f);
728
+ // The value given: a nonzero-or-zero key, a COption (close authority) or a plain key (permanent delegate).
729
+ const want = name === "closeAuthority" ? `(${held}.some ? ${held}.v : (SolPubkey){0})` : held;
730
+ checks.push(`{ SolPubkey k, w = ${want}; if (!ts_t22_key(${acct}, ${c.type}, ${at}, &k)) ts_memzero(&k, 32); if (!ts_key_eq(&k, &w)) TS_FAIL(${c.code + 1 + i}, "TypeSOL: the mint's ${name} ${f === "v" ? "key" : f} differs"); }`);
731
+ });
732
+ }
679
733
  // Base account (165, the mint padded to an account's size), the account type byte, then each TLV entry.
680
- return { space: 165 + 1 + space, lines };
734
+ return { space: 165 + 1 + space, lines, checks };
681
735
  }
682
736
  /**
683
737
  * `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
@@ -1847,6 +1901,58 @@ export class CpiEmitter extends CallEmitter {
1847
1901
  this.cpi(p, metas, data.buf, data.len, seedsAt(13));
1848
1902
  return undefined;
1849
1903
  }
1904
+ case "mintToCollection": {
1905
+ // MintToCollectionV1: MintV1's accounts plus the collection authority (signing), its record PDA (or null),
1906
+ // the collection mint, metadata (writable) and master edition, Bubblegum's signer PDA and Token Metadata;
1907
+ // MetadataArgs with Some({ verified: false, key: collection mint }) (Bubblegum verifies it).
1908
+ if (a.length !== 17 && a.length !== 18)
1909
+ this.fail(node, "mintToCollection(treeConfig, leafOwner, leafDelegate, merkleTree, payer, treeDelegate, collectionAuthority, collectionAuthorityRecordPda | null, collectionMint, collectionMetadata, collectionEdition, bubblegumSigner, logWrapper, compressionProgram, tokenMetadataProgram, systemProgram, { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable?, editionNonce? }, signerSeeds?)");
1910
+ const metas = [
1911
+ acct(0, "the tree config", true, false),
1912
+ acct(1, "the leaf owner", false, false),
1913
+ acct(2, "the leaf delegate", false, false),
1914
+ acct(3, "the merkle tree", true, false),
1915
+ acct(4, "the payer", false, true),
1916
+ acct(5, "the tree delegate", false, true),
1917
+ acct(6, "the collection authority", false, true),
1918
+ acct(7, "the collection authority record", false, false),
1919
+ acct(8, "the collection mint", false, false),
1920
+ acct(9, "the collection metadata", true, false),
1921
+ acct(10, "the collection edition", false, false),
1922
+ acct(11, "Bubblegum's signer", false, false),
1923
+ acct(12, "the log wrapper", false, false),
1924
+ acct(13, "the compression program", false, false),
1925
+ acct(14, "Token Metadata", false, false),
1926
+ acct(15, "the system program", false, false),
1927
+ ];
1928
+ const md = a[16];
1929
+ if (!ts.isObjectLiteralExpression(md))
1930
+ this.fail(md, "the metadata is { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable?, editionNonce? }");
1931
+ const props = this.literalProps(md, ["name", "symbol", "uri", "sellerFeeBasisPoints", "creators", "isMutable", "editionNonce"], "the metadata");
1932
+ const need = (k) => props.get(k) ?? this.fail(md, `the metadata needs ${k}`);
1933
+ // `collectionMint.key`, read where the call is.
1934
+ const key = ts.factory.createPropertyAccessExpression(a[8], "key");
1935
+ ts.setTextRange(key, a[8]);
1936
+ key.parent = node;
1937
+ const nonce = props.get("editionNonce");
1938
+ const data = this.mplData(node, 1024, [
1939
+ { bytes: [153, 18, 178, 47, 197, 158, 86, 15] },
1940
+ { value: need("name"), type: STRING },
1941
+ { value: need("symbol"), type: STRING },
1942
+ { value: need("uri"), type: STRING },
1943
+ { value: need("sellerFeeBasisPoints"), type: { kind: "int", name: "u16" } },
1944
+ { bytes: [0] }, // primarySaleHappened
1945
+ props.get("isMutable") ? { value: props.get("isMutable"), type: BOOL } : { bytes: [1] },
1946
+ ...(nonce ? [{ bytes: [1] }, { value: nonce, type: U8 }] : [{ bytes: [0] }]),
1947
+ { bytes: [1, 0] }, // tokenStandard Some(NonFungible)
1948
+ { bytes: [1, 0] }, // collection Some({ verified: false, … })
1949
+ { value: key, type: PUBKEY },
1950
+ { bytes: [0, 0] }, // uses None, tokenProgramVersion Original
1951
+ ...this.bubblegumCreators(props.get("creators")),
1952
+ ]);
1953
+ this.cpi(p, metas, data.buf, data.len, seedsAt(17));
1954
+ return undefined;
1955
+ }
1850
1956
  }
1851
1957
  return this.bubblegumV2(node, bg, method);
1852
1958
  }
@@ -2128,7 +2234,7 @@ export class CpiEmitter extends CallEmitter {
2128
2234
  this.cpi(p, metas, data.buf, data.len, seedsAt(proofAt + 1), proofRange(a[proofAt]));
2129
2235
  return undefined;
2130
2236
  }
2131
- this.fail(node.expression, `Bubblegum supports createTree, mint, createTreeV2, mintV2, transferV2, burnV2, ${Object.keys(BUBBLEGUM_OPS).join(", ")}, not ${method}`);
2237
+ this.fail(node.expression, `Bubblegum supports createTree, mint, mintToCollection, createTreeV2, mintV2, transferV2, burnV2, ${Object.keys(BUBBLEGUM_OPS).join(", ")}, not ${method}`);
2132
2238
  }
2133
2239
  /** Object literal properties by name, refusing others. */
2134
2240
  literalProps(o, allowed, what) {
@@ -23,6 +23,25 @@ export declare abstract class ExpressionEmitter extends StorageEmitter {
23
23
  /** Whether TypeScript sees this expression as a string (decides `+` between concatenation and arithmetic). */
24
24
  protected looksLikeString(node: ts.Expression): boolean;
25
25
  /** A string view of a string, integer or boolean (for concatenation and templates). */
26
+ /**
27
+ * A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
28
+ * TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
29
+ * first, as JS reads them then.
30
+ */
31
+ protected templatePieces(node: ts.TemplateExpression): {
32
+ text: string;
33
+ arr?: undefined;
34
+ n?: undefined;
35
+ } | {
36
+ text?: undefined;
37
+ arr: string;
38
+ n: number;
39
+ };
40
+ /**
41
+ * The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
42
+ * literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
43
+ */
44
+ protected constantText(e: ts.Expression): string | undefined;
26
45
  protected toText(node: ts.Expression): string;
27
46
  /** C statement writing the decimal text of an integer into the TS_Str `out`. */
28
47
  protected textOfInt(name: IntName, c: string, out: string): string;
@@ -1,4 +1,5 @@
1
1
  import ts from "typescript";
2
+ import { base58 } from "../../keys.js";
2
3
  import { calleeName, resolveValueType } from "../analyze.js";
3
4
  import { INT_INFO, unsignedC, optionKey, sameType, typeName } from "../model.js";
4
5
  import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC, vecCount } from "./shared.js";
@@ -151,22 +152,14 @@ export class ExpressionEmitter extends StorageEmitter {
151
152
  return { c: `ts_str((const uint8_t *)${cString(node.text)}, ${len})`, type: STRING, stringLiteral: node.text };
152
153
  }
153
154
  if (ts.isTemplateExpression(node)) {
154
- // `a${x}b${y}c` -> concatenations on the heap
155
- let acc = this.tmp("tpl");
156
- const head = node.head.text;
157
- this.line(`TS_Str ${acc} = ts_str((const uint8_t *)${cString(head)}, ${Buffer.byteLength(head, "utf8")});`);
158
- for (const span of node.templateSpans) {
159
- for (const piece of [
160
- this.toText(span.expression),
161
- `ts_str((const uint8_t *)${cString(span.literal.text)}, ${Buffer.byteLength(span.literal.text, "utf8")})`,
162
- ]) {
163
- const next = this.tmp("tpl");
164
- this.line(`TS_Str ${next};`);
165
- this.line(`TS_TRY(ts_str_concat(${acc}, ${piece}, &${next}));`);
166
- acc = next;
167
- }
168
- }
169
- return { c: acc, type: STRING };
155
+ // `a${x}b${y}c`: one join on the heap (or a literal when every piece is known at compile time).
156
+ const pieces = this.templatePieces(node);
157
+ if (pieces.text !== undefined)
158
+ return { c: `ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})`, type: STRING, stringLiteral: pieces.text };
159
+ const t = this.tmp("tpl");
160
+ this.line(`TS_Str ${t};`);
161
+ this.line(`TS_TRY(ts_str_join(${pieces.arr}, ${pieces.n}, &${t}));`);
162
+ return { c: t, type: STRING };
170
163
  }
171
164
  if (node.kind === ts.SyntaxKind.NullKeyword)
172
165
  return { c: "", type: BOOL, isNull: true };
@@ -430,6 +423,68 @@ export class ExpressionEmitter extends StorageEmitter {
430
423
  return isText(this.checker.getTypeAtLocation(node));
431
424
  }
432
425
  /** A string view of a string, integer or boolean (for concatenation and templates). */
426
+ /**
427
+ * A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
428
+ * TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
429
+ * first, as JS reads them then.
430
+ */
431
+ templatePieces(node) {
432
+ const parts = [];
433
+ const lit = (text) => {
434
+ if (!text)
435
+ return;
436
+ const last = parts.at(-1);
437
+ if (last?.text !== undefined)
438
+ last.text += text;
439
+ else
440
+ parts.push({ text });
441
+ };
442
+ lit(node.head.text);
443
+ for (const span of node.templateSpans) {
444
+ const known = this.constantText(span.expression);
445
+ if (known !== undefined)
446
+ lit(known);
447
+ else {
448
+ const mark = this.lines.length;
449
+ const c = this.toText(span.expression);
450
+ for (const p of parts)
451
+ if (p.c !== undefined)
452
+ p.c = this.settle({ c: p.c, type: STRING }, mark).c;
453
+ parts.push({ c });
454
+ }
455
+ lit(span.literal.text);
456
+ }
457
+ if (parts.every((p) => p.text !== undefined))
458
+ return { text: parts.map((p) => p.text).join("") };
459
+ const strOf = (text) => `ts_str((const uint8_t *)${cString(text)}, ${Buffer.byteLength(text, "utf8")})`;
460
+ const arr = this.tmp("parts");
461
+ this.line(`TS_Str ${arr}[${parts.length}] = {${parts.map((p) => (p.text !== undefined ? strOf(p.text) : p.c)).join(", ")}};`);
462
+ return { arr, n: parts.length };
463
+ }
464
+ /**
465
+ * The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
466
+ * literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
467
+ */
468
+ constantText(e) {
469
+ if (ts.isParenthesizedExpression(e))
470
+ return this.constantText(e.expression);
471
+ if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))
472
+ return e.text;
473
+ if (e.kind === ts.SyntaxKind.TrueKeyword)
474
+ return "true";
475
+ if (e.kind === ts.SyntaxKind.FalseKeyword)
476
+ return "false";
477
+ if (ts.isBigIntLiteral(e))
478
+ return BigInt(e.text.slice(0, -1).replace(/_/g, "")).toString();
479
+ if (ts.isIdentifier(e) && !this.lookup(e.text)) {
480
+ const k = this.model.constants.get(e.text);
481
+ if (k && typeof k.value === "string")
482
+ return k.value;
483
+ if (k && typeof k.value === "object")
484
+ return base58(Uint8Array.from(k.value.pubkey));
485
+ }
486
+ return undefined;
487
+ }
433
488
  toText(node) {
434
489
  const v = this.expr(node);
435
490
  if (v.type.kind === "string")
@@ -371,15 +371,17 @@ export class FunctionEmitter extends StatementEmitter {
371
371
  const k = a.kind.kind;
372
372
  if (c.hasOne.length && k !== "account" && k !== "token" && k !== "mint")
373
373
  this.reporter.fail(a.node, "HasOne applies to existing accounts (Account<T> / Mut<T>), TokenAccount and Mint");
374
- for (const f of c.hasOne) {
374
+ for (const entry of c.hasOne) {
375
+ // "field=param" (TokenOf / MintOf) or a field checked against the parameter of the same name (HasOne).
376
+ const [f, p = f] = entry.split("=");
375
377
  if (k === "token")
376
378
  out.push(f === "mint"
377
- ? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${f}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${f}");`
378
- : ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${f}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${f}");`);
379
+ ? ` 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}");`
380
+ : ` 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}");`);
379
381
  else if (k === "mint") {
380
382
  const at = f === "mintAuthority" ? 0 : 46;
381
383
  const code = f === "mintAuthority" ? "TS_ERR_MINT_AUTHORITY" : "TS_ERR_FREEZE_AUTHORITY";
382
- 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_${f}->key)) TS_FAIL(${code}, "TypeSOL: ${f} is not the ${f} of ${a.name}");`);
384
+ 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}");`);
383
385
  }
384
386
  else
385
387
  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");`);
@@ -842,7 +842,24 @@ export class StatementEmitter extends CpiEmitter {
842
842
  this.line(`TS_Str ${cur};`);
843
843
  this.line(`TS_TRY(ts_str_copy(${current.c}, &${cur}));`);
844
844
  }
845
+ // A local grows in place when it is the newest heap string (a loop of `s += …` copies each piece once); a
846
+ // template on the right is appended piece by piece.
847
+ const local = cur === current.c && place.write("X") === `${cur} = X`;
848
+ const bare = (e) => (ts.isParenthesizedExpression(e) ? bare(e.expression) : e);
849
+ const rhsNode = bare(valueNode);
850
+ if (local && ts.isTemplateExpression(rhsNode)) {
851
+ const pieces = this.templatePieces(rhsNode);
852
+ if (pieces.text !== undefined)
853
+ this.line(`TS_TRY(ts_str_append(&${cur}, ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})));`);
854
+ else
855
+ this.line(`TS_TRY(ts_str_append_parts(&${cur}, ${pieces.arr}, ${pieces.n}));`);
856
+ return;
857
+ }
845
858
  const rhs = this.toText(valueNode);
859
+ if (local) {
860
+ this.line(`TS_TRY(ts_str_append(&${cur}, ${rhs}));`);
861
+ return;
862
+ }
846
863
  const t = this.tmp("cat");
847
864
  this.line(`TS_Str ${t};`);
848
865
  this.line(`TS_TRY(ts_str_concat(${cur}, ${rhs}, &${t}));`);
package/docs/language.md CHANGED
@@ -11,7 +11,7 @@ The full reference. For a tour, start with the [README](../README.md).
11
11
  | Integers | `u8` `u16` `u32` `u64` `u128` `i8` `i16` `i32` `i64` `i128` — bigint literals (`1n`), every `+ - * / % **` (and `+=` … `**=`) checked (`**` aborts on a negative exponent, as JS throws); `& \| ^` on equal types, `~` within the width, `<<` / `>>` checked (amount below the width, `<<` may not lose bits) |
12
12
  | Integer helpers | `mulDiv(a, b, c)` / `mulDivCeil` (u64 through a 128-bit product, u128 through a 256-bit one), `isqrt(x)` (up to u128), `min(a, b)`, `max(a, b)` |
13
13
  | Other scalars | `boolean`, `Pubkey` (compare with `===` or `.equals`) |
14
- | Errors | `export enum MyError { Name = "message" }` + `require(cond, MyError.Name)` → codes 6000+ |
14
+ | Errors | `export enum MyError { Name = "message" }` + `require(cond, MyError.Name)` → codes 6000+; `require(cond, AnchorError.RequireKeysEqViolated)` fails with one of Anchor's own codes (2502 here; every anchor-lang `ErrorCode`) |
15
15
  | Casts | `u8(x)`, `u64(x)`, … abort if the value does not fit |
16
16
  | Logging | `log("text")`, `log(value)`, `log(pubkey)` |
17
17
  | Arrays | `scores: FixedArray<u64, 8>` in account data — `.get(i)`, `.set(i, v)`, `.length`, bounds-checked; elements can be structs or strings |
@@ -36,7 +36,7 @@ The full reference. For a tour, start with the [README](../README.md).
36
36
  | Constants | top-level `const SEED = "vault"`, `const FEE: u16 = 30n`, `const ON = true` — inlined, usable as seeds (and described in the IDL) |
37
37
  | Data enums | `export type Action = { kind: "Deposit"; amount: u64 } \| { kind: "Pause" }` — `a.kind === "Deposit"`, `switch (a.kind)`, TypeScript narrowing picks the variant (checked again at runtime, 5112); Borsh enums in arguments/events, fixed `{ tag, payload }` in account data. A variant may hold a `string` (`{ kind: "Say"; text: string }`): such an enum works in arguments, events and locals (Borsh, as Anchor encodes it) but not in account data |
38
38
  | Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
39
- | Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full) |
39
+ | Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full): a template is joined in one allocation, text known at compile time (literals, string and Pubkey constants) is folded in, and `s += …` grows the newest heap string in place |
40
40
  | `switch` | on integers (128-bit ones too), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
41
41
  | 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) |
42
42
  | 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">`) |
@@ -67,7 +67,7 @@ The full reference. For a tour, start with the [README](../README.md).
67
67
  | `Init<T>` | writable; `create(payer, seeds)` derives the PDA, verifies it, funds rent, writes the discriminator (up to 10 KiB); `create(payer, null)` creates a new keypair account that signs the transaction instead (3010 if it did not; a signer in the IDL); a third argument is Anchor's `space = …` (discriminator included, at least 8 + the struct's size) |
68
68
  | `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 |
69
69
  | `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 |
70
- | `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name |
70
+ | `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name; `TokenOf<TokenAccount, "mintParam", "authorityParam">` / `MintOf<Mint, "authorityParam", "freezeParam">` name the parameters, as Anchor's `token::mint = …` / `mint::authority = …` do (`never` leaves one out) |
71
71
  | `Ata<TokenAccount, "owner", "mint">` | the account is the associated token account of those parameters (2009; ATA `pda` in the IDL) |
72
72
  | `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 |
73
73
  | `Address<A, "4Nd1…">` | exact address (2012; `address` in the IDL) |
@@ -77,7 +77,7 @@ The full reference. For a tour, start with the [README](../README.md).
77
77
  | `ProgramData` | this program's ProgramData (address derived from the program id, upgradeable-loader owner and state checked); `upgradeAuthority`, `deploySlot` — gate admin setup on `programData.upgradeAuthority === admin.key` |
78
78
  | `EventAuthority`, `SelfProgram` | Anchor's `["__event_authority"]` PDA, and this program's own address, for `emitCpi` |
79
79
  | `Zeroed<T>` | large accounts (up to 10 MiB) the client allocated with System `CreateAccount`: owned by this program, big enough, rent-exempt, discriminator still zero — then initialized; like Anchor's zero-copy `zero` |
80
- | `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (exactly one token program parameter, plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
80
+ | `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (exactly one token program parameter, plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. With `createIfNeeded` an existing mint must carry groupPointer / groupMemberPointer / metadataPointer / closeAuthority / permanentDelegate / transferHook with those keys (Anchor's 2024–2039). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
81
81
  | `Init<TokenAccount>`, `InitIfNeeded<TokenAccount>` (no `Ata<…>`) | Anchor `init` / `init_if_needed` with `token::mint` / `token::authority`: `vault.create(payer, ["vault", mint.key], mint, authority)` (the authority may be `vault.key` itself) creates a token account at that PDA (or a keypair account with `null`), sized for the mint's extensions under Token-2022. `createIfNeeded` checks an existing one has that mint and owner (2014 / 2015) |
82
82
  | `TokenAccount` / `Mut<TokenAccount>` | owned by SPL Token or Token-2022 (extensions allowed, multisig accounts refused), initialized; `mint`, `owner`, `amount`, `delegate`, `delegatedAmount`, `isFrozen`, `isNative`, `closeAuthority`, `tokenProgram` |
83
83
  | `Mint` / `Mut<Mint>` | owned by SPL Token or Token-2022, initialized; `decimals`, `supply`, `mintAuthority`, `freezeAuthority`, `tokenProgram` |
@@ -159,8 +159,7 @@ Growable `Vec`s anywhere but the last account field (use `FixedVec<T, N>`), Fixe
159
159
  the next instruction), `?.` on anything but optional accounts and Option<struct> fields, `!` on anything but an Option
160
160
  or an optional account, Core's external plugin adapters (oracles, app data, lifecycle hooks: use `invoke`), closures
161
161
  stored or returned (they are called or passed to list methods), arrow literals and closures as helper arguments (pass a
162
- named function), `map` / `filter` (they would build a new list), mint extensions on `InitIfNeeded<Mint>` (create the
163
- mint with `create(…, extensions)`), floating point (by design).
162
+ named function), `map` / `filter` (they would build a new list), floating point (by design).
164
163
  They are rejected at compile time rather than half-supported.
165
164
 
166
165
  `Init.create` handles a PDA address someone pre-funded: it tops up rent, then allocates and assigns instead of
@@ -49,7 +49,7 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
49
49
  | `Program<'info, System>` | `SystemProgram` |
50
50
  | `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
51
51
  | `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
52
- | `token::mint = m, token::authority = a` | `HasOne<TokenAccount, "mint" \| "owner">` against parameters with those names |
52
+ | `token::mint = m, token::authority = a` | `TokenOf<TokenAccount, "m", "a">` (2014 / 2015), or `HasOne<TokenAccount, "mint" \| "owner">` when the parameters have those names; `mint::authority = a, mint::freeze_authority = f` on an existing mint: `MintOf<Mint, "a", "f">` (2016 / 2017) |
53
53
  | `extensions::metadata_pointer::authority = a, …` (with `mint::…`) | `mint.create(p, …, { metadataPointer: { authority: a.key, metadataAddress: mint.key }, … })`: transferHook, closeAuthority, permanentDelegate, nonTransferable, groupPointer, groupMemberPointer and more |
54
54
  | `#[account(init, payer = p, seeds = […], bump, mint::decimals = 6, mint::authority = a, mint::freeze_authority = f)]` | `Init<Mint>`, then `const bump = mint.create(p, […], 6n, a.key, f.key)` (`null` seeds: a keypair mint; `InitIfNeeded<Mint>` + `createIfNeeded`) |
55
55
  | `#[account(init, payer = p, seeds = […], bump, token::mint = m, token::authority = a)]` | `Init<TokenAccount>`, then `vault.create(p, […], m, a.key)` (`InitIfNeeded<TokenAccount>` + `createIfNeeded`) |
@@ -91,6 +91,8 @@ Anchor instruction that takes or returns a struct with `String` / `Vec` / `Optio
91
91
  | `-> Result<u64>` / a struct | `: u64` / a struct: sent as return data, listed in the IDL |
92
92
  | `#[error_code] enum MyError { #[msg("…")] X }` | `export enum MyError { X = "…" }` (codes from 6000, in order) |
93
93
  | `require!(c, MyError::X)`, `err!(MyError::X)` | `require(c, MyError.X)`, `require(false, MyError.X)` |
94
+ | `require_keys_eq!(a, b)`, `err!(ErrorCode::AccountDidNotDeserialize)` (Anchor's own codes) | `require(a === b, AnchorError.RequireKeysEqViolated)`, `require(false, AnchorError.AccountDidNotDeserialize)` |
95
+ | `init_if_needed` + `extensions::…` on a mint | `mint.createIfNeeded(…, { metadataPointer: {…}, … })`: an existing mint is checked as Anchor checks it (2024–2039) |
94
96
  | `require_keys_eq!(a, b)` | `require(a === b, …)` |
95
97
  | `a.checked_add(b).ok_or(…)?` | `a + b` (every operation is checked; overflow aborts with 5100) |
96
98
  | `emit!(Ev { … })` / `emit_cpi!` | `emit<Ev>({ … })` / `emitCpi<Ev>({ … }, eventAuthority, self)` |
package/lib/typesol.d.ts CHANGED
@@ -517,6 +517,20 @@ export type InitIfNeeded<T> = T extends TokenAccount
517
517
  freezeAuthority: Pubkey | null,
518
518
  tokenProgram?: TokenProgram | Token2022Program | TokenInterface,
519
519
  ): boolean;
520
+ /**
521
+ * With extensions: created with them, or an existing mint must carry them with these keys, as Anchor's
522
+ * `extensions::…` constraints check (2024–2039). groupPointer, groupMemberPointer, metadataPointer,
523
+ * closeAuthority, permanentDelegate and transferHook only.
524
+ */
525
+ createIfNeeded(
526
+ payer: Mut<Signer>,
527
+ seeds: Seed[] | null,
528
+ decimals: u8,
529
+ mintAuthority: Pubkey,
530
+ freezeAuthority: Pubkey | null,
531
+ extensions: MintExtensions,
532
+ tokenProgram?: Token2022Program | TokenInterface,
533
+ ): boolean;
520
534
  readonly pdaBump: u8;
521
535
  }
522
536
  : T &
@@ -539,6 +553,15 @@ export type InitIfNeeded<T> = T extends TokenAccount
539
553
  * They nest: `CloseTo<HasOne<Mut<Vault>, "owner">, "owner">`.
540
554
  */
541
555
  export type HasOne<A, Field extends string> = A & { readonly __hasOne?: Field };
556
+ /**
557
+ * Anchor's `token::mint = mintParam, token::authority = authorityParam` on an existing token account, naming the
558
+ * parameters (2014 / 2015); `never` leaves one out.
559
+ */
560
+ export type TokenOf<A, MintParam extends string, AuthorityParam extends string = never> = A & { readonly __tokenOf?: [MintParam, AuthorityParam] };
561
+ /** Anchor's `mint::authority = a, mint::freeze_authority = f` on an existing mint, naming the parameters (2016 / 2017). */
562
+ export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extends string = never> = A & {
563
+ readonly __mintOf?: [AuthorityParam, FreezeAuthorityParam];
564
+ };
542
565
  export type Seeds<A, Names extends readonly string[], Bump extends string = never, ProgramId extends string = never> = A & {
543
566
  readonly __seeds?: [Names, Bump, ProgramId];
544
567
  /** The PDA's bump (Anchor `ctx.bumps.<account>`): the stored bump field when one is named, else the canonical bump found. */
@@ -774,6 +797,31 @@ export interface BubblegumProgram extends AccountMeta {
774
797
  sellerFeeBasisPoints: u16,
775
798
  signerSeeds?: Seed[] | SignerSeeds,
776
799
  ): void;
800
+ /**
801
+ * MintToCollectionV1 on a V1 tree: the leaf joins a Token Metadata collection (Bubblegum verifies it; the collection
802
+ * authority signs, its record PDA is `null` when it is the update authority itself). Creators default to none,
803
+ * `isMutable` to true, `editionNonce` to None.
804
+ */
805
+ mintToCollection(
806
+ treeConfig: AccountMeta,
807
+ leafOwner: AccountMeta,
808
+ leafDelegate: AccountMeta,
809
+ merkleTree: AccountMeta,
810
+ payer: Signer,
811
+ treeDelegate: AccountMeta,
812
+ collectionAuthority: AccountMeta,
813
+ collectionAuthorityRecordPda: AccountMeta | null,
814
+ collectionMint: AccountMeta,
815
+ collectionMetadata: AccountMeta,
816
+ collectionEdition: AccountMeta,
817
+ bubblegumSigner: AccountMeta,
818
+ logWrapper: AccountMeta,
819
+ compressionProgram: AccountMeta,
820
+ tokenMetadataProgram: AccountMeta,
821
+ systemProgram: SystemProgram,
822
+ metadata: CompressedMetadata & { editionNonce?: u8 },
823
+ signerSeeds?: Seed[] | SignerSeeds,
824
+ ): void;
777
825
  /**
778
826
  * CreateTreeConfigV2: a tree on mpl-account-compression (mcmt6…, allocated by the client and owned by it) with the
779
827
  * mpl-noop log wrapper (mnoop…). `treeCreator` `null`: the payer.
@@ -1382,7 +1430,90 @@ export declare function invoke(
1382
1430
  ): void;
1383
1431
 
1384
1432
  /** Abort with the given error unless `condition` holds. Use a string enum member or a string literal. */
1385
- export declare function require(condition: boolean, error: string): asserts condition;
1433
+ /**
1434
+ * Anchor's own error codes (anchor-lang `ErrorCode`): `require(condition, AnchorError.AccountDidNotDeserialize)` fails
1435
+ * with that code, as `require!(condition, ErrorCode::AccountDidNotDeserialize)` does. Not listed in the IDL's errors.
1436
+ */
1437
+ export declare enum AnchorError {
1438
+ InstructionMissing = 100,
1439
+ InstructionFallbackNotFound = 101,
1440
+ InstructionDidNotDeserialize = 102,
1441
+ InstructionDidNotSerialize = 103,
1442
+ IdlInstructionStub = 1000,
1443
+ IdlInstructionInvalidProgram = 1001,
1444
+ IdlAccountNotEmpty = 1002,
1445
+ EventInstructionStub = 1500,
1446
+ ConstraintMut = 2000,
1447
+ ConstraintHasOne = 2001,
1448
+ ConstraintSigner = 2002,
1449
+ ConstraintRaw = 2003,
1450
+ ConstraintOwner = 2004,
1451
+ ConstraintRentExempt = 2005,
1452
+ ConstraintSeeds = 2006,
1453
+ ConstraintExecutable = 2007,
1454
+ ConstraintState = 2008,
1455
+ ConstraintAssociated = 2009,
1456
+ ConstraintAssociatedInit = 2010,
1457
+ ConstraintClose = 2011,
1458
+ ConstraintAddress = 2012,
1459
+ ConstraintZero = 2013,
1460
+ ConstraintTokenMint = 2014,
1461
+ ConstraintTokenOwner = 2015,
1462
+ ConstraintMintMintAuthority = 2016,
1463
+ ConstraintMintFreezeAuthority = 2017,
1464
+ ConstraintMintDecimals = 2018,
1465
+ ConstraintSpace = 2019,
1466
+ ConstraintAccountIsNone = 2020,
1467
+ ConstraintTokenTokenProgram = 2021,
1468
+ ConstraintMintTokenProgram = 2022,
1469
+ ConstraintAssociatedTokenTokenProgram = 2023,
1470
+ ConstraintMintGroupPointerExtension = 2024,
1471
+ ConstraintMintGroupPointerExtensionAuthority = 2025,
1472
+ ConstraintMintGroupPointerExtensionGroupAddress = 2026,
1473
+ ConstraintMintGroupMemberPointerExtension = 2027,
1474
+ ConstraintMintGroupMemberPointerExtensionAuthority = 2028,
1475
+ ConstraintMintGroupMemberPointerExtensionMemberAddress = 2029,
1476
+ ConstraintMintMetadataPointerExtension = 2030,
1477
+ ConstraintMintMetadataPointerExtensionAuthority = 2031,
1478
+ ConstraintMintMetadataPointerExtensionMetadataAddress = 2032,
1479
+ ConstraintMintCloseAuthorityExtension = 2033,
1480
+ ConstraintMintCloseAuthorityExtensionAuthority = 2034,
1481
+ ConstraintMintPermanentDelegateExtension = 2035,
1482
+ ConstraintMintPermanentDelegateExtensionDelegate = 2036,
1483
+ ConstraintMintTransferHookExtension = 2037,
1484
+ ConstraintMintTransferHookExtensionAuthority = 2038,
1485
+ ConstraintMintTransferHookExtensionProgramId = 2039,
1486
+ RequireViolated = 2500,
1487
+ RequireEqViolated = 2501,
1488
+ RequireKeysEqViolated = 2502,
1489
+ RequireNeqViolated = 2503,
1490
+ RequireKeysNeqViolated = 2504,
1491
+ RequireGtViolated = 2505,
1492
+ RequireGteViolated = 2506,
1493
+ AccountDiscriminatorAlreadySet = 3000,
1494
+ AccountDiscriminatorNotFound = 3001,
1495
+ AccountDiscriminatorMismatch = 3002,
1496
+ AccountDidNotDeserialize = 3003,
1497
+ AccountDidNotSerialize = 3004,
1498
+ AccountNotEnoughKeys = 3005,
1499
+ AccountNotMutable = 3006,
1500
+ AccountOwnedByWrongProgram = 3007,
1501
+ InvalidProgramId = 3008,
1502
+ InvalidProgramExecutable = 3009,
1503
+ AccountNotSigner = 3010,
1504
+ AccountNotSystemOwned = 3011,
1505
+ AccountNotInitialized = 3012,
1506
+ AccountNotProgramData = 3013,
1507
+ AccountNotAssociatedTokenAccount = 3014,
1508
+ AccountSysvarMismatch = 3015,
1509
+ AccountReallocExceedsLimit = 3016,
1510
+ AccountDuplicateReallocs = 3017,
1511
+ DeclaredProgramIdMismatch = 4100,
1512
+ TryingToInitPayerAsProgramAccount = 4101,
1513
+ InvalidNumericConversion = 4102,
1514
+ Deprecated = 5000,
1515
+ }
1516
+ export declare function require(condition: boolean, error: string | AnchorError): asserts condition;
1386
1517
 
1387
1518
  /** Write a string, integer or pubkey to the program log. */
1388
1519
  export declare function log(value: string | bigint | Pubkey | Hash): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1105,48 +1105,124 @@ static inline uint64_t ts_alloc(uint64_t n, uint8_t **out) {
1105
1105
  static uint64_t ts_base58_32(const uint8_t *in, TS_Str *out) {
1106
1106
  static const char digits[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1107
1107
  const uint64_t D = 656356768ULL; /* 58^5 */
1108
- uint32_t limb[8];
1109
- for (int i = 0; i < 8; i++)
1110
- limb[i] = (uint32_t)in[4 * i] << 24 | (uint32_t)in[4 * i + 1] << 16 | (uint32_t)in[4 * i + 2] << 8 | (uint32_t)in[4 * i + 3];
1111
- uint8_t rev[50]; /* base-58 digits, least significant first; 32 bytes need at most 44 */
1112
- uint64_t len = 0;
1108
+ uint64_t limb[8];
1109
+ for (int i = 0; i < 8; i++) limb[i] = __builtin_bswap32(*(const ts_u32_unaligned *)(in + 4 * i)); /* big-endian words */
1110
+ /* Digits are written as characters from the end of `buf`, five per pass; 32 bytes need at most 44. */
1111
+ char buf[50];
1112
+ uint64_t pos = sizeof(buf);
1113
1113
  int start = 0;
1114
1114
  while (start < 8 && limb[start] == 0) start++;
1115
1115
  while (start < 8) {
1116
1116
  uint64_t rem = 0;
1117
- for (int i = start; i < 8; i++) {
1118
- uint64_t cur = (rem << 32) | limb[i];
1119
- uint64_t q = cur / D;
1120
- rem = cur - q * D;
1121
- limb[i] = (uint32_t)q;
1117
+ /* One long-division pass from the first nonzero limb, unrolled. */
1118
+ #define TS_B58_STEP(i) \
1119
+ { \
1120
+ uint64_t cur = (rem << 32) | limb[i]; \
1121
+ uint64_t q = cur / D; \
1122
+ rem = cur - q * D; \
1123
+ limb[i] = q; \
1124
+ }
1125
+ switch (start) {
1126
+ case 0: TS_B58_STEP(0) /* fallthrough */
1127
+ case 1: TS_B58_STEP(1) /* fallthrough */
1128
+ case 2: TS_B58_STEP(2) /* fallthrough */
1129
+ case 3: TS_B58_STEP(3) /* fallthrough */
1130
+ case 4: TS_B58_STEP(4) /* fallthrough */
1131
+ case 5: TS_B58_STEP(5) /* fallthrough */
1132
+ case 6: TS_B58_STEP(6) /* fallthrough */
1133
+ default: TS_B58_STEP(7)
1122
1134
  }
1135
+ #undef TS_B58_STEP
1123
1136
  while (start < 8 && limb[start] == 0) start++;
1124
- for (int k = 0; k < 5; k++) {
1125
- uint64_t q = rem / 58;
1126
- rev[len++] = (uint8_t)(rem - q * 58);
1127
- rem = q;
1128
- }
1137
+ uint64_t q;
1138
+ q = rem / 58; buf[--pos] = digits[rem - q * 58]; rem = q;
1139
+ q = rem / 58; buf[--pos] = digits[rem - q * 58]; rem = q;
1140
+ q = rem / 58; buf[--pos] = digits[rem - q * 58]; rem = q;
1141
+ q = rem / 58; buf[--pos] = digits[rem - q * 58]; rem = q;
1142
+ buf[--pos] = digits[rem];
1129
1143
  }
1130
- while (len > 0 && rev[len - 1] == 0) len--;
1144
+ /* Leading zero digits of the number are not printed; each leading zero byte is a '1'. */
1145
+ while (pos < sizeof(buf) && buf[pos] == '1') pos++;
1131
1146
  uint64_t zeros = 0;
1132
1147
  while (zeros < 32 && in[zeros] == 0) zeros++;
1148
+ uint64_t len = sizeof(buf) - pos;
1133
1149
  uint8_t *p;
1134
1150
  TS_TRY(ts_alloc(zeros + len, &p));
1135
1151
  for (uint64_t i = 0; i < zeros; i++) p[i] = '1';
1136
- for (uint64_t j = 0; j < len; j++) p[zeros + j] = (uint8_t)digits[rev[len - 1 - j]];
1152
+ ts_memcpy(p + zeros, buf + pos, len);
1137
1153
  *out = ts_str(p, zeros + len);
1138
1154
  return SUCCESS;
1139
1155
  }
1140
1156
 
1141
- static inline uint64_t ts_str_concat(TS_Str a, TS_Str b, TS_Str *out) {
1157
+ /*
1158
+ * `s += …` on a local string: the pieces are copied right after `s` when it ends where the heap's free space starts
1159
+ * (it was the last thing allocated), and `s` grows in place; otherwise `s` and the pieces are joined into a new string.
1160
+ * Other views of the old `s` keep their length, so they still read the same bytes. The bytes written are past the
1161
+ * heap's top, where no view can point (ts_alloc is the only source of heap pointers and never hands out memory past
1162
+ * the top), so no piece is overwritten while it is copied.
1163
+ */
1164
+ static uint64_t ts_str_append_parts(TS_Str *s, const TS_Str *parts, uint64_t n) {
1165
+ uint64_t *next = (uint64_t *)HEAP_START_ADDRESS;
1166
+ uint64_t add = 0;
1167
+ for (uint64_t i = 0; i < n; i++) add += parts[i].len;
1168
+ if (*next != 0 && s->len > 0 && (uint64_t)(s->ptr + s->len) == HEAP_START_ADDRESS + *next && *next + add <= TS_HEAP_LENGTH) {
1169
+ for (uint64_t i = 0; i < n; i++) {
1170
+ ts_memcpy((uint8_t *)s->ptr + s->len, parts[i].ptr, parts[i].len);
1171
+ s->len += parts[i].len;
1172
+ }
1173
+ *next += add;
1174
+ return SUCCESS;
1175
+ }
1142
1176
  uint8_t *p;
1143
- TS_TRY(ts_alloc(a.len + b.len, &p));
1144
- ts_memcpy(p, a.ptr, a.len);
1145
- ts_memcpy(p + a.len, b.ptr, b.len);
1146
- *out = ts_str(p, a.len + b.len);
1177
+ TS_TRY(ts_alloc(s->len + add, &p));
1178
+ ts_memcpy(p, s->ptr, s->len);
1179
+ uint64_t at = s->len;
1180
+ for (uint64_t i = 0; i < n; i++) {
1181
+ ts_memcpy(p + at, parts[i].ptr, parts[i].len);
1182
+ at += parts[i].len;
1183
+ }
1184
+ *s = ts_str(p, at);
1147
1185
  return SUCCESS;
1148
1186
  }
1149
1187
 
1188
+ /* `s += b` (and `a + b`, with `s` a copy of `a`): the one-piece case of ts_str_append_parts. */
1189
+ static uint64_t ts_str_append(TS_Str *s, TS_Str b) {
1190
+ uint64_t *next = (uint64_t *)HEAP_START_ADDRESS;
1191
+ if (*next != 0 && s->len > 0 && (uint64_t)(s->ptr + s->len) == HEAP_START_ADDRESS + *next && *next + b.len <= TS_HEAP_LENGTH) {
1192
+ ts_memcpy((uint8_t *)s->ptr + s->len, b.ptr, b.len);
1193
+ *next += b.len;
1194
+ s->len += b.len;
1195
+ return SUCCESS;
1196
+ }
1197
+ uint8_t *p;
1198
+ TS_TRY(ts_alloc(s->len + b.len, &p));
1199
+ ts_memcpy(p, s->ptr, s->len);
1200
+ ts_memcpy(p + s->len, b.ptr, b.len);
1201
+ *s = ts_str(p, s->len + b.len);
1202
+ return SUCCESS;
1203
+ }
1204
+
1205
+ /* The pieces of a template literal, joined with one heap allocation and one copy each. */
1206
+ static uint64_t ts_str_join(const TS_Str *parts, uint64_t n, TS_Str *out) {
1207
+ uint64_t total = 0;
1208
+ for (uint64_t i = 0; i < n; i++) total += parts[i].len;
1209
+ uint8_t *p;
1210
+ TS_TRY(ts_alloc(total, &p));
1211
+ uint64_t at = 0;
1212
+ for (uint64_t i = 0; i < n; i++) {
1213
+ ts_memcpy(p + at, parts[i].ptr, parts[i].len);
1214
+ at += parts[i].len;
1215
+ }
1216
+ *out = ts_str(p, total);
1217
+ return SUCCESS;
1218
+ }
1219
+
1220
+ /* `a + b`: a new string (`a` grows in place when it is the newest heap string; its own view keeps its length). */
1221
+ static inline uint64_t ts_str_concat(TS_Str a, TS_Str b, TS_Str *out) {
1222
+ *out = a;
1223
+ return ts_str_append(out, b);
1224
+ }
1225
+
1150
1226
  /* Decimal text of an integer, on the heap. */
1151
1227
  /* Copy a string's bytes to the heap (a stable value, independent of where the view pointed). */
1152
1228
  static inline uint64_t ts_str_copy(TS_Str a, TS_Str *out) {