@typesol/cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +4 -4
- package/dist/clientgen.js +2 -0
- package/dist/compiler/analyze.d.ts +10 -1
- package/dist/compiler/analyze.js +165 -49
- package/dist/compiler/anchor-errors.d.ts +1 -0
- package/dist/compiler/anchor-errors.js +80 -0
- package/dist/compiler/emit/base.d.ts +29 -1
- package/dist/compiler/emit/base.js +79 -8
- package/dist/compiler/emit/borsh.js +71 -5
- package/dist/compiler/emit/calls.d.ts +2 -3
- package/dist/compiler/emit/calls.js +101 -15
- package/dist/compiler/emit/cpi.d.ts +31 -0
- package/dist/compiler/emit/cpi.js +1061 -39
- package/dist/compiler/emit/expressions.d.ts +24 -0
- package/dist/compiler/emit/expressions.js +137 -19
- package/dist/compiler/emit/function.js +57 -10
- package/dist/compiler/emit/shared.d.ts +8 -0
- package/dist/compiler/emit/shared.js +4 -1
- package/dist/compiler/emit/statements.d.ts +21 -0
- package/dist/compiler/emit/statements.js +386 -16
- package/dist/compiler/emit/storage.d.ts +2 -0
- package/dist/compiler/emit/storage.js +28 -4
- package/dist/compiler/idl.js +4 -1
- package/dist/compiler/model.d.ts +29 -2
- package/dist/portgen.js +32 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +31 -24
- package/docs/migrate-from-anchor.md +13 -3
- package/lib/typesol.d.ts +528 -22
- package/package.json +1 -1
- package/runtime/typesol_rt.h +279 -5
|
@@ -1,9 +1,153 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { discriminator, snakeCase } from "../analyze.js";
|
|
3
3
|
import { borshMinSize } from "./borsh.js";
|
|
4
|
-
import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
|
|
4
|
+
import { INT_INFO, isScalar, isWritable, optionKey, sameType, scalarSize, typeName, } from "../model.js";
|
|
5
5
|
import { MAX_ACCOUNTS, MAX_SEEDS, MAX_CPI_DATA, MAX_EVENT, U64, U8, BOOL, PUBKEY, STRING, cBytes, isStable, cString, scalarStorageC, optC, valueC, } from "./shared.js";
|
|
6
6
|
import { CallEmitter } from "./calls.js";
|
|
7
|
+
/**
|
|
8
|
+
* Bubblegum instructions that replace a leaf: discriminator, accounts in order ([label, writable, signer, optional,
|
|
9
|
+
* either-signs]) and the leaf's shape. Optional accounts take `null` (the program id). The Merkle proof follows the
|
|
10
|
+
* leaf (and metadata) as remaining accounts.
|
|
11
|
+
*/
|
|
12
|
+
const BUBBLEGUM_OPS = {
|
|
13
|
+
delegateV2: {
|
|
14
|
+
disc: [95, 87, 125, 140, 181, 131, 128, 227],
|
|
15
|
+
leaf: "delegate",
|
|
16
|
+
accounts: [
|
|
17
|
+
["treeConfig", true, false],
|
|
18
|
+
["payer", true, true],
|
|
19
|
+
["leafOwner", false, true, true],
|
|
20
|
+
["previousLeafDelegate", false, false, true],
|
|
21
|
+
["newLeafDelegate", false, false],
|
|
22
|
+
["merkleTree", true, false],
|
|
23
|
+
["logWrapper", false, false],
|
|
24
|
+
["compressionProgram", false, false],
|
|
25
|
+
["systemProgram", false, false],
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
...Object.fromEntries([
|
|
29
|
+
["freezeV2", [200, 151, 244, 102, 16, 195, 255, 3], false],
|
|
30
|
+
["thawV2", [96, 133, 101, 93, 82, 220, 146, 191], false],
|
|
31
|
+
["setNonTransferableV2", [181, 141, 206, 58, 242, 199, 152, 168], true],
|
|
32
|
+
].map(([name, disc, needsCollection]) => [
|
|
33
|
+
name,
|
|
34
|
+
{
|
|
35
|
+
disc: disc,
|
|
36
|
+
leaf: "v2",
|
|
37
|
+
accounts: [
|
|
38
|
+
["treeConfig", true, false],
|
|
39
|
+
["payer", true, true],
|
|
40
|
+
["authority", false, true, true],
|
|
41
|
+
["leafOwner", false, false],
|
|
42
|
+
["leafDelegate", false, false, name === "setNonTransferableV2"],
|
|
43
|
+
["merkleTree", true, false],
|
|
44
|
+
["coreCollection", false, false, !needsCollection],
|
|
45
|
+
["logWrapper", false, false],
|
|
46
|
+
["compressionProgram", false, false],
|
|
47
|
+
["systemProgram", false, false],
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
])),
|
|
51
|
+
updateMetadataV2: {
|
|
52
|
+
disc: [43, 103, 89, 42, 121, 242, 62, 72],
|
|
53
|
+
leaf: "meta",
|
|
54
|
+
accounts: [
|
|
55
|
+
["treeConfig", true, false],
|
|
56
|
+
["payer", true, true],
|
|
57
|
+
["authority", false, true, true],
|
|
58
|
+
["leafOwner", false, false],
|
|
59
|
+
["leafDelegate", false, false, true],
|
|
60
|
+
["merkleTree", true, false],
|
|
61
|
+
["coreCollection", false, false, true],
|
|
62
|
+
["logWrapper", false, false],
|
|
63
|
+
["compressionProgram", false, false],
|
|
64
|
+
["systemProgram", false, false],
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
...Object.fromEntries([
|
|
68
|
+
["verifyCreatorV2", [85, 138, 140, 42, 22, 241, 118, 102]],
|
|
69
|
+
["unverifyCreatorV2", [174, 112, 29, 142, 230, 100, 239, 7]],
|
|
70
|
+
].map(([name, disc]) => [
|
|
71
|
+
name,
|
|
72
|
+
{
|
|
73
|
+
disc: disc,
|
|
74
|
+
leaf: "meta",
|
|
75
|
+
accounts: [
|
|
76
|
+
["treeConfig", true, false],
|
|
77
|
+
["payer", true, true],
|
|
78
|
+
["creator", false, true, true],
|
|
79
|
+
["leafOwner", false, false],
|
|
80
|
+
["leafDelegate", false, false, true],
|
|
81
|
+
["merkleTree", true, false],
|
|
82
|
+
["logWrapper", false, false],
|
|
83
|
+
["compressionProgram", false, false],
|
|
84
|
+
["systemProgram", false, false],
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
])),
|
|
88
|
+
setCollectionV2: {
|
|
89
|
+
disc: [229, 35, 61, 91, 15, 14, 99, 160],
|
|
90
|
+
leaf: "meta",
|
|
91
|
+
accounts: [
|
|
92
|
+
["treeConfig", true, false],
|
|
93
|
+
["payer", true, true],
|
|
94
|
+
["authority", false, true, true],
|
|
95
|
+
["newCollectionAuthority", false, true, true],
|
|
96
|
+
["leafOwner", false, false],
|
|
97
|
+
["leafDelegate", false, false, true],
|
|
98
|
+
["merkleTree", true, false],
|
|
99
|
+
["coreCollection", true, false, true],
|
|
100
|
+
["newCoreCollection", true, false, true],
|
|
101
|
+
["mplCoreCpiSigner", false, false, true],
|
|
102
|
+
["logWrapper", false, false],
|
|
103
|
+
["compressionProgram", false, false],
|
|
104
|
+
["mplCoreProgram", false, false],
|
|
105
|
+
["systemProgram", false, false],
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
// V1 trees (SPL Account Compression, SPL Noop).
|
|
109
|
+
transfer: {
|
|
110
|
+
disc: [163, 52, 200, 231, 140, 3, 69, 186],
|
|
111
|
+
leaf: "v1",
|
|
112
|
+
accounts: [
|
|
113
|
+
["treeConfig", false, false],
|
|
114
|
+
["leafOwner", false, false, false, "owner"],
|
|
115
|
+
["leafDelegate", false, false, false, "delegate"],
|
|
116
|
+
["newLeafOwner", false, false],
|
|
117
|
+
["merkleTree", true, false],
|
|
118
|
+
["logWrapper", false, false],
|
|
119
|
+
["compressionProgram", false, false],
|
|
120
|
+
["systemProgram", false, false],
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
burn: {
|
|
124
|
+
disc: [116, 110, 29, 56, 107, 219, 42, 93],
|
|
125
|
+
leaf: "v1",
|
|
126
|
+
accounts: [
|
|
127
|
+
["treeConfig", false, false],
|
|
128
|
+
["leafOwner", false, false, false, "owner"],
|
|
129
|
+
["leafDelegate", false, false, false, "delegate"],
|
|
130
|
+
["merkleTree", true, false],
|
|
131
|
+
["logWrapper", false, false],
|
|
132
|
+
["compressionProgram", false, false],
|
|
133
|
+
["systemProgram", false, false],
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
delegate: {
|
|
137
|
+
disc: [90, 147, 75, 178, 85, 88, 4, 137],
|
|
138
|
+
leaf: "v1",
|
|
139
|
+
accounts: [
|
|
140
|
+
["treeConfig", false, false],
|
|
141
|
+
["leafOwner", false, false, false, "owner"],
|
|
142
|
+
["previousLeafDelegate", false, false],
|
|
143
|
+
["newLeafDelegate", false, false],
|
|
144
|
+
["merkleTree", true, false],
|
|
145
|
+
["logWrapper", false, false],
|
|
146
|
+
["compressionProgram", false, false],
|
|
147
|
+
["systemProgram", false, false],
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
};
|
|
7
151
|
/** Metaplex Core's Plugin / PluginType enums, in order (MasterEdition, VerifiedCreators and Autograph values are refused). */
|
|
8
152
|
const CORE_PLUGINS = [
|
|
9
153
|
"Royalties",
|
|
@@ -22,9 +166,17 @@ const CORE_PLUGINS = [
|
|
|
22
166
|
"VerifiedCreators",
|
|
23
167
|
"Autograph",
|
|
24
168
|
"BubblegumV2",
|
|
169
|
+
"FreezeExecute",
|
|
170
|
+
"PermanentFreezeExecute",
|
|
171
|
+
"Groups",
|
|
25
172
|
];
|
|
26
173
|
const CORE_PLUGIN_FIELDS = {
|
|
27
|
-
Royalties: ["basisPoints", "creators"],
|
|
174
|
+
Royalties: ["basisPoints", "creators", "ruleSet"],
|
|
175
|
+
MasterEdition: ["maxSupply", "name", "uri"],
|
|
176
|
+
VerifiedCreators: ["signatures"],
|
|
177
|
+
Autograph: ["signatures"],
|
|
178
|
+
FreezeExecute: ["frozen"],
|
|
179
|
+
PermanentFreezeExecute: ["frozen"],
|
|
28
180
|
FreezeDelegate: ["frozen"],
|
|
29
181
|
PermanentFreezeDelegate: ["frozen"],
|
|
30
182
|
UpdateDelegate: ["additionalDelegates"],
|
|
@@ -149,38 +301,437 @@ export class CpiEmitter extends CallEmitter {
|
|
|
149
301
|
this.ix.pdas.set(account.name, idl);
|
|
150
302
|
}
|
|
151
303
|
create(node, target, topLevel, ifNeeded = false) {
|
|
304
|
+
const call = `${target.name}.${ifNeeded ? "createIfNeeded" : "create"}`;
|
|
152
305
|
if (!topLevel)
|
|
153
|
-
this.fail(node, `${
|
|
306
|
+
this.fail(node, `${call}(...) must be a top-level statement of the instruction`);
|
|
154
307
|
if (this.created.has(target.name))
|
|
155
308
|
this.fail(node, `${target.name} is already created`);
|
|
156
|
-
if (node.arguments.length !== 2)
|
|
157
|
-
this.fail(node,
|
|
309
|
+
if (node.arguments.length !== 2 && node.arguments.length !== 3)
|
|
310
|
+
this.fail(node, `${call}(payer, seeds | null, space?) takes two or three arguments`);
|
|
158
311
|
const payer = this.accountParam(node.arguments[0], "the payer");
|
|
159
312
|
if (payer.kind.kind !== "signer" || !payer.kind.mut)
|
|
160
313
|
this.fail(node.arguments[0], "the payer must be a Mut<Signer>");
|
|
161
|
-
const seeds = this.seeds(node.arguments[1], 1);
|
|
162
|
-
this.recordPda(target, seeds.idl);
|
|
163
314
|
const s = target.kind.struct;
|
|
315
|
+
const keypair = node.arguments[1].kind === ts.SyntaxKind.NullKeyword;
|
|
316
|
+
if (keypair && ifNeeded)
|
|
317
|
+
this.fail(node.arguments[1], "createIfNeeded takes seeds: a keypair account cannot be checked to be the one meant");
|
|
318
|
+
const seeds = keypair ? undefined : this.seeds(node.arguments[1], 1);
|
|
319
|
+
if (seeds)
|
|
320
|
+
this.recordPda(target, seeds.idl);
|
|
164
321
|
const b = this.tmp("bump");
|
|
322
|
+
// The account's size: the struct's (8 + its fixed size, Anchor's InitSpace for @layout borsh), or Anchor's
|
|
323
|
+
// `space = …` when given, which must hold the struct.
|
|
324
|
+
const need = `8 + sizeof(TSS_${s.name})`;
|
|
325
|
+
let space = need;
|
|
326
|
+
const spaceArg = node.arguments[2];
|
|
327
|
+
if (spaceArg) {
|
|
328
|
+
if (s.vec)
|
|
329
|
+
this.fail(spaceArg, `${s.name} has a growable Vec: its size follows the Vec's length, so it takes no space argument`);
|
|
330
|
+
const raw = this.expr(spaceArg);
|
|
331
|
+
const v = this.expect(raw, U64, spaceArg);
|
|
332
|
+
const min = 8 + this.fieldBytes({ kind: "struct", name: s.name });
|
|
333
|
+
if (raw.literal !== undefined && raw.literal < BigInt(min))
|
|
334
|
+
this.fail(spaceArg, `${s.name} needs at least ${min} bytes (8 + its size)`);
|
|
335
|
+
space = this.tmp("space");
|
|
336
|
+
this.line(`uint64_t ${space} = ${v.c};`);
|
|
337
|
+
this.line(`if (${space} < ${need} || ${space} > 10240) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: space must hold the account and be at most 10240 bytes");`);
|
|
338
|
+
}
|
|
165
339
|
this.line(`uint8_t ${b};`);
|
|
340
|
+
const done = () => {
|
|
341
|
+
this.created.add(target.name);
|
|
342
|
+
this.pendingInit.delete(target.name);
|
|
343
|
+
this.bumps.set(target.name, b);
|
|
344
|
+
};
|
|
345
|
+
if (!seeds) {
|
|
346
|
+
// A new keypair account that signs the transaction (Anchor's `init` without seeds).
|
|
347
|
+
target.signsInit = true;
|
|
348
|
+
this.line(`TS_TRY(ts_create_owned(params, a_${payer.name}, a_${target.name}, NULL, 0, params->program_id, ${space}, &${b}));`);
|
|
349
|
+
this.line(`a_${target.name}->data_len = ${space};`);
|
|
350
|
+
this.line(`ts_memcpy(a_${target.name}->data, TS_DISC_${s.name}, 8);`);
|
|
351
|
+
done();
|
|
352
|
+
return { c: b, type: U8 };
|
|
353
|
+
}
|
|
166
354
|
if (ifNeeded) {
|
|
167
355
|
// Returns whether it was created now; an existing account must be this PDA, ours, and a ${s.name}.
|
|
168
356
|
const created = this.tmp("created");
|
|
169
357
|
this.line(`bool ${created};`);
|
|
170
|
-
this.line(`TS_TRY(ts_create_pda_if_needed(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name},
|
|
358
|
+
this.line(`TS_TRY(ts_create_pda_if_needed(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name}, ${space}, 8 + ${s.layout === "borsh" ? borshMinSize(s, this.model) : `sizeof(TSS_${s.name})`}, &${b}, &${created}));`);
|
|
171
359
|
// An existing @layout borsh account is unpacked now (a new one starts from the zeroed struct).
|
|
172
360
|
if (s.layout === "borsh")
|
|
173
|
-
this.line(`if (!${created}) TS_TRY(ts_unpack_${s.name}(a_${target.name}->data + 8, a_${target.name}->data_len - 8, d_${target.name}));`);
|
|
174
|
-
|
|
175
|
-
this.pendingInit.delete(target.name);
|
|
176
|
-
this.bumps.set(target.name, b);
|
|
361
|
+
this.line(`if (!${created}) TS_TRY(ts_unpack_${s.name}(a_${target.name}->data + 8, a_${target.name}->data_len - 8, d_${target.name}, NULL));`);
|
|
362
|
+
done();
|
|
177
363
|
return { c: created, type: BOOL };
|
|
178
364
|
}
|
|
179
|
-
this.line(`TS_TRY(ts_create_pda(params, a_${payer.name}, a_${target.name}, ${seeds.name}, ${seeds.count}, TS_DISC_${s.name},
|
|
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
|
+
const payer = this.accountParam(a[0], "the payer");
|
|
400
|
+
if (payer.kind.kind !== "signer" || !payer.kind.mut)
|
|
401
|
+
this.fail(a[0], "the payer must be a Mut<Signer>");
|
|
402
|
+
// The token program: named as the last argument, or the instruction's only token program parameter.
|
|
403
|
+
let programs = this.ix.accounts.filter((p) => p.kind.kind === "tokenProgram");
|
|
404
|
+
if (a[fixedArgs]) {
|
|
405
|
+
const named = this.accountParam(a[fixedArgs], "the token program");
|
|
406
|
+
if (named.kind.kind !== "tokenProgram")
|
|
407
|
+
this.fail(a[fixedArgs], "pass a TokenProgram, Token2022Program or TokenInterface parameter");
|
|
408
|
+
programs = [named];
|
|
409
|
+
}
|
|
410
|
+
if (programs.length !== 1)
|
|
411
|
+
this.fail(node, `${call}(...) needs one token program: pass it as the last argument, or have exactly one token program parameter`);
|
|
412
|
+
if (programs[0].optional)
|
|
413
|
+
this.fail(programs[0].node, `${call}(...) needs "${programs[0].name}": it cannot be optional`);
|
|
414
|
+
if (!this.ix.accounts.some((p) => p.kind.kind === "system" && !p.optional))
|
|
415
|
+
this.fail(node, `${call}(...) needs a SystemProgram parameter`);
|
|
416
|
+
const tp = `a_${programs[0].name}`;
|
|
417
|
+
const acct = `a_${target.name}`;
|
|
418
|
+
// Seeds, then the values, in source order.
|
|
419
|
+
let seeds = { name: "NULL", count: 0 };
|
|
420
|
+
if (a[1].kind !== ts.SyntaxKind.NullKeyword) {
|
|
421
|
+
const s = this.seeds(a[1], 1);
|
|
422
|
+
this.recordPda(target, s.idl);
|
|
423
|
+
seeds = s;
|
|
424
|
+
}
|
|
425
|
+
else
|
|
426
|
+
target.signsInit = true;
|
|
427
|
+
const vals = [];
|
|
428
|
+
const take = (n, t) => {
|
|
429
|
+
const mark = this.lines.length;
|
|
430
|
+
// The account's own address is known before it exists (`vault.create(payer, seeds, mint, vault.key)`).
|
|
431
|
+
const own = ts.isPropertyAccessExpression(n) && n.name.text === "key" && ts.isIdentifier(n.expression) && n.expression.text === target.name;
|
|
432
|
+
const v = own ? { c: `(&${acct}->key)`, type: PUBKEY } : this.expect(this.expr(n, t), t, n);
|
|
433
|
+
for (let j = 0; j < vals.length; j++)
|
|
434
|
+
vals[j] = this.settle(vals[j], mark);
|
|
435
|
+
vals.push(v);
|
|
436
|
+
};
|
|
437
|
+
let mint;
|
|
438
|
+
const extLines = [];
|
|
439
|
+
const extChecks = [];
|
|
440
|
+
let mintSpace = "TS_MINT_LEN";
|
|
441
|
+
if (isMint) {
|
|
442
|
+
take(a[2], U8);
|
|
443
|
+
take(a[3], PUBKEY);
|
|
444
|
+
if (a[4].kind !== ts.SyntaxKind.NullKeyword)
|
|
445
|
+
take(a[4], PUBKEY);
|
|
446
|
+
if (extArg) {
|
|
447
|
+
if (programs[0].kind.kind === "tokenProgram" && programs[0].kind.flavor === "token")
|
|
448
|
+
this.fail(extArg, "mint extensions are Token-2022's; use Token2022Program or TokenInterface");
|
|
449
|
+
const ext = this.mintExtensions(extArg, tp, acct, ifNeeded);
|
|
450
|
+
mintSpace = String(ext.space);
|
|
451
|
+
extLines.push(...ext.lines);
|
|
452
|
+
extChecks.push(...ext.checks);
|
|
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
|
+
for (const l of extChecks)
|
|
478
|
+
this.line(` ${l}`);
|
|
479
|
+
this.line(`}`);
|
|
480
|
+
}
|
|
481
|
+
else
|
|
482
|
+
for (const l of createLines)
|
|
483
|
+
this.line(l);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const [authority] = vals;
|
|
487
|
+
const space = this.tmp("space");
|
|
488
|
+
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}));`);
|
|
489
|
+
if (ifNeeded) {
|
|
490
|
+
this.line(`bool ${created} = ts_key_eq(&${acct}->owner, &TS_SYSTEM_PROGRAM_ID);`);
|
|
491
|
+
this.line(`if (${created}) {`);
|
|
492
|
+
for (const l of createLines)
|
|
493
|
+
this.line(` ${l}`);
|
|
494
|
+
this.line(`} else {`);
|
|
495
|
+
this.line(` TS_TRY(ts_token_existing(params, ${acct}, ${tp}, ${seeds.name}, ${seeds.count}, &${b}));`);
|
|
496
|
+
this.line(` TS_TRY(ts_check_token_account(${acct}));`);
|
|
497
|
+
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}");`);
|
|
498
|
+
this.line(` if (!ts_key_eq(ts_token_owner(${acct}), ${authority.c})) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${target.name} has another owner");`);
|
|
499
|
+
this.line(`}`);
|
|
500
|
+
}
|
|
501
|
+
else
|
|
502
|
+
for (const l of createLines)
|
|
503
|
+
this.line(l);
|
|
504
|
+
}
|
|
180
505
|
this.created.add(target.name);
|
|
181
506
|
this.pendingInit.delete(target.name);
|
|
182
507
|
this.bumps.set(target.name, b);
|
|
183
|
-
return { c: b, type: U8 };
|
|
508
|
+
return ifNeeded ? { c: created, type: BOOL } : { c: b, type: U8 };
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The extensions in `mint.create(…, { … })` (Anchor's `extensions::…` constraints): their values are evaluated now,
|
|
512
|
+
* in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
|
|
513
|
+
* InitializeMint2) run after the account is created, sized for exactly these extensions.
|
|
514
|
+
*/
|
|
515
|
+
mintExtensions(lit, tp, acct, ifNeeded = false) {
|
|
516
|
+
const optKey = { kind: "option", inner: PUBKEY };
|
|
517
|
+
this.model.options.set("pubkey", PUBKEY);
|
|
518
|
+
// name: [extension type length, initializer tag, the fields an object value takes ([] for a plain value)]
|
|
519
|
+
const KINDS = {
|
|
520
|
+
transferFee: { len: 108, fields: ["configAuthority", "withdrawAuthority", "basisPoints", "maximumFee"] },
|
|
521
|
+
closeAuthority: { len: 32, fields: [] },
|
|
522
|
+
defaultAccountState: { len: 1, fields: [] },
|
|
523
|
+
nonTransferable: { len: 0, fields: [] },
|
|
524
|
+
interestBearing: { len: 52, fields: ["rateAuthority", "rate"] },
|
|
525
|
+
permanentDelegate: { len: 32, fields: [] },
|
|
526
|
+
transferHook: { len: 64, fields: ["authority", "programId"] },
|
|
527
|
+
metadataPointer: { len: 64, fields: ["authority", "metadataAddress"] },
|
|
528
|
+
groupPointer: { len: 64, fields: ["authority", "groupAddress"] },
|
|
529
|
+
groupMemberPointer: { len: 64, fields: ["authority", "memberAddress"] },
|
|
530
|
+
};
|
|
531
|
+
const given = new Map();
|
|
532
|
+
for (const p of lit.properties) {
|
|
533
|
+
const value = ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) ? p.initializer : ts.isShorthandPropertyAssignment(p) ? p.name : undefined;
|
|
534
|
+
if (!value || !p.name || !ts.isIdentifier(p.name))
|
|
535
|
+
this.fail(p, "write the extensions as `name: value` entries");
|
|
536
|
+
if (!KINDS[p.name.text])
|
|
537
|
+
this.fail(p.name, `mint extensions: ${Object.keys(KINDS).join(", ")}`);
|
|
538
|
+
given.set(p.name.text, value);
|
|
539
|
+
}
|
|
540
|
+
const U16 = { kind: "int", name: "u16" };
|
|
541
|
+
const I16 = { kind: "int", name: "i16" };
|
|
542
|
+
// Values in source order, held in temporaries; the CPIs come later.
|
|
543
|
+
const held = new Map();
|
|
544
|
+
const keyTmp = (e, nonZero) => {
|
|
545
|
+
// OptionalNonZeroPubkey (32 bytes, zero for none) or COption (a tag byte, then 32).
|
|
546
|
+
const v = this.expect(this.expr(e, optKey), optKey, e);
|
|
547
|
+
const t = this.tmp("xk");
|
|
548
|
+
this.line(`${optC(PUBKEY)} ${t} = ${v.c};`);
|
|
549
|
+
if (!nonZero)
|
|
550
|
+
return t;
|
|
551
|
+
const z = this.tmp("xz");
|
|
552
|
+
this.line(`SolPubkey ${z};`);
|
|
553
|
+
this.line(`if (${t}.some) ${z} = ${t}.v; else ts_memzero(&${z}, 32);`);
|
|
554
|
+
return z;
|
|
555
|
+
};
|
|
556
|
+
const scalarTmp = (e, t) => {
|
|
557
|
+
const v = this.expect(this.expr(e, t), t, e);
|
|
558
|
+
const n = this.tmp("xv");
|
|
559
|
+
this.line(`${scalarStorageC(t)} ${n} = ${v.c};`);
|
|
560
|
+
return n;
|
|
561
|
+
};
|
|
562
|
+
for (const [name, e] of given) {
|
|
563
|
+
const k = KINDS[name];
|
|
564
|
+
const vals = new Map();
|
|
565
|
+
if (k.fields.length) {
|
|
566
|
+
if (!ts.isObjectLiteralExpression(e))
|
|
567
|
+
this.fail(e, `${name}: { ${k.fields.join(", ")} }`);
|
|
568
|
+
const props = new Map();
|
|
569
|
+
for (const p of e.properties) {
|
|
570
|
+
const value = ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) ? p.initializer : ts.isShorthandPropertyAssignment(p) ? p.name : undefined;
|
|
571
|
+
if (!value || !p.name || !ts.isIdentifier(p.name) || !k.fields.includes(p.name.text))
|
|
572
|
+
this.fail(p, `${name}: { ${k.fields.join(", ")} }`);
|
|
573
|
+
props.set(p.name.text, value);
|
|
574
|
+
}
|
|
575
|
+
for (const f of k.fields)
|
|
576
|
+
if (!props.has(f))
|
|
577
|
+
this.fail(e, `${name} needs ${f}`);
|
|
578
|
+
for (const [f, v] of props) {
|
|
579
|
+
if (f === "basisPoints") {
|
|
580
|
+
const t = scalarTmp(v, U16);
|
|
581
|
+
this.line(`if (${t} > 10000) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: transfer fee above 10000 basis points");`);
|
|
582
|
+
vals.set(f, t);
|
|
583
|
+
}
|
|
584
|
+
else if (f === "maximumFee")
|
|
585
|
+
vals.set(f, scalarTmp(v, U64));
|
|
586
|
+
else if (f === "rate")
|
|
587
|
+
vals.set(f, scalarTmp(v, I16));
|
|
588
|
+
else
|
|
589
|
+
vals.set(f, keyTmp(v, name !== "transferFee"));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else if (name === "closeAuthority")
|
|
593
|
+
vals.set("v", keyTmp(e, false));
|
|
594
|
+
else if (name === "permanentDelegate") {
|
|
595
|
+
const v = this.expect(this.expr(e, PUBKEY), PUBKEY, e);
|
|
596
|
+
const t = this.tmp("xk");
|
|
597
|
+
this.line(`SolPubkey ${t} = *(${v.c});`);
|
|
598
|
+
vals.set("v", t);
|
|
599
|
+
}
|
|
600
|
+
else if (name === "defaultAccountState") {
|
|
601
|
+
const v = this.expect(this.expr(e, BOOL), BOOL, e);
|
|
602
|
+
const t = this.tmp("xs");
|
|
603
|
+
this.line(`uint8_t ${t} = (${v.c}) ? 2 : 1;`); // frozen: AccountState::Frozen, else Initialized
|
|
604
|
+
vals.set("v", t);
|
|
605
|
+
}
|
|
606
|
+
else if (!(e.kind === ts.SyntaxKind.TrueKeyword))
|
|
607
|
+
this.fail(e, "nonTransferable: true");
|
|
608
|
+
held.set(name, vals);
|
|
609
|
+
}
|
|
610
|
+
// Each initializer: its bytes, then a CPI with the mint as the only (writable) account.
|
|
611
|
+
const lines = [];
|
|
612
|
+
const cpi = (bytes) => {
|
|
613
|
+
const total = bytes.reduce((n, [, k]) => n + k, 0);
|
|
614
|
+
const buf = this.tmp("xd");
|
|
615
|
+
lines.push(`{ uint8_t ${buf}[${total}]; uint64_t at = 0;`);
|
|
616
|
+
for (const [src, n] of bytes)
|
|
617
|
+
lines.push(` ts_memcpy(${buf} + at, ${src}, ${n}); at += ${n};`);
|
|
618
|
+
lines.push(` SolAccountMeta m[1] = {{&${acct}->key, true, false}};`, ` TS_TRY(ts_cpi(params, ${tp}, ${buf}, at, m, 1, NULL, 0)); }`);
|
|
619
|
+
};
|
|
620
|
+
const tag = (n) => `(const uint8_t[]){${n}}`;
|
|
621
|
+
const coption = (t) => [
|
|
622
|
+
[`(const uint8_t[]){${t}.some ? 1 : 0}`, 1],
|
|
623
|
+
[`&${t}.v`, 32],
|
|
624
|
+
];
|
|
625
|
+
let space = 0;
|
|
626
|
+
for (const name of Object.keys(KINDS)) {
|
|
627
|
+
const v = held.get(name);
|
|
628
|
+
if (!v)
|
|
629
|
+
continue;
|
|
630
|
+
space += 4 + KINDS[name].len;
|
|
631
|
+
switch (name) {
|
|
632
|
+
case "transferFee":
|
|
633
|
+
cpi([
|
|
634
|
+
[tag(26), 1],
|
|
635
|
+
[tag(0), 1],
|
|
636
|
+
...coption(v.get("configAuthority")),
|
|
637
|
+
...coption(v.get("withdrawAuthority")),
|
|
638
|
+
[`&${v.get("basisPoints")}`, 2],
|
|
639
|
+
[`&${v.get("maximumFee")}`, 8],
|
|
640
|
+
]);
|
|
641
|
+
break;
|
|
642
|
+
case "closeAuthority":
|
|
643
|
+
cpi([[tag(25), 1], ...coption(v.get("v"))]);
|
|
644
|
+
break;
|
|
645
|
+
case "defaultAccountState":
|
|
646
|
+
cpi([
|
|
647
|
+
[tag(28), 1],
|
|
648
|
+
[tag(0), 1],
|
|
649
|
+
[`&${v.get("v")}`, 1],
|
|
650
|
+
]);
|
|
651
|
+
break;
|
|
652
|
+
case "nonTransferable":
|
|
653
|
+
cpi([[tag(32), 1]]);
|
|
654
|
+
break;
|
|
655
|
+
case "interestBearing":
|
|
656
|
+
cpi([
|
|
657
|
+
[tag(33), 1],
|
|
658
|
+
[tag(0), 1],
|
|
659
|
+
[`&${v.get("rateAuthority")}`, 32],
|
|
660
|
+
[`&${v.get("rate")}`, 2],
|
|
661
|
+
]);
|
|
662
|
+
break;
|
|
663
|
+
case "permanentDelegate":
|
|
664
|
+
cpi([
|
|
665
|
+
[tag(35), 1],
|
|
666
|
+
[`&${v.get("v")}`, 32],
|
|
667
|
+
]);
|
|
668
|
+
break;
|
|
669
|
+
default: {
|
|
670
|
+
const t = { transferHook: 36, metadataPointer: 39, groupPointer: 40, groupMemberPointer: 41 }[name];
|
|
671
|
+
const [f1, f2] = KINDS[name].fields;
|
|
672
|
+
cpi([
|
|
673
|
+
[tag(t), 1],
|
|
674
|
+
[tag(0), 1],
|
|
675
|
+
[`&${v.get(f1)}`, 32],
|
|
676
|
+
[`&${v.get(f2)}`, 32],
|
|
677
|
+
]);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// An existing mint (createIfNeeded) is checked as Anchor's `extensions::…` constraints check it: the extension must
|
|
682
|
+
// be there (its Anchor code) and each key must match (the next codes). Anchor has constraints for these six only.
|
|
683
|
+
const CHECKS = {
|
|
684
|
+
groupPointer: {
|
|
685
|
+
type: 20,
|
|
686
|
+
code: 2024,
|
|
687
|
+
fields: [
|
|
688
|
+
["authority", 0],
|
|
689
|
+
["groupAddress", 32],
|
|
690
|
+
],
|
|
691
|
+
},
|
|
692
|
+
groupMemberPointer: {
|
|
693
|
+
type: 21,
|
|
694
|
+
code: 2027,
|
|
695
|
+
fields: [
|
|
696
|
+
["authority", 0],
|
|
697
|
+
["memberAddress", 32],
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
metadataPointer: {
|
|
701
|
+
type: 18,
|
|
702
|
+
code: 2030,
|
|
703
|
+
fields: [
|
|
704
|
+
["authority", 0],
|
|
705
|
+
["metadataAddress", 32],
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
closeAuthority: { type: 3, code: 2033, fields: [["v", 0]] },
|
|
709
|
+
permanentDelegate: { type: 12, code: 2035, fields: [["v", 0]] },
|
|
710
|
+
transferHook: {
|
|
711
|
+
type: 14,
|
|
712
|
+
code: 2037,
|
|
713
|
+
fields: [
|
|
714
|
+
["authority", 0],
|
|
715
|
+
["programId", 32],
|
|
716
|
+
],
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
const checks = [];
|
|
720
|
+
if (ifNeeded)
|
|
721
|
+
for (const [name, v] of held) {
|
|
722
|
+
const c = CHECKS[name];
|
|
723
|
+
if (!c)
|
|
724
|
+
this.fail(lit, `${name} cannot be checked on an existing mint (Anchor has no constraint for it): create the mint with create(…)`);
|
|
725
|
+
checks.push(`{ uint64_t eo, el; if (!ts_t22_ext(${acct}, ${c.type}, &eo, &el)) TS_FAIL(${c.code}, "TypeSOL: the mint has no ${name} extension"); }`);
|
|
726
|
+
c.fields.forEach(([f, at], i) => {
|
|
727
|
+
const held = v.get(f);
|
|
728
|
+
// The value given: a nonzero-or-zero key, a COption (close authority) or a plain key (permanent delegate).
|
|
729
|
+
const want = name === "closeAuthority" ? `(${held}.some ? ${held}.v : (SolPubkey){0})` : held;
|
|
730
|
+
checks.push(`{ SolPubkey k, w = ${want}; if (!ts_t22_key(${acct}, ${c.type}, ${at}, &k)) ts_memzero(&k, 32); if (!ts_key_eq(&k, &w)) TS_FAIL(${c.code + 1 + i}, "TypeSOL: the mint's ${name} ${f === "v" ? "key" : f} differs"); }`);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
// Base account (165, the mint padded to an account's size), the account type byte, then each TLV entry.
|
|
734
|
+
return { space: 165 + 1 + space, lines, checks };
|
|
184
735
|
}
|
|
185
736
|
/**
|
|
186
737
|
* `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
|
|
@@ -297,7 +848,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
297
848
|
this.line(`if (${metas.length} + (${tail.to} - ${tail.from}) > TS_MAX_ACCOUNTS) TS_FAIL(TS_ERR_TOO_MANY_ACCOUNTS, "TypeSOL: the CPI lists more than ${MAX_ACCOUNTS} accounts");`);
|
|
298
849
|
this.line(`SolAccountMeta ${m}[TS_MAX_ACCOUNTS] = {${fixed}};`);
|
|
299
850
|
this.line(`uint64_t ${cnt} = ${metas.length};`);
|
|
300
|
-
this.line(`for (uint64_t ${i} = ${n} + ${tail.from}; ${i} < ${n} + ${tail.to}; ${i}++) ${m}[${cnt}++] = (SolAccountMeta){¶ms->ka[${i}]->key, 0, 0};`);
|
|
851
|
+
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};`);
|
|
301
852
|
this.line(`TS_TRY(ts_cpi(params, ${program}, ${buf}, ${len}, ${m}, ${cnt}, ${seeds.name}, ${seeds.count}));`);
|
|
302
853
|
}
|
|
303
854
|
signerSeeds(node, authority, at) {
|
|
@@ -355,7 +906,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
355
906
|
}
|
|
356
907
|
case "mintTo": {
|
|
357
908
|
arity(4);
|
|
358
|
-
|
|
909
|
+
// A Mut<Mint>, or a mint created and initialized earlier in this instruction (Mut<Signer> / Mut<AccountInfo>).
|
|
910
|
+
const mint = this.accountParam(a[0], "the mint");
|
|
911
|
+
if (!isWritable(mint.kind) || (mint.kind.kind !== "mint" && mint.kind.kind !== "signer" && mint.kind.kind !== "info"))
|
|
912
|
+
this.fail(a[0], "the mint must be a Mut<Mint>, or a Mut<Signer> / Mut<AccountInfo> mint this instruction created");
|
|
359
913
|
const to = want(a[1], "token", true, "the destination");
|
|
360
914
|
const auth = this.accountParam(a[2], "the mint authority");
|
|
361
915
|
const amt = amount(a[3]);
|
|
@@ -378,7 +932,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
378
932
|
this.fail(node, "metadata lives in Token-2022 mints; use Token2022Program or TokenInterface");
|
|
379
933
|
if (a.length !== 6 && a.length !== 7)
|
|
380
934
|
this.fail(node, "initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri, signerSeeds?)");
|
|
381
|
-
|
|
935
|
+
// The mint may have been created and initialized earlier in this instruction (a Mut<Signer> or Mut<AccountInfo>).
|
|
936
|
+
const mint = this.accountParam(a[0], "the mint");
|
|
937
|
+
if (!isWritable(mint.kind))
|
|
938
|
+
this.fail(a[0], "the mint must be writable (Mut<...>)");
|
|
382
939
|
const ua = this.accountParam(a[1], "the update authority");
|
|
383
940
|
const ma = this.accountParam(a[2], "the mint authority");
|
|
384
941
|
const [nm, sym, uri] = [a[3], a[4], a[5]].map((n) => this.expect(this.expr(n, STRING), STRING, n).c);
|
|
@@ -388,7 +945,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
388
945
|
}
|
|
389
946
|
case "closeAccount": {
|
|
390
947
|
arity(3);
|
|
391
|
-
|
|
948
|
+
// A token account, or (Token-2022, with a MintCloseAuthority and no supply left) a mint.
|
|
949
|
+
const acct = this.accountParam(a[0], "the closed account");
|
|
950
|
+
if (!((acct.kind.kind === "token" || acct.kind.kind === "mint") && acct.kind.mut))
|
|
951
|
+
this.fail(a[0], "the closed account must be a Mut<TokenAccount> or a Mut<Mint>");
|
|
392
952
|
const dest = this.accountParam(a[1], "the destination");
|
|
393
953
|
if (!isWritable(dest.kind))
|
|
394
954
|
this.fail(a[1], "the destination must be writable (Mut<...>)");
|
|
@@ -509,6 +1069,194 @@ export class CpiEmitter extends CallEmitter {
|
|
|
509
1069
|
simple(22, [], [[acct, true, false]], none);
|
|
510
1070
|
return undefined;
|
|
511
1071
|
}
|
|
1072
|
+
case "updateDefaultAccountState": {
|
|
1073
|
+
token2022Only();
|
|
1074
|
+
if (a.length !== 3 && a.length !== 4)
|
|
1075
|
+
this.fail(node, "updateDefaultAccountState(mint, freezeAuthority, frozen, signerSeeds?)");
|
|
1076
|
+
const mint = anyAcct(a[0], "the mint", true);
|
|
1077
|
+
const auth = this.accountParam(a[1], "the freeze authority");
|
|
1078
|
+
const frozen = this.expect(this.expr(a[2], BOOL), BOOL, a[2]);
|
|
1079
|
+
const st = this.tmp("st");
|
|
1080
|
+
this.line(`uint8_t ${st} = (${frozen.c}) ? 2 : 1;`);
|
|
1081
|
+
simple(28, [byte(1), (put) => put(`&${st}`, "1", 1)], [
|
|
1082
|
+
[mint, true, false],
|
|
1083
|
+
[auth, false, true],
|
|
1084
|
+
], this.signerSeeds(a[3], auth, a[1]));
|
|
1085
|
+
return undefined;
|
|
1086
|
+
}
|
|
1087
|
+
case "enableRequiredMemoTransfers":
|
|
1088
|
+
case "disableRequiredMemoTransfers": {
|
|
1089
|
+
token2022Only();
|
|
1090
|
+
if (a.length !== 2 && a.length !== 3)
|
|
1091
|
+
this.fail(node, `${method}(account, owner, signerSeeds?)`);
|
|
1092
|
+
const acct = anyAcct(a[0], "the token account", true);
|
|
1093
|
+
const owner = this.accountParam(a[1], "the owner");
|
|
1094
|
+
simple(30, [byte(method === "enableRequiredMemoTransfers" ? 0 : 1)], [
|
|
1095
|
+
[acct, true, false],
|
|
1096
|
+
[owner, false, true],
|
|
1097
|
+
], this.signerSeeds(a[2], owner, a[1]));
|
|
1098
|
+
return undefined;
|
|
1099
|
+
}
|
|
1100
|
+
case "updateTransferHook": {
|
|
1101
|
+
token2022Only();
|
|
1102
|
+
if (a.length !== 3 && a.length !== 4)
|
|
1103
|
+
this.fail(node, "updateTransferHook(mint, authority, programId | null, signerSeeds?)");
|
|
1104
|
+
const mint = anyAcct(a[0], "the mint", true);
|
|
1105
|
+
const auth = this.accountParam(a[1], "the transfer hook authority");
|
|
1106
|
+
simple(36, [byte(1), nonZeroKey(a[2])], [
|
|
1107
|
+
[mint, true, false],
|
|
1108
|
+
[auth, false, true],
|
|
1109
|
+
], this.signerSeeds(a[3], auth, a[1]));
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
case "updateInterestRate": {
|
|
1113
|
+
token2022Only();
|
|
1114
|
+
if (a.length !== 3 && a.length !== 4)
|
|
1115
|
+
this.fail(node, "updateInterestRate(mint, rateAuthority, rate, signerSeeds?)");
|
|
1116
|
+
const mint = anyAcct(a[0], "the mint", true);
|
|
1117
|
+
const auth = this.accountParam(a[1], "the rate authority");
|
|
1118
|
+
simple(33, [byte(1), value(a[2], I16)], [
|
|
1119
|
+
[mint, true, false],
|
|
1120
|
+
[auth, false, true],
|
|
1121
|
+
], this.signerSeeds(a[3], auth, a[1]));
|
|
1122
|
+
return undefined;
|
|
1123
|
+
}
|
|
1124
|
+
case "transferCheckedWithFee": {
|
|
1125
|
+
token2022Only();
|
|
1126
|
+
if (a.length !== 6 && a.length !== 7)
|
|
1127
|
+
this.fail(node, "transferCheckedWithFee(from, mint, to, authority, amount, fee, signerSeeds?)");
|
|
1128
|
+
const from = want(a[0], "token", true, "the source");
|
|
1129
|
+
const mint = want(a[1], "mint", false, "the mint");
|
|
1130
|
+
const to = want(a[2], "token", true, "the destination");
|
|
1131
|
+
const auth = this.accountParam(a[3], "the authority");
|
|
1132
|
+
// The decimals are the mint's own (checked when the mint was), as transferChecked does.
|
|
1133
|
+
const dec = (put) => {
|
|
1134
|
+
const t = this.tmp("dec");
|
|
1135
|
+
this.line(`uint8_t ${t} = a_${mint.name}->data[44];`);
|
|
1136
|
+
put(`&${t}`, "1", 1);
|
|
1137
|
+
};
|
|
1138
|
+
simple(26, [byte(1), value(a[4], U64), dec, value(a[5], U64)], [
|
|
1139
|
+
[from, true, false],
|
|
1140
|
+
[mint, false, false],
|
|
1141
|
+
[to, true, false],
|
|
1142
|
+
[auth, false, true],
|
|
1143
|
+
], this.signerSeeds(a[6], auth, a[3]));
|
|
1144
|
+
return undefined;
|
|
1145
|
+
}
|
|
1146
|
+
case "harvestWithheldTokensToMint": {
|
|
1147
|
+
token2022Only();
|
|
1148
|
+
if (a.length !== 2)
|
|
1149
|
+
this.fail(node, "harvestWithheldTokensToMint(mint, sources): sources is remainingAccounts() or rest.slice(a, b)");
|
|
1150
|
+
if (!this.ix)
|
|
1151
|
+
this.fail(node, "harvestWithheldTokensToMint takes remaining accounts, so only instructions can call it");
|
|
1152
|
+
const mint = anyAcct(a[0], "the mint", true);
|
|
1153
|
+
const range = this.remainingRange(a[1]) ?? this.fail(a[1], "pass the source token accounts as remainingAccounts() or rest.slice(a, b)");
|
|
1154
|
+
const { buf, len, put, finish } = this.byteBuffer("tok", 2, "TS_ERR_INSTRUCTION_DATA", "token instruction");
|
|
1155
|
+
byte(26)(put);
|
|
1156
|
+
byte(4)(put);
|
|
1157
|
+
finish(node);
|
|
1158
|
+
// The sources go as they were passed (Token-2022 skips any it cannot harvest from).
|
|
1159
|
+
this.cpi(p, [[`a_${mint.name}`, true, false]], buf, len, none, { ...range, asPassed: true });
|
|
1160
|
+
return undefined;
|
|
1161
|
+
}
|
|
1162
|
+
case "withdrawWithheldTokensFromMint": {
|
|
1163
|
+
token2022Only();
|
|
1164
|
+
if (a.length !== 3 && a.length !== 4)
|
|
1165
|
+
this.fail(node, "withdrawWithheldTokensFromMint(mint, destination, authority, signerSeeds?)");
|
|
1166
|
+
const mint = anyAcct(a[0], "the mint", true);
|
|
1167
|
+
const dest = want(a[1], "token", true, "the destination");
|
|
1168
|
+
const auth = this.accountParam(a[2], "the withdraw authority");
|
|
1169
|
+
simple(26, [byte(2)], [
|
|
1170
|
+
[mint, true, false],
|
|
1171
|
+
[dest, true, false],
|
|
1172
|
+
[auth, false, true],
|
|
1173
|
+
], this.signerSeeds(a[3], auth, a[2]));
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
case "setTransferFee": {
|
|
1177
|
+
token2022Only();
|
|
1178
|
+
if (a.length !== 4 && a.length !== 5)
|
|
1179
|
+
this.fail(node, "setTransferFee(mint, configAuthority, basisPoints, maximumFee, signerSeeds?)");
|
|
1180
|
+
const mint = anyAcct(a[0], "the mint", true);
|
|
1181
|
+
const auth = this.accountParam(a[1], "the config authority");
|
|
1182
|
+
const bps = (put) => {
|
|
1183
|
+
const v = this.expect(this.expr(a[2], U16), U16, a[2]);
|
|
1184
|
+
this.line(`if (${v.c} > 10000) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: transfer fee above 10000 basis points");`);
|
|
1185
|
+
this.serialize(v, a[2], put);
|
|
1186
|
+
};
|
|
1187
|
+
simple(26, [byte(5), bps, value(a[3], U64)], [
|
|
1188
|
+
[mint, true, false],
|
|
1189
|
+
[auth, false, true],
|
|
1190
|
+
], this.signerSeeds(a[4], auth, a[1]));
|
|
1191
|
+
return undefined;
|
|
1192
|
+
}
|
|
1193
|
+
case "updateMetadataField":
|
|
1194
|
+
case "removeMetadataKey":
|
|
1195
|
+
case "updateMetadataAuthority":
|
|
1196
|
+
case "emitMetadata": {
|
|
1197
|
+
token2022Only();
|
|
1198
|
+
// The Token Metadata interface (spl-token-metadata-interface), served by Token-2022 for metadata in the mint.
|
|
1199
|
+
const usage = {
|
|
1200
|
+
updateMetadataField: "updateMetadataField(mint, updateAuthority, field, value, signerSeeds?)",
|
|
1201
|
+
removeMetadataKey: "removeMetadataKey(mint, updateAuthority, key, idempotent, signerSeeds?)",
|
|
1202
|
+
updateMetadataAuthority: "updateMetadataAuthority(mint, updateAuthority, newAuthority | null, signerSeeds?)",
|
|
1203
|
+
emitMetadata: "emitMetadata(mint)",
|
|
1204
|
+
}[method];
|
|
1205
|
+
const fixedArgs = { updateMetadataField: 4, removeMetadataKey: 4, updateMetadataAuthority: 3, emitMetadata: 1 }[method];
|
|
1206
|
+
if (a.length !== fixedArgs && (method === "emitMetadata" || a.length !== fixedArgs + 1))
|
|
1207
|
+
this.fail(node, usage);
|
|
1208
|
+
const disc = {
|
|
1209
|
+
updateMetadataField: "dde9312db5cadcc8",
|
|
1210
|
+
removeMetadataKey: "ea122038598d25b5",
|
|
1211
|
+
updateMetadataAuthority: "d7e4a6e45464567b",
|
|
1212
|
+
emitMetadata: "faa6b4fa0d0cb846",
|
|
1213
|
+
}[method];
|
|
1214
|
+
const mint = anyAcct(a[0], "the mint", method !== "emitMetadata");
|
|
1215
|
+
const auth = method === "emitMetadata" ? undefined : this.accountParam(a[1], "the update authority");
|
|
1216
|
+
const { buf, len, put, finish } = this.byteBuffer("tmd", 1232, "TS_ERR_INSTRUCTION_DATA", "token metadata instruction");
|
|
1217
|
+
const d = this.tmp("disc");
|
|
1218
|
+
this.line(`static const uint8_t ${d}[8] = ${cBytes([...Buffer.from(disc, "hex")])};`);
|
|
1219
|
+
put(d, "8", 8);
|
|
1220
|
+
if (method === "updateMetadataField") {
|
|
1221
|
+
// Field: Name / Symbol / Uri for those names, otherwise Key(field), as the Token-2022 CLI reads them.
|
|
1222
|
+
const f = this.expect(this.expr(a[2], STRING), STRING, a[2]);
|
|
1223
|
+
const ft = this.tmp("field");
|
|
1224
|
+
const tag = this.tmp("ftag");
|
|
1225
|
+
this.line(`TS_Str ${ft} = ${f.c};`);
|
|
1226
|
+
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;`);
|
|
1227
|
+
put(`&${tag}`, "1", 1);
|
|
1228
|
+
const kl = this.tmp("klen");
|
|
1229
|
+
this.line(`uint32_t ${kl} = (uint32_t)${ft}.len;`);
|
|
1230
|
+
put(`&${kl}`, `(${tag} == 3 ? 4 : 0)`);
|
|
1231
|
+
put(`${ft}.ptr`, `(${tag} == 3 ? ${ft}.len : 0)`);
|
|
1232
|
+
this.serialize(this.expect(this.expr(a[3], STRING), STRING, a[3]), a[3], put);
|
|
1233
|
+
}
|
|
1234
|
+
else if (method === "removeMetadataKey") {
|
|
1235
|
+
const key = this.expect(this.expr(a[2], STRING), STRING, a[2]);
|
|
1236
|
+
const idem = this.expect(this.expr(a[3], BOOL), BOOL, a[3]);
|
|
1237
|
+
const t = this.tmp("idem");
|
|
1238
|
+
this.line(`uint8_t ${t} = (${idem.c}) ? 1 : 0;`);
|
|
1239
|
+
put(`&${t}`, "1", 1);
|
|
1240
|
+
this.serialize(key, a[2], put);
|
|
1241
|
+
}
|
|
1242
|
+
else if (method === "updateMetadataAuthority") {
|
|
1243
|
+
nonZeroKey(a[2])(put);
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
// start: None, end: None (the whole TokenMetadata as return data).
|
|
1247
|
+
byte(0)(put);
|
|
1248
|
+
byte(0)(put);
|
|
1249
|
+
}
|
|
1250
|
+
finish(node);
|
|
1251
|
+
const seeds = auth ? this.signerSeeds(a[fixedArgs], auth, a[1]) : none;
|
|
1252
|
+
this.cpi(p, auth
|
|
1253
|
+
? [
|
|
1254
|
+
[`a_${mint.name}`, true, false],
|
|
1255
|
+
[`a_${auth.name}`, false, true],
|
|
1256
|
+
]
|
|
1257
|
+
: [[`a_${mint.name}`, false, false]], buf, len, seeds);
|
|
1258
|
+
return undefined;
|
|
1259
|
+
}
|
|
512
1260
|
case "approve": {
|
|
513
1261
|
arity(4);
|
|
514
1262
|
const src = want(a[0], "token", true, "the source");
|
|
@@ -585,7 +1333,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
585
1333
|
return undefined;
|
|
586
1334
|
}
|
|
587
1335
|
}
|
|
588
|
-
this.fail(node.expression, `token programs support transfer, transferChecked, mintTo, burn, closeAccount, approve, revoke, setAuthority, freezeAccount, thawAccount, syncNative, initializeMint2, initializeAccount3, initializeMetadata
|
|
1336
|
+
this.fail(node.expression, `token programs support transfer, transferChecked, mintTo, burn, closeAccount, approve, revoke, setAuthority, freezeAccount, thawAccount, syncNative, initializeMint2, initializeAccount3, initializeMetadata, updateMetadataField, removeMetadataKey, updateMetadataAuthority, emitMetadata, the Token-2022 extension instructions (updateDefaultAccountState, updateTransferHook, enableRequiredMemoTransfers, disableRequiredMemoTransfers, updateInterestRate, transferCheckedWithFee, harvestWithheldTokensToMint, withdrawWithheldTokensFromMint, setTransferFee) and initializers (initializeTransferFeeConfig, initializeMintCloseAuthority, initializeNonTransferableMint, initializePermanentDelegate, initializeDefaultAccountState, initializeMetadataPointer, initializeGroupPointer, initializeGroupMemberPointer, initializeTransferHook, initializeInterestBearingMint, initializeImmutableOwner), not ${method}`);
|
|
589
1337
|
}
|
|
590
1338
|
ataOp(node, program, method) {
|
|
591
1339
|
if (method !== "create" && method !== "createIdempotent")
|
|
@@ -807,8 +1555,8 @@ export class CpiEmitter extends CallEmitter {
|
|
|
807
1555
|
const index = CORE_PLUGINS.indexOf(kind);
|
|
808
1556
|
if (index < 0)
|
|
809
1557
|
this.fail(kindNode, `Core plugins are ${CORE_PLUGINS.join(", ")}`);
|
|
810
|
-
if (kind === "
|
|
811
|
-
this.fail(kindNode,
|
|
1558
|
+
if (kind === "Groups")
|
|
1559
|
+
this.fail(kindNode, "Groups is kept by Core's group instructions (AddCollectionsToGroupV1 …), not set as a plugin");
|
|
812
1560
|
const known = new Set(["kind", "authority", ...(CORE_PLUGIN_FIELDS[kind] ?? [])]);
|
|
813
1561
|
for (const [k, v] of props)
|
|
814
1562
|
if (!known.has(k))
|
|
@@ -852,9 +1600,55 @@ export class CpiEmitter extends CallEmitter {
|
|
|
852
1600
|
}
|
|
853
1601
|
if (constant && creators.length && sum !== 100n)
|
|
854
1602
|
this.fail(need("creators"), `creator percentages add up to ${sum}; Core requires 100`);
|
|
855
|
-
|
|
1603
|
+
// rule_set: None, or { programAllowList: [...] } / { programDenyList: [...] }
|
|
1604
|
+
const rs = props.get("ruleSet");
|
|
1605
|
+
if (!rs)
|
|
1606
|
+
fields.push({ bytes: [0] });
|
|
1607
|
+
else {
|
|
1608
|
+
const pick = ts.isObjectLiteralExpression(rs) && rs.properties.length === 1 ? rs.properties[0] : undefined;
|
|
1609
|
+
const which = pick && ts.isPropertyAssignment(pick) && ts.isIdentifier(pick.name) ? pick.name.text : undefined;
|
|
1610
|
+
if (!pick ||
|
|
1611
|
+
(which !== "programAllowList" && which !== "programDenyList") ||
|
|
1612
|
+
!ts.isArrayLiteralExpression(pick.initializer))
|
|
1613
|
+
this.fail(rs, "ruleSet is { programAllowList: [programs] } or { programDenyList: [programs] }");
|
|
1614
|
+
const progs = pick.initializer.elements;
|
|
1615
|
+
if (progs.length > 16)
|
|
1616
|
+
this.fail(rs, "at most 16 programs in a rule set");
|
|
1617
|
+
fields.push({ bytes: [which === "programAllowList" ? 1 : 2, ...u32(progs.length)] }, ...progs.map((k) => ({ value: k, type: PUBKEY })));
|
|
1618
|
+
}
|
|
856
1619
|
break;
|
|
857
1620
|
}
|
|
1621
|
+
case "MasterEdition": {
|
|
1622
|
+
// Option<u32>, Option<String>, Option<String>: left out or null is None.
|
|
1623
|
+
const opt = (k, t) => {
|
|
1624
|
+
const v = props.get(k);
|
|
1625
|
+
return !v || v.kind === ts.SyntaxKind.NullKeyword ? [{ bytes: [0] }] : [{ bytes: [1] }, { value: v, type: t }];
|
|
1626
|
+
};
|
|
1627
|
+
fields.push(...opt("maxSupply", { kind: "int", name: "u32" }), ...opt("name", STRING), ...opt("uri", STRING));
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1630
|
+
case "VerifiedCreators":
|
|
1631
|
+
case "Autograph": {
|
|
1632
|
+
const sigs = list("signatures", 16);
|
|
1633
|
+
fields.push({ bytes: u32(sigs.length) });
|
|
1634
|
+
const second = kind === "VerifiedCreators" ? "verified" : "message";
|
|
1635
|
+
for (const sg of sigs) {
|
|
1636
|
+
if (!ts.isObjectLiteralExpression(sg))
|
|
1637
|
+
this.fail(sg, `a signature is { address, ${second} }`);
|
|
1638
|
+
const get = (k) => {
|
|
1639
|
+
const q = sg.properties.find((x) => (ts.isPropertyAssignment(x) || ts.isShorthandPropertyAssignment(x)) && ts.isIdentifier(x.name) && x.name.text === k);
|
|
1640
|
+
if (!q)
|
|
1641
|
+
this.fail(sg, `a signature needs ${k}`);
|
|
1642
|
+
return ts.isPropertyAssignment(q) ? q.initializer : q.name;
|
|
1643
|
+
};
|
|
1644
|
+
fields.push({ value: get("address"), type: PUBKEY }, { value: get(second), type: kind === "VerifiedCreators" ? BOOL : STRING });
|
|
1645
|
+
}
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
case "FreezeExecute":
|
|
1649
|
+
case "PermanentFreezeExecute":
|
|
1650
|
+
fields.push({ value: need("frozen"), type: BOOL });
|
|
1651
|
+
break;
|
|
858
1652
|
case "FreezeDelegate":
|
|
859
1653
|
case "PermanentFreezeDelegate":
|
|
860
1654
|
fields.push({ value: need("frozen"), type: BOOL });
|
|
@@ -1107,6 +1901,58 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1107
1901
|
this.cpi(p, metas, data.buf, data.len, seedsAt(13));
|
|
1108
1902
|
return undefined;
|
|
1109
1903
|
}
|
|
1904
|
+
case "mintToCollection": {
|
|
1905
|
+
// MintToCollectionV1: MintV1's accounts plus the collection authority (signing), its record PDA (or null),
|
|
1906
|
+
// the collection mint, metadata (writable) and master edition, Bubblegum's signer PDA and Token Metadata;
|
|
1907
|
+
// MetadataArgs with Some({ verified: false, key: collection mint }) (Bubblegum verifies it).
|
|
1908
|
+
if (a.length !== 17 && a.length !== 18)
|
|
1909
|
+
this.fail(node, "mintToCollection(treeConfig, leafOwner, leafDelegate, merkleTree, payer, treeDelegate, collectionAuthority, collectionAuthorityRecordPda | null, collectionMint, collectionMetadata, collectionEdition, bubblegumSigner, logWrapper, compressionProgram, tokenMetadataProgram, systemProgram, { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable?, editionNonce? }, signerSeeds?)");
|
|
1910
|
+
const metas = [
|
|
1911
|
+
acct(0, "the tree config", true, false),
|
|
1912
|
+
acct(1, "the leaf owner", false, false),
|
|
1913
|
+
acct(2, "the leaf delegate", false, false),
|
|
1914
|
+
acct(3, "the merkle tree", true, false),
|
|
1915
|
+
acct(4, "the payer", false, true),
|
|
1916
|
+
acct(5, "the tree delegate", false, true),
|
|
1917
|
+
acct(6, "the collection authority", false, true),
|
|
1918
|
+
acct(7, "the collection authority record", false, false),
|
|
1919
|
+
acct(8, "the collection mint", false, false),
|
|
1920
|
+
acct(9, "the collection metadata", true, false),
|
|
1921
|
+
acct(10, "the collection edition", false, false),
|
|
1922
|
+
acct(11, "Bubblegum's signer", false, false),
|
|
1923
|
+
acct(12, "the log wrapper", false, false),
|
|
1924
|
+
acct(13, "the compression program", false, false),
|
|
1925
|
+
acct(14, "Token Metadata", false, false),
|
|
1926
|
+
acct(15, "the system program", false, false),
|
|
1927
|
+
];
|
|
1928
|
+
const md = a[16];
|
|
1929
|
+
if (!ts.isObjectLiteralExpression(md))
|
|
1930
|
+
this.fail(md, "the metadata is { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable?, editionNonce? }");
|
|
1931
|
+
const props = this.literalProps(md, ["name", "symbol", "uri", "sellerFeeBasisPoints", "creators", "isMutable", "editionNonce"], "the metadata");
|
|
1932
|
+
const need = (k) => props.get(k) ?? this.fail(md, `the metadata needs ${k}`);
|
|
1933
|
+
// `collectionMint.key`, read where the call is.
|
|
1934
|
+
const key = ts.factory.createPropertyAccessExpression(a[8], "key");
|
|
1935
|
+
ts.setTextRange(key, a[8]);
|
|
1936
|
+
key.parent = node;
|
|
1937
|
+
const nonce = props.get("editionNonce");
|
|
1938
|
+
const data = this.mplData(node, 1024, [
|
|
1939
|
+
{ bytes: [153, 18, 178, 47, 197, 158, 86, 15] },
|
|
1940
|
+
{ value: need("name"), type: STRING },
|
|
1941
|
+
{ value: need("symbol"), type: STRING },
|
|
1942
|
+
{ value: need("uri"), type: STRING },
|
|
1943
|
+
{ value: need("sellerFeeBasisPoints"), type: { kind: "int", name: "u16" } },
|
|
1944
|
+
{ bytes: [0] }, // primarySaleHappened
|
|
1945
|
+
props.get("isMutable") ? { value: props.get("isMutable"), type: BOOL } : { bytes: [1] },
|
|
1946
|
+
...(nonce ? [{ bytes: [1] }, { value: nonce, type: U8 }] : [{ bytes: [0] }]),
|
|
1947
|
+
{ bytes: [1, 0] }, // tokenStandard Some(NonFungible)
|
|
1948
|
+
{ bytes: [1, 0] }, // collection Some({ verified: false, … })
|
|
1949
|
+
{ value: key, type: PUBKEY },
|
|
1950
|
+
{ bytes: [0, 0] }, // uses None, tokenProgramVersion Original
|
|
1951
|
+
...this.bubblegumCreators(props.get("creators")),
|
|
1952
|
+
]);
|
|
1953
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(17));
|
|
1954
|
+
return undefined;
|
|
1955
|
+
}
|
|
1110
1956
|
}
|
|
1111
1957
|
return this.bubblegumV2(node, bg, method);
|
|
1112
1958
|
}
|
|
@@ -1119,10 +1965,17 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1119
1965
|
const U32 = { kind: "int", name: "u32" };
|
|
1120
1966
|
const U64v = { kind: "int", name: "u64" };
|
|
1121
1967
|
const HASH = { kind: "pubkey", hash: true };
|
|
1122
|
-
// The leaf
|
|
1123
|
-
|
|
1968
|
+
// The leaf an instruction replaces. "v2": { root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index };
|
|
1969
|
+
// "delegate": the same with collectionHash? before assetDataHash; "meta": { root, assetDataHash?, flags?, nonce,
|
|
1970
|
+
// index } (the hashes come from the metadata passed); "v1": { root, dataHash, creatorHash, nonce, index }.
|
|
1971
|
+
const leafFields = (n, shape = "v2") => {
|
|
1972
|
+
const known = shape === "v1"
|
|
1973
|
+
? ["root", "dataHash", "creatorHash", "nonce", "index"]
|
|
1974
|
+
: shape === "meta"
|
|
1975
|
+
? ["root", "assetDataHash", "flags", "nonce", "index"]
|
|
1976
|
+
: ["root", "dataHash", "creatorHash", ...(shape === "delegate" ? ["collectionHash"] : []), "assetDataHash", "flags", "nonce", "index"];
|
|
1124
1977
|
if (!ts.isObjectLiteralExpression(n))
|
|
1125
|
-
this.fail(n,
|
|
1978
|
+
this.fail(n, `the leaf is { ${known.join(", ")} }`);
|
|
1126
1979
|
const props = new Map();
|
|
1127
1980
|
for (const q of n.properties) {
|
|
1128
1981
|
if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
|
|
@@ -1132,27 +1985,64 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1132
1985
|
else
|
|
1133
1986
|
this.fail(q, "write leaf fields as name: value");
|
|
1134
1987
|
}
|
|
1135
|
-
const known = ["root", "dataHash", "creatorHash", "assetDataHash", "flags", "nonce", "index"];
|
|
1136
1988
|
for (const [k, v] of props)
|
|
1137
1989
|
if (!known.includes(k))
|
|
1138
1990
|
this.fail(v, `the leaf has no field ${k}`);
|
|
1139
1991
|
const need = (k) => props.get(k) ?? this.fail(n, `the leaf needs ${k}`);
|
|
1992
|
+
// Left out or null: None. A value that may be null at run time (`Hash | null`) goes as a Borsh Option.
|
|
1140
1993
|
const opt = (k, t) => {
|
|
1141
1994
|
const v = props.get(k);
|
|
1142
1995
|
if (!v || v.kind === ts.SyntaxKind.NullKeyword)
|
|
1143
1996
|
return [{ bytes: [0] }];
|
|
1144
|
-
|
|
1997
|
+
this.model.options.set(optionKey(t), t);
|
|
1998
|
+
return [{ value: v, type: { kind: "option", inner: t } }];
|
|
1145
1999
|
};
|
|
2000
|
+
const hashes = shape === "meta"
|
|
2001
|
+
? []
|
|
2002
|
+
: [
|
|
2003
|
+
{ value: need("dataHash"), type: HASH },
|
|
2004
|
+
{ value: need("creatorHash"), type: HASH },
|
|
2005
|
+
];
|
|
2006
|
+
const tail = [
|
|
2007
|
+
{ value: need("nonce"), type: U64v },
|
|
2008
|
+
{ value: need("index"), type: U32 },
|
|
2009
|
+
];
|
|
2010
|
+
if (shape === "v1")
|
|
2011
|
+
return [{ value: need("root"), type: HASH }, ...hashes, ...tail];
|
|
1146
2012
|
return [
|
|
1147
2013
|
{ value: need("root"), type: HASH },
|
|
1148
|
-
|
|
1149
|
-
|
|
2014
|
+
...hashes,
|
|
2015
|
+
...(shape === "delegate" ? opt("collectionHash", HASH) : []),
|
|
1150
2016
|
...opt("assetDataHash", HASH),
|
|
1151
2017
|
...opt("flags", U8),
|
|
1152
|
-
|
|
1153
|
-
{ value: need("index"), type: U32 },
|
|
2018
|
+
...tail,
|
|
1154
2019
|
];
|
|
1155
2020
|
};
|
|
2021
|
+
// MetadataArgsV2 from { name, symbol, uri, sellerFeeBasisPoints, primarySaleHappened?, isMutable?, creators?,
|
|
2022
|
+
// collection? } (a leaf's current metadata: the program hashes it to check the leaf).
|
|
2023
|
+
const metadataV2 = (md) => {
|
|
2024
|
+
if (!ts.isObjectLiteralExpression(md))
|
|
2025
|
+
this.fail(md, "the metadata is { name, symbol, uri, sellerFeeBasisPoints, primarySaleHappened?, isMutable?, creators?, collection? }");
|
|
2026
|
+
const props = this.literalProps(md, ["name", "symbol", "uri", "sellerFeeBasisPoints", "primarySaleHappened", "isMutable", "creators", "collection"], "the metadata");
|
|
2027
|
+
const need = (k) => props.get(k) ?? this.fail(md, `the metadata needs ${k}`);
|
|
2028
|
+
const fields = [
|
|
2029
|
+
{ value: need("name"), type: STRING },
|
|
2030
|
+
{ value: need("symbol"), type: STRING },
|
|
2031
|
+
{ value: need("uri"), type: STRING },
|
|
2032
|
+
{ value: need("sellerFeeBasisPoints"), type: { kind: "int", name: "u16" } },
|
|
2033
|
+
];
|
|
2034
|
+
const flag = (k, dflt) => (props.get(k) ? { value: props.get(k), type: BOOL } : { bytes: [dflt] });
|
|
2035
|
+
fields.push(flag("primarySaleHappened", 0), flag("isMutable", 1), { bytes: [1, 0] }); // token_standard: Some(NonFungible)
|
|
2036
|
+
fields.push(...this.bubblegumCreators(props.get("creators")));
|
|
2037
|
+
const coll = props.get("collection");
|
|
2038
|
+
if (!coll || coll.kind === ts.SyntaxKind.NullKeyword)
|
|
2039
|
+
fields.push({ bytes: [0] });
|
|
2040
|
+
else {
|
|
2041
|
+
this.model.options.set("pubkey", PUBKEY);
|
|
2042
|
+
fields.push({ value: coll, type: { kind: "option", inner: PUBKEY } });
|
|
2043
|
+
}
|
|
2044
|
+
return fields;
|
|
2045
|
+
};
|
|
1156
2046
|
const proofRange = (n) => {
|
|
1157
2047
|
if (!this.ix)
|
|
1158
2048
|
this.fail(n, "a Merkle proof comes from remainingAccounts(), so only instructions can pass one");
|
|
@@ -1311,7 +2201,102 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1311
2201
|
return undefined;
|
|
1312
2202
|
}
|
|
1313
2203
|
}
|
|
1314
|
-
|
|
2204
|
+
const op = BUBBLEGUM_OPS[method];
|
|
2205
|
+
if (op) {
|
|
2206
|
+
// accounts…, [leaf, metadata?, update?], proof, signerSeeds?
|
|
2207
|
+
const nAccounts = op.accounts.length;
|
|
2208
|
+
const extra = op.leaf === "meta" ? (method === "updateMetadataV2" ? 2 : 1) : 0;
|
|
2209
|
+
const proofAt = nAccounts + 1 + extra;
|
|
2210
|
+
if (a.length !== proofAt + 1 && a.length !== proofAt + 2)
|
|
2211
|
+
this.fail(node, `${method}(${op.accounts.map(([l, , , o]) => (o ? `${l} | null` : l)).join(", ")}, leaf${extra ? (extra === 2 ? ", currentMetadata, update" : ", metadata") : ""}, proof, signerSeeds?)`);
|
|
2212
|
+
const hasSeeds = !!a[proofAt + 1];
|
|
2213
|
+
const metas = op.accounts.map(([label, w, s, optional, either], i) => {
|
|
2214
|
+
if (!optional && a[i].kind === ts.SyntaxKind.NullKeyword)
|
|
2215
|
+
this.fail(a[i], `pass ${label}`);
|
|
2216
|
+
const m = this.mplMeta(a[i], bg, label, w, s);
|
|
2217
|
+
// V1 leaf owner / delegate: either may sign (a PDA owner signs with the seeds).
|
|
2218
|
+
if (either)
|
|
2219
|
+
return [
|
|
2220
|
+
m[0],
|
|
2221
|
+
w,
|
|
2222
|
+
either === "owner"
|
|
2223
|
+
? `(${m[0]}->is_signer || ${hasSeeds})`
|
|
2224
|
+
: `(${m[0]}->is_signer || (${hasSeeds} && ${m[0]} == ${`a_${this.accountParam(a[i - 1], "the leaf owner").name}`}))`,
|
|
2225
|
+
];
|
|
2226
|
+
return m;
|
|
2227
|
+
});
|
|
2228
|
+
const fields = [{ bytes: op.disc }, ...leafFields(a[nAccounts], op.leaf)];
|
|
2229
|
+
if (op.leaf === "meta")
|
|
2230
|
+
fields.push(...metadataV2(a[nAccounts + 1]));
|
|
2231
|
+
if (method === "updateMetadataV2")
|
|
2232
|
+
fields.push(...this.bubblegumUpdateArgs(a[nAccounts + 2]));
|
|
2233
|
+
const data = this.mplData(node, 2048, fields);
|
|
2234
|
+
this.cpi(p, metas, data.buf, data.len, seedsAt(proofAt + 1), proofRange(a[proofAt]));
|
|
2235
|
+
return undefined;
|
|
2236
|
+
}
|
|
2237
|
+
this.fail(node.expression, `Bubblegum supports createTree, mint, mintToCollection, createTreeV2, mintV2, transferV2, burnV2, ${Object.keys(BUBBLEGUM_OPS).join(", ")}, not ${method}`);
|
|
2238
|
+
}
|
|
2239
|
+
/** Object literal properties by name, refusing others. */
|
|
2240
|
+
literalProps(o, allowed, what) {
|
|
2241
|
+
const props = new Map();
|
|
2242
|
+
for (const q of o.properties) {
|
|
2243
|
+
if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
|
|
2244
|
+
props.set(q.name.text, q.initializer);
|
|
2245
|
+
else if (ts.isShorthandPropertyAssignment(q))
|
|
2246
|
+
props.set(q.name.text, q.name);
|
|
2247
|
+
else
|
|
2248
|
+
this.fail(q, `write ${what}'s fields as name: value`);
|
|
2249
|
+
}
|
|
2250
|
+
for (const [k, v] of props)
|
|
2251
|
+
if (!allowed.includes(k))
|
|
2252
|
+
this.fail(v, `${what} has no field ${k} (${allowed.join(", ")})`);
|
|
2253
|
+
return props;
|
|
2254
|
+
}
|
|
2255
|
+
/** Vec<Creator> (address, verified, share) from a literal list; shares add up to 100 when literal. */
|
|
2256
|
+
bubblegumCreators(creators) {
|
|
2257
|
+
if (creators && !ts.isArrayLiteralExpression(creators))
|
|
2258
|
+
this.fail(creators, "creators: [{ address, verified, share }, …] (an array literal)");
|
|
2259
|
+
const list = creators ? creators.elements : [];
|
|
2260
|
+
if (list.length > 5)
|
|
2261
|
+
this.fail(creators, "Bubblegum takes at most 5 creators");
|
|
2262
|
+
const fields = [{ bytes: [list.length, 0, 0, 0] }];
|
|
2263
|
+
let shares = 0n;
|
|
2264
|
+
let constant = true;
|
|
2265
|
+
for (const el of list) {
|
|
2266
|
+
if (!ts.isObjectLiteralExpression(el))
|
|
2267
|
+
this.fail(el, "a creator is { address, verified, share }");
|
|
2268
|
+
const props = this.literalProps(el, ["address", "verified", "share"], "a creator");
|
|
2269
|
+
const f = (k) => props.get(k) ?? this.fail(el, `a creator needs ${k}`);
|
|
2270
|
+
const share = f("share");
|
|
2271
|
+
if (ts.isBigIntLiteral(share))
|
|
2272
|
+
shares += BigInt(share.text.slice(0, -1));
|
|
2273
|
+
else
|
|
2274
|
+
constant = false;
|
|
2275
|
+
fields.push({ value: f("address"), type: PUBKEY }, { value: f("verified"), type: BOOL }, { value: share, type: U8 });
|
|
2276
|
+
}
|
|
2277
|
+
if (list.length && constant && shares !== 100n)
|
|
2278
|
+
this.fail(creators, `creator shares add up to ${shares}; Bubblegum requires 100`);
|
|
2279
|
+
return fields;
|
|
2280
|
+
}
|
|
2281
|
+
/** UpdateArgs: every field optional (left out or null: unchanged). */
|
|
2282
|
+
bubblegumUpdateArgs(n) {
|
|
2283
|
+
if (!ts.isObjectLiteralExpression(n))
|
|
2284
|
+
this.fail(n, "the update is { name?, symbol?, uri?, creators?, sellerFeeBasisPoints?, primarySaleHappened?, isMutable? }");
|
|
2285
|
+
const props = this.literalProps(n, ["name", "symbol", "uri", "creators", "sellerFeeBasisPoints", "primarySaleHappened", "isMutable"], "the update");
|
|
2286
|
+
const opt = (k, t) => {
|
|
2287
|
+
const v = props.get(k);
|
|
2288
|
+
return !v || v.kind === ts.SyntaxKind.NullKeyword ? [{ bytes: [0] }] : [{ bytes: [1] }, { value: v, type: t }];
|
|
2289
|
+
};
|
|
2290
|
+
const creators = props.get("creators");
|
|
2291
|
+
return [
|
|
2292
|
+
...opt("name", STRING),
|
|
2293
|
+
...opt("symbol", STRING),
|
|
2294
|
+
...opt("uri", STRING),
|
|
2295
|
+
...(creators && creators.kind !== ts.SyntaxKind.NullKeyword ? [{ bytes: [1] }, ...this.bubblegumCreators(creators)] : [{ bytes: [0] }]),
|
|
2296
|
+
...opt("sellerFeeBasisPoints", { kind: "int", name: "u16" }),
|
|
2297
|
+
...opt("primarySaleHappened", BOOL),
|
|
2298
|
+
...opt("isMutable", BOOL),
|
|
2299
|
+
];
|
|
1315
2300
|
}
|
|
1316
2301
|
/** `acct.key` of an account parameter, as an expression to serialize. */
|
|
1317
2302
|
keyOf(n) {
|
|
@@ -1446,6 +2431,20 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1446
2431
|
continue;
|
|
1447
2432
|
}
|
|
1448
2433
|
const vec = ts.isIdentifier(el) ? this.lookup(el.text) : undefined;
|
|
2434
|
+
if (vec?.type.kind === "vecArg" && vec.type.elem.kind === "string") {
|
|
2435
|
+
// Borsh Vec<String> again: the count, then each string's length and bytes.
|
|
2436
|
+
const n = this.tmp("vlen");
|
|
2437
|
+
const i = this.tmp("vi");
|
|
2438
|
+
const sl = this.tmp("slen");
|
|
2439
|
+
this.line(`uint32_t ${n} = ${vec.c}.len;`);
|
|
2440
|
+
put(`&${n}`, "4", 4);
|
|
2441
|
+
this.line(`for (uint32_t ${i} = 0; ${i} < ${n}; ${i}++) {`);
|
|
2442
|
+
this.line(`uint32_t ${sl} = (uint32_t)((const TS_Str *)${vec.c}.ptr)[${i}].len;`);
|
|
2443
|
+
put(`&${sl}`, "4");
|
|
2444
|
+
put(`((const TS_Str *)${vec.c}.ptr)[${i}].ptr`, `${sl}`);
|
|
2445
|
+
this.line(`}`);
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
1449
2448
|
if (vec?.type.kind === "vecArg") {
|
|
1450
2449
|
const n = this.tmp("vlen");
|
|
1451
2450
|
this.line(`uint32_t ${n} = ${vec.c}.len;`);
|
|
@@ -1606,11 +2605,11 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1606
2605
|
const s = targ && ts.isTypeReferenceNode(targ) && ts.isIdentifier(targ.typeName) ? this.model.structs.get(targ.typeName.text) : undefined;
|
|
1607
2606
|
if (!s)
|
|
1608
2607
|
this.fail(node, "emit needs an explicit event type: emit<MyEvent>({ ... })");
|
|
1609
|
-
if (s.layout === "borsh")
|
|
1610
|
-
this.fail(node, `${s.name} is @layout borsh, an account layout; declare a separate event struct`);
|
|
1611
2608
|
const obj = node.arguments[0];
|
|
1612
2609
|
if (node.arguments.length !== (cpi ? 3 : 1) || !ts.isObjectLiteralExpression(obj))
|
|
1613
2610
|
this.fail(node, "emit<T>({ field: value, ... }) takes one object literal");
|
|
2611
|
+
if (s.layout === "borsh")
|
|
2612
|
+
return this.emitBorshEvent(node, s, obj, cpi);
|
|
1614
2613
|
const given = new Map();
|
|
1615
2614
|
for (const p of obj.properties) {
|
|
1616
2615
|
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name))
|
|
@@ -1685,15 +2684,39 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1685
2684
|
staged.set(name, (put) => put(t, String(n), n));
|
|
1686
2685
|
}
|
|
1687
2686
|
}
|
|
2687
|
+
this.sendEvent(s, obj, cpi, (put) => {
|
|
2688
|
+
for (const f of s.fields)
|
|
2689
|
+
staged.get(f.name)(put);
|
|
2690
|
+
});
|
|
2691
|
+
return undefined;
|
|
2692
|
+
}
|
|
2693
|
+
/** emit<T> of a @layout borsh struct: built like account data, then packed as Anchor serializes it. */
|
|
2694
|
+
emitBorshEvent(node, s, obj, cpi) {
|
|
2695
|
+
this.model.events.add(s.name);
|
|
2696
|
+
const t = this.tmp("evs");
|
|
2697
|
+
const b = this.tmp("evb");
|
|
2698
|
+
const w = this.tmp("evw");
|
|
2699
|
+
// One pair of heap buffers per emit site, taken on first use: an emit in a loop does not take new heap each pass.
|
|
2700
|
+
this.hoisted.push(` TSS_${s.name} *${t} = NULL;`, ` uint8_t *${b} = NULL;`);
|
|
2701
|
+
this.line(`if (${t} == NULL) TS_TRY(ts_alloc(sizeof(TSS_${s.name}), (uint8_t **)&${t}));`);
|
|
2702
|
+
this.writeAggregate({ lv: `(*${t})`, type: { kind: "struct", name: s.name }, writable: true }, obj);
|
|
2703
|
+
// Borsh never takes more than the fixed layout (a String/Vec is 4 + used bytes, an Option 1 + payload).
|
|
2704
|
+
this.line(`uint64_t ${w} = 0;`);
|
|
2705
|
+
this.line(`if (${b} == NULL) TS_TRY(ts_alloc(sizeof(TSS_${s.name}), &${b}));`);
|
|
2706
|
+
this.line(`TS_TRY(ts_pack_${s.name}(${t}, ${b}, sizeof(TSS_${s.name}), &${w}));`);
|
|
2707
|
+
this.sendEvent(s, node, cpi, (put) => put(b, w));
|
|
2708
|
+
return undefined;
|
|
2709
|
+
}
|
|
2710
|
+
/** Discriminator + `body`, logged (emit) or self-invoked (emitCpi). */
|
|
2711
|
+
sendEvent(s, at, cpi, body) {
|
|
1688
2712
|
const { buf, len, put, finish } = this.byteBuffer("ev", MAX_EVENT + (cpi ? 8 : 0), "TS_ERR_EVENT_TOO_LARGE", "event");
|
|
1689
2713
|
if (cpi)
|
|
1690
2714
|
put("TS_EVENT_IX_TAG", "8", 8);
|
|
1691
2715
|
const disc = this.tmp("disc");
|
|
1692
2716
|
this.line(`static const uint8_t ${disc}[8] = ${cBytes(s.eventDiscriminator)};`);
|
|
1693
2717
|
put(disc, "8", 8);
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
finish(obj);
|
|
2718
|
+
body(put);
|
|
2719
|
+
finish(at);
|
|
1697
2720
|
if (cpi) {
|
|
1698
2721
|
// Invoke ourselves with the event authority PDA as the only (signing) account; the entrypoint accepts it.
|
|
1699
2722
|
const m = this.tmp("metas");
|
|
@@ -1705,12 +2728,11 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1705
2728
|
this.line(`SolInstruction ${ix} = {&a_${cpi.program}->key, ${m}, 1, ${buf}, ${len}};`);
|
|
1706
2729
|
this.line(`SolSignerSeeds ${signer} = {${sd}, 2};`);
|
|
1707
2730
|
this.line(`TS_TRY(ts_invoke(params, &${ix}, &${signer}, 1));`);
|
|
1708
|
-
return
|
|
2731
|
+
return;
|
|
1709
2732
|
}
|
|
1710
2733
|
const bytes = this.tmp("evbytes");
|
|
1711
2734
|
this.line(`SolBytes ${bytes} = {${buf}, ${len}};`);
|
|
1712
2735
|
this.line(`sol_log_data(&${bytes}, 1);`);
|
|
1713
|
-
return undefined;
|
|
1714
2736
|
}
|
|
1715
2737
|
/** Structs reachable through the fields of struct `name` (not including itself unless it recurses). */
|
|
1716
2738
|
nestedStructs(name) {
|