@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.
@@ -1,5 +1,6 @@
1
1
  import ts from "typescript";
2
- import { resolveScalarType } from "../analyze.js";
2
+ import { calleeName, resolveScalarType } from "../analyze.js";
3
+ import { ANCHOR_ERRORS } from "../anchor-errors.js";
3
4
  import { decodeAddress } from "../../keys.js";
4
5
  import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
5
6
  import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
@@ -7,6 +8,8 @@ import { ExpressionEmitter } from "./expressions.js";
7
8
  /**
8
9
  * Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
9
10
  */
11
+ /** List methods taking a callback (or a value) that expand into a loop. */
12
+ const LIST_METHODS = new Set(["forEach", "some", "every", "find", "findIndex", "reduce", "includes", "indexOf"]);
10
13
  export class CallEmitter extends ExpressionEmitter {
11
14
  call(node, topLevel) {
12
15
  const callee = node.expression;
@@ -126,14 +129,26 @@ export class CallEmitter extends ExpressionEmitter {
126
129
  }
127
130
  case "mulDiv":
128
131
  case "mulDivCeil": {
129
- // a * b / c with a 128-bit intermediate (u64), rounding down or up; aborts if the result does not fit.
132
+ // a * b / c with a double-width intermediate (128-bit for u64, 256-bit for u128), rounding down or up;
133
+ // aborts if the result does not fit. All three are u64, or all u128 (literals take the others' type).
130
134
  if (args.length !== 3)
131
- this.fail(node, `${builtin}(a, b, c) takes three u64 values`);
132
- const [x, y, z] = this.lowerInOrder(args, [U64, U64, U64]);
135
+ this.fail(node, `${builtin}(a, b, c) takes three u64 or three u128 values`);
136
+ const vals = [];
137
+ for (const a of args) {
138
+ const mark = this.lines.length;
139
+ const v = this.expr(a);
140
+ for (let j = 0; j < vals.length; j++)
141
+ vals[j] = this.settle(vals[j], mark);
142
+ vals.push(v);
143
+ }
144
+ const typed = vals.find((v) => v.literal === undefined);
145
+ const wide = typed?.type.kind === "int" && typed.type.name === "u128";
146
+ const ty = wide ? { kind: "int", name: "u128" } : U64;
147
+ const [x, y, z] = vals.map((v, i) => this.expect(v.literal !== undefined ? this.literal(v.literal, ty, args[i]) : v, ty, args[i]));
133
148
  const t = this.tmp("md");
134
- this.line(`uint64_t ${t};`);
135
- this.line(`TS_TRY(ts_mul_div(${x.c}, ${y.c}, ${z.c}, ${builtin === "mulDivCeil"}, &${t}));`);
136
- return { c: t, type: U64 };
149
+ this.line(`${wide ? "unsigned __int128" : "uint64_t"} ${t};`);
150
+ this.line(`TS_TRY(ts_mul_div${wide ? "_u128" : ""}(${x.c}, ${y.c}, ${z.c}, ${builtin === "mulDivCeil"}, &${t}));`);
151
+ return { c: t, type: ty };
137
152
  }
138
153
  case "isqrt": {
139
154
  if (args.length !== 1)
@@ -219,7 +234,18 @@ export class CallEmitter extends ExpressionEmitter {
219
234
  this.line(`TS_TRY(ts_t22_transfer_fee(${m}, ${amt.c}, &${t}));`);
220
235
  return { c: t, type: U64 };
221
236
  }
237
+ case "isTransferring": {
238
+ // TransferHookAccount (15) on a Token-2022 token account: set only while Token-2022 runs a transfer of it.
239
+ if (args.length !== 1)
240
+ this.fail(node, "isTransferring(tokenAccount) takes the source token account");
241
+ const m = this.anyAccount(args[0], "the token account");
242
+ this.line(`TS_TRY(ts_t22_layout(${m}, 2));`);
243
+ const off = this.tmp("eo");
244
+ this.line(`uint64_t ${off}, ${off}l;`);
245
+ return { c: `(ts_t22_ext(${m}, 15, &${off}, &${off}l) && ${off}l >= 1 && ${m}->data[${off}] != 0)`, type: BOOL };
246
+ }
222
247
  case "transferHookProgram":
248
+ case "transferHookAuthority":
223
249
  case "permanentDelegate":
224
250
  case "mintCloseAuthority": {
225
251
  // Token-2022 mint extensions holding an optional Pubkey: TransferHook (14, program at +32),
@@ -228,7 +254,7 @@ export class CallEmitter extends ExpressionEmitter {
228
254
  this.fail(node, `${builtin}(mint) takes the mint`);
229
255
  const m = this.anyAccount(args[0], "the mint");
230
256
  this.line(`TS_TRY(ts_t22_layout(${m}, 1));`);
231
- const [type, field] = builtin === "transferHookProgram" ? [14, 32] : builtin === "permanentDelegate" ? [12, 0] : [3, 0];
257
+ const [type, field] = builtin === "transferHookProgram" ? [14, 32] : builtin === "transferHookAuthority" ? [14, 0] : builtin === "permanentDelegate" ? [12, 0] : [3, 0];
232
258
  this.model.options.set("pubkey", PUBKEY);
233
259
  const t = this.tmp("ext");
234
260
  this.line(`${optC(PUBKEY)} ${t};`);
@@ -434,11 +460,22 @@ export class CallEmitter extends ExpressionEmitter {
434
460
  }
435
461
  if (builtin)
436
462
  this.fail(callee, `${builtin} cannot be called here`);
463
+ if (ts.isIdentifier(callee)) {
464
+ const local = this.lookup(callee.text);
465
+ if (local?.closure)
466
+ return this.expandClosure(local.closure, node.arguments, node);
467
+ }
437
468
  const helper = this.helperOf(node);
438
469
  if (helper)
439
470
  return this.callHelper(node, helper);
440
471
  if (ts.isPropertyAccessExpression(callee)) {
441
472
  const method = callee.name.text;
473
+ // xs.some(…) and friends on a Vec, FixedArray, FixedVec or Vec argument: a loop with the callback expanded in it.
474
+ if (LIST_METHODS.has(method)) {
475
+ const l = ts.isIdentifier(callee.expression) ? this.lookup(callee.expression.text) : undefined;
476
+ if (l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(callee.expression))
477
+ return this.listMethod(node, method);
478
+ }
442
479
  // Vec<T> instruction arguments are read-only views.
443
480
  if (ts.isIdentifier(callee.expression)) {
444
481
  const l = this.lookup(callee.expression.text);
@@ -446,6 +483,10 @@ export class CallEmitter extends ExpressionEmitter {
446
483
  if (method !== "get" || args.length !== 1)
447
484
  this.fail(node, "Vec arguments are read-only: use .get(i) and .length");
448
485
  const elem = l.type.elem;
486
+ if (elem.kind === "string") {
487
+ const idx = this.index(args[0], `(uint64_t)${l.c}.len`);
488
+ return { c: `(((const TS_Str *)${l.c}.ptr)[${idx}])`, type: STRING };
489
+ }
449
490
  if (!isScalar(elem))
450
491
  this.fail(node, `read a field of the element: ${callee.expression.text}.get(i).field`);
451
492
  const idx = this.index(args[0], `(uint64_t)${l.c}.len`);
@@ -506,8 +547,10 @@ export class CallEmitter extends ExpressionEmitter {
506
547
  // Program accounts used through their methods (systemProgram.transfer, tokenProgram.*…) may be optional.
507
548
  if (target?.optional)
508
549
  this.assertUsable(target, callee.expression);
509
- if (target?.kind.kind === "token" && target.kind.init && method === (target.kind.init === "create" ? "create" : "createIfNeeded"))
510
- return this.createAta(node, target, topLevel);
550
+ if ((target?.kind.kind === "token" || target?.kind.kind === "mint") &&
551
+ target.kind.init &&
552
+ method === (target.kind.init === "create" ? "create" : "createIfNeeded"))
553
+ return target.kind.kind === "token" && target.constraints.ata ? this.createAta(node, target, topLevel) : this.createToken(node, target, topLevel);
511
554
  if (target?.kind.kind === "init" && method === "create")
512
555
  return this.create(node, target, topLevel);
513
556
  if (target?.kind.kind === "initIfNeeded" && method === "createIfNeeded")
@@ -574,7 +617,7 @@ export class CallEmitter extends ExpressionEmitter {
574
617
  if (info.cap !== undefined)
575
618
  return; // a FixedVec has its room already
576
619
  if (!info.resizable || !v.account)
577
- this.fail(node, "only Vecs of declared Mut<...> / Init<...> accounts can grow or shrink");
620
+ this.fail(node, "only Vecs of Mut<...> / Init<...> accounts can grow or shrink");
578
621
  this.line(`TS_TRY(ts_resize(params, ${payer}, a_${v.account.name}, 8 + ${info.fixed} + (${newLen}) * ${info.elemSize}, entry_${v.account.name}));`);
579
622
  };
580
623
  switch (method) {
@@ -899,8 +942,37 @@ export class CallEmitter extends ExpressionEmitter {
899
942
  }
900
943
  return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
901
944
  }
945
+ /** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
946
+ helperGrows(body, seen = new Set()) {
947
+ if (seen.has(body))
948
+ return false;
949
+ seen.add(body);
950
+ let found = false;
951
+ const visit = (n) => {
952
+ if (found)
953
+ return;
954
+ if (ts.isCallExpression(n)) {
955
+ // Vec.push(value, payer) grows the account; FixedVec.push(value) does not.
956
+ if (ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push" && n.arguments.length === 2)
957
+ found = true;
958
+ else if (ts.isIdentifier(n.expression)) {
959
+ const name = calleeName(n.expression, this.model.helperDecls, this.checker);
960
+ const callee = name ? (this.model.generics.get(name)?.node ?? this.model.helpers.get(name)?.node) : undefined;
961
+ if (callee && this.helperGrows(callee, seen))
962
+ found = true;
963
+ }
964
+ }
965
+ ts.forEachChild(n, visit);
966
+ };
967
+ visit(body);
968
+ return found;
969
+ }
902
970
  callHelper(node, h) {
903
- if (node.arguments.length !== h.params.length)
971
+ if (this.ix && !this.ix.accounts.some((a) => a.kind.kind === "system" && !a.optional) && h.params.some((p) => p.account) && this.helperGrows(h.node))
972
+ this.fail(node, `${h.display}() grows a Vec (push): the instruction needs a SystemProgram parameter to top up rent`);
973
+ // Function arguments were bound when the helper was instantiated: they have no C parameter.
974
+ const argNodes = h.fnArgIndexes ? node.arguments.filter((_, i) => !h.fnArgIndexes.includes(i)) : node.arguments;
975
+ if (argNodes.length !== h.params.length)
904
976
  this.fail(node, `${h.display}() takes ${h.params.length} argument(s)`);
905
977
  // Arguments are evaluated left to right: settle earlier ones when a later one runs statements.
906
978
  // A helper that takes accounts may change account data, so account text it gets is copied first.
@@ -908,7 +980,7 @@ export class CallEmitter extends ExpressionEmitter {
908
980
  const values = [];
909
981
  const pieces = [];
910
982
  h.params.forEach((p, i) => {
911
- const n = node.arguments[i];
983
+ const n = argNodes[i];
912
984
  const mark = this.lines.length;
913
985
  const raw = this.helperArg(h, p, n);
914
986
  for (let j = 0; j < values.length; j++)
@@ -922,8 +994,8 @@ export class CallEmitter extends ExpressionEmitter {
922
994
  });
923
995
  // A helper that takes a writable struct account may replace anything stored in it (see assertNotAliased).
924
996
  for (const [i, p] of h.params.entries())
925
- if (p.account && p.account.kind === "account" && p.account.mut && ts.isIdentifier(node.arguments[i]))
926
- this.assertNotAliased([node.arguments[i].text], node, h.display);
997
+ if (p.account && p.account.kind === "account" && p.account.mut && ts.isIdentifier(argNodes[i]))
998
+ this.assertNotAliased([argNodes[i].text], node, h.display);
927
999
  const args = ["params", ...pieces.map((x) => (typeof x === "number" ? (values[x].type.kind === "pubkey" ? `*(${values[x].c})` : values[x].c) : x))];
928
1000
  if (!h.returns) {
929
1001
  this.line(`TS_TRY(fn_${h.name}(${args.join(", ")}));`);
@@ -937,6 +1009,11 @@ export class CallEmitter extends ExpressionEmitter {
937
1009
  this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
938
1010
  return { c: `(&${t})`, type: { kind: "structRef", struct: h.returns.name, writable: true } };
939
1011
  }
1012
+ if (h.returns.kind === "string") {
1013
+ this.line(`TS_Str ${t};`);
1014
+ this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
1015
+ return { c: t, type: STRING };
1016
+ }
940
1017
  this.line(`${scalarC(h.returns)} ${t};`);
941
1018
  this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
942
1019
  return h.returns.kind === "pubkey" ? { c: `(&${t})`, type: h.returns } : { c: t, type: h.returns };
@@ -951,6 +1028,8 @@ export class CallEmitter extends ExpressionEmitter {
951
1028
  // A @layout borsh account goes with the struct it is being worked on as.
952
1029
  if (p.account.kind === "account" && p.account.struct.layout === "borsh")
953
1030
  return `a_${acct.name}, d_${acct.name}`;
1031
+ if (p.account.kind === "account" && p.account.mut && p.account.struct.vec)
1032
+ return `a_${acct.name}, entry_${acct.name}`;
954
1033
  return `a_${acct.name}`;
955
1034
  }
956
1035
  switch (p.type.kind) {
@@ -1013,6 +1092,13 @@ export class CallEmitter extends ExpressionEmitter {
1013
1092
  this.fail(n, "pass signer([...seeds with the bump]) or a SignerSeeds parameter");
1014
1093
  }
1015
1094
  errorRef(node) {
1095
+ // `AnchorError.X`: one of Anchor's own codes (not one of this program's errors, so not in the IDL).
1096
+ if (ts.isPropertyAccessExpression(node) && this.exportName(node.expression) === "AnchorError") {
1097
+ const code = ANCHOR_ERRORS[node.name.text];
1098
+ if (code === undefined)
1099
+ this.fail(node.name, `AnchorError has no ${node.name.text}`);
1100
+ return { code, name: node.name.text, msg: node.name.text };
1101
+ }
1016
1102
  if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
1017
1103
  const def = this.model.errorEnums.get(node.expression.text)?.get(node.name.text);
1018
1104
  if (def)
@@ -26,6 +26,26 @@ export declare abstract class CpiEmitter extends CallEmitter {
26
26
  protected describeSeed(el: ts.Expression): IdlSeed | undefined;
27
27
  protected recordPda(account: AccountParam, idl: IdlSeed[] | undefined): void;
28
28
  protected create(node: ts.CallExpression, target: AccountParam, topLevel: boolean, ifNeeded?: boolean): Value;
29
+ /**
30
+ * `vault.create(payer, seeds | null, mint, authority)` on `Init<TokenAccount>` and `mint.create(payer, seeds | null,
31
+ * decimals, authority, freezeAuthority | null)` on `Init<Mint>` (Anchor's `init` with `token::` / `mint::`
32
+ * constraints): a PDA of this program (or, with `null`, a new keypair account that signs), created owned by the
33
+ * instruction's token program and initialized. `createIfNeeded` on `InitIfNeeded<…>` does that unless the account
34
+ * exists, and then checks it is that PDA, owned by the token program, and has that mint and owner / those
35
+ * authorities and decimals (Anchor's init_if_needed checks). create returns the bump (0 for a keypair),
36
+ * createIfNeeded whether it created the account.
37
+ */
38
+ protected createToken(node: ts.CallExpression, target: AccountParam, topLevel: boolean): Value;
39
+ /**
40
+ * The extensions in `mint.create(…, { … })` (Anchor's `extensions::…` constraints): their values are evaluated now,
41
+ * in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
42
+ * InitializeMint2) run after the account is created, sized for exactly these extensions.
43
+ */
44
+ protected mintExtensions(lit: ts.ObjectLiteralExpression, tp: string, acct: string, ifNeeded?: boolean): {
45
+ space: number;
46
+ lines: string[];
47
+ checks: string[];
48
+ };
29
49
  /**
30
50
  * `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
31
51
  * (Anchor's `init` / `init_if_needed` with `associated_token::…`). create always calls the ATA program's Create, which
@@ -47,6 +67,7 @@ export declare abstract class CpiEmitter extends CallEmitter {
47
67
  }, tail?: {
48
68
  from: string;
49
69
  to: string;
70
+ asPassed?: boolean;
50
71
  }): void;
51
72
  protected signerSeeds(node: ts.Expression | undefined, authority: AccountParam, at: ts.Node): {
52
73
  name: string;
@@ -87,6 +108,12 @@ export declare abstract class CpiEmitter extends CallEmitter {
87
108
  protected bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
88
109
  /** Bubblegum V2 (trees on mpl-account-compression with mpl-noop; optional Metaplex Core collections). */
89
110
  protected bubblegumV2(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
111
+ /** Object literal properties by name, refusing others. */
112
+ protected literalProps(o: ts.ObjectLiteralExpression, allowed: string[], what: string): Map<string, ts.Expression>;
113
+ /** Vec<Creator> (address, verified, share) from a literal list; shares add up to 100 when literal. */
114
+ protected bubblegumCreators(creators: ts.Expression | undefined): MplField[];
115
+ /** UpdateArgs: every field optional (left out or null: unchanged). */
116
+ protected bubblegumUpdateArgs(n: ts.Expression): MplField[];
90
117
  /** `acct.key` of an account parameter, as an expression to serialize. */
91
118
  protected keyOf(n: ts.Expression): ts.Expression;
92
119
  /**
@@ -117,6 +144,10 @@ export declare abstract class CpiEmitter extends CallEmitter {
117
144
  authority: string;
118
145
  program: string;
119
146
  }): undefined;
147
+ /** emit<T> of a @layout borsh struct: built like account data, then packed as Anchor serializes it. */
148
+ private emitBorshEvent;
149
+ /** Discriminator + `body`, logged (emit) or self-invoked (emitCpi). */
150
+ private sendEvent;
120
151
  /** Structs reachable through the fields of struct `name` (not including itself unless it recurses). */
121
152
  protected nestedStructs(name: string): Set<string>;
122
153
  /** Structs whose IDL description is the fixed layout: account data, arguments, and everything nested in them. */