@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
package/dist/compiler/analyze.js
CHANGED
|
@@ -4,7 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { CompileError, Reporter } from "./diagnostics.js";
|
|
6
6
|
import { decodeAddress } from "../keys.js";
|
|
7
|
-
import {
|
|
7
|
+
import { borshIdentical } from "./emit/borsh.js";
|
|
8
|
+
import { optionKey, INT_INFO, INT_NAMES, scalarKey, fixedVecName, isScalar, scalarSize, typeName, } from "./model.js";
|
|
8
9
|
export const LIB_PATH = fileURLToPath(new URL("../../lib/typesol.d.ts", import.meta.url));
|
|
9
10
|
/** Names a struct field may not use because account parameters already expose them. */
|
|
10
11
|
const RESERVED_FIELDS = new Set(["key", "lamports", "dataLen", "create", "equals", "get", "set", "length", "isSigner", "isWritable", "pdaBump"]);
|
|
@@ -57,6 +58,7 @@ const TYPE_EXPORTS = new Set([
|
|
|
57
58
|
"TokenInterface",
|
|
58
59
|
"FixedArray",
|
|
59
60
|
"FixedString",
|
|
61
|
+
"FixedVec",
|
|
60
62
|
"Program",
|
|
61
63
|
"UncheckedProgram",
|
|
62
64
|
"CpiAccount",
|
|
@@ -88,6 +90,7 @@ const TYPE_EXPORTS = new Set([
|
|
|
88
90
|
"SelfProgram",
|
|
89
91
|
"ProgramData",
|
|
90
92
|
"SignerSeeds",
|
|
93
|
+
"SystemAccount",
|
|
91
94
|
]);
|
|
92
95
|
/** Largest fixed array a struct may declare, and largest FixedString. */
|
|
93
96
|
const MAX_ARRAY_LEN = 1_000_000;
|
|
@@ -97,6 +100,12 @@ export const MAX_ACCOUNT_SIZE = 10 * 1024 * 1024;
|
|
|
97
100
|
/** sol_set_return_data's limit. */
|
|
98
101
|
export const MAX_RETURN_DATA = 1024;
|
|
99
102
|
export const MAX_CPI_ALLOCATION = 10 * 1024;
|
|
103
|
+
/** A struct named by a type reference, through the type parameters of a generic helper's instance. */
|
|
104
|
+
export function structNameOf(node, scope) {
|
|
105
|
+
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName) || node.typeArguments?.length)
|
|
106
|
+
return undefined;
|
|
107
|
+
return scope.structArgs?.get(node.typeName.text) ?? node.typeName.text;
|
|
108
|
+
}
|
|
100
109
|
export function snakeCase(name) {
|
|
101
110
|
return name
|
|
102
111
|
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
@@ -225,7 +234,7 @@ export function analyze(entry) {
|
|
|
225
234
|
const isExported = (n) => ts.canHaveModifiers(n) && (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
226
235
|
const declare = (name, file) => {
|
|
227
236
|
// These prefixes name the IDL types TypeSOL generates for FixedString<N>, Option and data enums in account data.
|
|
228
|
-
if (/^(FixedString\d|FixedOption_|FixedEnum_)/.test(name.text))
|
|
237
|
+
if (/^(FixedString\d|FixedVec\d|FixedOption_|FixedEnum_)/.test(name.text))
|
|
229
238
|
reporter.fail(name, `"${name.text}" is reserved for IDL types TypeSOL generates; pick another name`);
|
|
230
239
|
const prev = declared.get(name.text);
|
|
231
240
|
if (prev)
|
|
@@ -313,7 +322,8 @@ export function analyze(entry) {
|
|
|
313
322
|
else {
|
|
314
323
|
if (stmt.name)
|
|
315
324
|
declare(stmt.name, file);
|
|
316
|
-
|
|
325
|
+
if (stmt.name)
|
|
326
|
+
helperNodes.push({ node: stmt, name: stmt.name.text, display: stmt.name.text, decl: stmt, imports });
|
|
317
327
|
}
|
|
318
328
|
}
|
|
319
329
|
else if (ts.isVariableStatement(stmt)) {
|
|
@@ -324,6 +334,14 @@ export function analyze(entry) {
|
|
|
324
334
|
if (!ts.isIdentifier(d.name) || !d.initializer)
|
|
325
335
|
reporter.fail(d, "declare const NAME = literal");
|
|
326
336
|
const init = d.initializer;
|
|
337
|
+
// `const f = (…): T => …` / `const f = function (…) {…}`: a helper.
|
|
338
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
339
|
+
if (isEntry && isExported(stmt))
|
|
340
|
+
reporter.fail(d.name, "instructions are function declarations: export function name(…)");
|
|
341
|
+
declare(d.name, file);
|
|
342
|
+
helperNodes.push({ node: init, name: d.name.text, display: d.name.text, decl: d, imports });
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
327
345
|
let value;
|
|
328
346
|
if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && imports.get(init.expression.text) === "pubkey") {
|
|
329
347
|
const arg = init.arguments[0];
|
|
@@ -358,6 +376,7 @@ export function analyze(entry) {
|
|
|
358
376
|
}
|
|
359
377
|
}
|
|
360
378
|
const fstrings = new Set();
|
|
379
|
+
const fixedVecs = new Map();
|
|
361
380
|
const scopeFor = (imports) => ({ imports, enums, options, dataEnums });
|
|
362
381
|
for (const c of pendingConstTypes) {
|
|
363
382
|
reporter.guard(() => {
|
|
@@ -372,14 +391,113 @@ export function analyze(entry) {
|
|
|
372
391
|
c.def.type = t;
|
|
373
392
|
});
|
|
374
393
|
}
|
|
375
|
-
const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings);
|
|
394
|
+
const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs);
|
|
395
|
+
const checker = program.getTypeChecker();
|
|
396
|
+
// Local `const f = (…) => …` in any function body (and in those local functions): lifted to helpers.
|
|
397
|
+
const locals = [];
|
|
398
|
+
const taken = new Set();
|
|
399
|
+
const collectLocals = (body, outer, imports, fn) => {
|
|
400
|
+
const generic = !!fn.typeParameters?.length;
|
|
401
|
+
const visit = (n) => {
|
|
402
|
+
if (ts.isVariableDeclaration(n) && n.initializer && (ts.isArrowFunction(n.initializer) || ts.isFunctionExpression(n.initializer))) {
|
|
403
|
+
reporter.guard(() => {
|
|
404
|
+
if (!ts.isIdentifier(n.name))
|
|
405
|
+
reporter.fail(n.name, "name local functions: const f = (…) => …");
|
|
406
|
+
if (generic)
|
|
407
|
+
reporter.fail(n, "local functions inside a generic helper are not supported; declare this one at the top level");
|
|
408
|
+
if (!(n.parent.flags & ts.NodeFlags.Const) || !ts.isVariableStatement(n.parent.parent))
|
|
409
|
+
reporter.fail(n, "declare local functions with const: const f = (…) => …");
|
|
410
|
+
if (n.name.text.includes("__") || n.name.text.includes("$"))
|
|
411
|
+
reporter.fail(n.name, `"${n.name.text}": "__" and "$" are reserved in names`);
|
|
412
|
+
let name = `${outer}__${n.name.text}`;
|
|
413
|
+
for (let i = 2; taken.has(name); i++)
|
|
414
|
+
name = `${outer}__${n.name.text}__${i}`;
|
|
415
|
+
taken.add(name);
|
|
416
|
+
const src = { node: n.initializer, name, display: n.name.text, decl: n, imports, outer: fn };
|
|
417
|
+
locals.push(src);
|
|
418
|
+
if (src.node.body)
|
|
419
|
+
collectLocals(src.node.body, name, imports, src.node);
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
ts.forEachChild(n, visit);
|
|
424
|
+
};
|
|
425
|
+
visit(body);
|
|
426
|
+
};
|
|
427
|
+
for (const f of functions)
|
|
428
|
+
if (f.node.body && f.node.name)
|
|
429
|
+
collectLocals(f.node.body, f.node.name.text, f.imports, f.node);
|
|
430
|
+
for (const h of [...helperNodes])
|
|
431
|
+
if (h.node.body)
|
|
432
|
+
collectLocals(h.node.body, h.name, h.imports, h.node);
|
|
433
|
+
const localDecls = new Set(locals.map((l) => l.decl));
|
|
434
|
+
// A local function that reads the enclosing function's variables (or calls one that does) is a closure: it is
|
|
435
|
+
// expanded where it is called, in the enclosing function. The others become helpers of their own.
|
|
436
|
+
const inline = new Set(locals.filter((l) => captures(l, localDecls, checker)).map((l) => l.decl));
|
|
437
|
+
for (let changed = true; changed;) {
|
|
438
|
+
changed = false;
|
|
439
|
+
for (const l of locals)
|
|
440
|
+
if (!inline.has(l.decl) && references(l, inline, checker)) {
|
|
441
|
+
inline.add(l.decl);
|
|
442
|
+
changed = true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const closureDecls = new Map(locals.filter((l) => inline.has(l.decl)).map((l) => [l.decl, l.display]));
|
|
376
446
|
const helpers = new Map();
|
|
377
|
-
|
|
378
|
-
|
|
447
|
+
const generics = new Map();
|
|
448
|
+
const helperDecls = new Map();
|
|
449
|
+
for (const fn of [...helperNodes, ...locals.filter((l) => !inline.has(l.decl))]) {
|
|
450
|
+
helperDecls.set(fn.decl, fn.name);
|
|
451
|
+
const fnParams = fn.node.parameters.map((p, i) => (p.type && ts.isFunctionTypeNode(p.type) ? i : -1)).filter((i) => i >= 0);
|
|
452
|
+
if (fn.node.typeParameters?.length || fnParams.length) {
|
|
453
|
+
reporter.guard(() => {
|
|
454
|
+
for (const tp of fn.node.typeParameters ?? [])
|
|
455
|
+
if (tp.default)
|
|
456
|
+
reporter.fail(tp, "type parameters of helpers take no default");
|
|
457
|
+
generics.set(fn.name, {
|
|
458
|
+
name: fn.name,
|
|
459
|
+
display: fn.display,
|
|
460
|
+
node: fn.node,
|
|
461
|
+
typeParams: (fn.node.typeParameters ?? []).map((t) => t.name.text),
|
|
462
|
+
fnParams,
|
|
463
|
+
imports: fn.imports,
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const h = reporter.guard(() => analyzeHelper(fn.node, fn.name, fn.display, reporter, scopeFor(fn.imports), structs));
|
|
379
469
|
if (h)
|
|
380
470
|
helpers.set(h.name, h);
|
|
381
471
|
}
|
|
382
|
-
|
|
472
|
+
const instances = new Map();
|
|
473
|
+
const instantiate = (g, typeArgs, fnArgs = []) => {
|
|
474
|
+
const argKey = (t) => (t.kind === "struct" ? `S${t.name}` : scalarKey(t));
|
|
475
|
+
const key = [g.name, ...typeArgs.map(argKey), ...fnArgs.map((f) => `fn_${f}`)].join("__");
|
|
476
|
+
const done = instances.get(key);
|
|
477
|
+
if (done)
|
|
478
|
+
return done;
|
|
479
|
+
const bound = new Map();
|
|
480
|
+
const boundStructs = new Map();
|
|
481
|
+
g.typeParams.forEach((n, i) => {
|
|
482
|
+
const t = typeArgs[i];
|
|
483
|
+
if (t.kind === "struct")
|
|
484
|
+
boundStructs.set(n, t.name);
|
|
485
|
+
else
|
|
486
|
+
bound.set(n, t);
|
|
487
|
+
});
|
|
488
|
+
const display = `${g.display}<${[...typeArgs.map((t) => (t.kind === "struct" ? t.name : typeName(t))), ...fnArgs].join(", ")}>`;
|
|
489
|
+
const h = analyzeHelper(g.node, key, display, reporter, { ...scopeFor(g.imports), typeArgs: bound, structArgs: boundStructs }, structs, new Set(g.fnParams));
|
|
490
|
+
h.typeArgs = bound;
|
|
491
|
+
h.structArgs = boundStructs;
|
|
492
|
+
if (g.fnParams.length) {
|
|
493
|
+
h.fnArgs = new Map(g.fnParams.map((i, k) => [g.node.parameters[i].name.text, fnArgs[k]]));
|
|
494
|
+
h.fnArgIndexes = g.fnParams;
|
|
495
|
+
}
|
|
496
|
+
instances.set(key, h);
|
|
497
|
+
helpers.set(key, h);
|
|
498
|
+
return h;
|
|
499
|
+
};
|
|
500
|
+
reporter.guard(() => checkRecursion([...helperNodes, ...locals.filter((l) => !inline.has(l.decl))], helperDecls, checker, reporter));
|
|
383
501
|
const accountStructs = new Set();
|
|
384
502
|
const argStructs = new Set();
|
|
385
503
|
const instructions = [];
|
|
@@ -390,12 +508,32 @@ export function analyze(entry) {
|
|
|
390
508
|
instructions.push(ix);
|
|
391
509
|
for (const a of ix.args)
|
|
392
510
|
collectStructs(a.type, structs, dataEnums, argStructs);
|
|
511
|
+
for (const a of ix.args) {
|
|
512
|
+
const seen = new Set();
|
|
513
|
+
collectStructs(a.type, structs, dataEnums, seen);
|
|
514
|
+
// A @layout borsh struct is decoded as a whole argument; inside lists or enums it would need a fixed size.
|
|
515
|
+
const whole = a.type.kind === "structRef" ? a.type.struct : undefined;
|
|
516
|
+
for (const s of seen)
|
|
517
|
+
if (structs.get(s).layout === "borsh" && s !== whole)
|
|
518
|
+
reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} is @layout borsh; pass it as a whole argument, not inside a list or enum`);
|
|
519
|
+
}
|
|
520
|
+
// Arguments are Borsh: a FixedVec's padded layout is account data only (a @layout borsh struct's FixedVec is a Borsh Vec).
|
|
521
|
+
for (const a of ix.args) {
|
|
522
|
+
const seen = new Set();
|
|
523
|
+
collectStructs(a.type, structs, dataEnums, seen);
|
|
524
|
+
for (const s of seen)
|
|
525
|
+
if (structs.get(s).layout !== "borsh" && structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
|
|
526
|
+
reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; pass a Vec<T> argument instead`);
|
|
527
|
+
}
|
|
393
528
|
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
394
529
|
if (ix.returns?.kind === "struct")
|
|
395
530
|
collectStructs(ix.returns, structs, dataEnums, argStructs);
|
|
396
531
|
for (const a of ix.accounts)
|
|
397
|
-
if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded")
|
|
532
|
+
if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded") {
|
|
398
533
|
accountStructs.add(a.kind.struct.name);
|
|
534
|
+
if (a.kind.struct.viewsOnly)
|
|
535
|
+
reporter.error(a.node, `${a.kind.struct.name} holds a string (or an enum carrying one), so it is an argument, return value or event; account data uses FixedString<N>`);
|
|
536
|
+
}
|
|
399
537
|
}
|
|
400
538
|
checkSnakeDuplicates(instructions.map((ix) => ({ name: ix.node.name.text, node: ix.node.name })), "instructions", reporter);
|
|
401
539
|
// emitCpi's event instruction (Anchor's EVENT_IX_TAG) is dispatched before every other instruction.
|
|
@@ -426,6 +564,9 @@ export function analyze(entry) {
|
|
|
426
564
|
structs,
|
|
427
565
|
instructions,
|
|
428
566
|
helpers,
|
|
567
|
+
generics,
|
|
568
|
+
helperDecls,
|
|
569
|
+
closureDecls,
|
|
429
570
|
errors,
|
|
430
571
|
errorEnums,
|
|
431
572
|
enums,
|
|
@@ -436,12 +577,14 @@ export function analyze(entry) {
|
|
|
436
577
|
accountStructs,
|
|
437
578
|
argStructs,
|
|
438
579
|
fstrings,
|
|
580
|
+
fixedVecs,
|
|
439
581
|
},
|
|
440
582
|
sourceFile,
|
|
441
583
|
files: programFiles(program, entryPath).map((f) => f.fileName),
|
|
442
584
|
imports: entryImports,
|
|
443
585
|
reporter,
|
|
444
|
-
checker
|
|
586
|
+
checker,
|
|
587
|
+
instantiate,
|
|
445
588
|
};
|
|
446
589
|
}
|
|
447
590
|
/** Every struct an argument type contains (transitively), for IDL layout decisions. */
|
|
@@ -479,7 +622,7 @@ export function resolveArgAggregate(node, scope, structs) {
|
|
|
479
622
|
if (scalar)
|
|
480
623
|
return scalar;
|
|
481
624
|
if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && !t.typeArguments?.length) {
|
|
482
|
-
const s = structs.get(t
|
|
625
|
+
const s = structs.get(structNameOf(t, scope));
|
|
483
626
|
if (s && s.size !== undefined && !s.vec)
|
|
484
627
|
return { kind: "struct", name: s.name };
|
|
485
628
|
}
|
|
@@ -515,6 +658,11 @@ export function typesolName(node, imports) {
|
|
|
515
658
|
export function resolveScalarType(node, scope) {
|
|
516
659
|
if (node.kind === ts.SyntaxKind.BooleanKeyword)
|
|
517
660
|
return { kind: "bool" };
|
|
661
|
+
if (scope.typeArgs && ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && !node.typeArguments) {
|
|
662
|
+
const t = scope.typeArgs.get(node.typeName.text);
|
|
663
|
+
if (t)
|
|
664
|
+
return t;
|
|
665
|
+
}
|
|
518
666
|
const n = typesolName(node, scope.imports);
|
|
519
667
|
if (n && INT_NAMES.includes(n))
|
|
520
668
|
return { kind: "int", name: n };
|
|
@@ -557,6 +705,9 @@ export function resolveValueType(node, scope, allowVec = false) {
|
|
|
557
705
|
return { kind: "dataEnumArg", name: node.typeName.text };
|
|
558
706
|
if (allowVec && typesolName(node, scope.imports) === "Vec") {
|
|
559
707
|
const elemNode = node.typeArguments?.[0];
|
|
708
|
+
// Vec<string>: Borsh Vec<String>, read as views of the instruction data.
|
|
709
|
+
if (elemNode?.kind === ts.SyntaxKind.StringKeyword)
|
|
710
|
+
return { kind: "vecArg", elem: { kind: "string" } };
|
|
560
711
|
const elem = elemNode && resolveScalarType(elemNode, scope);
|
|
561
712
|
if (elem)
|
|
562
713
|
return { kind: "vecArg", elem };
|
|
@@ -572,7 +723,7 @@ function numericLiteralArg(node) {
|
|
|
572
723
|
* Resolve every exported interface. Field types may name other interfaces, so names are
|
|
573
724
|
* collected first; sizes are computed after, rejecting cycles.
|
|
574
725
|
*/
|
|
575
|
-
function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings) {
|
|
726
|
+
function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs) {
|
|
576
727
|
const names = new Set(items.map((d) => d.decl.name.text));
|
|
577
728
|
const enumNames = new Set(aliases.map((a) => a.decl.name.text));
|
|
578
729
|
const structs = new Map();
|
|
@@ -628,6 +779,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
628
779
|
reporter.fail(targs[1], `array length must be 1..${MAX_ARRAY_LEN}`);
|
|
629
780
|
return { kind: "array", elem, len };
|
|
630
781
|
}
|
|
782
|
+
if (ext === "FixedVec") {
|
|
783
|
+
if (!targs[0])
|
|
784
|
+
reporter.fail(node, "FixedVec<T, N> needs an element type");
|
|
785
|
+
const elem = fieldType(targs[0]);
|
|
786
|
+
if (elem.kind === "string" || elem.kind === "vec")
|
|
787
|
+
reporter.fail(targs[0], "FixedVec elements must have a fixed size (use FixedString<N>)");
|
|
788
|
+
const cap = numericLiteralArg(targs[1]);
|
|
789
|
+
if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_ARRAY_LEN)
|
|
790
|
+
reporter.fail(node, `FixedVec<T, N> needs a literal capacity 1..${MAX_ARRAY_LEN}, e.g. FixedVec<u64, 16>`);
|
|
791
|
+
const t = { kind: "vec", elem, cap };
|
|
792
|
+
fixedVecs.set(fixedVecName(t), t);
|
|
793
|
+
return t;
|
|
794
|
+
}
|
|
631
795
|
if (ext === "FixedString") {
|
|
632
796
|
const cap = numericLiteralArg(targs[0]);
|
|
633
797
|
if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_STRING_CAP)
|
|
@@ -637,7 +801,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
637
801
|
}
|
|
638
802
|
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
|
|
639
803
|
return { kind: "struct", name: node.typeName.text };
|
|
640
|
-
reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, string (events only) or another struct");
|
|
804
|
+
reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, FixedVec<T, N>, Vec<T> (last), string (events only) or another struct");
|
|
641
805
|
};
|
|
642
806
|
for (const { decl, imports: fileImports } of items) {
|
|
643
807
|
scope = scopeFor(fileImports);
|
|
@@ -658,12 +822,17 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
658
822
|
if (problem)
|
|
659
823
|
reporter.fail(m.name, problem);
|
|
660
824
|
const type = fieldType(m.type);
|
|
661
|
-
if (type.kind === "vec" && m !== decl.members[decl.members.length - 1])
|
|
662
|
-
reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size)");
|
|
825
|
+
if (type.kind === "vec" && type.cap === undefined && m !== decl.members[decl.members.length - 1])
|
|
826
|
+
reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size); use FixedVec<T, N> for a list of at most N elsewhere");
|
|
663
827
|
fields.push({ name, type });
|
|
664
828
|
}
|
|
665
829
|
checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
|
|
666
830
|
const name = decl.name.text;
|
|
831
|
+
// `/** @layout borsh */`: stored as Anchor stores it.
|
|
832
|
+
const layoutTag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "layout");
|
|
833
|
+
const layoutText = typeof layoutTag?.comment === "string" ? layoutTag.comment.trim() : undefined;
|
|
834
|
+
if (layoutTag && layoutText !== "borsh" && layoutText !== "fixed")
|
|
835
|
+
reporter.fail(layoutTag, "@layout is borsh (Anchor's account bytes) or fixed (the default)");
|
|
667
836
|
structs.set(name, {
|
|
668
837
|
name,
|
|
669
838
|
fields,
|
|
@@ -671,6 +840,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
671
840
|
accountDiscriminator: discriminator(`account:${name}`),
|
|
672
841
|
eventDiscriminator: discriminator(`event:${name}`),
|
|
673
842
|
node: decl,
|
|
843
|
+
layout: layoutText === "borsh" ? "borsh" : "fixed",
|
|
674
844
|
});
|
|
675
845
|
});
|
|
676
846
|
}
|
|
@@ -766,8 +936,14 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
766
936
|
return undefined;
|
|
767
937
|
return 1 + e.payload;
|
|
768
938
|
}
|
|
769
|
-
case "vec":
|
|
770
|
-
|
|
939
|
+
case "vec": {
|
|
940
|
+
if (t.cap === undefined)
|
|
941
|
+
return 4; // the u32 length; elements live past the fixed part
|
|
942
|
+
const e = sizeOf(t.elem, at);
|
|
943
|
+
if (e === undefined)
|
|
944
|
+
reporter.fail(at, "FixedVec elements must have a fixed size");
|
|
945
|
+
return 4 + e * t.cap;
|
|
946
|
+
}
|
|
771
947
|
case "string":
|
|
772
948
|
return undefined;
|
|
773
949
|
case "fstring":
|
|
@@ -799,14 +975,18 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
799
975
|
state.set(s.name, "visiting");
|
|
800
976
|
let total = 0;
|
|
801
977
|
for (const f of s.fields) {
|
|
802
|
-
|
|
978
|
+
// In a @layout borsh struct a string (or a data enum carrying one) is held as a TS_Str view: a C size, no layout.
|
|
979
|
+
const view = s.layout === "borsh" && (f.type.kind === "string" || (f.type.kind === "dataEnum" && sizeOf(f.type, s.node) === undefined));
|
|
980
|
+
if (view)
|
|
981
|
+
s.viewsOnly = true;
|
|
982
|
+
const fsz = view ? (f.type.kind === "string" ? 16 : 1 + dataEnums.get(f.type.name).payload) : sizeOf(f.type, s.node);
|
|
803
983
|
total = total === undefined || fsz === undefined ? undefined : total + fsz;
|
|
804
984
|
}
|
|
805
985
|
s.size = total;
|
|
806
986
|
if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
|
|
807
987
|
reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
|
|
808
988
|
const last = s.fields.at(-1);
|
|
809
|
-
if (last?.type.kind === "vec") {
|
|
989
|
+
if (last?.type.kind === "vec" && last.type.cap === undefined) {
|
|
810
990
|
const elemSize = sizeOf(last.type.elem, s.node);
|
|
811
991
|
if (elemSize === undefined)
|
|
812
992
|
reporter.fail(s.node, "Vec elements must have a fixed size");
|
|
@@ -816,27 +996,118 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
816
996
|
};
|
|
817
997
|
for (const s of structs.values())
|
|
818
998
|
reporter.guard(() => visit(s));
|
|
999
|
+
for (const s of structs.values())
|
|
1000
|
+
if (s.layout === "borsh")
|
|
1001
|
+
reporter.guard(() => checkBorshLayout(s, structs, dataEnums, reporter));
|
|
819
1002
|
for (const e of dataEnums.values())
|
|
820
1003
|
reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
|
|
821
1004
|
return structs;
|
|
822
1005
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1006
|
+
const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
|
|
1007
|
+
/** Whether a local function reads a variable or parameter of a function around it (a closure). */
|
|
1008
|
+
function captures(l, localDecls, checker) {
|
|
1009
|
+
const fn = l.node;
|
|
1010
|
+
let found = false;
|
|
1011
|
+
const visit = (n) => {
|
|
1012
|
+
if (found)
|
|
1013
|
+
return;
|
|
1014
|
+
if (ts.isIdentifier(n)) {
|
|
1015
|
+
const sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n);
|
|
1016
|
+
for (const d of sym?.declarations ?? []) {
|
|
1017
|
+
if (!(ts.isVariableDeclaration(d) || ts.isParameter(d) || ts.isBindingElement(d)) || localDecls.has(d))
|
|
1018
|
+
continue;
|
|
1019
|
+
if (d.getSourceFile() !== fn.getSourceFile() || (d.pos >= fn.pos && d.end <= fn.end))
|
|
1020
|
+
continue;
|
|
1021
|
+
for (let p = d.parent; p; p = p.parent)
|
|
1022
|
+
if (isFunctionNode(p))
|
|
1023
|
+
found = true;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
ts.forEachChild(n, visit);
|
|
1027
|
+
};
|
|
1028
|
+
if (fn.body)
|
|
1029
|
+
visit(fn.body);
|
|
1030
|
+
return found;
|
|
1031
|
+
}
|
|
1032
|
+
/** Whether a function's body names one of `decls` (a closure it calls). */
|
|
1033
|
+
function references(l, decls, checker) {
|
|
1034
|
+
let found = false;
|
|
1035
|
+
const visit = (n) => {
|
|
1036
|
+
if (found)
|
|
1037
|
+
return;
|
|
1038
|
+
if (ts.isIdentifier(n))
|
|
1039
|
+
for (const d of checker.getSymbolAtLocation(n)?.declarations ?? [])
|
|
1040
|
+
if (decls.has(d))
|
|
1041
|
+
found = true;
|
|
1042
|
+
ts.forEachChild(n, visit);
|
|
1043
|
+
};
|
|
1044
|
+
if (l.node.body)
|
|
1045
|
+
visit(l.node.body);
|
|
1046
|
+
return found;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* A `@layout borsh` struct: its own fields may be FixedString (Borsh String), FixedVec (Borsh Vec) and Option; anything
|
|
1050
|
+
* nested must already be Borsh-identical (fixed-size scalars, Pubkeys, enums, arrays and structs of them).
|
|
1051
|
+
*/
|
|
1052
|
+
function checkBorshLayout(s, structs, dataEnums, reporter) {
|
|
1053
|
+
const model = { structs };
|
|
1054
|
+
const fixed = (t) => borshIdentical(t, model);
|
|
1055
|
+
for (const f of s.fields) {
|
|
1056
|
+
const t = f.type;
|
|
1057
|
+
const at = s.node.members.find((m) => m.name && ts.isIdentifier(m.name) && m.name.text === f.name) ?? s.node;
|
|
1058
|
+
if (fixed(t) || t.kind === "fstring" || t.kind === "string")
|
|
1059
|
+
continue;
|
|
1060
|
+
if (t.kind === "dataEnum") {
|
|
1061
|
+
// Borsh enum: tag, then the variant's fields; each a fixed-size value or a string.
|
|
1062
|
+
for (const v of dataEnums.get(t.name)?.variants ?? [])
|
|
1063
|
+
for (const vf of v.fields)
|
|
1064
|
+
if (!fixed(vf.type) && vf.type.kind !== "string")
|
|
1065
|
+
reporter.fail(at, `${t.name}.${v.name}.${vf.name} in a @layout borsh struct: enum variants hold fixed-size values and strings`);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (t.kind === "vec" && t.cap === undefined)
|
|
1069
|
+
reporter.fail(at, "a @layout borsh struct cannot end in a growable Vec<T>; use FixedVec<T, N> (Anchor's #[max_len(N)] Vec)");
|
|
1070
|
+
if (t.kind === "vec" && (fixed(t.elem) || t.elem.kind === "fstring"))
|
|
1071
|
+
continue;
|
|
1072
|
+
if (t.kind === "option" && fixed(t.inner))
|
|
1073
|
+
continue;
|
|
1074
|
+
if (t.kind === "struct" && structs.get(t.name)?.layout === "borsh")
|
|
1075
|
+
reporter.fail(at, `${t.name} is @layout borsh itself: only account structs take a layout; nest a struct without one`);
|
|
1076
|
+
reporter.fail(at, `${typeName(t)} in a @layout borsh struct: its fields may be fixed-size values, FixedString<N>, FixedVec<T, N> (of those or of FixedString) and Option<T>; nested structs and lists hold fixed-size values only`);
|
|
1077
|
+
}
|
|
1078
|
+
for (const other of structs.values())
|
|
1079
|
+
for (const f of other.fields) {
|
|
1080
|
+
const inner = f.type.kind === "array" || f.type.kind === "vec" ? f.type.elem : f.type.kind === "option" ? f.type.inner : f.type;
|
|
1081
|
+
if (inner.kind === "struct" && inner.name === s.name)
|
|
1082
|
+
reporter.error(s.node.name, `${s.name} is @layout borsh, so it is account data only; ${other.name}.${f.name} cannot hold one`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new Set()) {
|
|
1086
|
+
if (!fn.body)
|
|
1087
|
+
reporter.fail(fn, "helper functions need a body");
|
|
1088
|
+
if (fn.asteriskToken)
|
|
1089
|
+
reporter.fail(fn, "helper functions cannot be generators");
|
|
1090
|
+
if ((ts.getModifiers(fn) ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))
|
|
1091
|
+
reporter.fail(fn, "helper functions cannot be async");
|
|
828
1092
|
if (!fn.type)
|
|
829
|
-
reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
|
|
830
|
-
let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword
|
|
1093
|
+
reporter.fail(fn.name ?? fn, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
|
|
1094
|
+
let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword
|
|
1095
|
+
? undefined
|
|
1096
|
+
: fn.type.kind === ts.SyntaxKind.StringKeyword
|
|
1097
|
+
? { kind: "string" }
|
|
1098
|
+
: resolveScalarType(fn.type, scope);
|
|
831
1099
|
if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
|
|
832
|
-
const
|
|
1100
|
+
const sn = structNameOf(fn.type, scope);
|
|
1101
|
+
const s = sn ? structs.get(sn) : undefined;
|
|
833
1102
|
if (s && (s.size === undefined || s.vec))
|
|
834
1103
|
reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
|
|
835
1104
|
if (!s)
|
|
836
|
-
reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey or a
|
|
1105
|
+
reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, string, a struct or a type parameter");
|
|
837
1106
|
returns = { kind: "struct", name: s.name };
|
|
838
1107
|
}
|
|
839
|
-
const params = fn.parameters
|
|
1108
|
+
const params = fn.parameters
|
|
1109
|
+
.filter((_, i) => !skip.has(i))
|
|
1110
|
+
.map((p) => {
|
|
840
1111
|
if (!ts.isIdentifier(p.name) || !p.type || p.initializer || p.questionToken || p.dotDotDotToken)
|
|
841
1112
|
reporter.fail(p, "parameters need a plain name and an explicit type");
|
|
842
1113
|
// `p_<name>__n` carries a SignerSeeds count: "__" in a name could collide with it.
|
|
@@ -856,16 +1127,43 @@ function analyzeHelper(fn, reporter, scope, structs) {
|
|
|
856
1127
|
reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
|
|
857
1128
|
return { name: p.name.text, type: { kind: "account" }, account, node: p };
|
|
858
1129
|
});
|
|
859
|
-
return { name
|
|
1130
|
+
return { name, display, params, returns, node: fn, statements: bodyStatements(fn, !returns), imports: scope.imports };
|
|
1131
|
+
}
|
|
1132
|
+
/** A function's statements; an arrow's expression body is a `return expr;` (or `expr;` when it returns void). */
|
|
1133
|
+
function bodyStatements(fn, isVoid) {
|
|
1134
|
+
const body = fn.body;
|
|
1135
|
+
if (ts.isBlock(body))
|
|
1136
|
+
return body.statements;
|
|
1137
|
+
const f = ts.factory;
|
|
1138
|
+
const stmt = isVoid ? f.createExpressionStatement(body) : f.createReturnStatement(body);
|
|
1139
|
+
ts.setTextRange(stmt, body);
|
|
1140
|
+
stmt.parent = fn;
|
|
1141
|
+
return [stmt];
|
|
1142
|
+
}
|
|
1143
|
+
/** The helper (or generic) a call's callee names: a local function by its declaration, others by name. */
|
|
1144
|
+
export function calleeName(callee, decls, checker) {
|
|
1145
|
+
let sym = checker.getSymbolAtLocation(callee);
|
|
1146
|
+
if (sym && sym.flags & ts.SymbolFlags.Alias)
|
|
1147
|
+
sym = checker.getAliasedSymbol(sym);
|
|
1148
|
+
for (const d of sym?.declarations ?? []) {
|
|
1149
|
+
const name = decls.get(d);
|
|
1150
|
+
if (name)
|
|
1151
|
+
return name;
|
|
1152
|
+
}
|
|
1153
|
+
return undefined;
|
|
860
1154
|
}
|
|
861
1155
|
/** Helpers may call each other, but not recursively: SBF has a small fixed call depth. */
|
|
862
|
-
function checkRecursion(
|
|
1156
|
+
function checkRecursion(sources, decls, checker, reporter) {
|
|
863
1157
|
const calls = new Map();
|
|
864
|
-
|
|
1158
|
+
const byName = new Map(sources.map((s) => [s.name, s]));
|
|
1159
|
+
for (const h of sources) {
|
|
865
1160
|
const out = [];
|
|
866
1161
|
const visit = (n) => {
|
|
867
|
-
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)
|
|
868
|
-
|
|
1162
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
1163
|
+
const c = calleeName(n.expression, decls, checker);
|
|
1164
|
+
if (c)
|
|
1165
|
+
out.push(c);
|
|
1166
|
+
}
|
|
869
1167
|
ts.forEachChild(n, visit);
|
|
870
1168
|
};
|
|
871
1169
|
visit(h.node.body);
|
|
@@ -875,14 +1173,15 @@ function checkRecursion(helpers, reporter) {
|
|
|
875
1173
|
const walk = (name, path) => {
|
|
876
1174
|
if (state.get(name) === "done")
|
|
877
1175
|
return;
|
|
1176
|
+
const src = byName.get(name);
|
|
878
1177
|
if (state.get(name) === "visiting")
|
|
879
|
-
reporter.fail(
|
|
1178
|
+
reporter.fail(src.node.name ?? src.node, `recursion is not supported: ${[...path, name].map((n) => byName.get(n).display).join(" -> ")}`);
|
|
880
1179
|
state.set(name, "visiting");
|
|
881
1180
|
for (const c of calls.get(name) ?? [])
|
|
882
1181
|
walk(c, [...path, name]);
|
|
883
1182
|
state.set(name, "done");
|
|
884
1183
|
};
|
|
885
|
-
for (const name of
|
|
1184
|
+
for (const name of byName.keys())
|
|
886
1185
|
walk(name, []);
|
|
887
1186
|
}
|
|
888
1187
|
function analyzeInstruction(fn, reporter, scope, structs) {
|
|
@@ -895,7 +1194,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
895
1194
|
reporter.fail(fn, "instructions cannot be async or generators");
|
|
896
1195
|
let returns;
|
|
897
1196
|
if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword) {
|
|
898
|
-
returns = resolveScalarType(fn.type, scope);
|
|
1197
|
+
returns = fn.type.kind === ts.SyntaxKind.StringKeyword ? { kind: "string" } : resolveScalarType(fn.type, scope);
|
|
899
1198
|
const s = !returns && ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments
|
|
900
1199
|
? structs.get(fn.type.typeName.text)
|
|
901
1200
|
: undefined;
|
|
@@ -908,7 +1207,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
908
1207
|
returns = { kind: "struct", name: s.name };
|
|
909
1208
|
}
|
|
910
1209
|
if (!returns)
|
|
911
|
-
reporter.fail(fn.type, "instructions return void, one integer, boolean or
|
|
1210
|
+
reporter.fail(fn.type, "instructions return void, one integer, boolean, Pubkey or string, or a fixed-size struct (sent as return data)");
|
|
912
1211
|
}
|
|
913
1212
|
const accounts = [];
|
|
914
1213
|
const args = [];
|
|
@@ -948,7 +1247,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
948
1247
|
const inner = unwrapConstraints(typeNode, reporter, imports, constraints);
|
|
949
1248
|
const kind = resolveAccountKind(inner, reporter, imports, structs);
|
|
950
1249
|
if (optional &&
|
|
951
|
-
(["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1250
|
+
(["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1251
|
+
((kind.kind === "token" || kind.kind === "mint") && kind.init)))
|
|
952
1252
|
reporter.fail(p, "Init, InitIfNeeded, Zeroed, PythPriceUpdate, EventAuthority, SelfProgram and ProgramData accounts cannot be optional");
|
|
953
1253
|
accounts.push({ name: p.name.text, index: accounts.length, kind, node: p, constraints, ...(optional ? { optional } : {}) });
|
|
954
1254
|
});
|
|
@@ -961,10 +1261,11 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
961
1261
|
// Constraints refer to other parameters by name: check them now that all are known.
|
|
962
1262
|
for (const a of accounts) {
|
|
963
1263
|
const c = a.constraints;
|
|
964
|
-
if (a.kind.kind === "token" && a.kind.init &&
|
|
965
|
-
reporter.fail(a.node, 'Init<TokenAccount> / InitIfNeeded<TokenAccount> create associated token accounts: wrap them in Ata<…, "owner", "mint">');
|
|
966
|
-
if (a.kind.kind === "token" && a.kind.init && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
|
|
1264
|
+
if (a.kind.kind === "token" && a.kind.init && c.ata && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
|
|
967
1265
|
reporter.fail(a.node, "an associated token account created here is checked against Ata<…>'s owner and mint; drop HasOne/Seeds/Address/Owner/Executable");
|
|
1266
|
+
// Token accounts and mints created in the body are checked there (seeds, mint, authorities).
|
|
1267
|
+
if ((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init && !c.ata && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
|
|
1268
|
+
reporter.fail(a.node, `${a.name}.create(...) takes the seeds, mint and authorities; drop HasOne/Seeds/Address/Owner/Executable`);
|
|
968
1269
|
if (!c.hasOne.length && !c.seeds && !c.closeTo && !c.ata)
|
|
969
1270
|
continue;
|
|
970
1271
|
const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
|
|
@@ -1038,7 +1339,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1038
1339
|
const pushes = (() => {
|
|
1039
1340
|
let found = false;
|
|
1040
1341
|
const visit = (n) => {
|
|
1041
|
-
|
|
1342
|
+
// Vec.push(value, payer) grows the account; FixedVec.push(value) does not.
|
|
1343
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push" && n.arguments.length === 2)
|
|
1042
1344
|
found = true;
|
|
1043
1345
|
ts.forEachChild(n, visit);
|
|
1044
1346
|
};
|
|
@@ -1155,6 +1457,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1155
1457
|
// Any account (helpers that accept every kind, e.g. generated CPI helpers).
|
|
1156
1458
|
case "AccountMeta":
|
|
1157
1459
|
return { kind: "info", mut: false };
|
|
1460
|
+
case "SystemAccount":
|
|
1461
|
+
return { kind: "info", mut: false, systemOwned: true };
|
|
1158
1462
|
case "TokenAccount":
|
|
1159
1463
|
return { kind: "token", mut: false };
|
|
1160
1464
|
case "Mint":
|
|
@@ -1212,9 +1516,11 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1212
1516
|
case "Init": {
|
|
1213
1517
|
if (typesolName(inner(), imports) === "TokenAccount")
|
|
1214
1518
|
return { kind: "token", mut: true, init: "create" };
|
|
1519
|
+
if (typesolName(inner(), imports) === "Mint")
|
|
1520
|
+
return { kind: "mint", mut: true, init: "create" };
|
|
1215
1521
|
const s = structOf(inner());
|
|
1216
1522
|
if (!s)
|
|
1217
|
-
reporter.fail(node, "Init<T> needs a struct declared in this program");
|
|
1523
|
+
reporter.fail(node, "Init<T> needs a struct declared in this program, TokenAccount or Mint");
|
|
1218
1524
|
if (s.size + 8 > MAX_CPI_ALLOCATION)
|
|
1219
1525
|
reporter.fail(node, `${s.name} needs ${s.size + 8} bytes but a program can create at most ${MAX_CPI_ALLOCATION} through Init<T>; have the client allocate it and take it as Zeroed<${s.name}>`);
|
|
1220
1526
|
return { kind: "init", struct: s };
|
|
@@ -1222,6 +1528,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1222
1528
|
case "InitIfNeeded": {
|
|
1223
1529
|
if (typesolName(inner(), imports) === "TokenAccount")
|
|
1224
1530
|
return { kind: "token", mut: true, init: "ifNeeded" };
|
|
1531
|
+
if (typesolName(inner(), imports) === "Mint")
|
|
1532
|
+
return { kind: "mint", mut: true, init: "ifNeeded" };
|
|
1225
1533
|
const s = structOf(inner());
|
|
1226
1534
|
if (!s)
|
|
1227
1535
|
reporter.fail(node, "InitIfNeeded<T> needs a struct declared in this program");
|
|
@@ -1235,6 +1543,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1235
1543
|
reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
|
|
1236
1544
|
if (s.vec)
|
|
1237
1545
|
reporter.fail(node, `${s.name} ends in a Vec; Zeroed<T> is for fixed-size (large) accounts`);
|
|
1546
|
+
if (s.layout === "borsh")
|
|
1547
|
+
reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
|
|
1238
1548
|
return { kind: "zeroed", struct: s };
|
|
1239
1549
|
}
|
|
1240
1550
|
case "Mut": {
|
|
@@ -1247,6 +1557,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1247
1557
|
return { kind: "signer", mut: true };
|
|
1248
1558
|
if (innerName === "AccountInfo" || innerName === "AccountMeta")
|
|
1249
1559
|
return { kind: "info", mut: true };
|
|
1560
|
+
if (innerName === "SystemAccount")
|
|
1561
|
+
return { kind: "info", mut: true, systemOwned: true };
|
|
1250
1562
|
if (innerName === "TokenAccount")
|
|
1251
1563
|
return { kind: "token", mut: true };
|
|
1252
1564
|
if (innerName === "Mint")
|