@typesol/cli 0.16.0 → 0.17.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,8 +1,36 @@
1
1
  import ts from "typescript";
2
2
  import { discriminator, snakeCase } from "../analyze.js";
3
+ import { borshMinSize } from "./borsh.js";
3
4
  import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
4
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";
5
6
  import { CallEmitter } from "./calls.js";
7
+ /** Metaplex Core's Plugin / PluginType enums, in order (MasterEdition, VerifiedCreators and Autograph values are refused). */
8
+ const CORE_PLUGINS = [
9
+ "Royalties",
10
+ "FreezeDelegate",
11
+ "BurnDelegate",
12
+ "TransferDelegate",
13
+ "UpdateDelegate",
14
+ "PermanentFreezeDelegate",
15
+ "Attributes",
16
+ "PermanentTransferDelegate",
17
+ "PermanentBurnDelegate",
18
+ "Edition",
19
+ "MasterEdition",
20
+ "AddBlocker",
21
+ "ImmutableMetadata",
22
+ "VerifiedCreators",
23
+ "Autograph",
24
+ "BubblegumV2",
25
+ ];
26
+ const CORE_PLUGIN_FIELDS = {
27
+ Royalties: ["basisPoints", "creators"],
28
+ FreezeDelegate: ["frozen"],
29
+ PermanentFreezeDelegate: ["frozen"],
30
+ UpdateDelegate: ["additionalDelegates"],
31
+ Attributes: ["attributes"],
32
+ Edition: ["number"],
33
+ };
6
34
  /**
7
35
  * Other programs: PDAs and seeds, account creation, System/Token/ATA/Metaplex CPIs, generic invoke, Borsh
8
36
  * serialization and events.
@@ -139,7 +167,10 @@ export class CpiEmitter extends CallEmitter {
139
167
  // Returns whether it was created now; an existing account must be this PDA, ours, and a ${s.name}.
140
168
  const created = this.tmp("created");
141
169
  this.line(`bool ${created};`);
142
- 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}), &${b}, &${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}));`);
171
+ // An existing @layout borsh account is unpacked now (a new one starts from the zeroed struct).
172
+ 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}));`);
143
174
  this.created.add(target.name);
144
175
  this.pendingInit.delete(target.name);
145
176
  this.bumps.set(target.name, b);
@@ -248,11 +279,26 @@ export class CpiEmitter extends CallEmitter {
248
279
  ], buf, len, seeds);
249
280
  return undefined;
250
281
  }
251
- /** Emit a CPI to the program account `program` with these metas (account, writable, signer). */
252
- cpi(program, metas, buf, len, seeds) {
282
+ /**
283
+ * Emit a CPI to the program account `program` with these metas (account, writable, signer), and then (`tail`) a range
284
+ * of the remaining accounts, read-only (a Merkle proof).
285
+ */
286
+ cpi(program, metas, buf, len, seeds, tail) {
253
287
  const m = this.tmp("metas");
254
- this.line(`SolAccountMeta ${m}[${metas.length}] = {${metas.map(([acc, w, sg]) => `{&${acc}->key, ${w}, ${sg}}`).join(", ")}};`);
255
- this.line(`TS_TRY(ts_cpi(params, ${program}, ${buf}, ${len}, ${m}, ${metas.length}, ${seeds.name}, ${seeds.count}));`);
288
+ const fixed = metas.map(([acc, w, sg]) => `{&${acc}->key, ${w}, ${sg}}`).join(", ");
289
+ if (!tail) {
290
+ this.line(`SolAccountMeta ${m}[${metas.length}] = {${fixed}};`);
291
+ this.line(`TS_TRY(ts_cpi(params, ${program}, ${buf}, ${len}, ${m}, ${metas.length}, ${seeds.name}, ${seeds.count}));`);
292
+ return;
293
+ }
294
+ const n = this.ix.accounts.length;
295
+ const cnt = this.tmp("nmetas");
296
+ const i = this.tmp("ri");
297
+ 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
+ this.line(`SolAccountMeta ${m}[TS_MAX_ACCOUNTS] = {${fixed}};`);
299
+ 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};`);
301
+ this.line(`TS_TRY(ts_cpi(params, ${program}, ${buf}, ${len}, ${m}, ${cnt}, ${seeds.name}, ${seeds.count}));`);
256
302
  }
257
303
  signerSeeds(node, authority, at) {
258
304
  if (!node) {
@@ -562,10 +608,12 @@ export class CpiEmitter extends CallEmitter {
562
608
  return undefined;
563
609
  }
564
610
  metadataOp(node, program, method) {
611
+ if (method === "createMasterEditionV3" || method === "verifyCollection")
612
+ return this.metadataCollectionOp(node, program, method);
565
613
  if (method !== "createV3")
566
- this.fail(node.expression, "the Token Metadata program supports createV3");
614
+ this.fail(node.expression, `the Token Metadata program supports createV3, createMasterEditionV3 and verifyCollection, not ${method}`);
567
615
  const a = node.arguments;
568
- const usage = "createV3(metadata, mint, mintAuthority, payer, updateAuthority, systemProgram, name, symbol, uri, sellerFeeBasisPoints, isMutable, { creators?, collection? }?, signerSeeds?)";
616
+ const usage = "createV3(metadata, mint, mintAuthority, payer, updateAuthority, systemProgram, name, symbol, uri, sellerFeeBasisPoints, isMutable, { creators?, collection?, isCollection? }?, signerSeeds?)";
569
617
  // An object literal after isMutable carries the optional creators / collection; signerSeeds stays last.
570
618
  const extras = a[11] && ts.isObjectLiteralExpression(a[11]) ? a[11] : undefined;
571
619
  const seedsArg = extras ? a[12] : a[11];
@@ -598,10 +646,11 @@ export class CpiEmitter extends CallEmitter {
598
646
  let creators = "NULL";
599
647
  let ncreators = "-1";
600
648
  let collection = "NULL";
649
+ let sized = "false";
601
650
  if (extras) {
602
651
  for (const p of extras.properties) {
603
652
  if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name))
604
- this.fail(p, "use creators: [...] and collection: key");
653
+ this.fail(p, "use creators: [...], collection: key and isCollection: boolean");
605
654
  if (p.name.text === "creators") {
606
655
  if (!ts.isArrayLiteralExpression(p.initializer))
607
656
  this.fail(p.initializer, "creators: [{ address, verified, share }, …] (an array literal)");
@@ -646,14 +695,66 @@ export class CpiEmitter extends CallEmitter {
646
695
  this.line(`SolPubkey ${t} = *(${key.c});`);
647
696
  collection = `&${t}`;
648
697
  }
698
+ else if (p.name.text === "isCollection") {
699
+ const v = this.expect(this.expr(p.initializer, BOOL), BOOL, p.initializer);
700
+ const t = this.tmp("sized");
701
+ this.line(`bool ${t} = ${v.c};`);
702
+ sized = t;
703
+ }
649
704
  else
650
- this.fail(p.name, `createV3 extras are creators and collection, not ${p.name.text}`);
705
+ this.fail(p.name, `createV3 extras are creators, collection and isCollection, not ${p.name.text}`);
651
706
  }
652
707
  }
653
708
  const seeds = this.signerSeeds(seedsArg, mintAuth, a[2]);
654
709
  settleAll(extrasMark);
655
710
  const [name, symbol, uri, fee, mutable] = slots.map((i) => vals[i]);
656
- this.line(`TS_TRY(ts_mpl_create_v3(params, a_${program.name}, a_${metadata.name}, a_${mint.name}, a_${mintAuth.name}, a_${payer.name}, a_${updateAuth.name}, a_${sys.name}, ${name.c}, ${symbol.c}, ${uri.c}, ${fee.c}, ${mutable.c}, ${creators}, ${ncreators}, ${collection}, ${seeds.name}, ${seeds.count}));`);
711
+ this.line(`TS_TRY(ts_mpl_create_v3(params, a_${program.name}, a_${metadata.name}, a_${mint.name}, a_${mintAuth.name}, a_${payer.name}, a_${updateAuth.name}, a_${sys.name}, ${name.c}, ${symbol.c}, ${uri.c}, ${fee.c}, ${mutable.c}, ${creators}, ${ncreators}, ${collection}, ${sized}, ${seeds.name}, ${seeds.count}));`);
712
+ return undefined;
713
+ }
714
+ /** CreateMasterEditionV3 (17) and Verify (52) with VerificationArgs::CollectionV1. */
715
+ metadataCollectionOp(node, program, method) {
716
+ const a = node.arguments;
717
+ const p = `a_${program.name}`;
718
+ const seedsAt = (i) => (a[i] ? this.signingSeeds(a[i]) : { name: "NULL", count: 0 });
719
+ const acct = (i, what, w, s) => {
720
+ if (a[i].kind === ts.SyntaxKind.NullKeyword)
721
+ this.fail(a[i], `pass ${what}`);
722
+ return this.mplMeta(a[i], program, what, w, s);
723
+ };
724
+ if (method === "createMasterEditionV3") {
725
+ if (a.length !== 9 && a.length !== 10)
726
+ this.fail(node, "createMasterEditionV3(edition, mint, updateAuthority, mintAuthority, payer, metadata, tokenProgram, systemProgram, maxSupply | null, signerSeeds?)");
727
+ const metas = [
728
+ acct(0, "the edition", true, false),
729
+ acct(1, "the mint", true, false),
730
+ acct(2, "the update authority", false, true),
731
+ acct(3, "the mint authority", false, true),
732
+ acct(4, "the payer", true, true),
733
+ acct(5, "the metadata", true, false),
734
+ acct(6, "the token program", false, false),
735
+ acct(7, "the system program", false, false),
736
+ ];
737
+ // max_supply: Option<u64>; Some(0) makes a one of one, None allows unlimited prints.
738
+ const data = this.mplData(node, 16, a[8].kind === ts.SyntaxKind.NullKeyword ? [{ bytes: [17, 0] }] : [{ bytes: [17, 1] }, { value: a[8], type: { kind: "int", name: "u64" } }]);
739
+ this.cpi(p, metas, data.buf, data.len, seedsAt(9));
740
+ return undefined;
741
+ }
742
+ // Verify: authority, delegate record (absent: the program id), metadata, collection mint, collection metadata,
743
+ // collection master edition, system program, instructions sysvar.
744
+ if (a.length !== 7 && a.length !== 8)
745
+ this.fail(node, "verifyCollection(authority, metadata, collectionMint, collectionMetadata, collectionMasterEdition, systemProgram, instructionsSysvar, signerSeeds?)");
746
+ const metas = [
747
+ acct(0, "the collection authority", false, true),
748
+ [p, false, false],
749
+ acct(1, "the metadata", true, false),
750
+ acct(2, "the collection mint", false, false),
751
+ acct(3, "the collection metadata", true, false),
752
+ acct(4, "the collection master edition", false, false),
753
+ acct(5, "the system program", false, false),
754
+ acct(6, "the instructions sysvar", false, false),
755
+ ];
756
+ const data = this.mplData(node, 8, [{ bytes: [52, 1] }]);
757
+ this.cpi(p, metas, data.buf, data.len, seedsAt(7));
657
758
  return undefined;
658
759
  }
659
760
  /**
@@ -683,17 +784,156 @@ export class CpiEmitter extends CallEmitter {
683
784
  finish(at);
684
785
  return { buf, len };
685
786
  }
686
- /** Metaplex Core: createAsset, createCollection, transferAsset, burnAsset (no plugins). */
787
+ /**
788
+ * Borsh of one Metaplex Core plugin literal (`{ kind: "FreezeDelegate", frozen: true, authority?: … }`): the Plugin
789
+ * enum, then (`withAuthority`) the Option<PluginAuthority> that CreateV1's PluginAuthorityPair and AddPluginV1 take.
790
+ */
791
+ corePlugin(n, withAuthority) {
792
+ if (!ts.isObjectLiteralExpression(n))
793
+ this.fail(n, 'a Core plugin is an object literal: { kind: "FreezeDelegate", frozen: false }');
794
+ const props = new Map();
795
+ for (const q of n.properties) {
796
+ if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
797
+ props.set(q.name.text, q.initializer);
798
+ else if (ts.isShorthandPropertyAssignment(q))
799
+ props.set(q.name.text, q.name);
800
+ else
801
+ this.fail(q, "write plugin fields as name: value");
802
+ }
803
+ const kindNode = props.get("kind");
804
+ if (!kindNode || !ts.isStringLiteral(kindNode))
805
+ this.fail(n, 'a Core plugin needs kind: "Royalties" | "FreezeDelegate" | …');
806
+ const kind = kindNode.text;
807
+ const index = CORE_PLUGINS.indexOf(kind);
808
+ if (index < 0)
809
+ 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)`);
812
+ const known = new Set(["kind", "authority", ...(CORE_PLUGIN_FIELDS[kind] ?? [])]);
813
+ for (const [k, v] of props)
814
+ if (!known.has(k))
815
+ this.fail(v, `${kind} has no field ${k}`);
816
+ const need = (k) => props.get(k) ?? this.fail(n, `${kind} needs ${k}`);
817
+ const list = (k, max) => {
818
+ const e = need(k);
819
+ if (!ts.isArrayLiteralExpression(e))
820
+ this.fail(e, `${k} is an array literal`);
821
+ if (e.elements.length > max)
822
+ this.fail(e, `at most ${max} entries in ${k}`);
823
+ return e.elements;
824
+ };
825
+ const u32 = (x) => [x & 0xff, (x >> 8) & 0xff, (x >> 16) & 0xff, x >>> 24];
826
+ const fields = [{ bytes: [index] }];
827
+ switch (kind) {
828
+ case "Royalties": {
829
+ const bps = need("basisPoints");
830
+ if (ts.isBigIntLiteral(bps) && BigInt(bps.text.slice(0, -1)) > 10000n)
831
+ this.fail(bps, "basisPoints is at most 10000 (100%)");
832
+ fields.push({ value: bps, type: { kind: "int", name: "u16" } });
833
+ const creators = list("creators", 5);
834
+ fields.push({ bytes: u32(creators.length) });
835
+ let sum = 0n;
836
+ let constant = true;
837
+ for (const c of creators) {
838
+ if (!ts.isObjectLiteralExpression(c))
839
+ this.fail(c, "a creator is { address, percentage }");
840
+ const get = (k) => {
841
+ const q = c.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
842
+ if (!q)
843
+ this.fail(c, `a creator needs ${k}`);
844
+ return ts.isPropertyAssignment(q) ? q.initializer : q.name;
845
+ };
846
+ const pct = get("percentage");
847
+ if (ts.isBigIntLiteral(pct))
848
+ sum += BigInt(pct.text.slice(0, -1));
849
+ else
850
+ constant = false;
851
+ fields.push({ value: get("address"), type: PUBKEY }, { value: pct, type: U8 });
852
+ }
853
+ if (constant && creators.length && sum !== 100n)
854
+ this.fail(need("creators"), `creator percentages add up to ${sum}; Core requires 100`);
855
+ fields.push({ bytes: [0] }); // rule_set: RuleSet::None
856
+ break;
857
+ }
858
+ case "FreezeDelegate":
859
+ case "PermanentFreezeDelegate":
860
+ fields.push({ value: need("frozen"), type: BOOL });
861
+ break;
862
+ case "UpdateDelegate": {
863
+ const keys = props.has("additionalDelegates") ? list("additionalDelegates", 8) : [];
864
+ fields.push({ bytes: u32(keys.length) }, ...keys.map((k) => ({ value: k, type: PUBKEY })));
865
+ break;
866
+ }
867
+ case "Attributes": {
868
+ const attrs = list("attributes", 32);
869
+ fields.push({ bytes: u32(attrs.length) });
870
+ for (const at of attrs) {
871
+ if (!ts.isObjectLiteralExpression(at))
872
+ this.fail(at, "an attribute is { key, value }");
873
+ for (const k of ["key", "value"]) {
874
+ const q = at.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
875
+ if (!q)
876
+ this.fail(at, `an attribute needs ${k}`);
877
+ fields.push({ value: ts.isPropertyAssignment(q) ? q.initializer : q.name, type: STRING });
878
+ }
879
+ }
880
+ break;
881
+ }
882
+ case "Edition":
883
+ fields.push({ value: need("number"), type: { kind: "int", name: "u32" } });
884
+ break;
885
+ }
886
+ const auth = props.get("authority");
887
+ if (!withAuthority) {
888
+ if (auth)
889
+ this.fail(auth, "a plugin's authority is set when it is added; change it with Core's approve / revoke");
890
+ return fields;
891
+ }
892
+ if (!auth)
893
+ fields.push({ bytes: [0] }); // the plugin type's default authority
894
+ else if (ts.isStringLiteral(auth)) {
895
+ const i = ["none", "owner", "updateAuthority"].indexOf(auth.text);
896
+ if (i < 0)
897
+ this.fail(auth, 'authority is "none", "owner", "updateAuthority" or a Pubkey');
898
+ fields.push({ bytes: [1, i] });
899
+ }
900
+ else
901
+ fields.push({ bytes: [1, 3] }, { value: auth, type: PUBKEY });
902
+ return fields;
903
+ }
904
+ /** CreateV1 / CreateCollectionV1 plugins: Option<Vec<PluginAuthorityPair>>. */
905
+ corePluginList(n) {
906
+ if (!n)
907
+ return [{ bytes: [0] }];
908
+ const els = n.elements;
909
+ if (els.length > 16)
910
+ this.fail(n, "at most 16 plugins");
911
+ const kinds = els.map((e) => ts.isObjectLiteralExpression(e) ? e.properties.find((q) => q.name && ts.isIdentifier(q.name) && q.name.text === "kind") : undefined);
912
+ const seen = new Set();
913
+ kinds.forEach((k, i) => {
914
+ const lit = k && ts.isPropertyAssignment(k) && ts.isStringLiteral(k.initializer) ? k.initializer.text : undefined;
915
+ if (lit && seen.has(lit))
916
+ this.fail(els[i], `${lit} is listed twice; an asset has one plugin of each kind`);
917
+ if (lit)
918
+ seen.add(lit);
919
+ });
920
+ return [{ bytes: [1, els.length & 0xff, 0, 0, 0] }, ...els.flatMap((e) => this.corePlugin(e, true))];
921
+ }
922
+ /** Metaplex Core: assets and collections (with plugins), plugins added, updated and removed, transfer and burn. */
687
923
  coreOp(node, core, method) {
688
924
  const a = node.arguments;
689
925
  const p = `a_${core.name}`;
690
926
  const seedsAt = (i) => (a[i] ? this.signingSeeds(a[i]) : { name: "NULL", count: 0 });
691
927
  const str = (n) => ({ value: n, type: STRING });
928
+ // An array literal of object literals after the uri is the plugin list; the signer seeds come after it.
929
+ const isPlugins = (n) => !!n && ts.isArrayLiteralExpression(n) && (n.elements.length === 0 || n.elements.every((e) => ts.isObjectLiteralExpression(e)));
930
+ const self = [p, false, false];
692
931
  switch (method) {
693
932
  case "createAsset": {
694
933
  // CreateV1: asset, collection, authority, payer, owner, updateAuthority, systemProgram, logWrapper.
695
- if (a.length !== 8 && a.length !== 9)
696
- this.fail(node, "createAsset(asset, collection | null, authority | null, payer, owner | null, systemProgram, name, uri, signerSeeds?)");
934
+ const plugins = isPlugins(a[8]) ? a[8] : undefined;
935
+ if (a.length < 8 || a.length > (plugins ? 10 : 9))
936
+ this.fail(node, "createAsset(asset, collection | null, authority | null, payer, owner | null, systemProgram, name, uri, plugins?, signerSeeds?)");
697
937
  const metas = [
698
938
  this.mplMeta(a[0], core, "the asset", true, true),
699
939
  this.mplMeta(a[1], core, "the collection", true, false),
@@ -704,24 +944,64 @@ export class CpiEmitter extends CallEmitter {
704
944
  this.mplMeta(a[5], core, "the system program", false, false),
705
945
  [p, false, false],
706
946
  ];
707
- const data = this.mplData(node, 1024, [{ bytes: [0, 0] }, str(a[6]), str(a[7]), { bytes: [0] }]);
708
- this.cpi(p, metas, data.buf, data.len, seedsAt(8));
947
+ const data = this.mplData(node, 2048, [{ bytes: [0, 0] }, str(a[6]), str(a[7]), ...this.corePluginList(plugins)]);
948
+ this.cpi(p, metas, data.buf, data.len, seedsAt(plugins ? 9 : 8));
709
949
  return undefined;
710
950
  }
711
951
  case "createCollection": {
712
952
  // CreateCollectionV1: collection, updateAuthority, payer, systemProgram.
713
- if (a.length !== 6 && a.length !== 7)
714
- this.fail(node, "createCollection(collection, updateAuthority | null, payer, systemProgram, name, uri, signerSeeds?)");
953
+ const plugins = isPlugins(a[6]) ? a[6] : undefined;
954
+ if (a.length < 6 || a.length > (plugins ? 8 : 7))
955
+ this.fail(node, "createCollection(collection, updateAuthority | null, payer, systemProgram, name, uri, plugins?, signerSeeds?)");
715
956
  const metas = [
716
957
  this.mplMeta(a[0], core, "the collection", true, true),
717
958
  this.mplMeta(a[1], core, "the update authority", false, false),
718
959
  this.mplMeta(a[2], core, "the payer", true, true),
719
960
  this.mplMeta(a[3], core, "the system program", false, false),
720
961
  ];
721
- const data = this.mplData(node, 1024, [{ bytes: [1] }, str(a[4]), str(a[5]), { bytes: [0] }]);
962
+ const data = this.mplData(node, 2048, [{ bytes: [1] }, str(a[4]), str(a[5]), ...this.corePluginList(plugins)]);
963
+ this.cpi(p, metas, data.buf, data.len, seedsAt(plugins ? 7 : 6));
964
+ return undefined;
965
+ }
966
+ case "addPlugin":
967
+ case "updatePlugin":
968
+ case "removePlugin": {
969
+ // Add/Remove/UpdatePluginV1: asset, collection, payer, authority, systemProgram, logWrapper.
970
+ const last = method === "removePlugin" ? "kind" : "plugin";
971
+ if (a.length !== 6 && a.length !== 7)
972
+ this.fail(node, `${method}(asset, collection | null, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
973
+ const metas = [
974
+ this.mplMeta(a[0], core, "the asset", true, false),
975
+ this.mplMeta(a[1], core, "the collection", true, false),
976
+ this.mplMeta(a[2], core, "the payer", true, true),
977
+ this.mplMeta(a[3], core, "the authority", false, true),
978
+ this.mplMeta(a[4], core, "the system program", false, false),
979
+ self,
980
+ ];
981
+ const body = method === "removePlugin" ? [this.corePluginType(a[5])] : this.corePlugin(a[5], method === "addPlugin");
982
+ const data = this.mplData(node, 2048, [{ bytes: [{ addPlugin: 2, removePlugin: 4, updatePlugin: 6 }[method]] }, ...body]);
722
983
  this.cpi(p, metas, data.buf, data.len, seedsAt(6));
723
984
  return undefined;
724
985
  }
986
+ case "addCollectionPlugin":
987
+ case "updateCollectionPlugin":
988
+ case "removeCollectionPlugin": {
989
+ // Add/Remove/UpdateCollectionPluginV1: collection, payer, authority, systemProgram, logWrapper.
990
+ const last = method === "removeCollectionPlugin" ? "kind" : "plugin";
991
+ if (a.length !== 5 && a.length !== 6)
992
+ this.fail(node, `${method}(collection, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
993
+ const metas = [
994
+ this.mplMeta(a[0], core, "the collection", true, false),
995
+ this.mplMeta(a[1], core, "the payer", true, true),
996
+ this.mplMeta(a[2], core, "the authority", false, true),
997
+ this.mplMeta(a[3], core, "the system program", false, false),
998
+ self,
999
+ ];
1000
+ const body = method === "removeCollectionPlugin" ? [this.corePluginType(a[4])] : this.corePlugin(a[4], method === "addCollectionPlugin");
1001
+ const data = this.mplData(node, 2048, [{ bytes: [{ addCollectionPlugin: 3, removeCollectionPlugin: 5, updateCollectionPlugin: 7 }[method]] }, ...body]);
1002
+ this.cpi(p, metas, data.buf, data.len, seedsAt(5));
1003
+ return undefined;
1004
+ }
725
1005
  case "transferAsset": {
726
1006
  // TransferV1: asset, collection, payer, authority, newOwner, systemProgram, logWrapper.
727
1007
  if (a.length !== 5 && a.length !== 6)
@@ -756,7 +1036,16 @@ export class CpiEmitter extends CallEmitter {
756
1036
  return undefined;
757
1037
  }
758
1038
  }
759
- this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, transferAsset and burnAsset, not ${method}`);
1039
+ this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin (and the Collection forms), transferAsset and burnAsset, not ${method}`);
1040
+ }
1041
+ /** PluginType (u8) from a kind string literal: removePlugin(…, "FreezeDelegate"). */
1042
+ corePluginType(n) {
1043
+ if (!ts.isStringLiteral(n))
1044
+ this.fail(n, 'name the plugin kind as a string literal: "FreezeDelegate"');
1045
+ const i = CORE_PLUGINS.indexOf(n.text);
1046
+ if (i < 0)
1047
+ this.fail(n, `Core plugins are ${CORE_PLUGINS.join(", ")}`);
1048
+ return { bytes: [i] };
760
1049
  }
761
1050
  /** Bubblegum (compressed NFTs on SPL account compression): createTree, mint (MintV1, no collection, no creators). */
762
1051
  bubblegumOp(node, bg, method) {
@@ -819,7 +1108,218 @@ export class CpiEmitter extends CallEmitter {
819
1108
  return undefined;
820
1109
  }
821
1110
  }
822
- this.fail(node.expression, `Bubblegum supports createTree and mint, not ${method}`);
1111
+ return this.bubblegumV2(node, bg, method);
1112
+ }
1113
+ /** Bubblegum V2 (trees on mpl-account-compression with mpl-noop; optional Metaplex Core collections). */
1114
+ bubblegumV2(node, bg, method) {
1115
+ const a = node.arguments;
1116
+ const p = `a_${bg.name}`;
1117
+ const acct = (i, what, w, s) => this.mplMeta(a[i], bg, what, w, s);
1118
+ const seedsAt = (i) => (a[i] ? this.signingSeeds(a[i]) : { name: "NULL", count: 0 });
1119
+ const U32 = { kind: "int", name: "u32" };
1120
+ const U64v = { kind: "int", name: "u64" };
1121
+ 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) => {
1124
+ if (!ts.isObjectLiteralExpression(n))
1125
+ this.fail(n, "the leaf is { root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index }");
1126
+ const props = new Map();
1127
+ for (const q of n.properties) {
1128
+ if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
1129
+ props.set(q.name.text, q.initializer);
1130
+ else if (ts.isShorthandPropertyAssignment(q))
1131
+ props.set(q.name.text, q.name);
1132
+ else
1133
+ this.fail(q, "write leaf fields as name: value");
1134
+ }
1135
+ const known = ["root", "dataHash", "creatorHash", "assetDataHash", "flags", "nonce", "index"];
1136
+ for (const [k, v] of props)
1137
+ if (!known.includes(k))
1138
+ this.fail(v, `the leaf has no field ${k}`);
1139
+ const need = (k) => props.get(k) ?? this.fail(n, `the leaf needs ${k}`);
1140
+ const opt = (k, t) => {
1141
+ const v = props.get(k);
1142
+ if (!v || v.kind === ts.SyntaxKind.NullKeyword)
1143
+ return [{ bytes: [0] }];
1144
+ return [{ bytes: [1] }, { value: v, type: t }];
1145
+ };
1146
+ return [
1147
+ { value: need("root"), type: HASH },
1148
+ { value: need("dataHash"), type: HASH },
1149
+ { value: need("creatorHash"), type: HASH },
1150
+ ...opt("assetDataHash", HASH),
1151
+ ...opt("flags", U8),
1152
+ { value: need("nonce"), type: U64v },
1153
+ { value: need("index"), type: U32 },
1154
+ ];
1155
+ };
1156
+ const proofRange = (n) => {
1157
+ if (!this.ix)
1158
+ this.fail(n, "a Merkle proof comes from remainingAccounts(), so only instructions can pass one");
1159
+ return this.remainingRange(n) ?? this.fail(n, "pass the proof as remainingAccounts() or rest.slice(a, b)");
1160
+ };
1161
+ switch (method) {
1162
+ case "createTreeV2": {
1163
+ // CreateTreeConfigV2: treeConfig, merkleTree, payer, treeCreator, logWrapper, compressionProgram, systemProgram.
1164
+ if (a.length !== 10 && a.length !== 11)
1165
+ this.fail(node, "createTreeV2(treeConfig, merkleTree, payer, treeCreator | null, logWrapper, compressionProgram, systemProgram, maxDepth, maxBufferSize, isPublic, signerSeeds?)");
1166
+ const metas = [
1167
+ acct(0, "the tree config", true, false),
1168
+ acct(1, "the merkle tree", true, false),
1169
+ acct(2, "the payer", true, true),
1170
+ acct(3, "the tree creator", false, true),
1171
+ acct(4, "the log wrapper", false, false),
1172
+ acct(5, "the compression program", false, false),
1173
+ acct(6, "the system program", false, false),
1174
+ ];
1175
+ const data = this.mplData(node, 32, [
1176
+ { bytes: [55, 99, 95, 215, 142, 203, 227, 205] },
1177
+ { value: a[7], type: U32 },
1178
+ { value: a[8], type: U32 },
1179
+ { bytes: [1] },
1180
+ { value: a[9], type: BOOL },
1181
+ ]);
1182
+ this.cpi(p, metas, data.buf, data.len, seedsAt(10));
1183
+ return undefined;
1184
+ }
1185
+ case "mintV2": {
1186
+ // MintV2: treeConfig, payer, treeCreatorOrDelegate, collectionAuthority, leafOwner, leafDelegate, merkleTree,
1187
+ // coreCollection, mplCoreCpiSigner, logWrapper, compressionProgram, mplCoreProgram, systemProgram.
1188
+ const usage = "mintV2(treeConfig, payer, treeDelegate | null, leafOwner, leafDelegate | null, merkleTree, logWrapper, compressionProgram, mplCoreProgram, systemProgram, { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable? }, { collection, authority, cpiSigner }?, signerSeeds?)";
1189
+ const collection = a[11] && ts.isObjectLiteralExpression(a[11]) ? a[11] : undefined;
1190
+ if (a.length < 11 || a.length > (collection ? 13 : 12))
1191
+ this.fail(node, usage);
1192
+ const md = a[10];
1193
+ if (!ts.isObjectLiteralExpression(md))
1194
+ this.fail(md, "the metadata is { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable? }");
1195
+ const field = (o, k) => {
1196
+ const q = o.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
1197
+ return q ? (ts.isPropertyAssignment(q) ? q.initializer : q.name) : undefined;
1198
+ };
1199
+ for (const q of md.properties)
1200
+ if (!q.name || !ts.isIdentifier(q.name) || !["name", "symbol", "uri", "sellerFeeBasisPoints", "creators", "isMutable"].includes(q.name.text))
1201
+ this.fail(q, "the metadata takes name, symbol, uri, sellerFeeBasisPoints, creators and isMutable");
1202
+ const coll = collection
1203
+ ? ["collection", "authority", "cpiSigner"].map((k) => field(collection, k) ?? this.fail(collection, `the collection needs ${k}`))
1204
+ : undefined;
1205
+ if (collection)
1206
+ for (const q of collection.properties)
1207
+ if (!q.name || !ts.isIdentifier(q.name) || !["collection", "authority", "cpiSigner"].includes(q.name.text))
1208
+ this.fail(q, "the collection is { collection, authority, cpiSigner }");
1209
+ const metas = [
1210
+ acct(0, "the tree config", true, false),
1211
+ acct(1, "the payer", true, true),
1212
+ acct(2, "the tree delegate", false, true),
1213
+ coll ? this.mplMeta(coll[1], bg, "the collection authority", false, true) : [p, false, false],
1214
+ acct(3, "the leaf owner", false, false),
1215
+ acct(4, "the leaf delegate", false, false),
1216
+ acct(5, "the merkle tree", true, false),
1217
+ coll ? this.mplMeta(coll[0], bg, "the core collection", true, false) : [p, false, false],
1218
+ coll ? this.mplMeta(coll[2], bg, "the mpl-core CPI signer", false, false) : [p, false, false],
1219
+ acct(6, "the log wrapper", false, false),
1220
+ acct(7, "the compression program", false, false),
1221
+ acct(8, "the mpl-core program", false, false),
1222
+ acct(9, "the system program", false, false),
1223
+ ];
1224
+ const need = (k) => field(md, k) ?? this.fail(md, `the metadata needs ${k}`);
1225
+ const fields = [
1226
+ { bytes: [120, 121, 23, 146, 173, 110, 199, 205] },
1227
+ { value: need("name"), type: STRING },
1228
+ { value: need("symbol"), type: STRING },
1229
+ { value: need("uri"), type: STRING },
1230
+ { value: need("sellerFeeBasisPoints"), type: { kind: "int", name: "u16" } },
1231
+ { bytes: [0] }, // primary_sale_happened
1232
+ ];
1233
+ const mutable = field(md, "isMutable");
1234
+ fields.push(mutable ? { value: mutable, type: BOOL } : { bytes: [1] });
1235
+ fields.push({ bytes: [1, 0] }); // token_standard: Some(NonFungible)
1236
+ const creators = field(md, "creators");
1237
+ if (creators && !ts.isArrayLiteralExpression(creators))
1238
+ this.fail(creators, "creators: [{ address, verified, share }, …] (an array literal)");
1239
+ const list = creators ? creators.elements : [];
1240
+ if (list.length > 5)
1241
+ this.fail(creators, "Bubblegum takes at most 5 creators");
1242
+ fields.push({ bytes: [list.length, 0, 0, 0] });
1243
+ let shares = 0n;
1244
+ let constant = true;
1245
+ for (const el of list) {
1246
+ if (!ts.isObjectLiteralExpression(el))
1247
+ this.fail(el, "a creator is { address, verified, share }");
1248
+ const f = (k) => field(el, k) ?? this.fail(el, `a creator needs ${k}`);
1249
+ const share = f("share");
1250
+ if (ts.isBigIntLiteral(share))
1251
+ shares += BigInt(share.text.slice(0, -1));
1252
+ else
1253
+ constant = false;
1254
+ fields.push({ value: f("address"), type: PUBKEY }, { value: f("verified"), type: BOOL }, { value: share, type: U8 });
1255
+ }
1256
+ if (list.length && constant && shares !== 100n)
1257
+ this.fail(creators, `creator shares add up to ${shares}; Bubblegum requires 100`);
1258
+ // collection: Some(the core collection's key) when minting into one.
1259
+ if (coll)
1260
+ fields.push({ bytes: [1] }, { value: this.keyOf(coll[0]), type: PUBKEY });
1261
+ else
1262
+ fields.push({ bytes: [0] });
1263
+ fields.push({ bytes: [0, 0] }); // asset_data, asset_data_schema: None
1264
+ const data = this.mplData(node, 1024, fields);
1265
+ this.cpi(p, metas, data.buf, data.len, seedsAt(collection ? 12 : 11));
1266
+ return undefined;
1267
+ }
1268
+ case "transferV2": {
1269
+ // TransferV2: treeConfig, payer, authority, leafOwner, leafDelegate, newLeafOwner, merkleTree, coreCollection,
1270
+ // logWrapper, compressionProgram, systemProgram; the proof follows as remaining accounts.
1271
+ if (a.length !== 13 && a.length !== 14)
1272
+ this.fail(node, "transferV2(treeConfig, payer, authority | null, leafOwner, leafDelegate | null, newLeafOwner, merkleTree, coreCollection | null, logWrapper, compressionProgram, systemProgram, leaf, proof, signerSeeds?)");
1273
+ const metas = [
1274
+ acct(0, "the tree config", true, false),
1275
+ acct(1, "the payer", true, true),
1276
+ acct(2, "the authority", false, true),
1277
+ acct(3, "the leaf owner", false, false),
1278
+ acct(4, "the leaf delegate", false, false),
1279
+ acct(5, "the new leaf owner", false, false),
1280
+ acct(6, "the merkle tree", true, false),
1281
+ acct(7, "the core collection", false, false),
1282
+ acct(8, "the log wrapper", false, false),
1283
+ acct(9, "the compression program", false, false),
1284
+ acct(10, "the system program", false, false),
1285
+ ];
1286
+ const data = this.mplData(node, 160, [{ bytes: [119, 40, 6, 235, 234, 221, 248, 49] }, ...leafFields(a[11])]);
1287
+ this.cpi(p, metas, data.buf, data.len, seedsAt(13), proofRange(a[12]));
1288
+ return undefined;
1289
+ }
1290
+ case "burnV2": {
1291
+ // BurnV2: treeConfig, payer, authority, leafOwner, leafDelegate, merkleTree, coreCollection, mplCoreCpiSigner,
1292
+ // logWrapper, compressionProgram, mplCoreProgram, systemProgram; the proof follows as remaining accounts.
1293
+ if (a.length !== 14 && a.length !== 15)
1294
+ this.fail(node, "burnV2(treeConfig, payer, authority | null, leafOwner, leafDelegate | null, merkleTree, coreCollection | null, mplCoreCpiSigner | null, logWrapper, compressionProgram, mplCoreProgram, systemProgram, leaf, proof, signerSeeds?)");
1295
+ const metas = [
1296
+ acct(0, "the tree config", true, false),
1297
+ acct(1, "the payer", true, true),
1298
+ acct(2, "the authority", false, true),
1299
+ acct(3, "the leaf owner", false, false),
1300
+ acct(4, "the leaf delegate", false, false),
1301
+ acct(5, "the merkle tree", true, false),
1302
+ acct(6, "the core collection", true, false),
1303
+ acct(7, "the mpl-core CPI signer", false, false),
1304
+ acct(8, "the log wrapper", false, false),
1305
+ acct(9, "the compression program", false, false),
1306
+ acct(10, "the mpl-core program", false, false),
1307
+ acct(11, "the system program", false, false),
1308
+ ];
1309
+ const data = this.mplData(node, 160, [{ bytes: [115, 210, 34, 240, 232, 143, 183, 16] }, ...leafFields(a[12])]);
1310
+ this.cpi(p, metas, data.buf, data.len, seedsAt(14), proofRange(a[13]));
1311
+ return undefined;
1312
+ }
1313
+ }
1314
+ this.fail(node.expression, `Bubblegum supports createTree, mint, createTreeV2, mintV2, transferV2 and burnV2, not ${method}`);
1315
+ }
1316
+ /** `acct.key` of an account parameter, as an expression to serialize. */
1317
+ keyOf(n) {
1318
+ const f = ts.factory;
1319
+ const e = f.createPropertyAccessExpression(n, "key");
1320
+ ts.setTextRange(e, n);
1321
+ e.parent = n.parent;
1322
+ return e;
823
1323
  }
824
1324
  /**
825
1325
  * `remainingAccounts()` (or a const bound to it): every remaining account; `rest.slice(a, b?)`: indexes a..b of them
@@ -1106,6 +1606,8 @@ export class CpiEmitter extends CallEmitter {
1106
1606
  const s = targ && ts.isTypeReferenceNode(targ) && ts.isIdentifier(targ.typeName) ? this.model.structs.get(targ.typeName.text) : undefined;
1107
1607
  if (!s)
1108
1608
  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`);
1109
1611
  const obj = node.arguments[0];
1110
1612
  if (node.arguments.length !== (cpi ? 3 : 1) || !ts.isObjectLiteralExpression(obj))
1111
1613
  this.fail(node, "emit<T>({ field: value, ... }) takes one object literal");