@typesol/cli 0.16.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.
- package/CHANGELOG.md +121 -0
- package/README.md +4 -4
- package/dist/clientgen.js +39 -0
- package/dist/compiler/analyze.d.ts +16 -1
- package/dist/compiler/analyze.js +355 -43
- package/dist/compiler/emit/base.d.ts +36 -1
- package/dist/compiler/emit/base.js +135 -4
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +298 -0
- package/dist/compiler/emit/calls.d.ts +2 -3
- package/dist/compiler/emit/calls.js +135 -29
- package/dist/compiler/emit/cpi.d.ts +62 -8
- package/dist/compiler/emit/cpi.js +1458 -40
- package/dist/compiler/emit/expressions.d.ts +5 -0
- package/dist/compiler/emit/expressions.js +85 -5
- package/dist/compiler/emit/function.js +82 -14
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +20 -0
- package/dist/compiler/emit/shared.js +23 -3
- package/dist/compiler/emit/statements.d.ts +21 -0
- package/dist/compiler/emit/statements.js +380 -19
- package/dist/compiler/emit/storage.d.ts +6 -0
- package/dist/compiler/emit/storage.js +88 -16
- package/dist/compiler/idl.js +37 -5
- package/dist/compiler/model.d.ts +70 -4
- package/dist/compiler/model.js +8 -2
- package/dist/portgen.js +90 -4
- package/docs/errors.md +6 -4
- package/docs/language.md +32 -20
- package/docs/migrate-from-anchor.md +24 -7
- package/docs/test-your-program.md +1 -1
- package/lib/typesol.d.ts +652 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +230 -7
|
@@ -1,8 +1,188 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { discriminator, snakeCase } from "../analyze.js";
|
|
3
|
-
import {
|
|
3
|
+
import { borshMinSize } from "./borsh.js";
|
|
4
|
+
import { INT_INFO, isScalar, isWritable, optionKey, 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
|
+
/**
|
|
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
|
+
};
|
|
151
|
+
/** Metaplex Core's Plugin / PluginType enums, in order (MasterEdition, VerifiedCreators and Autograph values are refused). */
|
|
152
|
+
const CORE_PLUGINS = [
|
|
153
|
+
"Royalties",
|
|
154
|
+
"FreezeDelegate",
|
|
155
|
+
"BurnDelegate",
|
|
156
|
+
"TransferDelegate",
|
|
157
|
+
"UpdateDelegate",
|
|
158
|
+
"PermanentFreezeDelegate",
|
|
159
|
+
"Attributes",
|
|
160
|
+
"PermanentTransferDelegate",
|
|
161
|
+
"PermanentBurnDelegate",
|
|
162
|
+
"Edition",
|
|
163
|
+
"MasterEdition",
|
|
164
|
+
"AddBlocker",
|
|
165
|
+
"ImmutableMetadata",
|
|
166
|
+
"VerifiedCreators",
|
|
167
|
+
"Autograph",
|
|
168
|
+
"BubblegumV2",
|
|
169
|
+
"FreezeExecute",
|
|
170
|
+
"PermanentFreezeExecute",
|
|
171
|
+
"Groups",
|
|
172
|
+
];
|
|
173
|
+
const CORE_PLUGIN_FIELDS = {
|
|
174
|
+
Royalties: ["basisPoints", "creators", "ruleSet"],
|
|
175
|
+
MasterEdition: ["maxSupply", "name", "uri"],
|
|
176
|
+
VerifiedCreators: ["signatures"],
|
|
177
|
+
Autograph: ["signatures"],
|
|
178
|
+
FreezeExecute: ["frozen"],
|
|
179
|
+
PermanentFreezeExecute: ["frozen"],
|
|
180
|
+
FreezeDelegate: ["frozen"],
|
|
181
|
+
PermanentFreezeDelegate: ["frozen"],
|
|
182
|
+
UpdateDelegate: ["additionalDelegates"],
|
|
183
|
+
Attributes: ["attributes"],
|
|
184
|
+
Edition: ["number"],
|
|
185
|
+
};
|
|
6
186
|
/**
|
|
7
187
|
* Other programs: PDAs and seeds, account creation, System/Token/ATA/Metaplex CPIs, generic invoke, Borsh
|
|
8
188
|
* serialization and events.
|
|
@@ -121,35 +301,383 @@ export class CpiEmitter extends CallEmitter {
|
|
|
121
301
|
this.ix.pdas.set(account.name, idl);
|
|
122
302
|
}
|
|
123
303
|
create(node, target, topLevel, ifNeeded = false) {
|
|
304
|
+
const call = `${target.name}.${ifNeeded ? "createIfNeeded" : "create"}`;
|
|
124
305
|
if (!topLevel)
|
|
125
|
-
this.fail(node, `${
|
|
306
|
+
this.fail(node, `${call}(...) must be a top-level statement of the instruction`);
|
|
126
307
|
if (this.created.has(target.name))
|
|
127
308
|
this.fail(node, `${target.name} is already created`);
|
|
128
|
-
if (node.arguments.length !== 2)
|
|
129
|
-
this.fail(node,
|
|
309
|
+
if (node.arguments.length !== 2 && node.arguments.length !== 3)
|
|
310
|
+
this.fail(node, `${call}(payer, seeds | null, space?) takes two or three arguments`);
|
|
130
311
|
const payer = this.accountParam(node.arguments[0], "the payer");
|
|
131
312
|
if (payer.kind.kind !== "signer" || !payer.kind.mut)
|
|
132
313
|
this.fail(node.arguments[0], "the payer must be a Mut<Signer>");
|
|
133
|
-
const seeds = this.seeds(node.arguments[1], 1);
|
|
134
|
-
this.recordPda(target, seeds.idl);
|
|
135
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);
|
|
136
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
|
+
}
|
|
137
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
|
+
}
|
|
138
354
|
if (ifNeeded) {
|
|
139
355
|
// Returns whether it was created now; an existing account must be this PDA, ours, and a ${s.name}.
|
|
140
356
|
const created = this.tmp("created");
|
|
141
357
|
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}));`);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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}));`);
|
|
359
|
+
// An existing @layout borsh account is unpacked now (a new one starts from the zeroed struct).
|
|
360
|
+
if (s.layout === "borsh")
|
|
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();
|
|
146
363
|
return { c: created, type: BOOL };
|
|
147
364
|
}
|
|
148
|
-
this.line(`TS_TRY(ts_create_pda(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name},
|
|
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
|
+
}
|
|
149
503
|
this.created.add(target.name);
|
|
150
504
|
this.pendingInit.delete(target.name);
|
|
151
505
|
this.bumps.set(target.name, b);
|
|
152
|
-
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 };
|
|
153
681
|
}
|
|
154
682
|
/**
|
|
155
683
|
* `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
|
|
@@ -248,11 +776,26 @@ export class CpiEmitter extends CallEmitter {
|
|
|
248
776
|
], buf, len, seeds);
|
|
249
777
|
return undefined;
|
|
250
778
|
}
|
|
251
|
-
/**
|
|
252
|
-
|
|
779
|
+
/**
|
|
780
|
+
* Emit a CPI to the program account `program` with these metas (account, writable, signer), and then (`tail`) a range
|
|
781
|
+
* of the remaining accounts, read-only (a Merkle proof).
|
|
782
|
+
*/
|
|
783
|
+
cpi(program, metas, buf, len, seeds, tail) {
|
|
253
784
|
const m = this.tmp("metas");
|
|
254
|
-
|
|
255
|
-
|
|
785
|
+
const fixed = metas.map(([acc, w, sg]) => `{&${acc}->key, ${w}, ${sg}}`).join(", ");
|
|
786
|
+
if (!tail) {
|
|
787
|
+
this.line(`SolAccountMeta ${m}[${metas.length}] = {${fixed}};`);
|
|
788
|
+
this.line(`TS_TRY(ts_cpi(params, ${program}, ${buf}, ${len}, ${m}, ${metas.length}, ${seeds.name}, ${seeds.count}));`);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const n = this.ix.accounts.length;
|
|
792
|
+
const cnt = this.tmp("nmetas");
|
|
793
|
+
const i = this.tmp("ri");
|
|
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");`);
|
|
795
|
+
this.line(`SolAccountMeta ${m}[TS_MAX_ACCOUNTS] = {${fixed}};`);
|
|
796
|
+
this.line(`uint64_t ${cnt} = ${metas.length};`);
|
|
797
|
+
this.line(`for (uint64_t ${i} = ${n} + ${tail.from}; ${i} < ${n} + ${tail.to}; ${i}++) ${m}[${cnt}++] = (SolAccountMeta){¶ms->ka[${i}]->key, ${tail.asPassed ? `params->ka[${i}]->is_writable` : 0}, 0};`);
|
|
798
|
+
this.line(`TS_TRY(ts_cpi(params, ${program}, ${buf}, ${len}, ${m}, ${cnt}, ${seeds.name}, ${seeds.count}));`);
|
|
256
799
|
}
|
|
257
800
|
signerSeeds(node, authority, at) {
|
|
258
801
|
if (!node) {
|
|
@@ -309,7 +852,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
309
852
|
}
|
|
310
853
|
case "mintTo": {
|
|
311
854
|
arity(4);
|
|
312
|
-
|
|
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");
|
|
313
859
|
const to = want(a[1], "token", true, "the destination");
|
|
314
860
|
const auth = this.accountParam(a[2], "the mint authority");
|
|
315
861
|
const amt = amount(a[3]);
|
|
@@ -332,7 +878,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
332
878
|
this.fail(node, "metadata lives in Token-2022 mints; use Token2022Program or TokenInterface");
|
|
333
879
|
if (a.length !== 6 && a.length !== 7)
|
|
334
880
|
this.fail(node, "initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri, signerSeeds?)");
|
|
335
|
-
|
|
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<...>)");
|
|
336
885
|
const ua = this.accountParam(a[1], "the update authority");
|
|
337
886
|
const ma = this.accountParam(a[2], "the mint authority");
|
|
338
887
|
const [nm, sym, uri] = [a[3], a[4], a[5]].map((n) => this.expect(this.expr(n, STRING), STRING, n).c);
|
|
@@ -342,7 +891,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
342
891
|
}
|
|
343
892
|
case "closeAccount": {
|
|
344
893
|
arity(3);
|
|
345
|
-
|
|
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>");
|
|
346
898
|
const dest = this.accountParam(a[1], "the destination");
|
|
347
899
|
if (!isWritable(dest.kind))
|
|
348
900
|
this.fail(a[1], "the destination must be writable (Mut<...>)");
|
|
@@ -463,6 +1015,194 @@ export class CpiEmitter extends CallEmitter {
|
|
|
463
1015
|
simple(22, [], [[acct, true, false]], none);
|
|
464
1016
|
return undefined;
|
|
465
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
|
+
}
|
|
466
1206
|
case "approve": {
|
|
467
1207
|
arity(4);
|
|
468
1208
|
const src = want(a[0], "token", true, "the source");
|
|
@@ -539,7 +1279,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
539
1279
|
return undefined;
|
|
540
1280
|
}
|
|
541
1281
|
}
|
|
542
|
-
this.fail(node.expression, `token programs support transfer, transferChecked, mintTo, burn, closeAccount, approve, revoke, setAuthority, freezeAccount, thawAccount, syncNative, initializeMint2, initializeAccount3, initializeMetadata
|
|
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}`);
|
|
543
1283
|
}
|
|
544
1284
|
ataOp(node, program, method) {
|
|
545
1285
|
if (method !== "create" && method !== "createIdempotent")
|
|
@@ -562,10 +1302,12 @@ export class CpiEmitter extends CallEmitter {
|
|
|
562
1302
|
return undefined;
|
|
563
1303
|
}
|
|
564
1304
|
metadataOp(node, program, method) {
|
|
1305
|
+
if (method === "createMasterEditionV3" || method === "verifyCollection")
|
|
1306
|
+
return this.metadataCollectionOp(node, program, method);
|
|
565
1307
|
if (method !== "createV3")
|
|
566
|
-
this.fail(node.expression,
|
|
1308
|
+
this.fail(node.expression, `the Token Metadata program supports createV3, createMasterEditionV3 and verifyCollection, not ${method}`);
|
|
567
1309
|
const a = node.arguments;
|
|
568
|
-
const usage = "createV3(metadata, mint, mintAuthority, payer, updateAuthority, systemProgram, name, symbol, uri, sellerFeeBasisPoints, isMutable, { creators?, collection? }?, signerSeeds?)";
|
|
1310
|
+
const usage = "createV3(metadata, mint, mintAuthority, payer, updateAuthority, systemProgram, name, symbol, uri, sellerFeeBasisPoints, isMutable, { creators?, collection?, isCollection? }?, signerSeeds?)";
|
|
569
1311
|
// An object literal after isMutable carries the optional creators / collection; signerSeeds stays last.
|
|
570
1312
|
const extras = a[11] && ts.isObjectLiteralExpression(a[11]) ? a[11] : undefined;
|
|
571
1313
|
const seedsArg = extras ? a[12] : a[11];
|
|
@@ -598,10 +1340,11 @@ export class CpiEmitter extends CallEmitter {
|
|
|
598
1340
|
let creators = "NULL";
|
|
599
1341
|
let ncreators = "-1";
|
|
600
1342
|
let collection = "NULL";
|
|
1343
|
+
let sized = "false";
|
|
601
1344
|
if (extras) {
|
|
602
1345
|
for (const p of extras.properties) {
|
|
603
1346
|
if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name))
|
|
604
|
-
this.fail(p, "use creators: [...] and
|
|
1347
|
+
this.fail(p, "use creators: [...], collection: key and isCollection: boolean");
|
|
605
1348
|
if (p.name.text === "creators") {
|
|
606
1349
|
if (!ts.isArrayLiteralExpression(p.initializer))
|
|
607
1350
|
this.fail(p.initializer, "creators: [{ address, verified, share }, …] (an array literal)");
|
|
@@ -646,14 +1389,66 @@ export class CpiEmitter extends CallEmitter {
|
|
|
646
1389
|
this.line(`SolPubkey ${t} = *(${key.c});`);
|
|
647
1390
|
collection = `&${t}`;
|
|
648
1391
|
}
|
|
1392
|
+
else if (p.name.text === "isCollection") {
|
|
1393
|
+
const v = this.expect(this.expr(p.initializer, BOOL), BOOL, p.initializer);
|
|
1394
|
+
const t = this.tmp("sized");
|
|
1395
|
+
this.line(`bool ${t} = ${v.c};`);
|
|
1396
|
+
sized = t;
|
|
1397
|
+
}
|
|
649
1398
|
else
|
|
650
|
-
this.fail(p.name, `createV3 extras are creators and
|
|
1399
|
+
this.fail(p.name, `createV3 extras are creators, collection and isCollection, not ${p.name.text}`);
|
|
651
1400
|
}
|
|
652
1401
|
}
|
|
653
1402
|
const seeds = this.signerSeeds(seedsArg, mintAuth, a[2]);
|
|
654
1403
|
settleAll(extrasMark);
|
|
655
1404
|
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}));`);
|
|
1405
|
+
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}));`);
|
|
1406
|
+
return undefined;
|
|
1407
|
+
}
|
|
1408
|
+
/** CreateMasterEditionV3 (17) and Verify (52) with VerificationArgs::CollectionV1. */
|
|
1409
|
+
metadataCollectionOp(node, program, method) {
|
|
1410
|
+
const a = node.arguments;
|
|
1411
|
+
const p = `a_${program.name}`;
|
|
1412
|
+
const seedsAt = (i) => (a[i] ? this.signingSeeds(a[i]) : { name: "NULL", count: 0 });
|
|
1413
|
+
const acct = (i, what, w, s) => {
|
|
1414
|
+
if (a[i].kind === ts.SyntaxKind.NullKeyword)
|
|
1415
|
+
this.fail(a[i], `pass ${what}`);
|
|
1416
|
+
return this.mplMeta(a[i], program, what, w, s);
|
|
1417
|
+
};
|
|
1418
|
+
if (method === "createMasterEditionV3") {
|
|
1419
|
+
if (a.length !== 9 && a.length !== 10)
|
|
1420
|
+
this.fail(node, "createMasterEditionV3(edition, mint, updateAuthority, mintAuthority, payer, metadata, tokenProgram, systemProgram, maxSupply | null, signerSeeds?)");
|
|
1421
|
+
const metas = [
|
|
1422
|
+
acct(0, "the edition", true, false),
|
|
1423
|
+
acct(1, "the mint", true, false),
|
|
1424
|
+
acct(2, "the update authority", false, true),
|
|
1425
|
+
acct(3, "the mint authority", false, true),
|
|
1426
|
+
acct(4, "the payer", true, true),
|
|
1427
|
+
acct(5, "the metadata", true, false),
|
|
1428
|
+
acct(6, "the token program", false, false),
|
|
1429
|
+
acct(7, "the system program", false, false),
|
|
1430
|
+
];
|
|
1431
|
+
// max_supply: Option<u64>; Some(0) makes a one of one, None allows unlimited prints.
|
|
1432
|
+
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" } }]);
|
|
1433
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(9));
|
|
1434
|
+
return undefined;
|
|
1435
|
+
}
|
|
1436
|
+
// Verify: authority, delegate record (absent: the program id), metadata, collection mint, collection metadata,
|
|
1437
|
+
// collection master edition, system program, instructions sysvar.
|
|
1438
|
+
if (a.length !== 7 && a.length !== 8)
|
|
1439
|
+
this.fail(node, "verifyCollection(authority, metadata, collectionMint, collectionMetadata, collectionMasterEdition, systemProgram, instructionsSysvar, signerSeeds?)");
|
|
1440
|
+
const metas = [
|
|
1441
|
+
acct(0, "the collection authority", false, true),
|
|
1442
|
+
[p, false, false],
|
|
1443
|
+
acct(1, "the metadata", true, false),
|
|
1444
|
+
acct(2, "the collection mint", false, false),
|
|
1445
|
+
acct(3, "the collection metadata", true, false),
|
|
1446
|
+
acct(4, "the collection master edition", false, false),
|
|
1447
|
+
acct(5, "the system program", false, false),
|
|
1448
|
+
acct(6, "the instructions sysvar", false, false),
|
|
1449
|
+
];
|
|
1450
|
+
const data = this.mplData(node, 8, [{ bytes: [52, 1] }]);
|
|
1451
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(7));
|
|
657
1452
|
return undefined;
|
|
658
1453
|
}
|
|
659
1454
|
/**
|
|
@@ -683,17 +1478,202 @@ export class CpiEmitter extends CallEmitter {
|
|
|
683
1478
|
finish(at);
|
|
684
1479
|
return { buf, len };
|
|
685
1480
|
}
|
|
686
|
-
/**
|
|
1481
|
+
/**
|
|
1482
|
+
* Borsh of one Metaplex Core plugin literal (`{ kind: "FreezeDelegate", frozen: true, authority?: … }`): the Plugin
|
|
1483
|
+
* enum, then (`withAuthority`) the Option<PluginAuthority> that CreateV1's PluginAuthorityPair and AddPluginV1 take.
|
|
1484
|
+
*/
|
|
1485
|
+
corePlugin(n, withAuthority) {
|
|
1486
|
+
if (!ts.isObjectLiteralExpression(n))
|
|
1487
|
+
this.fail(n, 'a Core plugin is an object literal: { kind: "FreezeDelegate", frozen: false }');
|
|
1488
|
+
const props = new Map();
|
|
1489
|
+
for (const q of n.properties) {
|
|
1490
|
+
if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
|
|
1491
|
+
props.set(q.name.text, q.initializer);
|
|
1492
|
+
else if (ts.isShorthandPropertyAssignment(q))
|
|
1493
|
+
props.set(q.name.text, q.name);
|
|
1494
|
+
else
|
|
1495
|
+
this.fail(q, "write plugin fields as name: value");
|
|
1496
|
+
}
|
|
1497
|
+
const kindNode = props.get("kind");
|
|
1498
|
+
if (!kindNode || !ts.isStringLiteral(kindNode))
|
|
1499
|
+
this.fail(n, 'a Core plugin needs kind: "Royalties" | "FreezeDelegate" | …');
|
|
1500
|
+
const kind = kindNode.text;
|
|
1501
|
+
const index = CORE_PLUGINS.indexOf(kind);
|
|
1502
|
+
if (index < 0)
|
|
1503
|
+
this.fail(kindNode, `Core plugins are ${CORE_PLUGINS.join(", ")}`);
|
|
1504
|
+
if (kind === "Groups")
|
|
1505
|
+
this.fail(kindNode, "Groups is kept by Core's group instructions (AddCollectionsToGroupV1 …), not set as a plugin");
|
|
1506
|
+
const known = new Set(["kind", "authority", ...(CORE_PLUGIN_FIELDS[kind] ?? [])]);
|
|
1507
|
+
for (const [k, v] of props)
|
|
1508
|
+
if (!known.has(k))
|
|
1509
|
+
this.fail(v, `${kind} has no field ${k}`);
|
|
1510
|
+
const need = (k) => props.get(k) ?? this.fail(n, `${kind} needs ${k}`);
|
|
1511
|
+
const list = (k, max) => {
|
|
1512
|
+
const e = need(k);
|
|
1513
|
+
if (!ts.isArrayLiteralExpression(e))
|
|
1514
|
+
this.fail(e, `${k} is an array literal`);
|
|
1515
|
+
if (e.elements.length > max)
|
|
1516
|
+
this.fail(e, `at most ${max} entries in ${k}`);
|
|
1517
|
+
return e.elements;
|
|
1518
|
+
};
|
|
1519
|
+
const u32 = (x) => [x & 0xff, (x >> 8) & 0xff, (x >> 16) & 0xff, x >>> 24];
|
|
1520
|
+
const fields = [{ bytes: [index] }];
|
|
1521
|
+
switch (kind) {
|
|
1522
|
+
case "Royalties": {
|
|
1523
|
+
const bps = need("basisPoints");
|
|
1524
|
+
if (ts.isBigIntLiteral(bps) && BigInt(bps.text.slice(0, -1)) > 10000n)
|
|
1525
|
+
this.fail(bps, "basisPoints is at most 10000 (100%)");
|
|
1526
|
+
fields.push({ value: bps, type: { kind: "int", name: "u16" } });
|
|
1527
|
+
const creators = list("creators", 5);
|
|
1528
|
+
fields.push({ bytes: u32(creators.length) });
|
|
1529
|
+
let sum = 0n;
|
|
1530
|
+
let constant = true;
|
|
1531
|
+
for (const c of creators) {
|
|
1532
|
+
if (!ts.isObjectLiteralExpression(c))
|
|
1533
|
+
this.fail(c, "a creator is { address, percentage }");
|
|
1534
|
+
const get = (k) => {
|
|
1535
|
+
const q = c.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
|
|
1536
|
+
if (!q)
|
|
1537
|
+
this.fail(c, `a creator needs ${k}`);
|
|
1538
|
+
return ts.isPropertyAssignment(q) ? q.initializer : q.name;
|
|
1539
|
+
};
|
|
1540
|
+
const pct = get("percentage");
|
|
1541
|
+
if (ts.isBigIntLiteral(pct))
|
|
1542
|
+
sum += BigInt(pct.text.slice(0, -1));
|
|
1543
|
+
else
|
|
1544
|
+
constant = false;
|
|
1545
|
+
fields.push({ value: get("address"), type: PUBKEY }, { value: pct, type: U8 });
|
|
1546
|
+
}
|
|
1547
|
+
if (constant && creators.length && sum !== 100n)
|
|
1548
|
+
this.fail(need("creators"), `creator percentages add up to ${sum}; Core requires 100`);
|
|
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
|
+
}
|
|
1592
|
+
break;
|
|
1593
|
+
}
|
|
1594
|
+
case "FreezeExecute":
|
|
1595
|
+
case "PermanentFreezeExecute":
|
|
1596
|
+
fields.push({ value: need("frozen"), type: BOOL });
|
|
1597
|
+
break;
|
|
1598
|
+
case "FreezeDelegate":
|
|
1599
|
+
case "PermanentFreezeDelegate":
|
|
1600
|
+
fields.push({ value: need("frozen"), type: BOOL });
|
|
1601
|
+
break;
|
|
1602
|
+
case "UpdateDelegate": {
|
|
1603
|
+
const keys = props.has("additionalDelegates") ? list("additionalDelegates", 8) : [];
|
|
1604
|
+
fields.push({ bytes: u32(keys.length) }, ...keys.map((k) => ({ value: k, type: PUBKEY })));
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
case "Attributes": {
|
|
1608
|
+
const attrs = list("attributes", 32);
|
|
1609
|
+
fields.push({ bytes: u32(attrs.length) });
|
|
1610
|
+
for (const at of attrs) {
|
|
1611
|
+
if (!ts.isObjectLiteralExpression(at))
|
|
1612
|
+
this.fail(at, "an attribute is { key, value }");
|
|
1613
|
+
for (const k of ["key", "value"]) {
|
|
1614
|
+
const q = at.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
|
|
1615
|
+
if (!q)
|
|
1616
|
+
this.fail(at, `an attribute needs ${k}`);
|
|
1617
|
+
fields.push({ value: ts.isPropertyAssignment(q) ? q.initializer : q.name, type: STRING });
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
case "Edition":
|
|
1623
|
+
fields.push({ value: need("number"), type: { kind: "int", name: "u32" } });
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
const auth = props.get("authority");
|
|
1627
|
+
if (!withAuthority) {
|
|
1628
|
+
if (auth)
|
|
1629
|
+
this.fail(auth, "a plugin's authority is set when it is added; change it with Core's approve / revoke");
|
|
1630
|
+
return fields;
|
|
1631
|
+
}
|
|
1632
|
+
if (!auth)
|
|
1633
|
+
fields.push({ bytes: [0] }); // the plugin type's default authority
|
|
1634
|
+
else if (ts.isStringLiteral(auth)) {
|
|
1635
|
+
const i = ["none", "owner", "updateAuthority"].indexOf(auth.text);
|
|
1636
|
+
if (i < 0)
|
|
1637
|
+
this.fail(auth, 'authority is "none", "owner", "updateAuthority" or a Pubkey');
|
|
1638
|
+
fields.push({ bytes: [1, i] });
|
|
1639
|
+
}
|
|
1640
|
+
else
|
|
1641
|
+
fields.push({ bytes: [1, 3] }, { value: auth, type: PUBKEY });
|
|
1642
|
+
return fields;
|
|
1643
|
+
}
|
|
1644
|
+
/** CreateV1 / CreateCollectionV1 plugins: Option<Vec<PluginAuthorityPair>>. */
|
|
1645
|
+
corePluginList(n) {
|
|
1646
|
+
if (!n)
|
|
1647
|
+
return [{ bytes: [0] }];
|
|
1648
|
+
const els = n.elements;
|
|
1649
|
+
if (els.length > 16)
|
|
1650
|
+
this.fail(n, "at most 16 plugins");
|
|
1651
|
+
const kinds = els.map((e) => ts.isObjectLiteralExpression(e) ? e.properties.find((q) => q.name && ts.isIdentifier(q.name) && q.name.text === "kind") : undefined);
|
|
1652
|
+
const seen = new Set();
|
|
1653
|
+
kinds.forEach((k, i) => {
|
|
1654
|
+
const lit = k && ts.isPropertyAssignment(k) && ts.isStringLiteral(k.initializer) ? k.initializer.text : undefined;
|
|
1655
|
+
if (lit && seen.has(lit))
|
|
1656
|
+
this.fail(els[i], `${lit} is listed twice; an asset has one plugin of each kind`);
|
|
1657
|
+
if (lit)
|
|
1658
|
+
seen.add(lit);
|
|
1659
|
+
});
|
|
1660
|
+
return [{ bytes: [1, els.length & 0xff, 0, 0, 0] }, ...els.flatMap((e) => this.corePlugin(e, true))];
|
|
1661
|
+
}
|
|
1662
|
+
/** Metaplex Core: assets and collections (with plugins), plugins added, updated and removed, transfer and burn. */
|
|
687
1663
|
coreOp(node, core, method) {
|
|
688
1664
|
const a = node.arguments;
|
|
689
1665
|
const p = `a_${core.name}`;
|
|
690
1666
|
const seedsAt = (i) => (a[i] ? this.signingSeeds(a[i]) : { name: "NULL", count: 0 });
|
|
691
1667
|
const str = (n) => ({ value: n, type: STRING });
|
|
1668
|
+
// An array literal of object literals after the uri is the plugin list; the signer seeds come after it.
|
|
1669
|
+
const isPlugins = (n) => !!n && ts.isArrayLiteralExpression(n) && (n.elements.length === 0 || n.elements.every((e) => ts.isObjectLiteralExpression(e)));
|
|
1670
|
+
const self = [p, false, false];
|
|
692
1671
|
switch (method) {
|
|
693
1672
|
case "createAsset": {
|
|
694
1673
|
// CreateV1: asset, collection, authority, payer, owner, updateAuthority, systemProgram, logWrapper.
|
|
695
|
-
|
|
696
|
-
|
|
1674
|
+
const plugins = isPlugins(a[8]) ? a[8] : undefined;
|
|
1675
|
+
if (a.length < 8 || a.length > (plugins ? 10 : 9))
|
|
1676
|
+
this.fail(node, "createAsset(asset, collection | null, authority | null, payer, owner | null, systemProgram, name, uri, plugins?, signerSeeds?)");
|
|
697
1677
|
const metas = [
|
|
698
1678
|
this.mplMeta(a[0], core, "the asset", true, true),
|
|
699
1679
|
this.mplMeta(a[1], core, "the collection", true, false),
|
|
@@ -704,24 +1684,64 @@ export class CpiEmitter extends CallEmitter {
|
|
|
704
1684
|
this.mplMeta(a[5], core, "the system program", false, false),
|
|
705
1685
|
[p, false, false],
|
|
706
1686
|
];
|
|
707
|
-
const data = this.mplData(node,
|
|
708
|
-
this.cpi(p, metas, data.buf, data.len, seedsAt(8));
|
|
1687
|
+
const data = this.mplData(node, 2048, [{ bytes: [0, 0] }, str(a[6]), str(a[7]), ...this.corePluginList(plugins)]);
|
|
1688
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(plugins ? 9 : 8));
|
|
709
1689
|
return undefined;
|
|
710
1690
|
}
|
|
711
1691
|
case "createCollection": {
|
|
712
1692
|
// CreateCollectionV1: collection, updateAuthority, payer, systemProgram.
|
|
713
|
-
|
|
714
|
-
|
|
1693
|
+
const plugins = isPlugins(a[6]) ? a[6] : undefined;
|
|
1694
|
+
if (a.length < 6 || a.length > (plugins ? 8 : 7))
|
|
1695
|
+
this.fail(node, "createCollection(collection, updateAuthority | null, payer, systemProgram, name, uri, plugins?, signerSeeds?)");
|
|
715
1696
|
const metas = [
|
|
716
1697
|
this.mplMeta(a[0], core, "the collection", true, true),
|
|
717
1698
|
this.mplMeta(a[1], core, "the update authority", false, false),
|
|
718
1699
|
this.mplMeta(a[2], core, "the payer", true, true),
|
|
719
1700
|
this.mplMeta(a[3], core, "the system program", false, false),
|
|
720
1701
|
];
|
|
721
|
-
const data = this.mplData(node,
|
|
1702
|
+
const data = this.mplData(node, 2048, [{ bytes: [1] }, str(a[4]), str(a[5]), ...this.corePluginList(plugins)]);
|
|
1703
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(plugins ? 7 : 6));
|
|
1704
|
+
return undefined;
|
|
1705
|
+
}
|
|
1706
|
+
case "addPlugin":
|
|
1707
|
+
case "updatePlugin":
|
|
1708
|
+
case "removePlugin": {
|
|
1709
|
+
// Add/Remove/UpdatePluginV1: asset, collection, payer, authority, systemProgram, logWrapper.
|
|
1710
|
+
const last = method === "removePlugin" ? "kind" : "plugin";
|
|
1711
|
+
if (a.length !== 6 && a.length !== 7)
|
|
1712
|
+
this.fail(node, `${method}(asset, collection | null, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
|
|
1713
|
+
const metas = [
|
|
1714
|
+
this.mplMeta(a[0], core, "the asset", true, false),
|
|
1715
|
+
this.mplMeta(a[1], core, "the collection", true, false),
|
|
1716
|
+
this.mplMeta(a[2], core, "the payer", true, true),
|
|
1717
|
+
this.mplMeta(a[3], core, "the authority", false, true),
|
|
1718
|
+
this.mplMeta(a[4], core, "the system program", false, false),
|
|
1719
|
+
self,
|
|
1720
|
+
];
|
|
1721
|
+
const body = method === "removePlugin" ? [this.corePluginType(a[5])] : this.corePlugin(a[5], method === "addPlugin");
|
|
1722
|
+
const data = this.mplData(node, 2048, [{ bytes: [{ addPlugin: 2, removePlugin: 4, updatePlugin: 6 }[method]] }, ...body]);
|
|
722
1723
|
this.cpi(p, metas, data.buf, data.len, seedsAt(6));
|
|
723
1724
|
return undefined;
|
|
724
1725
|
}
|
|
1726
|
+
case "addCollectionPlugin":
|
|
1727
|
+
case "updateCollectionPlugin":
|
|
1728
|
+
case "removeCollectionPlugin": {
|
|
1729
|
+
// Add/Remove/UpdateCollectionPluginV1: collection, payer, authority, systemProgram, logWrapper.
|
|
1730
|
+
const last = method === "removeCollectionPlugin" ? "kind" : "plugin";
|
|
1731
|
+
if (a.length !== 5 && a.length !== 6)
|
|
1732
|
+
this.fail(node, `${method}(collection, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
|
|
1733
|
+
const metas = [
|
|
1734
|
+
this.mplMeta(a[0], core, "the collection", true, false),
|
|
1735
|
+
this.mplMeta(a[1], core, "the payer", true, true),
|
|
1736
|
+
this.mplMeta(a[2], core, "the authority", false, true),
|
|
1737
|
+
this.mplMeta(a[3], core, "the system program", false, false),
|
|
1738
|
+
self,
|
|
1739
|
+
];
|
|
1740
|
+
const body = method === "removeCollectionPlugin" ? [this.corePluginType(a[4])] : this.corePlugin(a[4], method === "addCollectionPlugin");
|
|
1741
|
+
const data = this.mplData(node, 2048, [{ bytes: [{ addCollectionPlugin: 3, removeCollectionPlugin: 5, updateCollectionPlugin: 7 }[method]] }, ...body]);
|
|
1742
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(5));
|
|
1743
|
+
return undefined;
|
|
1744
|
+
}
|
|
725
1745
|
case "transferAsset": {
|
|
726
1746
|
// TransferV1: asset, collection, payer, authority, newOwner, systemProgram, logWrapper.
|
|
727
1747
|
if (a.length !== 5 && a.length !== 6)
|
|
@@ -756,7 +1776,16 @@ export class CpiEmitter extends CallEmitter {
|
|
|
756
1776
|
return undefined;
|
|
757
1777
|
}
|
|
758
1778
|
}
|
|
759
|
-
this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, transferAsset and burnAsset, not ${method}`);
|
|
1779
|
+
this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin (and the Collection forms), transferAsset and burnAsset, not ${method}`);
|
|
1780
|
+
}
|
|
1781
|
+
/** PluginType (u8) from a kind string literal: removePlugin(…, "FreezeDelegate"). */
|
|
1782
|
+
corePluginType(n) {
|
|
1783
|
+
if (!ts.isStringLiteral(n))
|
|
1784
|
+
this.fail(n, 'name the plugin kind as a string literal: "FreezeDelegate"');
|
|
1785
|
+
const i = CORE_PLUGINS.indexOf(n.text);
|
|
1786
|
+
if (i < 0)
|
|
1787
|
+
this.fail(n, `Core plugins are ${CORE_PLUGINS.join(", ")}`);
|
|
1788
|
+
return { bytes: [i] };
|
|
760
1789
|
}
|
|
761
1790
|
/** Bubblegum (compressed NFTs on SPL account compression): createTree, mint (MintV1, no collection, no creators). */
|
|
762
1791
|
bubblegumOp(node, bg, method) {
|
|
@@ -819,7 +1848,357 @@ export class CpiEmitter extends CallEmitter {
|
|
|
819
1848
|
return undefined;
|
|
820
1849
|
}
|
|
821
1850
|
}
|
|
822
|
-
this.
|
|
1851
|
+
return this.bubblegumV2(node, bg, method);
|
|
1852
|
+
}
|
|
1853
|
+
/** Bubblegum V2 (trees on mpl-account-compression with mpl-noop; optional Metaplex Core collections). */
|
|
1854
|
+
bubblegumV2(node, bg, method) {
|
|
1855
|
+
const a = node.arguments;
|
|
1856
|
+
const p = `a_${bg.name}`;
|
|
1857
|
+
const acct = (i, what, w, s) => this.mplMeta(a[i], bg, what, w, s);
|
|
1858
|
+
const seedsAt = (i) => (a[i] ? this.signingSeeds(a[i]) : { name: "NULL", count: 0 });
|
|
1859
|
+
const U32 = { kind: "int", name: "u32" };
|
|
1860
|
+
const U64v = { kind: "int", name: "u64" };
|
|
1861
|
+
const HASH = { kind: "pubkey", hash: true };
|
|
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"];
|
|
1871
|
+
if (!ts.isObjectLiteralExpression(n))
|
|
1872
|
+
this.fail(n, `the leaf is { ${known.join(", ")} }`);
|
|
1873
|
+
const props = new Map();
|
|
1874
|
+
for (const q of n.properties) {
|
|
1875
|
+
if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
|
|
1876
|
+
props.set(q.name.text, q.initializer);
|
|
1877
|
+
else if (ts.isShorthandPropertyAssignment(q))
|
|
1878
|
+
props.set(q.name.text, q.name);
|
|
1879
|
+
else
|
|
1880
|
+
this.fail(q, "write leaf fields as name: value");
|
|
1881
|
+
}
|
|
1882
|
+
for (const [k, v] of props)
|
|
1883
|
+
if (!known.includes(k))
|
|
1884
|
+
this.fail(v, `the leaf has no field ${k}`);
|
|
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.
|
|
1887
|
+
const opt = (k, t) => {
|
|
1888
|
+
const v = props.get(k);
|
|
1889
|
+
if (!v || v.kind === ts.SyntaxKind.NullKeyword)
|
|
1890
|
+
return [{ bytes: [0] }];
|
|
1891
|
+
this.model.options.set(optionKey(t), t);
|
|
1892
|
+
return [{ value: v, type: { kind: "option", inner: t } }];
|
|
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];
|
|
1906
|
+
return [
|
|
1907
|
+
{ value: need("root"), type: HASH },
|
|
1908
|
+
...hashes,
|
|
1909
|
+
...(shape === "delegate" ? opt("collectionHash", HASH) : []),
|
|
1910
|
+
...opt("assetDataHash", HASH),
|
|
1911
|
+
...opt("flags", U8),
|
|
1912
|
+
...tail,
|
|
1913
|
+
];
|
|
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
|
+
};
|
|
1940
|
+
const proofRange = (n) => {
|
|
1941
|
+
if (!this.ix)
|
|
1942
|
+
this.fail(n, "a Merkle proof comes from remainingAccounts(), so only instructions can pass one");
|
|
1943
|
+
return this.remainingRange(n) ?? this.fail(n, "pass the proof as remainingAccounts() or rest.slice(a, b)");
|
|
1944
|
+
};
|
|
1945
|
+
switch (method) {
|
|
1946
|
+
case "createTreeV2": {
|
|
1947
|
+
// CreateTreeConfigV2: treeConfig, merkleTree, payer, treeCreator, logWrapper, compressionProgram, systemProgram.
|
|
1948
|
+
if (a.length !== 10 && a.length !== 11)
|
|
1949
|
+
this.fail(node, "createTreeV2(treeConfig, merkleTree, payer, treeCreator | null, logWrapper, compressionProgram, systemProgram, maxDepth, maxBufferSize, isPublic, signerSeeds?)");
|
|
1950
|
+
const metas = [
|
|
1951
|
+
acct(0, "the tree config", true, false),
|
|
1952
|
+
acct(1, "the merkle tree", true, false),
|
|
1953
|
+
acct(2, "the payer", true, true),
|
|
1954
|
+
acct(3, "the tree creator", false, true),
|
|
1955
|
+
acct(4, "the log wrapper", false, false),
|
|
1956
|
+
acct(5, "the compression program", false, false),
|
|
1957
|
+
acct(6, "the system program", false, false),
|
|
1958
|
+
];
|
|
1959
|
+
const data = this.mplData(node, 32, [
|
|
1960
|
+
{ bytes: [55, 99, 95, 215, 142, 203, 227, 205] },
|
|
1961
|
+
{ value: a[7], type: U32 },
|
|
1962
|
+
{ value: a[8], type: U32 },
|
|
1963
|
+
{ bytes: [1] },
|
|
1964
|
+
{ value: a[9], type: BOOL },
|
|
1965
|
+
]);
|
|
1966
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(10));
|
|
1967
|
+
return undefined;
|
|
1968
|
+
}
|
|
1969
|
+
case "mintV2": {
|
|
1970
|
+
// MintV2: treeConfig, payer, treeCreatorOrDelegate, collectionAuthority, leafOwner, leafDelegate, merkleTree,
|
|
1971
|
+
// coreCollection, mplCoreCpiSigner, logWrapper, compressionProgram, mplCoreProgram, systemProgram.
|
|
1972
|
+
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?)";
|
|
1973
|
+
const collection = a[11] && ts.isObjectLiteralExpression(a[11]) ? a[11] : undefined;
|
|
1974
|
+
if (a.length < 11 || a.length > (collection ? 13 : 12))
|
|
1975
|
+
this.fail(node, usage);
|
|
1976
|
+
const md = a[10];
|
|
1977
|
+
if (!ts.isObjectLiteralExpression(md))
|
|
1978
|
+
this.fail(md, "the metadata is { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable? }");
|
|
1979
|
+
const field = (o, k) => {
|
|
1980
|
+
const q = o.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
|
|
1981
|
+
return q ? (ts.isPropertyAssignment(q) ? q.initializer : q.name) : undefined;
|
|
1982
|
+
};
|
|
1983
|
+
for (const q of md.properties)
|
|
1984
|
+
if (!q.name || !ts.isIdentifier(q.name) || !["name", "symbol", "uri", "sellerFeeBasisPoints", "creators", "isMutable"].includes(q.name.text))
|
|
1985
|
+
this.fail(q, "the metadata takes name, symbol, uri, sellerFeeBasisPoints, creators and isMutable");
|
|
1986
|
+
const coll = collection
|
|
1987
|
+
? ["collection", "authority", "cpiSigner"].map((k) => field(collection, k) ?? this.fail(collection, `the collection needs ${k}`))
|
|
1988
|
+
: undefined;
|
|
1989
|
+
if (collection)
|
|
1990
|
+
for (const q of collection.properties)
|
|
1991
|
+
if (!q.name || !ts.isIdentifier(q.name) || !["collection", "authority", "cpiSigner"].includes(q.name.text))
|
|
1992
|
+
this.fail(q, "the collection is { collection, authority, cpiSigner }");
|
|
1993
|
+
const metas = [
|
|
1994
|
+
acct(0, "the tree config", true, false),
|
|
1995
|
+
acct(1, "the payer", true, true),
|
|
1996
|
+
acct(2, "the tree delegate", false, true),
|
|
1997
|
+
coll ? this.mplMeta(coll[1], bg, "the collection authority", false, true) : [p, false, false],
|
|
1998
|
+
acct(3, "the leaf owner", false, false),
|
|
1999
|
+
acct(4, "the leaf delegate", false, false),
|
|
2000
|
+
acct(5, "the merkle tree", true, false),
|
|
2001
|
+
coll ? this.mplMeta(coll[0], bg, "the core collection", true, false) : [p, false, false],
|
|
2002
|
+
coll ? this.mplMeta(coll[2], bg, "the mpl-core CPI signer", false, false) : [p, false, false],
|
|
2003
|
+
acct(6, "the log wrapper", false, false),
|
|
2004
|
+
acct(7, "the compression program", false, false),
|
|
2005
|
+
acct(8, "the mpl-core program", false, false),
|
|
2006
|
+
acct(9, "the system program", false, false),
|
|
2007
|
+
];
|
|
2008
|
+
const need = (k) => field(md, k) ?? this.fail(md, `the metadata needs ${k}`);
|
|
2009
|
+
const fields = [
|
|
2010
|
+
{ bytes: [120, 121, 23, 146, 173, 110, 199, 205] },
|
|
2011
|
+
{ value: need("name"), type: STRING },
|
|
2012
|
+
{ value: need("symbol"), type: STRING },
|
|
2013
|
+
{ value: need("uri"), type: STRING },
|
|
2014
|
+
{ value: need("sellerFeeBasisPoints"), type: { kind: "int", name: "u16" } },
|
|
2015
|
+
{ bytes: [0] }, // primary_sale_happened
|
|
2016
|
+
];
|
|
2017
|
+
const mutable = field(md, "isMutable");
|
|
2018
|
+
fields.push(mutable ? { value: mutable, type: BOOL } : { bytes: [1] });
|
|
2019
|
+
fields.push({ bytes: [1, 0] }); // token_standard: Some(NonFungible)
|
|
2020
|
+
const creators = field(md, "creators");
|
|
2021
|
+
if (creators && !ts.isArrayLiteralExpression(creators))
|
|
2022
|
+
this.fail(creators, "creators: [{ address, verified, share }, …] (an array literal)");
|
|
2023
|
+
const list = creators ? creators.elements : [];
|
|
2024
|
+
if (list.length > 5)
|
|
2025
|
+
this.fail(creators, "Bubblegum takes at most 5 creators");
|
|
2026
|
+
fields.push({ bytes: [list.length, 0, 0, 0] });
|
|
2027
|
+
let shares = 0n;
|
|
2028
|
+
let constant = true;
|
|
2029
|
+
for (const el of list) {
|
|
2030
|
+
if (!ts.isObjectLiteralExpression(el))
|
|
2031
|
+
this.fail(el, "a creator is { address, verified, share }");
|
|
2032
|
+
const f = (k) => field(el, k) ?? this.fail(el, `a creator needs ${k}`);
|
|
2033
|
+
const share = f("share");
|
|
2034
|
+
if (ts.isBigIntLiteral(share))
|
|
2035
|
+
shares += BigInt(share.text.slice(0, -1));
|
|
2036
|
+
else
|
|
2037
|
+
constant = false;
|
|
2038
|
+
fields.push({ value: f("address"), type: PUBKEY }, { value: f("verified"), type: BOOL }, { value: share, type: U8 });
|
|
2039
|
+
}
|
|
2040
|
+
if (list.length && constant && shares !== 100n)
|
|
2041
|
+
this.fail(creators, `creator shares add up to ${shares}; Bubblegum requires 100`);
|
|
2042
|
+
// collection: Some(the core collection's key) when minting into one.
|
|
2043
|
+
if (coll)
|
|
2044
|
+
fields.push({ bytes: [1] }, { value: this.keyOf(coll[0]), type: PUBKEY });
|
|
2045
|
+
else
|
|
2046
|
+
fields.push({ bytes: [0] });
|
|
2047
|
+
fields.push({ bytes: [0, 0] }); // asset_data, asset_data_schema: None
|
|
2048
|
+
const data = this.mplData(node, 1024, fields);
|
|
2049
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(collection ? 12 : 11));
|
|
2050
|
+
return undefined;
|
|
2051
|
+
}
|
|
2052
|
+
case "transferV2": {
|
|
2053
|
+
// TransferV2: treeConfig, payer, authority, leafOwner, leafDelegate, newLeafOwner, merkleTree, coreCollection,
|
|
2054
|
+
// logWrapper, compressionProgram, systemProgram; the proof follows as remaining accounts.
|
|
2055
|
+
if (a.length !== 13 && a.length !== 14)
|
|
2056
|
+
this.fail(node, "transferV2(treeConfig, payer, authority | null, leafOwner, leafDelegate | null, newLeafOwner, merkleTree, coreCollection | null, logWrapper, compressionProgram, systemProgram, leaf, proof, signerSeeds?)");
|
|
2057
|
+
const metas = [
|
|
2058
|
+
acct(0, "the tree config", true, false),
|
|
2059
|
+
acct(1, "the payer", true, true),
|
|
2060
|
+
acct(2, "the authority", false, true),
|
|
2061
|
+
acct(3, "the leaf owner", false, false),
|
|
2062
|
+
acct(4, "the leaf delegate", false, false),
|
|
2063
|
+
acct(5, "the new leaf owner", false, false),
|
|
2064
|
+
acct(6, "the merkle tree", true, false),
|
|
2065
|
+
acct(7, "the core collection", false, false),
|
|
2066
|
+
acct(8, "the log wrapper", false, false),
|
|
2067
|
+
acct(9, "the compression program", false, false),
|
|
2068
|
+
acct(10, "the system program", false, false),
|
|
2069
|
+
];
|
|
2070
|
+
const data = this.mplData(node, 160, [{ bytes: [119, 40, 6, 235, 234, 221, 248, 49] }, ...leafFields(a[11])]);
|
|
2071
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(13), proofRange(a[12]));
|
|
2072
|
+
return undefined;
|
|
2073
|
+
}
|
|
2074
|
+
case "burnV2": {
|
|
2075
|
+
// BurnV2: treeConfig, payer, authority, leafOwner, leafDelegate, merkleTree, coreCollection, mplCoreCpiSigner,
|
|
2076
|
+
// logWrapper, compressionProgram, mplCoreProgram, systemProgram; the proof follows as remaining accounts.
|
|
2077
|
+
if (a.length !== 14 && a.length !== 15)
|
|
2078
|
+
this.fail(node, "burnV2(treeConfig, payer, authority | null, leafOwner, leafDelegate | null, merkleTree, coreCollection | null, mplCoreCpiSigner | null, logWrapper, compressionProgram, mplCoreProgram, systemProgram, leaf, proof, signerSeeds?)");
|
|
2079
|
+
const metas = [
|
|
2080
|
+
acct(0, "the tree config", true, false),
|
|
2081
|
+
acct(1, "the payer", true, true),
|
|
2082
|
+
acct(2, "the authority", false, true),
|
|
2083
|
+
acct(3, "the leaf owner", false, false),
|
|
2084
|
+
acct(4, "the leaf delegate", false, false),
|
|
2085
|
+
acct(5, "the merkle tree", true, false),
|
|
2086
|
+
acct(6, "the core collection", true, false),
|
|
2087
|
+
acct(7, "the mpl-core CPI signer", false, false),
|
|
2088
|
+
acct(8, "the log wrapper", false, false),
|
|
2089
|
+
acct(9, "the compression program", false, false),
|
|
2090
|
+
acct(10, "the mpl-core program", false, false),
|
|
2091
|
+
acct(11, "the system program", false, false),
|
|
2092
|
+
];
|
|
2093
|
+
const data = this.mplData(node, 160, [{ bytes: [115, 210, 34, 240, 232, 143, 183, 16] }, ...leafFields(a[12])]);
|
|
2094
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(14), proofRange(a[13]));
|
|
2095
|
+
return undefined;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
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
|
+
];
|
|
2194
|
+
}
|
|
2195
|
+
/** `acct.key` of an account parameter, as an expression to serialize. */
|
|
2196
|
+
keyOf(n) {
|
|
2197
|
+
const f = ts.factory;
|
|
2198
|
+
const e = f.createPropertyAccessExpression(n, "key");
|
|
2199
|
+
ts.setTextRange(e, n);
|
|
2200
|
+
e.parent = n.parent;
|
|
2201
|
+
return e;
|
|
823
2202
|
}
|
|
824
2203
|
/**
|
|
825
2204
|
* `remainingAccounts()` (or a const bound to it): every remaining account; `rest.slice(a, b?)`: indexes a..b of them
|
|
@@ -946,6 +2325,20 @@ export class CpiEmitter extends CallEmitter {
|
|
|
946
2325
|
continue;
|
|
947
2326
|
}
|
|
948
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
|
+
}
|
|
949
2342
|
if (vec?.type.kind === "vecArg") {
|
|
950
2343
|
const n = this.tmp("vlen");
|
|
951
2344
|
this.line(`uint32_t ${n} = ${vec.c}.len;`);
|
|
@@ -1109,6 +2502,8 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1109
2502
|
const obj = node.arguments[0];
|
|
1110
2503
|
if (node.arguments.length !== (cpi ? 3 : 1) || !ts.isObjectLiteralExpression(obj))
|
|
1111
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);
|
|
1112
2507
|
const given = new Map();
|
|
1113
2508
|
for (const p of obj.properties) {
|
|
1114
2509
|
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name))
|
|
@@ -1183,15 +2578,39 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1183
2578
|
staged.set(name, (put) => put(t, String(n), n));
|
|
1184
2579
|
}
|
|
1185
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) {
|
|
1186
2606
|
const { buf, len, put, finish } = this.byteBuffer("ev", MAX_EVENT + (cpi ? 8 : 0), "TS_ERR_EVENT_TOO_LARGE", "event");
|
|
1187
2607
|
if (cpi)
|
|
1188
2608
|
put("TS_EVENT_IX_TAG", "8", 8);
|
|
1189
2609
|
const disc = this.tmp("disc");
|
|
1190
2610
|
this.line(`static const uint8_t ${disc}[8] = ${cBytes(s.eventDiscriminator)};`);
|
|
1191
2611
|
put(disc, "8", 8);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
finish(obj);
|
|
2612
|
+
body(put);
|
|
2613
|
+
finish(at);
|
|
1195
2614
|
if (cpi) {
|
|
1196
2615
|
// Invoke ourselves with the event authority PDA as the only (signing) account; the entrypoint accepts it.
|
|
1197
2616
|
const m = this.tmp("metas");
|
|
@@ -1203,12 +2622,11 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1203
2622
|
this.line(`SolInstruction ${ix} = {&a_${cpi.program}->key, ${m}, 1, ${buf}, ${len}};`);
|
|
1204
2623
|
this.line(`SolSignerSeeds ${signer} = {${sd}, 2};`);
|
|
1205
2624
|
this.line(`TS_TRY(ts_invoke(params, &${ix}, &${signer}, 1));`);
|
|
1206
|
-
return
|
|
2625
|
+
return;
|
|
1207
2626
|
}
|
|
1208
2627
|
const bytes = this.tmp("evbytes");
|
|
1209
2628
|
this.line(`SolBytes ${bytes} = {${buf}, ${len}};`);
|
|
1210
2629
|
this.line(`sol_log_data(&${bytes}, 1);`);
|
|
1211
|
-
return undefined;
|
|
1212
2630
|
}
|
|
1213
2631
|
/** Structs reachable through the fields of struct `name` (not including itself unless it recurses). */
|
|
1214
2632
|
nestedStructs(name) {
|