@typesol/cli 0.17.0 → 0.18.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.
@@ -1,9 +1,153 @@
1
1
  import ts from "typescript";
2
2
  import { discriminator, snakeCase } from "../analyze.js";
3
3
  import { borshMinSize } from "./borsh.js";
4
- import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
4
+ import { INT_INFO, isScalar, isWritable, optionKey, sameType, scalarSize, typeName, } from "../model.js";
5
5
  import { MAX_ACCOUNTS, MAX_SEEDS, MAX_CPI_DATA, MAX_EVENT, U64, U8, BOOL, PUBKEY, STRING, cBytes, isStable, cString, scalarStorageC, optC, valueC, } from "./shared.js";
6
6
  import { CallEmitter } from "./calls.js";
7
+ /**
8
+ * Bubblegum instructions that replace a leaf: discriminator, accounts in order ([label, writable, signer, optional,
9
+ * either-signs]) and the leaf's shape. Optional accounts take `null` (the program id). The Merkle proof follows the
10
+ * leaf (and metadata) as remaining accounts.
11
+ */
12
+ const BUBBLEGUM_OPS = {
13
+ delegateV2: {
14
+ disc: [95, 87, 125, 140, 181, 131, 128, 227],
15
+ leaf: "delegate",
16
+ accounts: [
17
+ ["treeConfig", true, false],
18
+ ["payer", true, true],
19
+ ["leafOwner", false, true, true],
20
+ ["previousLeafDelegate", false, false, true],
21
+ ["newLeafDelegate", false, false],
22
+ ["merkleTree", true, false],
23
+ ["logWrapper", false, false],
24
+ ["compressionProgram", false, false],
25
+ ["systemProgram", false, false],
26
+ ],
27
+ },
28
+ ...Object.fromEntries([
29
+ ["freezeV2", [200, 151, 244, 102, 16, 195, 255, 3], false],
30
+ ["thawV2", [96, 133, 101, 93, 82, 220, 146, 191], false],
31
+ ["setNonTransferableV2", [181, 141, 206, 58, 242, 199, 152, 168], true],
32
+ ].map(([name, disc, needsCollection]) => [
33
+ name,
34
+ {
35
+ disc: disc,
36
+ leaf: "v2",
37
+ accounts: [
38
+ ["treeConfig", true, false],
39
+ ["payer", true, true],
40
+ ["authority", false, true, true],
41
+ ["leafOwner", false, false],
42
+ ["leafDelegate", false, false, name === "setNonTransferableV2"],
43
+ ["merkleTree", true, false],
44
+ ["coreCollection", false, false, !needsCollection],
45
+ ["logWrapper", false, false],
46
+ ["compressionProgram", false, false],
47
+ ["systemProgram", false, false],
48
+ ],
49
+ },
50
+ ])),
51
+ updateMetadataV2: {
52
+ disc: [43, 103, 89, 42, 121, 242, 62, 72],
53
+ leaf: "meta",
54
+ accounts: [
55
+ ["treeConfig", true, false],
56
+ ["payer", true, true],
57
+ ["authority", false, true, true],
58
+ ["leafOwner", false, false],
59
+ ["leafDelegate", false, false, true],
60
+ ["merkleTree", true, false],
61
+ ["coreCollection", false, false, true],
62
+ ["logWrapper", false, false],
63
+ ["compressionProgram", false, false],
64
+ ["systemProgram", false, false],
65
+ ],
66
+ },
67
+ ...Object.fromEntries([
68
+ ["verifyCreatorV2", [85, 138, 140, 42, 22, 241, 118, 102]],
69
+ ["unverifyCreatorV2", [174, 112, 29, 142, 230, 100, 239, 7]],
70
+ ].map(([name, disc]) => [
71
+ name,
72
+ {
73
+ disc: disc,
74
+ leaf: "meta",
75
+ accounts: [
76
+ ["treeConfig", true, false],
77
+ ["payer", true, true],
78
+ ["creator", false, true, true],
79
+ ["leafOwner", false, false],
80
+ ["leafDelegate", false, false, true],
81
+ ["merkleTree", true, false],
82
+ ["logWrapper", false, false],
83
+ ["compressionProgram", false, false],
84
+ ["systemProgram", false, false],
85
+ ],
86
+ },
87
+ ])),
88
+ setCollectionV2: {
89
+ disc: [229, 35, 61, 91, 15, 14, 99, 160],
90
+ leaf: "meta",
91
+ accounts: [
92
+ ["treeConfig", true, false],
93
+ ["payer", true, true],
94
+ ["authority", false, true, true],
95
+ ["newCollectionAuthority", false, true, true],
96
+ ["leafOwner", false, false],
97
+ ["leafDelegate", false, false, true],
98
+ ["merkleTree", true, false],
99
+ ["coreCollection", true, false, true],
100
+ ["newCoreCollection", true, false, true],
101
+ ["mplCoreCpiSigner", false, false, true],
102
+ ["logWrapper", false, false],
103
+ ["compressionProgram", false, false],
104
+ ["mplCoreProgram", false, false],
105
+ ["systemProgram", false, false],
106
+ ],
107
+ },
108
+ // V1 trees (SPL Account Compression, SPL Noop).
109
+ transfer: {
110
+ disc: [163, 52, 200, 231, 140, 3, 69, 186],
111
+ leaf: "v1",
112
+ accounts: [
113
+ ["treeConfig", false, false],
114
+ ["leafOwner", false, false, false, "owner"],
115
+ ["leafDelegate", false, false, false, "delegate"],
116
+ ["newLeafOwner", false, false],
117
+ ["merkleTree", true, false],
118
+ ["logWrapper", false, false],
119
+ ["compressionProgram", false, false],
120
+ ["systemProgram", false, false],
121
+ ],
122
+ },
123
+ burn: {
124
+ disc: [116, 110, 29, 56, 107, 219, 42, 93],
125
+ leaf: "v1",
126
+ accounts: [
127
+ ["treeConfig", false, false],
128
+ ["leafOwner", false, false, false, "owner"],
129
+ ["leafDelegate", false, false, false, "delegate"],
130
+ ["merkleTree", true, false],
131
+ ["logWrapper", false, false],
132
+ ["compressionProgram", false, false],
133
+ ["systemProgram", false, false],
134
+ ],
135
+ },
136
+ delegate: {
137
+ disc: [90, 147, 75, 178, 85, 88, 4, 137],
138
+ leaf: "v1",
139
+ accounts: [
140
+ ["treeConfig", false, false],
141
+ ["leafOwner", false, false, false, "owner"],
142
+ ["previousLeafDelegate", false, false],
143
+ ["newLeafDelegate", false, false],
144
+ ["merkleTree", true, false],
145
+ ["logWrapper", false, false],
146
+ ["compressionProgram", false, false],
147
+ ["systemProgram", false, false],
148
+ ],
149
+ },
150
+ };
7
151
  /** Metaplex Core's Plugin / PluginType enums, in order (MasterEdition, VerifiedCreators and Autograph values are refused). */
8
152
  const CORE_PLUGINS = [
9
153
  "Royalties",
@@ -22,9 +166,17 @@ const CORE_PLUGINS = [
22
166
  "VerifiedCreators",
23
167
  "Autograph",
24
168
  "BubblegumV2",
169
+ "FreezeExecute",
170
+ "PermanentFreezeExecute",
171
+ "Groups",
25
172
  ];
26
173
  const CORE_PLUGIN_FIELDS = {
27
- Royalties: ["basisPoints", "creators"],
174
+ Royalties: ["basisPoints", "creators", "ruleSet"],
175
+ MasterEdition: ["maxSupply", "name", "uri"],
176
+ VerifiedCreators: ["signatures"],
177
+ Autograph: ["signatures"],
178
+ FreezeExecute: ["frozen"],
179
+ PermanentFreezeExecute: ["frozen"],
28
180
  FreezeDelegate: ["frozen"],
29
181
  PermanentFreezeDelegate: ["frozen"],
30
182
  UpdateDelegate: ["additionalDelegates"],
@@ -149,38 +301,383 @@ export class CpiEmitter extends CallEmitter {
149
301
  this.ix.pdas.set(account.name, idl);
150
302
  }
151
303
  create(node, target, topLevel, ifNeeded = false) {
304
+ const call = `${target.name}.${ifNeeded ? "createIfNeeded" : "create"}`;
152
305
  if (!topLevel)
153
- this.fail(node, `${target.name}.${ifNeeded ? "createIfNeeded" : "create"}(...) must be a top-level statement of the instruction`);
306
+ this.fail(node, `${call}(...) must be a top-level statement of the instruction`);
154
307
  if (this.created.has(target.name))
155
308
  this.fail(node, `${target.name} is already created`);
156
- if (node.arguments.length !== 2)
157
- this.fail(node, "create(payer, seeds) takes two arguments");
309
+ if (node.arguments.length !== 2 && node.arguments.length !== 3)
310
+ this.fail(node, `${call}(payer, seeds | null, space?) takes two or three arguments`);
158
311
  const payer = this.accountParam(node.arguments[0], "the payer");
159
312
  if (payer.kind.kind !== "signer" || !payer.kind.mut)
160
313
  this.fail(node.arguments[0], "the payer must be a Mut<Signer>");
161
- const seeds = this.seeds(node.arguments[1], 1);
162
- this.recordPda(target, seeds.idl);
163
314
  const s = target.kind.struct;
315
+ const keypair = node.arguments[1].kind === ts.SyntaxKind.NullKeyword;
316
+ if (keypair && ifNeeded)
317
+ this.fail(node.arguments[1], "createIfNeeded takes seeds: a keypair account cannot be checked to be the one meant");
318
+ const seeds = keypair ? undefined : this.seeds(node.arguments[1], 1);
319
+ if (seeds)
320
+ this.recordPda(target, seeds.idl);
164
321
  const b = this.tmp("bump");
322
+ // The account's size: the struct's (8 + its fixed size, Anchor's InitSpace for @layout borsh), or Anchor's
323
+ // `space = …` when given, which must hold the struct.
324
+ const need = `8 + sizeof(TSS_${s.name})`;
325
+ let space = need;
326
+ const spaceArg = node.arguments[2];
327
+ if (spaceArg) {
328
+ if (s.vec)
329
+ this.fail(spaceArg, `${s.name} has a growable Vec: its size follows the Vec's length, so it takes no space argument`);
330
+ const raw = this.expr(spaceArg);
331
+ const v = this.expect(raw, U64, spaceArg);
332
+ const min = 8 + this.fieldBytes({ kind: "struct", name: s.name });
333
+ if (raw.literal !== undefined && raw.literal < BigInt(min))
334
+ this.fail(spaceArg, `${s.name} needs at least ${min} bytes (8 + its size)`);
335
+ space = this.tmp("space");
336
+ this.line(`uint64_t ${space} = ${v.c};`);
337
+ this.line(`if (${space} < ${need} || ${space} > 10240) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: space must hold the account and be at most 10240 bytes");`);
338
+ }
165
339
  this.line(`uint8_t ${b};`);
340
+ const done = () => {
341
+ this.created.add(target.name);
342
+ this.pendingInit.delete(target.name);
343
+ this.bumps.set(target.name, b);
344
+ };
345
+ if (!seeds) {
346
+ // A new keypair account that signs the transaction (Anchor's `init` without seeds).
347
+ target.signsInit = true;
348
+ this.line(`TS_TRY(ts_create_owned(params, a_${payer.name}, a_${target.name}, NULL, 0, params->program_id, ${space}, &${b}));`);
349
+ this.line(`a_${target.name}->data_len = ${space};`);
350
+ this.line(`ts_memcpy(a_${target.name}->data, TS_DISC_${s.name}, 8);`);
351
+ done();
352
+ return { c: b, type: U8 };
353
+ }
166
354
  if (ifNeeded) {
167
355
  // Returns whether it was created now; an existing account must be this PDA, ours, and a ${s.name}.
168
356
  const created = this.tmp("created");
169
357
  this.line(`bool ${created};`);
170
- this.line(`TS_TRY(ts_create_pda_if_needed(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name}, 8 + sizeof(TSS_${s.name}), 8 + ${s.layout === "borsh" ? borshMinSize(s, this.model) : `sizeof(TSS_${s.name})`}, &${b}, &${created}));`);
358
+ this.line(`TS_TRY(ts_create_pda_if_needed(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name}, ${space}, 8 + ${s.layout === "borsh" ? borshMinSize(s, this.model) : `sizeof(TSS_${s.name})`}, &${b}, &${created}));`);
171
359
  // An existing @layout borsh account is unpacked now (a new one starts from the zeroed struct).
172
360
  if (s.layout === "borsh")
173
- this.line(`if (!${created}) TS_TRY(ts_unpack_${s.name}(a_${target.name}->data + 8, a_${target.name}->data_len - 8, d_${target.name}));`);
174
- this.created.add(target.name);
175
- this.pendingInit.delete(target.name);
176
- this.bumps.set(target.name, b);
361
+ this.line(`if (!${created}) TS_TRY(ts_unpack_${s.name}(a_${target.name}->data + 8, a_${target.name}->data_len - 8, d_${target.name}, NULL));`);
362
+ done();
177
363
  return { c: created, type: BOOL };
178
364
  }
179
- this.line(`TS_TRY(ts_create_pda(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name}, 8 + sizeof(TSS_${s.name}), &${b}));`);
365
+ this.line(`TS_TRY(ts_create_pda(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name}, ${space}, &${b}));`);
366
+ done();
367
+ return { c: b, type: U8 };
368
+ }
369
+ /**
370
+ * `vault.create(payer, seeds | null, mint, authority)` on `Init<TokenAccount>` and `mint.create(payer, seeds | null,
371
+ * decimals, authority, freezeAuthority | null)` on `Init<Mint>` (Anchor's `init` with `token::` / `mint::`
372
+ * constraints): a PDA of this program (or, with `null`, a new keypair account that signs), created owned by the
373
+ * instruction's token program and initialized. `createIfNeeded` on `InitIfNeeded<…>` does that unless the account
374
+ * exists, and then checks it is that PDA, owned by the token program, and has that mint and owner / those
375
+ * authorities and decimals (Anchor's init_if_needed checks). create returns the bump (0 for a keypair),
376
+ * createIfNeeded whether it created the account.
377
+ */
378
+ createToken(node, target, topLevel) {
379
+ const isMint = target.kind.kind === "mint";
380
+ const ifNeeded = target.kind.init === "ifNeeded";
381
+ const call = `${target.name}.${ifNeeded ? "createIfNeeded" : "create"}`;
382
+ const usage = isMint
383
+ ? `${call}(payer, seeds | null, decimals, mintAuthority, freezeAuthority | null, extensions?, tokenProgram?)`
384
+ : `${call}(payer, seeds | null, mint, authority, tokenProgram?)`;
385
+ if (!topLevel)
386
+ this.fail(node, `${call}(...) must be a top-level statement of the instruction`);
387
+ if (this.created.has(target.name))
388
+ this.fail(node, `${target.name} is already created`);
389
+ const a = node.arguments;
390
+ if (!isMint && a.length === 1)
391
+ this.fail(node, `${call}(payer) creates an associated token account: wrap it in Ata<…, "owner", "mint">; or call ${usage.slice(call.length)}`);
392
+ // A mint may list its Token-2022 extensions next (Anchor's `extensions::…`), as an object literal.
393
+ const bare = (e) => e && (ts.isAsExpression(e) || ts.isParenthesizedExpression(e) || ts.isSatisfiesExpression(e)) ? bare(e.expression) : e;
394
+ const ext5 = bare(a[5]);
395
+ const extArg = isMint && ext5 && ts.isObjectLiteralExpression(ext5) ? ext5 : undefined;
396
+ const fixedArgs = isMint ? (extArg ? 6 : 5) : 4;
397
+ if (a.length !== fixedArgs && a.length !== fixedArgs + 1)
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
+ const payer = this.accountParam(a[0], "the payer");
402
+ if (payer.kind.kind !== "signer" || !payer.kind.mut)
403
+ this.fail(a[0], "the payer must be a Mut<Signer>");
404
+ // The token program: named as the last argument, or the instruction's only token program parameter.
405
+ let programs = this.ix.accounts.filter((p) => p.kind.kind === "tokenProgram");
406
+ if (a[fixedArgs]) {
407
+ const named = this.accountParam(a[fixedArgs], "the token program");
408
+ if (named.kind.kind !== "tokenProgram")
409
+ this.fail(a[fixedArgs], "pass a TokenProgram, Token2022Program or TokenInterface parameter");
410
+ programs = [named];
411
+ }
412
+ if (programs.length !== 1)
413
+ this.fail(node, `${call}(...) needs one token program: pass it as the last argument, or have exactly one token program parameter`);
414
+ if (programs[0].optional)
415
+ this.fail(programs[0].node, `${call}(...) needs "${programs[0].name}": it cannot be optional`);
416
+ if (!this.ix.accounts.some((p) => p.kind.kind === "system" && !p.optional))
417
+ this.fail(node, `${call}(...) needs a SystemProgram parameter`);
418
+ const tp = `a_${programs[0].name}`;
419
+ const acct = `a_${target.name}`;
420
+ // Seeds, then the values, in source order.
421
+ let seeds = { name: "NULL", count: 0 };
422
+ if (a[1].kind !== ts.SyntaxKind.NullKeyword) {
423
+ const s = this.seeds(a[1], 1);
424
+ this.recordPda(target, s.idl);
425
+ seeds = s;
426
+ }
427
+ else
428
+ target.signsInit = true;
429
+ const vals = [];
430
+ const take = (n, t) => {
431
+ const mark = this.lines.length;
432
+ // The account's own address is known before it exists (`vault.create(payer, seeds, mint, vault.key)`).
433
+ const own = ts.isPropertyAccessExpression(n) && n.name.text === "key" && ts.isIdentifier(n.expression) && n.expression.text === target.name;
434
+ const v = own ? { c: `(&${acct}->key)`, type: PUBKEY } : this.expect(this.expr(n, t), t, n);
435
+ for (let j = 0; j < vals.length; j++)
436
+ vals[j] = this.settle(vals[j], mark);
437
+ vals.push(v);
438
+ };
439
+ let mint;
440
+ const extLines = [];
441
+ let mintSpace = "TS_MINT_LEN";
442
+ if (isMint) {
443
+ take(a[2], U8);
444
+ take(a[3], PUBKEY);
445
+ if (a[4].kind !== ts.SyntaxKind.NullKeyword)
446
+ take(a[4], PUBKEY);
447
+ if (extArg) {
448
+ if (programs[0].kind.kind === "tokenProgram" && programs[0].kind.flavor === "token")
449
+ this.fail(extArg, "mint extensions are Token-2022's; use Token2022Program or TokenInterface");
450
+ const ext = this.mintExtensions(extArg, tp, acct);
451
+ mintSpace = String(ext.space);
452
+ extLines.push(...ext.lines);
453
+ }
454
+ }
455
+ else {
456
+ mint = this.accountParam(a[2], "the mint");
457
+ if (mint.kind.kind !== "mint")
458
+ this.fail(a[2], "pass the Mint parameter the account will hold");
459
+ take(a[3], PUBKEY);
460
+ }
461
+ const b = this.tmp("bump");
462
+ this.line(`uint8_t ${b};`);
463
+ const created = this.tmp("created");
464
+ const createLines = [];
465
+ if (isMint) {
466
+ const [decimals, authority, freeze] = vals;
467
+ createLines.push(`TS_TRY(ts_create_owned(params, a_${payer.name}, ${acct}, ${seeds.name}, ${seeds.count}, &${tp}->key, ${mintSpace}, &${b}));`, ...extLines, `TS_TRY(ts_token_init_mint(params, ${tp}, ${acct}, (uint8_t)(${decimals.c}), ${authority.c}, ${freeze ? freeze.c : "NULL"}));`);
468
+ if (ifNeeded) {
469
+ this.line(`bool ${created} = ts_key_eq(&${acct}->owner, &TS_SYSTEM_PROGRAM_ID);`);
470
+ this.line(`if (${created}) {`);
471
+ for (const l of createLines)
472
+ this.line(` ${l}`);
473
+ this.line(`} else {`);
474
+ this.line(` TS_TRY(ts_token_existing(params, ${acct}, ${tp}, ${seeds.name}, ${seeds.count}, &${b}));`);
475
+ this.line(` TS_TRY(ts_check_mint(${acct}));`);
476
+ this.line(` TS_TRY(ts_check_mint_is(${acct}, (uint8_t)(${decimals.c}), ${authority.c}, ${freeze ? freeze.c : "NULL"}));`);
477
+ this.line(`}`);
478
+ }
479
+ else
480
+ for (const l of createLines)
481
+ this.line(l);
482
+ }
483
+ else {
484
+ const [authority] = vals;
485
+ const space = this.tmp("space");
486
+ createLines.push(`uint64_t ${space};`, `TS_TRY(ts_token_account_space(params, ${tp}, a_${mint.name}, &${space}));`, `TS_TRY(ts_create_owned(params, a_${payer.name}, ${acct}, ${seeds.name}, ${seeds.count}, &${tp}->key, ${space}, &${b}));`, `TS_TRY(ts_token_init_account(params, ${tp}, ${acct}, a_${mint.name}, ${authority.c}));`);
487
+ if (ifNeeded) {
488
+ this.line(`bool ${created} = ts_key_eq(&${acct}->owner, &TS_SYSTEM_PROGRAM_ID);`);
489
+ this.line(`if (${created}) {`);
490
+ for (const l of createLines)
491
+ this.line(` ${l}`);
492
+ this.line(`} else {`);
493
+ this.line(` TS_TRY(ts_token_existing(params, ${acct}, ${tp}, ${seeds.name}, ${seeds.count}, &${b}));`);
494
+ this.line(` TS_TRY(ts_check_token_account(${acct}));`);
495
+ this.line(` if (!ts_key_eq(ts_token_mint(${acct}), &a_${mint.name}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${target.name} is not a token account of ${mint.name}");`);
496
+ this.line(` if (!ts_key_eq(ts_token_owner(${acct}), ${authority.c})) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${target.name} has another owner");`);
497
+ this.line(`}`);
498
+ }
499
+ else
500
+ for (const l of createLines)
501
+ this.line(l);
502
+ }
180
503
  this.created.add(target.name);
181
504
  this.pendingInit.delete(target.name);
182
505
  this.bumps.set(target.name, b);
183
- return { c: b, type: U8 };
506
+ return ifNeeded ? { c: created, type: BOOL } : { c: b, type: U8 };
507
+ }
508
+ /**
509
+ * The extensions in `mint.create(…, { … })` (Anchor's `extensions::…` constraints): their values are evaluated now,
510
+ * in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
511
+ * InitializeMint2) run after the account is created, sized for exactly these extensions.
512
+ */
513
+ mintExtensions(lit, tp, acct) {
514
+ const optKey = { kind: "option", inner: PUBKEY };
515
+ this.model.options.set("pubkey", PUBKEY);
516
+ // name: [extension type length, initializer tag, the fields an object value takes ([] for a plain value)]
517
+ const KINDS = {
518
+ transferFee: { len: 108, fields: ["configAuthority", "withdrawAuthority", "basisPoints", "maximumFee"] },
519
+ closeAuthority: { len: 32, fields: [] },
520
+ defaultAccountState: { len: 1, fields: [] },
521
+ nonTransferable: { len: 0, fields: [] },
522
+ interestBearing: { len: 52, fields: ["rateAuthority", "rate"] },
523
+ permanentDelegate: { len: 32, fields: [] },
524
+ transferHook: { len: 64, fields: ["authority", "programId"] },
525
+ metadataPointer: { len: 64, fields: ["authority", "metadataAddress"] },
526
+ groupPointer: { len: 64, fields: ["authority", "groupAddress"] },
527
+ groupMemberPointer: { len: 64, fields: ["authority", "memberAddress"] },
528
+ };
529
+ const given = new Map();
530
+ for (const p of lit.properties) {
531
+ const value = ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) ? p.initializer : ts.isShorthandPropertyAssignment(p) ? p.name : undefined;
532
+ if (!value || !p.name || !ts.isIdentifier(p.name))
533
+ this.fail(p, "write the extensions as `name: value` entries");
534
+ if (!KINDS[p.name.text])
535
+ this.fail(p.name, `mint extensions: ${Object.keys(KINDS).join(", ")}`);
536
+ given.set(p.name.text, value);
537
+ }
538
+ const U16 = { kind: "int", name: "u16" };
539
+ const I16 = { kind: "int", name: "i16" };
540
+ // Values in source order, held in temporaries; the CPIs come later.
541
+ const held = new Map();
542
+ const keyTmp = (e, nonZero) => {
543
+ // OptionalNonZeroPubkey (32 bytes, zero for none) or COption (a tag byte, then 32).
544
+ const v = this.expect(this.expr(e, optKey), optKey, e);
545
+ const t = this.tmp("xk");
546
+ this.line(`${optC(PUBKEY)} ${t} = ${v.c};`);
547
+ if (!nonZero)
548
+ return t;
549
+ const z = this.tmp("xz");
550
+ this.line(`SolPubkey ${z};`);
551
+ this.line(`if (${t}.some) ${z} = ${t}.v; else ts_memzero(&${z}, 32);`);
552
+ return z;
553
+ };
554
+ const scalarTmp = (e, t) => {
555
+ const v = this.expect(this.expr(e, t), t, e);
556
+ const n = this.tmp("xv");
557
+ this.line(`${scalarStorageC(t)} ${n} = ${v.c};`);
558
+ return n;
559
+ };
560
+ for (const [name, e] of given) {
561
+ const k = KINDS[name];
562
+ const vals = new Map();
563
+ if (k.fields.length) {
564
+ if (!ts.isObjectLiteralExpression(e))
565
+ this.fail(e, `${name}: { ${k.fields.join(", ")} }`);
566
+ const props = new Map();
567
+ for (const p of e.properties) {
568
+ const value = ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) ? p.initializer : ts.isShorthandPropertyAssignment(p) ? p.name : undefined;
569
+ if (!value || !p.name || !ts.isIdentifier(p.name) || !k.fields.includes(p.name.text))
570
+ this.fail(p, `${name}: { ${k.fields.join(", ")} }`);
571
+ props.set(p.name.text, value);
572
+ }
573
+ for (const f of k.fields)
574
+ if (!props.has(f))
575
+ this.fail(e, `${name} needs ${f}`);
576
+ for (const [f, v] of props) {
577
+ if (f === "basisPoints") {
578
+ const t = scalarTmp(v, U16);
579
+ this.line(`if (${t} > 10000) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: transfer fee above 10000 basis points");`);
580
+ vals.set(f, t);
581
+ }
582
+ else if (f === "maximumFee")
583
+ vals.set(f, scalarTmp(v, U64));
584
+ else if (f === "rate")
585
+ vals.set(f, scalarTmp(v, I16));
586
+ else
587
+ vals.set(f, keyTmp(v, name !== "transferFee"));
588
+ }
589
+ }
590
+ else if (name === "closeAuthority")
591
+ vals.set("v", keyTmp(e, false));
592
+ else if (name === "permanentDelegate") {
593
+ const v = this.expect(this.expr(e, PUBKEY), PUBKEY, e);
594
+ const t = this.tmp("xk");
595
+ this.line(`SolPubkey ${t} = *(${v.c});`);
596
+ vals.set("v", t);
597
+ }
598
+ else if (name === "defaultAccountState") {
599
+ const v = this.expect(this.expr(e, BOOL), BOOL, e);
600
+ const t = this.tmp("xs");
601
+ this.line(`uint8_t ${t} = (${v.c}) ? 2 : 1;`); // frozen: AccountState::Frozen, else Initialized
602
+ vals.set("v", t);
603
+ }
604
+ else if (!(e.kind === ts.SyntaxKind.TrueKeyword))
605
+ this.fail(e, "nonTransferable: true");
606
+ held.set(name, vals);
607
+ }
608
+ // Each initializer: its bytes, then a CPI with the mint as the only (writable) account.
609
+ const lines = [];
610
+ const cpi = (bytes) => {
611
+ const total = bytes.reduce((n, [, k]) => n + k, 0);
612
+ const buf = this.tmp("xd");
613
+ lines.push(`{ uint8_t ${buf}[${total}]; uint64_t at = 0;`);
614
+ for (const [src, n] of bytes)
615
+ lines.push(` ts_memcpy(${buf} + at, ${src}, ${n}); at += ${n};`);
616
+ lines.push(` SolAccountMeta m[1] = {{&${acct}->key, true, false}};`, ` TS_TRY(ts_cpi(params, ${tp}, ${buf}, at, m, 1, NULL, 0)); }`);
617
+ };
618
+ const tag = (n) => `(const uint8_t[]){${n}}`;
619
+ const coption = (t) => [
620
+ [`(const uint8_t[]){${t}.some ? 1 : 0}`, 1],
621
+ [`&${t}.v`, 32],
622
+ ];
623
+ let space = 0;
624
+ for (const name of Object.keys(KINDS)) {
625
+ const v = held.get(name);
626
+ if (!v)
627
+ continue;
628
+ space += 4 + KINDS[name].len;
629
+ switch (name) {
630
+ case "transferFee":
631
+ cpi([
632
+ [tag(26), 1],
633
+ [tag(0), 1],
634
+ ...coption(v.get("configAuthority")),
635
+ ...coption(v.get("withdrawAuthority")),
636
+ [`&${v.get("basisPoints")}`, 2],
637
+ [`&${v.get("maximumFee")}`, 8],
638
+ ]);
639
+ break;
640
+ case "closeAuthority":
641
+ cpi([[tag(25), 1], ...coption(v.get("v"))]);
642
+ break;
643
+ case "defaultAccountState":
644
+ cpi([
645
+ [tag(28), 1],
646
+ [tag(0), 1],
647
+ [`&${v.get("v")}`, 1],
648
+ ]);
649
+ break;
650
+ case "nonTransferable":
651
+ cpi([[tag(32), 1]]);
652
+ break;
653
+ case "interestBearing":
654
+ cpi([
655
+ [tag(33), 1],
656
+ [tag(0), 1],
657
+ [`&${v.get("rateAuthority")}`, 32],
658
+ [`&${v.get("rate")}`, 2],
659
+ ]);
660
+ break;
661
+ case "permanentDelegate":
662
+ cpi([
663
+ [tag(35), 1],
664
+ [`&${v.get("v")}`, 32],
665
+ ]);
666
+ break;
667
+ default: {
668
+ const t = { transferHook: 36, metadataPointer: 39, groupPointer: 40, groupMemberPointer: 41 }[name];
669
+ const [f1, f2] = KINDS[name].fields;
670
+ cpi([
671
+ [tag(t), 1],
672
+ [tag(0), 1],
673
+ [`&${v.get(f1)}`, 32],
674
+ [`&${v.get(f2)}`, 32],
675
+ ]);
676
+ }
677
+ }
678
+ }
679
+ // 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 };
184
681
  }
185
682
  /**
186
683
  * `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
@@ -297,7 +794,7 @@ export class CpiEmitter extends CallEmitter {
297
794
  this.line(`if (${metas.length} + (${tail.to} - ${tail.from}) > TS_MAX_ACCOUNTS) TS_FAIL(TS_ERR_TOO_MANY_ACCOUNTS, "TypeSOL: the CPI lists more than ${MAX_ACCOUNTS} accounts");`);
298
795
  this.line(`SolAccountMeta ${m}[TS_MAX_ACCOUNTS] = {${fixed}};`);
299
796
  this.line(`uint64_t ${cnt} = ${metas.length};`);
300
- this.line(`for (uint64_t ${i} = ${n} + ${tail.from}; ${i} < ${n} + ${tail.to}; ${i}++) ${m}[${cnt}++] = (SolAccountMeta){&params->ka[${i}]->key, 0, 0};`);
797
+ this.line(`for (uint64_t ${i} = ${n} + ${tail.from}; ${i} < ${n} + ${tail.to}; ${i}++) ${m}[${cnt}++] = (SolAccountMeta){&params->ka[${i}]->key, ${tail.asPassed ? `params->ka[${i}]->is_writable` : 0}, 0};`);
301
798
  this.line(`TS_TRY(ts_cpi(params, ${program}, ${buf}, ${len}, ${m}, ${cnt}, ${seeds.name}, ${seeds.count}));`);
302
799
  }
303
800
  signerSeeds(node, authority, at) {
@@ -355,7 +852,10 @@ export class CpiEmitter extends CallEmitter {
355
852
  }
356
853
  case "mintTo": {
357
854
  arity(4);
358
- const mint = want(a[0], "mint", true, "the mint");
855
+ // A Mut<Mint>, or a mint created and initialized earlier in this instruction (Mut<Signer> / Mut<AccountInfo>).
856
+ const mint = this.accountParam(a[0], "the mint");
857
+ if (!isWritable(mint.kind) || (mint.kind.kind !== "mint" && mint.kind.kind !== "signer" && mint.kind.kind !== "info"))
858
+ this.fail(a[0], "the mint must be a Mut<Mint>, or a Mut<Signer> / Mut<AccountInfo> mint this instruction created");
359
859
  const to = want(a[1], "token", true, "the destination");
360
860
  const auth = this.accountParam(a[2], "the mint authority");
361
861
  const amt = amount(a[3]);
@@ -378,7 +878,10 @@ export class CpiEmitter extends CallEmitter {
378
878
  this.fail(node, "metadata lives in Token-2022 mints; use Token2022Program or TokenInterface");
379
879
  if (a.length !== 6 && a.length !== 7)
380
880
  this.fail(node, "initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri, signerSeeds?)");
381
- const mint = want(a[0], "mint", true, "the mint");
881
+ // The mint may have been created and initialized earlier in this instruction (a Mut<Signer> or Mut<AccountInfo>).
882
+ const mint = this.accountParam(a[0], "the mint");
883
+ if (!isWritable(mint.kind))
884
+ this.fail(a[0], "the mint must be writable (Mut<...>)");
382
885
  const ua = this.accountParam(a[1], "the update authority");
383
886
  const ma = this.accountParam(a[2], "the mint authority");
384
887
  const [nm, sym, uri] = [a[3], a[4], a[5]].map((n) => this.expect(this.expr(n, STRING), STRING, n).c);
@@ -388,7 +891,10 @@ export class CpiEmitter extends CallEmitter {
388
891
  }
389
892
  case "closeAccount": {
390
893
  arity(3);
391
- const acct = want(a[0], "token", true, "the closed account");
894
+ // A token account, or (Token-2022, with a MintCloseAuthority and no supply left) a mint.
895
+ const acct = this.accountParam(a[0], "the closed account");
896
+ if (!((acct.kind.kind === "token" || acct.kind.kind === "mint") && acct.kind.mut))
897
+ this.fail(a[0], "the closed account must be a Mut<TokenAccount> or a Mut<Mint>");
392
898
  const dest = this.accountParam(a[1], "the destination");
393
899
  if (!isWritable(dest.kind))
394
900
  this.fail(a[1], "the destination must be writable (Mut<...>)");
@@ -509,6 +1015,194 @@ export class CpiEmitter extends CallEmitter {
509
1015
  simple(22, [], [[acct, true, false]], none);
510
1016
  return undefined;
511
1017
  }
1018
+ case "updateDefaultAccountState": {
1019
+ token2022Only();
1020
+ if (a.length !== 3 && a.length !== 4)
1021
+ this.fail(node, "updateDefaultAccountState(mint, freezeAuthority, frozen, signerSeeds?)");
1022
+ const mint = anyAcct(a[0], "the mint", true);
1023
+ const auth = this.accountParam(a[1], "the freeze authority");
1024
+ const frozen = this.expect(this.expr(a[2], BOOL), BOOL, a[2]);
1025
+ const st = this.tmp("st");
1026
+ this.line(`uint8_t ${st} = (${frozen.c}) ? 2 : 1;`);
1027
+ simple(28, [byte(1), (put) => put(`&${st}`, "1", 1)], [
1028
+ [mint, true, false],
1029
+ [auth, false, true],
1030
+ ], this.signerSeeds(a[3], auth, a[1]));
1031
+ return undefined;
1032
+ }
1033
+ case "enableRequiredMemoTransfers":
1034
+ case "disableRequiredMemoTransfers": {
1035
+ token2022Only();
1036
+ if (a.length !== 2 && a.length !== 3)
1037
+ this.fail(node, `${method}(account, owner, signerSeeds?)`);
1038
+ const acct = anyAcct(a[0], "the token account", true);
1039
+ const owner = this.accountParam(a[1], "the owner");
1040
+ simple(30, [byte(method === "enableRequiredMemoTransfers" ? 0 : 1)], [
1041
+ [acct, true, false],
1042
+ [owner, false, true],
1043
+ ], this.signerSeeds(a[2], owner, a[1]));
1044
+ return undefined;
1045
+ }
1046
+ case "updateTransferHook": {
1047
+ token2022Only();
1048
+ if (a.length !== 3 && a.length !== 4)
1049
+ this.fail(node, "updateTransferHook(mint, authority, programId | null, signerSeeds?)");
1050
+ const mint = anyAcct(a[0], "the mint", true);
1051
+ const auth = this.accountParam(a[1], "the transfer hook authority");
1052
+ simple(36, [byte(1), nonZeroKey(a[2])], [
1053
+ [mint, true, false],
1054
+ [auth, false, true],
1055
+ ], this.signerSeeds(a[3], auth, a[1]));
1056
+ return undefined;
1057
+ }
1058
+ case "updateInterestRate": {
1059
+ token2022Only();
1060
+ if (a.length !== 3 && a.length !== 4)
1061
+ this.fail(node, "updateInterestRate(mint, rateAuthority, rate, signerSeeds?)");
1062
+ const mint = anyAcct(a[0], "the mint", true);
1063
+ const auth = this.accountParam(a[1], "the rate authority");
1064
+ simple(33, [byte(1), value(a[2], I16)], [
1065
+ [mint, true, false],
1066
+ [auth, false, true],
1067
+ ], this.signerSeeds(a[3], auth, a[1]));
1068
+ return undefined;
1069
+ }
1070
+ case "transferCheckedWithFee": {
1071
+ token2022Only();
1072
+ if (a.length !== 6 && a.length !== 7)
1073
+ this.fail(node, "transferCheckedWithFee(from, mint, to, authority, amount, fee, signerSeeds?)");
1074
+ const from = want(a[0], "token", true, "the source");
1075
+ const mint = want(a[1], "mint", false, "the mint");
1076
+ const to = want(a[2], "token", true, "the destination");
1077
+ const auth = this.accountParam(a[3], "the authority");
1078
+ // The decimals are the mint's own (checked when the mint was), as transferChecked does.
1079
+ const dec = (put) => {
1080
+ const t = this.tmp("dec");
1081
+ this.line(`uint8_t ${t} = a_${mint.name}->data[44];`);
1082
+ put(`&${t}`, "1", 1);
1083
+ };
1084
+ simple(26, [byte(1), value(a[4], U64), dec, value(a[5], U64)], [
1085
+ [from, true, false],
1086
+ [mint, false, false],
1087
+ [to, true, false],
1088
+ [auth, false, true],
1089
+ ], this.signerSeeds(a[6], auth, a[3]));
1090
+ return undefined;
1091
+ }
1092
+ case "harvestWithheldTokensToMint": {
1093
+ token2022Only();
1094
+ if (a.length !== 2)
1095
+ this.fail(node, "harvestWithheldTokensToMint(mint, sources): sources is remainingAccounts() or rest.slice(a, b)");
1096
+ if (!this.ix)
1097
+ this.fail(node, "harvestWithheldTokensToMint takes remaining accounts, so only instructions can call it");
1098
+ const mint = anyAcct(a[0], "the mint", true);
1099
+ const range = this.remainingRange(a[1]) ?? this.fail(a[1], "pass the source token accounts as remainingAccounts() or rest.slice(a, b)");
1100
+ const { buf, len, put, finish } = this.byteBuffer("tok", 2, "TS_ERR_INSTRUCTION_DATA", "token instruction");
1101
+ byte(26)(put);
1102
+ byte(4)(put);
1103
+ finish(node);
1104
+ // The sources go as they were passed (Token-2022 skips any it cannot harvest from).
1105
+ this.cpi(p, [[`a_${mint.name}`, true, false]], buf, len, none, { ...range, asPassed: true });
1106
+ return undefined;
1107
+ }
1108
+ case "withdrawWithheldTokensFromMint": {
1109
+ token2022Only();
1110
+ if (a.length !== 3 && a.length !== 4)
1111
+ this.fail(node, "withdrawWithheldTokensFromMint(mint, destination, authority, signerSeeds?)");
1112
+ const mint = anyAcct(a[0], "the mint", true);
1113
+ const dest = want(a[1], "token", true, "the destination");
1114
+ const auth = this.accountParam(a[2], "the withdraw authority");
1115
+ simple(26, [byte(2)], [
1116
+ [mint, true, false],
1117
+ [dest, true, false],
1118
+ [auth, false, true],
1119
+ ], this.signerSeeds(a[3], auth, a[2]));
1120
+ return undefined;
1121
+ }
1122
+ case "setTransferFee": {
1123
+ token2022Only();
1124
+ if (a.length !== 4 && a.length !== 5)
1125
+ this.fail(node, "setTransferFee(mint, configAuthority, basisPoints, maximumFee, signerSeeds?)");
1126
+ const mint = anyAcct(a[0], "the mint", true);
1127
+ const auth = this.accountParam(a[1], "the config authority");
1128
+ const bps = (put) => {
1129
+ const v = this.expect(this.expr(a[2], U16), U16, a[2]);
1130
+ this.line(`if (${v.c} > 10000) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: transfer fee above 10000 basis points");`);
1131
+ this.serialize(v, a[2], put);
1132
+ };
1133
+ simple(26, [byte(5), bps, value(a[3], U64)], [
1134
+ [mint, true, false],
1135
+ [auth, false, true],
1136
+ ], this.signerSeeds(a[4], auth, a[1]));
1137
+ return undefined;
1138
+ }
1139
+ case "updateMetadataField":
1140
+ case "removeMetadataKey":
1141
+ case "updateMetadataAuthority":
1142
+ case "emitMetadata": {
1143
+ token2022Only();
1144
+ // The Token Metadata interface (spl-token-metadata-interface), served by Token-2022 for metadata in the mint.
1145
+ const usage = {
1146
+ updateMetadataField: "updateMetadataField(mint, updateAuthority, field, value, signerSeeds?)",
1147
+ removeMetadataKey: "removeMetadataKey(mint, updateAuthority, key, idempotent, signerSeeds?)",
1148
+ updateMetadataAuthority: "updateMetadataAuthority(mint, updateAuthority, newAuthority | null, signerSeeds?)",
1149
+ emitMetadata: "emitMetadata(mint)",
1150
+ }[method];
1151
+ const fixedArgs = { updateMetadataField: 4, removeMetadataKey: 4, updateMetadataAuthority: 3, emitMetadata: 1 }[method];
1152
+ if (a.length !== fixedArgs && (method === "emitMetadata" || a.length !== fixedArgs + 1))
1153
+ this.fail(node, usage);
1154
+ const disc = {
1155
+ updateMetadataField: "dde9312db5cadcc8",
1156
+ removeMetadataKey: "ea122038598d25b5",
1157
+ updateMetadataAuthority: "d7e4a6e45464567b",
1158
+ emitMetadata: "faa6b4fa0d0cb846",
1159
+ }[method];
1160
+ const mint = anyAcct(a[0], "the mint", method !== "emitMetadata");
1161
+ const auth = method === "emitMetadata" ? undefined : this.accountParam(a[1], "the update authority");
1162
+ const { buf, len, put, finish } = this.byteBuffer("tmd", 1232, "TS_ERR_INSTRUCTION_DATA", "token metadata instruction");
1163
+ const d = this.tmp("disc");
1164
+ this.line(`static const uint8_t ${d}[8] = ${cBytes([...Buffer.from(disc, "hex")])};`);
1165
+ put(d, "8", 8);
1166
+ if (method === "updateMetadataField") {
1167
+ // Field: Name / Symbol / Uri for those names, otherwise Key(field), as the Token-2022 CLI reads them.
1168
+ const f = this.expect(this.expr(a[2], STRING), STRING, a[2]);
1169
+ const ft = this.tmp("field");
1170
+ const tag = this.tmp("ftag");
1171
+ this.line(`TS_Str ${ft} = ${f.c};`);
1172
+ this.line(`uint8_t ${tag} = ts_str_eq(${ft}, ts_str((const uint8_t *)"name", 4)) ? 0 : ts_str_eq(${ft}, ts_str((const uint8_t *)"symbol", 6)) ? 1 : ts_str_eq(${ft}, ts_str((const uint8_t *)"uri", 3)) ? 2 : 3;`);
1173
+ put(`&${tag}`, "1", 1);
1174
+ const kl = this.tmp("klen");
1175
+ this.line(`uint32_t ${kl} = (uint32_t)${ft}.len;`);
1176
+ put(`&${kl}`, `(${tag} == 3 ? 4 : 0)`);
1177
+ put(`${ft}.ptr`, `(${tag} == 3 ? ${ft}.len : 0)`);
1178
+ this.serialize(this.expect(this.expr(a[3], STRING), STRING, a[3]), a[3], put);
1179
+ }
1180
+ else if (method === "removeMetadataKey") {
1181
+ const key = this.expect(this.expr(a[2], STRING), STRING, a[2]);
1182
+ const idem = this.expect(this.expr(a[3], BOOL), BOOL, a[3]);
1183
+ const t = this.tmp("idem");
1184
+ this.line(`uint8_t ${t} = (${idem.c}) ? 1 : 0;`);
1185
+ put(`&${t}`, "1", 1);
1186
+ this.serialize(key, a[2], put);
1187
+ }
1188
+ else if (method === "updateMetadataAuthority") {
1189
+ nonZeroKey(a[2])(put);
1190
+ }
1191
+ else {
1192
+ // start: None, end: None (the whole TokenMetadata as return data).
1193
+ byte(0)(put);
1194
+ byte(0)(put);
1195
+ }
1196
+ finish(node);
1197
+ const seeds = auth ? this.signerSeeds(a[fixedArgs], auth, a[1]) : none;
1198
+ this.cpi(p, auth
1199
+ ? [
1200
+ [`a_${mint.name}`, true, false],
1201
+ [`a_${auth.name}`, false, true],
1202
+ ]
1203
+ : [[`a_${mint.name}`, false, false]], buf, len, seeds);
1204
+ return undefined;
1205
+ }
512
1206
  case "approve": {
513
1207
  arity(4);
514
1208
  const src = want(a[0], "token", true, "the source");
@@ -585,7 +1279,7 @@ export class CpiEmitter extends CallEmitter {
585
1279
  return undefined;
586
1280
  }
587
1281
  }
588
- this.fail(node.expression, `token programs support transfer, transferChecked, mintTo, burn, closeAccount, approve, revoke, setAuthority, freezeAccount, thawAccount, syncNative, initializeMint2, initializeAccount3, initializeMetadata and the Token-2022 extension initializers (initializeTransferFeeConfig, initializeMintCloseAuthority, initializeNonTransferableMint, initializePermanentDelegate, initializeDefaultAccountState, initializeMetadataPointer, initializeGroupPointer, initializeGroupMemberPointer, initializeTransferHook, initializeInterestBearingMint, initializeImmutableOwner), not ${method}`);
1282
+ this.fail(node.expression, `token programs support transfer, transferChecked, mintTo, burn, closeAccount, approve, revoke, setAuthority, freezeAccount, thawAccount, syncNative, initializeMint2, initializeAccount3, initializeMetadata, updateMetadataField, removeMetadataKey, updateMetadataAuthority, emitMetadata, the Token-2022 extension instructions (updateDefaultAccountState, updateTransferHook, enableRequiredMemoTransfers, disableRequiredMemoTransfers, updateInterestRate, transferCheckedWithFee, harvestWithheldTokensToMint, withdrawWithheldTokensFromMint, setTransferFee) and initializers (initializeTransferFeeConfig, initializeMintCloseAuthority, initializeNonTransferableMint, initializePermanentDelegate, initializeDefaultAccountState, initializeMetadataPointer, initializeGroupPointer, initializeGroupMemberPointer, initializeTransferHook, initializeInterestBearingMint, initializeImmutableOwner), not ${method}`);
589
1283
  }
590
1284
  ataOp(node, program, method) {
591
1285
  if (method !== "create" && method !== "createIdempotent")
@@ -807,8 +1501,8 @@ export class CpiEmitter extends CallEmitter {
807
1501
  const index = CORE_PLUGINS.indexOf(kind);
808
1502
  if (index < 0)
809
1503
  this.fail(kindNode, `Core plugins are ${CORE_PLUGINS.join(", ")}`);
810
- if (kind === "MasterEdition" || kind === "VerifiedCreators" || kind === "Autograph")
811
- this.fail(kindNode, `the ${kind} plugin is not supported yet (removePlugin can remove one)`);
1504
+ if (kind === "Groups")
1505
+ this.fail(kindNode, "Groups is kept by Core's group instructions (AddCollectionsToGroupV1 …), not set as a plugin");
812
1506
  const known = new Set(["kind", "authority", ...(CORE_PLUGIN_FIELDS[kind] ?? [])]);
813
1507
  for (const [k, v] of props)
814
1508
  if (!known.has(k))
@@ -852,9 +1546,55 @@ export class CpiEmitter extends CallEmitter {
852
1546
  }
853
1547
  if (constant && creators.length && sum !== 100n)
854
1548
  this.fail(need("creators"), `creator percentages add up to ${sum}; Core requires 100`);
855
- fields.push({ bytes: [0] }); // rule_set: RuleSet::None
1549
+ // rule_set: None, or { programAllowList: [...] } / { programDenyList: [...] }
1550
+ const rs = props.get("ruleSet");
1551
+ if (!rs)
1552
+ fields.push({ bytes: [0] });
1553
+ else {
1554
+ const pick = ts.isObjectLiteralExpression(rs) && rs.properties.length === 1 ? rs.properties[0] : undefined;
1555
+ const which = pick && ts.isPropertyAssignment(pick) && ts.isIdentifier(pick.name) ? pick.name.text : undefined;
1556
+ if (!pick ||
1557
+ (which !== "programAllowList" && which !== "programDenyList") ||
1558
+ !ts.isArrayLiteralExpression(pick.initializer))
1559
+ this.fail(rs, "ruleSet is { programAllowList: [programs] } or { programDenyList: [programs] }");
1560
+ const progs = pick.initializer.elements;
1561
+ if (progs.length > 16)
1562
+ this.fail(rs, "at most 16 programs in a rule set");
1563
+ fields.push({ bytes: [which === "programAllowList" ? 1 : 2, ...u32(progs.length)] }, ...progs.map((k) => ({ value: k, type: PUBKEY })));
1564
+ }
1565
+ break;
1566
+ }
1567
+ case "MasterEdition": {
1568
+ // Option<u32>, Option<String>, Option<String>: left out or null is None.
1569
+ const opt = (k, t) => {
1570
+ const v = props.get(k);
1571
+ return !v || v.kind === ts.SyntaxKind.NullKeyword ? [{ bytes: [0] }] : [{ bytes: [1] }, { value: v, type: t }];
1572
+ };
1573
+ fields.push(...opt("maxSupply", { kind: "int", name: "u32" }), ...opt("name", STRING), ...opt("uri", STRING));
1574
+ break;
1575
+ }
1576
+ case "VerifiedCreators":
1577
+ case "Autograph": {
1578
+ const sigs = list("signatures", 16);
1579
+ fields.push({ bytes: u32(sigs.length) });
1580
+ const second = kind === "VerifiedCreators" ? "verified" : "message";
1581
+ for (const sg of sigs) {
1582
+ if (!ts.isObjectLiteralExpression(sg))
1583
+ this.fail(sg, `a signature is { address, ${second} }`);
1584
+ const get = (k) => {
1585
+ const q = sg.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
1586
+ if (!q)
1587
+ this.fail(sg, `a signature needs ${k}`);
1588
+ return ts.isPropertyAssignment(q) ? q.initializer : q.name;
1589
+ };
1590
+ fields.push({ value: get("address"), type: PUBKEY }, { value: get(second), type: kind === "VerifiedCreators" ? BOOL : STRING });
1591
+ }
856
1592
  break;
857
1593
  }
1594
+ case "FreezeExecute":
1595
+ case "PermanentFreezeExecute":
1596
+ fields.push({ value: need("frozen"), type: BOOL });
1597
+ break;
858
1598
  case "FreezeDelegate":
859
1599
  case "PermanentFreezeDelegate":
860
1600
  fields.push({ value: need("frozen"), type: BOOL });
@@ -1119,10 +1859,17 @@ export class CpiEmitter extends CallEmitter {
1119
1859
  const U32 = { kind: "int", name: "u32" };
1120
1860
  const U64v = { kind: "int", name: "u64" };
1121
1861
  const HASH = { kind: "pubkey", hash: true };
1122
- // The leaf a transfer or burn replaces: { root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index }.
1123
- const leafFields = (n) => {
1862
+ // The leaf an instruction replaces. "v2": { root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index };
1863
+ // "delegate": the same with collectionHash? before assetDataHash; "meta": { root, assetDataHash?, flags?, nonce,
1864
+ // index } (the hashes come from the metadata passed); "v1": { root, dataHash, creatorHash, nonce, index }.
1865
+ const leafFields = (n, shape = "v2") => {
1866
+ const known = shape === "v1"
1867
+ ? ["root", "dataHash", "creatorHash", "nonce", "index"]
1868
+ : shape === "meta"
1869
+ ? ["root", "assetDataHash", "flags", "nonce", "index"]
1870
+ : ["root", "dataHash", "creatorHash", ...(shape === "delegate" ? ["collectionHash"] : []), "assetDataHash", "flags", "nonce", "index"];
1124
1871
  if (!ts.isObjectLiteralExpression(n))
1125
- this.fail(n, "the leaf is { root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index }");
1872
+ this.fail(n, `the leaf is { ${known.join(", ")} }`);
1126
1873
  const props = new Map();
1127
1874
  for (const q of n.properties) {
1128
1875
  if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
@@ -1132,27 +1879,64 @@ export class CpiEmitter extends CallEmitter {
1132
1879
  else
1133
1880
  this.fail(q, "write leaf fields as name: value");
1134
1881
  }
1135
- const known = ["root", "dataHash", "creatorHash", "assetDataHash", "flags", "nonce", "index"];
1136
1882
  for (const [k, v] of props)
1137
1883
  if (!known.includes(k))
1138
1884
  this.fail(v, `the leaf has no field ${k}`);
1139
1885
  const need = (k) => props.get(k) ?? this.fail(n, `the leaf needs ${k}`);
1886
+ // Left out or null: None. A value that may be null at run time (`Hash | null`) goes as a Borsh Option.
1140
1887
  const opt = (k, t) => {
1141
1888
  const v = props.get(k);
1142
1889
  if (!v || v.kind === ts.SyntaxKind.NullKeyword)
1143
1890
  return [{ bytes: [0] }];
1144
- return [{ bytes: [1] }, { value: v, type: t }];
1891
+ this.model.options.set(optionKey(t), t);
1892
+ return [{ value: v, type: { kind: "option", inner: t } }];
1145
1893
  };
1894
+ const hashes = shape === "meta"
1895
+ ? []
1896
+ : [
1897
+ { value: need("dataHash"), type: HASH },
1898
+ { value: need("creatorHash"), type: HASH },
1899
+ ];
1900
+ const tail = [
1901
+ { value: need("nonce"), type: U64v },
1902
+ { value: need("index"), type: U32 },
1903
+ ];
1904
+ if (shape === "v1")
1905
+ return [{ value: need("root"), type: HASH }, ...hashes, ...tail];
1146
1906
  return [
1147
1907
  { value: need("root"), type: HASH },
1148
- { value: need("dataHash"), type: HASH },
1149
- { value: need("creatorHash"), type: HASH },
1908
+ ...hashes,
1909
+ ...(shape === "delegate" ? opt("collectionHash", HASH) : []),
1150
1910
  ...opt("assetDataHash", HASH),
1151
1911
  ...opt("flags", U8),
1152
- { value: need("nonce"), type: U64v },
1153
- { value: need("index"), type: U32 },
1912
+ ...tail,
1154
1913
  ];
1155
1914
  };
1915
+ // MetadataArgsV2 from { name, symbol, uri, sellerFeeBasisPoints, primarySaleHappened?, isMutable?, creators?,
1916
+ // collection? } (a leaf's current metadata: the program hashes it to check the leaf).
1917
+ const metadataV2 = (md) => {
1918
+ if (!ts.isObjectLiteralExpression(md))
1919
+ this.fail(md, "the metadata is { name, symbol, uri, sellerFeeBasisPoints, primarySaleHappened?, isMutable?, creators?, collection? }");
1920
+ const props = this.literalProps(md, ["name", "symbol", "uri", "sellerFeeBasisPoints", "primarySaleHappened", "isMutable", "creators", "collection"], "the metadata");
1921
+ const need = (k) => props.get(k) ?? this.fail(md, `the metadata needs ${k}`);
1922
+ const fields = [
1923
+ { value: need("name"), type: STRING },
1924
+ { value: need("symbol"), type: STRING },
1925
+ { value: need("uri"), type: STRING },
1926
+ { value: need("sellerFeeBasisPoints"), type: { kind: "int", name: "u16" } },
1927
+ ];
1928
+ const flag = (k, dflt) => (props.get(k) ? { value: props.get(k), type: BOOL } : { bytes: [dflt] });
1929
+ fields.push(flag("primarySaleHappened", 0), flag("isMutable", 1), { bytes: [1, 0] }); // token_standard: Some(NonFungible)
1930
+ fields.push(...this.bubblegumCreators(props.get("creators")));
1931
+ const coll = props.get("collection");
1932
+ if (!coll || coll.kind === ts.SyntaxKind.NullKeyword)
1933
+ fields.push({ bytes: [0] });
1934
+ else {
1935
+ this.model.options.set("pubkey", PUBKEY);
1936
+ fields.push({ value: coll, type: { kind: "option", inner: PUBKEY } });
1937
+ }
1938
+ return fields;
1939
+ };
1156
1940
  const proofRange = (n) => {
1157
1941
  if (!this.ix)
1158
1942
  this.fail(n, "a Merkle proof comes from remainingAccounts(), so only instructions can pass one");
@@ -1311,7 +2095,102 @@ export class CpiEmitter extends CallEmitter {
1311
2095
  return undefined;
1312
2096
  }
1313
2097
  }
1314
- this.fail(node.expression, `Bubblegum supports createTree, mint, createTreeV2, mintV2, transferV2 and burnV2, not ${method}`);
2098
+ const op = BUBBLEGUM_OPS[method];
2099
+ if (op) {
2100
+ // accounts…, [leaf, metadata?, update?], proof, signerSeeds?
2101
+ const nAccounts = op.accounts.length;
2102
+ const extra = op.leaf === "meta" ? (method === "updateMetadataV2" ? 2 : 1) : 0;
2103
+ const proofAt = nAccounts + 1 + extra;
2104
+ if (a.length !== proofAt + 1 && a.length !== proofAt + 2)
2105
+ this.fail(node, `${method}(${op.accounts.map(([l, , , o]) => (o ? `${l} | null` : l)).join(", ")}, leaf${extra ? (extra === 2 ? ", currentMetadata, update" : ", metadata") : ""}, proof, signerSeeds?)`);
2106
+ const hasSeeds = !!a[proofAt + 1];
2107
+ const metas = op.accounts.map(([label, w, s, optional, either], i) => {
2108
+ if (!optional && a[i].kind === ts.SyntaxKind.NullKeyword)
2109
+ this.fail(a[i], `pass ${label}`);
2110
+ const m = this.mplMeta(a[i], bg, label, w, s);
2111
+ // V1 leaf owner / delegate: either may sign (a PDA owner signs with the seeds).
2112
+ if (either)
2113
+ return [
2114
+ m[0],
2115
+ w,
2116
+ either === "owner"
2117
+ ? `(${m[0]}->is_signer || ${hasSeeds})`
2118
+ : `(${m[0]}->is_signer || (${hasSeeds} && ${m[0]} == ${`a_${this.accountParam(a[i - 1], "the leaf owner").name}`}))`,
2119
+ ];
2120
+ return m;
2121
+ });
2122
+ const fields = [{ bytes: op.disc }, ...leafFields(a[nAccounts], op.leaf)];
2123
+ if (op.leaf === "meta")
2124
+ fields.push(...metadataV2(a[nAccounts + 1]));
2125
+ if (method === "updateMetadataV2")
2126
+ fields.push(...this.bubblegumUpdateArgs(a[nAccounts + 2]));
2127
+ const data = this.mplData(node, 2048, fields);
2128
+ this.cpi(p, metas, data.buf, data.len, seedsAt(proofAt + 1), proofRange(a[proofAt]));
2129
+ return undefined;
2130
+ }
2131
+ this.fail(node.expression, `Bubblegum supports createTree, mint, createTreeV2, mintV2, transferV2, burnV2, ${Object.keys(BUBBLEGUM_OPS).join(", ")}, not ${method}`);
2132
+ }
2133
+ /** Object literal properties by name, refusing others. */
2134
+ literalProps(o, allowed, what) {
2135
+ const props = new Map();
2136
+ for (const q of o.properties) {
2137
+ if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
2138
+ props.set(q.name.text, q.initializer);
2139
+ else if (ts.isShorthandPropertyAssignment(q))
2140
+ props.set(q.name.text, q.name);
2141
+ else
2142
+ this.fail(q, `write ${what}'s fields as name: value`);
2143
+ }
2144
+ for (const [k, v] of props)
2145
+ if (!allowed.includes(k))
2146
+ this.fail(v, `${what} has no field ${k} (${allowed.join(", ")})`);
2147
+ return props;
2148
+ }
2149
+ /** Vec<Creator> (address, verified, share) from a literal list; shares add up to 100 when literal. */
2150
+ bubblegumCreators(creators) {
2151
+ if (creators && !ts.isArrayLiteralExpression(creators))
2152
+ this.fail(creators, "creators: [{ address, verified, share }, …] (an array literal)");
2153
+ const list = creators ? creators.elements : [];
2154
+ if (list.length > 5)
2155
+ this.fail(creators, "Bubblegum takes at most 5 creators");
2156
+ const fields = [{ bytes: [list.length, 0, 0, 0] }];
2157
+ let shares = 0n;
2158
+ let constant = true;
2159
+ for (const el of list) {
2160
+ if (!ts.isObjectLiteralExpression(el))
2161
+ this.fail(el, "a creator is { address, verified, share }");
2162
+ const props = this.literalProps(el, ["address", "verified", "share"], "a creator");
2163
+ const f = (k) => props.get(k) ?? this.fail(el, `a creator needs ${k}`);
2164
+ const share = f("share");
2165
+ if (ts.isBigIntLiteral(share))
2166
+ shares += BigInt(share.text.slice(0, -1));
2167
+ else
2168
+ constant = false;
2169
+ fields.push({ value: f("address"), type: PUBKEY }, { value: f("verified"), type: BOOL }, { value: share, type: U8 });
2170
+ }
2171
+ if (list.length && constant && shares !== 100n)
2172
+ this.fail(creators, `creator shares add up to ${shares}; Bubblegum requires 100`);
2173
+ return fields;
2174
+ }
2175
+ /** UpdateArgs: every field optional (left out or null: unchanged). */
2176
+ bubblegumUpdateArgs(n) {
2177
+ if (!ts.isObjectLiteralExpression(n))
2178
+ this.fail(n, "the update is { name?, symbol?, uri?, creators?, sellerFeeBasisPoints?, primarySaleHappened?, isMutable? }");
2179
+ const props = this.literalProps(n, ["name", "symbol", "uri", "creators", "sellerFeeBasisPoints", "primarySaleHappened", "isMutable"], "the update");
2180
+ const opt = (k, t) => {
2181
+ const v = props.get(k);
2182
+ return !v || v.kind === ts.SyntaxKind.NullKeyword ? [{ bytes: [0] }] : [{ bytes: [1] }, { value: v, type: t }];
2183
+ };
2184
+ const creators = props.get("creators");
2185
+ return [
2186
+ ...opt("name", STRING),
2187
+ ...opt("symbol", STRING),
2188
+ ...opt("uri", STRING),
2189
+ ...(creators && creators.kind !== ts.SyntaxKind.NullKeyword ? [{ bytes: [1] }, ...this.bubblegumCreators(creators)] : [{ bytes: [0] }]),
2190
+ ...opt("sellerFeeBasisPoints", { kind: "int", name: "u16" }),
2191
+ ...opt("primarySaleHappened", BOOL),
2192
+ ...opt("isMutable", BOOL),
2193
+ ];
1315
2194
  }
1316
2195
  /** `acct.key` of an account parameter, as an expression to serialize. */
1317
2196
  keyOf(n) {
@@ -1446,6 +2325,20 @@ export class CpiEmitter extends CallEmitter {
1446
2325
  continue;
1447
2326
  }
1448
2327
  const vec = ts.isIdentifier(el) ? this.lookup(el.text) : undefined;
2328
+ if (vec?.type.kind === "vecArg" && vec.type.elem.kind === "string") {
2329
+ // Borsh Vec<String> again: the count, then each string's length and bytes.
2330
+ const n = this.tmp("vlen");
2331
+ const i = this.tmp("vi");
2332
+ const sl = this.tmp("slen");
2333
+ this.line(`uint32_t ${n} = ${vec.c}.len;`);
2334
+ put(`&${n}`, "4", 4);
2335
+ this.line(`for (uint32_t ${i} = 0; ${i} < ${n}; ${i}++) {`);
2336
+ this.line(`uint32_t ${sl} = (uint32_t)((const TS_Str *)${vec.c}.ptr)[${i}].len;`);
2337
+ put(`&${sl}`, "4");
2338
+ put(`((const TS_Str *)${vec.c}.ptr)[${i}].ptr`, `${sl}`);
2339
+ this.line(`}`);
2340
+ continue;
2341
+ }
1449
2342
  if (vec?.type.kind === "vecArg") {
1450
2343
  const n = this.tmp("vlen");
1451
2344
  this.line(`uint32_t ${n} = ${vec.c}.len;`);
@@ -1606,11 +2499,11 @@ export class CpiEmitter extends CallEmitter {
1606
2499
  const s = targ && ts.isTypeReferenceNode(targ) && ts.isIdentifier(targ.typeName) ? this.model.structs.get(targ.typeName.text) : undefined;
1607
2500
  if (!s)
1608
2501
  this.fail(node, "emit needs an explicit event type: emit<MyEvent>({ ... })");
1609
- if (s.layout === "borsh")
1610
- this.fail(node, `${s.name} is @layout borsh, an account layout; declare a separate event struct`);
1611
2502
  const obj = node.arguments[0];
1612
2503
  if (node.arguments.length !== (cpi ? 3 : 1) || !ts.isObjectLiteralExpression(obj))
1613
2504
  this.fail(node, "emit<T>({ field: value, ... }) takes one object literal");
2505
+ if (s.layout === "borsh")
2506
+ return this.emitBorshEvent(node, s, obj, cpi);
1614
2507
  const given = new Map();
1615
2508
  for (const p of obj.properties) {
1616
2509
  if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name))
@@ -1685,15 +2578,39 @@ export class CpiEmitter extends CallEmitter {
1685
2578
  staged.set(name, (put) => put(t, String(n), n));
1686
2579
  }
1687
2580
  }
2581
+ this.sendEvent(s, obj, cpi, (put) => {
2582
+ for (const f of s.fields)
2583
+ staged.get(f.name)(put);
2584
+ });
2585
+ return undefined;
2586
+ }
2587
+ /** emit<T> of a @layout borsh struct: built like account data, then packed as Anchor serializes it. */
2588
+ emitBorshEvent(node, s, obj, cpi) {
2589
+ this.model.events.add(s.name);
2590
+ const t = this.tmp("evs");
2591
+ const b = this.tmp("evb");
2592
+ const w = this.tmp("evw");
2593
+ // One pair of heap buffers per emit site, taken on first use: an emit in a loop does not take new heap each pass.
2594
+ this.hoisted.push(` TSS_${s.name} *${t} = NULL;`, ` uint8_t *${b} = NULL;`);
2595
+ this.line(`if (${t} == NULL) TS_TRY(ts_alloc(sizeof(TSS_${s.name}), (uint8_t **)&${t}));`);
2596
+ this.writeAggregate({ lv: `(*${t})`, type: { kind: "struct", name: s.name }, writable: true }, obj);
2597
+ // Borsh never takes more than the fixed layout (a String/Vec is 4 + used bytes, an Option 1 + payload).
2598
+ this.line(`uint64_t ${w} = 0;`);
2599
+ this.line(`if (${b} == NULL) TS_TRY(ts_alloc(sizeof(TSS_${s.name}), &${b}));`);
2600
+ this.line(`TS_TRY(ts_pack_${s.name}(${t}, ${b}, sizeof(TSS_${s.name}), &${w}));`);
2601
+ this.sendEvent(s, node, cpi, (put) => put(b, w));
2602
+ return undefined;
2603
+ }
2604
+ /** Discriminator + `body`, logged (emit) or self-invoked (emitCpi). */
2605
+ sendEvent(s, at, cpi, body) {
1688
2606
  const { buf, len, put, finish } = this.byteBuffer("ev", MAX_EVENT + (cpi ? 8 : 0), "TS_ERR_EVENT_TOO_LARGE", "event");
1689
2607
  if (cpi)
1690
2608
  put("TS_EVENT_IX_TAG", "8", 8);
1691
2609
  const disc = this.tmp("disc");
1692
2610
  this.line(`static const uint8_t ${disc}[8] = ${cBytes(s.eventDiscriminator)};`);
1693
2611
  put(disc, "8", 8);
1694
- for (const f of s.fields)
1695
- staged.get(f.name)(put);
1696
- finish(obj);
2612
+ body(put);
2613
+ finish(at);
1697
2614
  if (cpi) {
1698
2615
  // Invoke ourselves with the event authority PDA as the only (signing) account; the entrypoint accepts it.
1699
2616
  const m = this.tmp("metas");
@@ -1705,12 +2622,11 @@ export class CpiEmitter extends CallEmitter {
1705
2622
  this.line(`SolInstruction ${ix} = {&a_${cpi.program}->key, ${m}, 1, ${buf}, ${len}};`);
1706
2623
  this.line(`SolSignerSeeds ${signer} = {${sd}, 2};`);
1707
2624
  this.line(`TS_TRY(ts_invoke(params, &${ix}, &${signer}, 1));`);
1708
- return undefined;
2625
+ return;
1709
2626
  }
1710
2627
  const bytes = this.tmp("evbytes");
1711
2628
  this.line(`SolBytes ${bytes} = {${buf}, ${len}};`);
1712
2629
  this.line(`sol_log_data(&${bytes}, 1);`);
1713
- return undefined;
1714
2630
  }
1715
2631
  /** Structs reachable through the fields of struct `name` (not including itself unless it recurses). */
1716
2632
  nestedStructs(name) {