@typesol/cli 0.18.0 → 0.20.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.
@@ -396,8 +396,6 @@ export class CpiEmitter extends CallEmitter {
396
396
  const fixedArgs = isMint ? (extArg ? 6 : 5) : 4;
397
397
  if (a.length !== fixedArgs && a.length !== fixedArgs + 1)
398
398
  this.fail(node, usage);
399
- if (extArg && ifNeeded)
400
- this.fail(extArg, "createIfNeeded does not take extensions yet: create the mint with create(…)");
401
399
  const payer = this.accountParam(a[0], "the payer");
402
400
  if (payer.kind.kind !== "signer" || !payer.kind.mut)
403
401
  this.fail(a[0], "the payer must be a Mut<Signer>");
@@ -438,6 +436,7 @@ export class CpiEmitter extends CallEmitter {
438
436
  };
439
437
  let mint;
440
438
  const extLines = [];
439
+ const extChecks = [];
441
440
  let mintSpace = "TS_MINT_LEN";
442
441
  if (isMint) {
443
442
  take(a[2], U8);
@@ -447,9 +446,10 @@ export class CpiEmitter extends CallEmitter {
447
446
  if (extArg) {
448
447
  if (programs[0].kind.kind === "tokenProgram" && programs[0].kind.flavor === "token")
449
448
  this.fail(extArg, "mint extensions are Token-2022's; use Token2022Program or TokenInterface");
450
- const ext = this.mintExtensions(extArg, tp, acct);
449
+ const ext = this.mintExtensions(extArg, tp, acct, ifNeeded);
451
450
  mintSpace = String(ext.space);
452
451
  extLines.push(...ext.lines);
452
+ extChecks.push(...ext.checks);
453
453
  }
454
454
  }
455
455
  else {
@@ -474,6 +474,8 @@ export class CpiEmitter extends CallEmitter {
474
474
  this.line(` TS_TRY(ts_token_existing(params, ${acct}, ${tp}, ${seeds.name}, ${seeds.count}, &${b}));`);
475
475
  this.line(` TS_TRY(ts_check_mint(${acct}));`);
476
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}`);
477
479
  this.line(`}`);
478
480
  }
479
481
  else
@@ -510,7 +512,7 @@ export class CpiEmitter extends CallEmitter {
510
512
  * in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
511
513
  * InitializeMint2) run after the account is created, sized for exactly these extensions.
512
514
  */
513
- mintExtensions(lit, tp, acct) {
515
+ mintExtensions(lit, tp, acct, ifNeeded = false) {
514
516
  const optKey = { kind: "option", inner: PUBKEY };
515
517
  this.model.options.set("pubkey", PUBKEY);
516
518
  // name: [extension type length, initializer tag, the fields an object value takes ([] for a plain value)]
@@ -676,8 +678,60 @@ export class CpiEmitter extends CallEmitter {
676
678
  }
677
679
  }
678
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
+ }
679
733
  // Base account (165, the mint padded to an account's size), the account type byte, then each TLV entry.
680
- return { space: 165 + 1 + space, lines };
734
+ return { space: 165 + 1 + space, lines, checks };
681
735
  }
682
736
  /**
683
737
  * `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
@@ -1847,6 +1901,58 @@ export class CpiEmitter extends CallEmitter {
1847
1901
  this.cpi(p, metas, data.buf, data.len, seedsAt(13));
1848
1902
  return undefined;
1849
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
+ }
1850
1956
  }
1851
1957
  return this.bubblegumV2(node, bg, method);
1852
1958
  }
@@ -2128,7 +2234,7 @@ export class CpiEmitter extends CallEmitter {
2128
2234
  this.cpi(p, metas, data.buf, data.len, seedsAt(proofAt + 1), proofRange(a[proofAt]));
2129
2235
  return undefined;
2130
2236
  }
2131
- this.fail(node.expression, `Bubblegum supports createTree, mint, createTreeV2, mintV2, transferV2, burnV2, ${Object.keys(BUBBLEGUM_OPS).join(", ")}, not ${method}`);
2237
+ this.fail(node.expression, `Bubblegum supports createTree, mint, mintToCollection, createTreeV2, mintV2, transferV2, burnV2, ${Object.keys(BUBBLEGUM_OPS).join(", ")}, not ${method}`);
2132
2238
  }
2133
2239
  /** Object literal properties by name, refusing others. */
2134
2240
  literalProps(o, allowed, what) {
@@ -23,6 +23,25 @@ export declare abstract class ExpressionEmitter extends StorageEmitter {
23
23
  /** Whether TypeScript sees this expression as a string (decides `+` between concatenation and arithmetic). */
24
24
  protected looksLikeString(node: ts.Expression): boolean;
25
25
  /** A string view of a string, integer or boolean (for concatenation and templates). */
26
+ /**
27
+ * A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
28
+ * TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
29
+ * first, as JS reads them then.
30
+ */
31
+ protected templatePieces(node: ts.TemplateExpression): {
32
+ text: string;
33
+ arr?: undefined;
34
+ n?: undefined;
35
+ } | {
36
+ text?: undefined;
37
+ arr: string;
38
+ n: number;
39
+ };
40
+ /**
41
+ * The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
42
+ * literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
43
+ */
44
+ protected constantText(e: ts.Expression): string | undefined;
26
45
  protected toText(node: ts.Expression): string;
27
46
  /** C statement writing the decimal text of an integer into the TS_Str `out`. */
28
47
  protected textOfInt(name: IntName, c: string, out: string): string;
@@ -1,4 +1,5 @@
1
1
  import ts from "typescript";
2
+ import { base58 } from "../../keys.js";
2
3
  import { calleeName, resolveValueType } from "../analyze.js";
3
4
  import { INT_INFO, unsignedC, optionKey, sameType, typeName } from "../model.js";
4
5
  import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC, vecCount } from "./shared.js";
@@ -151,22 +152,14 @@ export class ExpressionEmitter extends StorageEmitter {
151
152
  return { c: `ts_str((const uint8_t *)${cString(node.text)}, ${len})`, type: STRING, stringLiteral: node.text };
152
153
  }
153
154
  if (ts.isTemplateExpression(node)) {
154
- // `a${x}b${y}c` -> concatenations on the heap
155
- let acc = this.tmp("tpl");
156
- const head = node.head.text;
157
- this.line(`TS_Str ${acc} = ts_str((const uint8_t *)${cString(head)}, ${Buffer.byteLength(head, "utf8")});`);
158
- for (const span of node.templateSpans) {
159
- for (const piece of [
160
- this.toText(span.expression),
161
- `ts_str((const uint8_t *)${cString(span.literal.text)}, ${Buffer.byteLength(span.literal.text, "utf8")})`,
162
- ]) {
163
- const next = this.tmp("tpl");
164
- this.line(`TS_Str ${next};`);
165
- this.line(`TS_TRY(ts_str_concat(${acc}, ${piece}, &${next}));`);
166
- acc = next;
167
- }
168
- }
169
- return { c: acc, type: STRING };
155
+ // `a${x}b${y}c`: one join on the heap (or a literal when every piece is known at compile time).
156
+ const pieces = this.templatePieces(node);
157
+ if (pieces.text !== undefined)
158
+ return { c: `ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})`, type: STRING, stringLiteral: pieces.text };
159
+ const t = this.tmp("tpl");
160
+ this.line(`TS_Str ${t};`);
161
+ this.line(`TS_TRY(ts_str_join(${pieces.arr}, ${pieces.n}, &${t}));`);
162
+ return { c: t, type: STRING };
170
163
  }
171
164
  if (node.kind === ts.SyntaxKind.NullKeyword)
172
165
  return { c: "", type: BOOL, isNull: true };
@@ -430,6 +423,68 @@ export class ExpressionEmitter extends StorageEmitter {
430
423
  return isText(this.checker.getTypeAtLocation(node));
431
424
  }
432
425
  /** A string view of a string, integer or boolean (for concatenation and templates). */
426
+ /**
427
+ * A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
428
+ * TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
429
+ * first, as JS reads them then.
430
+ */
431
+ templatePieces(node) {
432
+ const parts = [];
433
+ const lit = (text) => {
434
+ if (!text)
435
+ return;
436
+ const last = parts.at(-1);
437
+ if (last?.text !== undefined)
438
+ last.text += text;
439
+ else
440
+ parts.push({ text });
441
+ };
442
+ lit(node.head.text);
443
+ for (const span of node.templateSpans) {
444
+ const known = this.constantText(span.expression);
445
+ if (known !== undefined)
446
+ lit(known);
447
+ else {
448
+ const mark = this.lines.length;
449
+ const c = this.toText(span.expression);
450
+ for (const p of parts)
451
+ if (p.c !== undefined)
452
+ p.c = this.settle({ c: p.c, type: STRING }, mark).c;
453
+ parts.push({ c });
454
+ }
455
+ lit(span.literal.text);
456
+ }
457
+ if (parts.every((p) => p.text !== undefined))
458
+ return { text: parts.map((p) => p.text).join("") };
459
+ const strOf = (text) => `ts_str((const uint8_t *)${cString(text)}, ${Buffer.byteLength(text, "utf8")})`;
460
+ const arr = this.tmp("parts");
461
+ this.line(`TS_Str ${arr}[${parts.length}] = {${parts.map((p) => (p.text !== undefined ? strOf(p.text) : p.c)).join(", ")}};`);
462
+ return { arr, n: parts.length };
463
+ }
464
+ /**
465
+ * The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
466
+ * literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
467
+ */
468
+ constantText(e) {
469
+ if (ts.isParenthesizedExpression(e))
470
+ return this.constantText(e.expression);
471
+ if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))
472
+ return e.text;
473
+ if (e.kind === ts.SyntaxKind.TrueKeyword)
474
+ return "true";
475
+ if (e.kind === ts.SyntaxKind.FalseKeyword)
476
+ return "false";
477
+ if (ts.isBigIntLiteral(e))
478
+ return BigInt(e.text.slice(0, -1).replace(/_/g, "")).toString();
479
+ if (ts.isIdentifier(e) && !this.lookup(e.text)) {
480
+ const k = this.model.constants.get(e.text);
481
+ if (k && typeof k.value === "string")
482
+ return k.value;
483
+ if (k && typeof k.value === "object")
484
+ return base58(Uint8Array.from(k.value.pubkey));
485
+ }
486
+ return undefined;
487
+ }
433
488
  toText(node) {
434
489
  const v = this.expr(node);
435
490
  if (v.type.kind === "string")
@@ -371,15 +371,17 @@ export class FunctionEmitter extends StatementEmitter {
371
371
  const k = a.kind.kind;
372
372
  if (c.hasOne.length && k !== "account" && k !== "token" && k !== "mint")
373
373
  this.reporter.fail(a.node, "HasOne applies to existing accounts (Account<T> / Mut<T>), TokenAccount and Mint");
374
- for (const f of c.hasOne) {
374
+ for (const entry of c.hasOne) {
375
+ // "field=param" (TokenOf / MintOf) or a field checked against the parameter of the same name (HasOne).
376
+ const [f, p = f] = entry.split("=");
375
377
  if (k === "token")
376
378
  out.push(f === "mint"
377
- ? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${f}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${f}");`
378
- : ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${f}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${f}");`);
379
+ ? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${p}");`
380
+ : ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${p}");`);
379
381
  else if (k === "mint") {
380
382
  const at = f === "mintAuthority" ? 0 : 46;
381
383
  const code = f === "mintAuthority" ? "TS_ERR_MINT_AUTHORITY" : "TS_ERR_FREEZE_AUTHORITY";
382
- out.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${f}->key)) TS_FAIL(${code}, "TypeSOL: ${f} is not the ${f} of ${a.name}");`);
384
+ out.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${p}->key)) TS_FAIL(${code}, "TypeSOL: ${p} is not the ${f} of ${a.name}");`);
383
385
  }
384
386
  else
385
387
  out.push(` if (!ts_key_eq(&d_${a.name}->${f}, &a_${f}->key)) TS_FAIL(TS_ERR_CONSTRAINT, "TypeSOL: ${a.name}.${f} must be the ${f} account");`);
@@ -842,7 +842,24 @@ export class StatementEmitter extends CpiEmitter {
842
842
  this.line(`TS_Str ${cur};`);
843
843
  this.line(`TS_TRY(ts_str_copy(${current.c}, &${cur}));`);
844
844
  }
845
+ // A local grows in place when it is the newest heap string (a loop of `s += …` copies each piece once); a
846
+ // template on the right is appended piece by piece.
847
+ const local = cur === current.c && place.write("X") === `${cur} = X`;
848
+ const bare = (e) => (ts.isParenthesizedExpression(e) ? bare(e.expression) : e);
849
+ const rhsNode = bare(valueNode);
850
+ if (local && ts.isTemplateExpression(rhsNode)) {
851
+ const pieces = this.templatePieces(rhsNode);
852
+ if (pieces.text !== undefined)
853
+ this.line(`TS_TRY(ts_str_append(&${cur}, ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})));`);
854
+ else
855
+ this.line(`TS_TRY(ts_str_append_parts(&${cur}, ${pieces.arr}, ${pieces.n}));`);
856
+ return;
857
+ }
845
858
  const rhs = this.toText(valueNode);
859
+ if (local) {
860
+ this.line(`TS_TRY(ts_str_append(&${cur}, ${rhs}));`);
861
+ return;
862
+ }
846
863
  const t = this.tmp("cat");
847
864
  this.line(`TS_Str ${t};`);
848
865
  this.line(`TS_TRY(ts_str_concat(${cur}, ${rhs}, &${t}));`);
package/docs/client.md CHANGED
@@ -25,23 +25,45 @@ ledgerbook.errorCode(e); // 6002
25
25
  // Events written by `emit`, from a transaction's log messages
26
26
  for (const ev of ledgerbook.parseEvents(logs)) if (ev.name === "Filed") ev.data.amount;
27
27
 
28
- // Return data of an instruction that returns a value
28
+ // Return data of an instruction that returns a value, or the value itself by simulation (Anchor's `.view()`)
29
29
  ledgerbook.returns.total(returnDataBytes); // bigint
30
+ await ledgerbook.views.total(rpc, signer, { book }, { status: Status.Approved }); // bigint, nothing sent
31
+
32
+ // Send and confirm (one transaction, signers in the accounts sign too), or simulate
33
+ const signature = await ledgerbook.sendInstructions({ rpc, rpcSubscriptions }, signer, [create, file]);
34
+ const { err, logs, unitsConsumed, returnData } = await ledgerbook.simulate(rpc, signer, [file]);
35
+
36
+ // Every Book of the program, or those with given bytes at an offset (after the 8-byte discriminator)
37
+ const books = await ledgerbook.accounts.Book.fetchAll(rpc); // [{ address, data }]
38
+ const several = await ledgerbook.accounts.Book.fetchMultiple(rpc, [a, b]); // (Book | null)[]
39
+
40
+ // Live updates, until the AbortSignal aborts
41
+ for await (const book of ledgerbook.accounts.Book.watch(rpcSubscriptions, address, { abortSignal })) render(book);
42
+ for await (const { signature, event } of ledgerbook.watchEvents(rpcSubscriptions, { abortSignal })) show(event);
43
+
44
+ // Remaining accounts go after the instruction's own (Anchor's `.remainingAccounts([...])`)
45
+ await ledgerbook.approveMany({ approver: signer, book }, { indices: [0n, 1n] }, [{ address: extra, role: AccountRole.WRITABLE }]);
46
+
47
+ // Events of a fetched transaction (getTransaction, json): log events and emitCpi events from inner instructions
48
+ ledgerbook.eventsOf(await rpc.getTransaction(signature, { maxSupportedTransactionVersion: 0 }).send());
30
49
  ```
31
50
 
32
- Send instructions the usual kit way (`createTransactionMessage` … `signTransactionMessageWithSigners`). Signer
33
- accounts take a `TransactionSigner`, which kit uses to sign; an `Address` works too when you sign separately.
51
+ Signer accounts take a `TransactionSigner`, which kit uses to sign; an `Address` works too when you sign separately.
52
+ Kit's own transaction functions work with the instructions as well; `sendInstructions` is that pipeline in one call.
34
53
 
35
54
  ## What the client contains
36
55
 
37
56
  | | |
38
57
  | --- | --- |
39
58
  | `<program>.<instruction>(accounts, args)` | Builds the `Instruction`. Accounts with a fixed address and PDAs whose seeds are constants, arguments or other accounts' keys are optional and filled in; pass one to override. An optional account passed as `null` is left out (sent as the program id, the convention TypeSOL and Anchor share); left undefined, it is filled in like any other. |
40
- | `<program>.accounts.<Name>` | `decode(bytes)`, `encode(value)`, `fetch(rpc, address)`, `fetchMaybe(rpc, address)`, `discriminator`. `decode` checks the discriminator. |
59
+ | `<program>.<instruction>(accounts, args, remaining?)` | The same, with remaining accounts appended in order (kit `AccountMeta`s). |
60
+ | `<program>.accounts.<Name>` | `decode(bytes)`, `encode(value)`, `fetch(rpc, address)`, `fetchMaybe(rpc, address)`, `fetchMultiple(rpc, addresses)`, `fetchAll(rpc, filters?)` (getProgramAccounts by discriminator, plus `{ offset, bytes }` filters), `watch(rpcSubscriptions, address, { abortSignal })`, `discriminator`. `decode` checks the discriminator. |
41
61
  | `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives. |
42
62
  | `<program>.returns.<instruction>(bytes)` | Decodes return data. |
43
- | `<program>.errors`, `errorCode(e)`, `isError(e, name)` | The program's errors (6000+), and for TypeSOL programs also TypeSOL's runtime errors and the Anchor framework errors it uses (2001, 2006, 3007, …). |
44
- | `<program>.parseEvents(logs)` | Events from `Program data:` log lines. |
63
+ | `<program>.views.<instruction>(rpc, feePayer, accounts, args)` | Simulates an instruction that returns a value and decodes it (throws with the logs if it fails). |
64
+ | `<program>.sendInstructions(connection, feePayer, instructions)`, `simulate(rpc, feePayer, instructions)` | One transaction sent and confirmed (kit's sendAndConfirm), or simulated without signatures (error, logs, units, this program's return data). |
65
+ | `<program>.errors`, `errorCode(e)`, `isError(e, name)` | The program's errors (6000+), and for TypeSOL programs also every Anchor framework error (`AnchorError` and the constraints raise them) and TypeSOL's runtime errors. |
66
+ | `<program>.parseEvents(logs)`, `parseCpiEvents(data)`, `eventsOf(tx)`, `watchEvents(rpcSubscriptions, { abortSignal })` | Events from `Program data:` log lines, from `emitCpi` inner instructions (bytes or base58), from a fetched transaction (both), or live from log subscriptions. |
45
67
  | Types and codecs | Every defined type as a TypeScript type (`Book`, the decoded value) and an input type (`BookArgs`, which also accepts `number` for 64- and 128-bit integers), with a kit codec (`getBookCodec()`). |
46
68
 
47
69
  Types follow the program's TypeScript: `u64`/`i64`/`u128`/`i128` are `bigint` (inputs accept `number` too), smaller
@@ -65,5 +87,6 @@ Anchor IDLs are refused with a hint to convert them (`anchor idl convert`).
65
87
  for each.
66
88
  - **Left out of a client generated from an Anchor IDL**, with a note: generic types and anything using them, and
67
89
  zero-copy (`bytemuck`) accounts.
68
- - **Not done yet**: events sent with `emitCpi` (they travel in inner instructions, not logs), and a LiteSVM loader for
69
- tests (use `svm.addProgramFromFile(ledgerbook.address, soPath)`).
90
+ - **Not done yet**: a LiteSVM loader for tests (use `svm.addProgramFromFile(ledgerbook.address, soPath)`, or
91
+ `typesol/testing`). `watchEvents` reads log events; `emitCpi` events are in inner instructions, which log
92
+ subscriptions do not carry (use `eventsOf` on the fetched transaction).
package/docs/language.md CHANGED
@@ -11,7 +11,7 @@ The full reference. For a tour, start with the [README](../README.md).
11
11
  | Integers | `u8` `u16` `u32` `u64` `u128` `i8` `i16` `i32` `i64` `i128` — bigint literals (`1n`), every `+ - * / % **` (and `+=` … `**=`) checked (`**` aborts on a negative exponent, as JS throws); `& \| ^` on equal types, `~` within the width, `<<` / `>>` checked (amount below the width, `<<` may not lose bits) |
12
12
  | Integer helpers | `mulDiv(a, b, c)` / `mulDivCeil` (u64 through a 128-bit product, u128 through a 256-bit one), `isqrt(x)` (up to u128), `min(a, b)`, `max(a, b)` |
13
13
  | Other scalars | `boolean`, `Pubkey` (compare with `===` or `.equals`) |
14
- | Errors | `export enum MyError { Name = "message" }` + `require(cond, MyError.Name)` → codes 6000+ |
14
+ | Errors | `export enum MyError { Name = "message" }` + `require(cond, MyError.Name)` → codes 6000+; `require(cond, AnchorError.RequireKeysEqViolated)` fails with one of Anchor's own codes (2502 here; every anchor-lang `ErrorCode`) |
15
15
  | Casts | `u8(x)`, `u64(x)`, … abort if the value does not fit |
16
16
  | Logging | `log("text")`, `log(value)`, `log(pubkey)` |
17
17
  | Arrays | `scores: FixedArray<u64, 8>` in account data — `.get(i)`, `.set(i, v)`, `.length`, bounds-checked; elements can be structs or strings |
@@ -36,7 +36,7 @@ The full reference. For a tour, start with the [README](../README.md).
36
36
  | Constants | top-level `const SEED = "vault"`, `const FEE: u16 = 30n`, `const ON = true` — inlined, usable as seeds (and described in the IDL) |
37
37
  | Data enums | `export type Action = { kind: "Deposit"; amount: u64 } \| { kind: "Pause" }` — `a.kind === "Deposit"`, `switch (a.kind)`, TypeScript narrowing picks the variant (checked again at runtime, 5112); Borsh enums in arguments/events, fixed `{ tag, payload }` in account data. A variant may hold a `string` (`{ kind: "Say"; text: string }`): such an enum works in arguments, events and locals (Borsh, as Anchor encodes it) but not in account data |
38
38
  | Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
39
- | Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full) |
39
+ | Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full): a template is joined in one allocation, text known at compile time (literals, string and Pubkey constants) is folded in, and `s += …` grows the newest heap string in place |
40
40
  | `switch` | on integers (128-bit ones too), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
41
41
  | Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
42
42
  | Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds)` (fully verified, feed-matched, fresh); raw `update.price / conf / exponent / publishTime`; `switchboardValue(feed: SwitchboardFeed, expectedFeed, maxStaleSlots, decimals)` (the feed must be the expected one: anyone can create feeds) (mainnet program unless `SwitchboardFeed<"devnet">`) |
@@ -67,7 +67,7 @@ The full reference. For a tour, start with the [README](../README.md).
67
67
  | `Init<T>` | writable; `create(payer, seeds)` derives the PDA, verifies it, funds rent, writes the discriminator (up to 10 KiB); `create(payer, null)` creates a new keypair account that signs the transaction instead (3010 if it did not; a signer in the IDL); a third argument is Anchor's `space = …` (discriminator included, at least 8 + the struct's size) |
68
68
  | `InitIfNeeded<T>` | `const created = acct.createIfNeeded(payer, seeds)` — creates once; afterwards re-derives the PDA and checks owner, discriminator and size (Anchor `init_if_needed`); `acct.pdaBump` after create |
69
69
  | `HasOne<A, "owner">`, `Seeds<A, ["vault", "@owner"], "bump">`, `CloseTo<A, "owner">` | declarative constraints like Anchor's `has_one`, `seeds`/`bump` and `close`: checked before the body (2001 / 2006), close runs when the instruction succeeds; `"@name"` = account key or argument bytes, `"@acct.field"` = a Pubkey, integer or unit enum field of a struct account parameter (also the account's own: `["offer", "@maker", "@offer.id"]`), other strings are literals; recorded as `relations` / `pda` in the IDL. `acct.pdaBump` is the bump (Anchor `ctx.bumps.acct`): the stored field when one is named, else the canonical bump the check found — use it to sign as the PDA |
70
- | `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name |
70
+ | `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name; `TokenOf<TokenAccount, "mintParam", "authorityParam">` / `MintOf<Mint, "authorityParam", "freezeParam">` name the parameters, as Anchor's `token::mint = …` / `mint::authority = …` do (`never` leaves one out) |
71
71
  | `Ata<TokenAccount, "owner", "mint">` | the account is the associated token account of those parameters (2009; ATA `pda` in the IDL) |
72
72
  | `Ata<Init<TokenAccount>, "owner", "mint">`, `Ata<InitIfNeeded<TokenAccount>, "owner", "mint">` | Anchor `init` / `init_if_needed` with `associated_token::…`: `acct.create(payer)` creates the ATA (fails if it exists); `const created = acct.createIfNeeded(payer)` creates it only when missing, otherwise checks it is that ATA (2009). Then it is a `TokenAccount` of that mint and owner (2014 / 2015). Needs `AssociatedTokenProgram`, `SystemProgram` and a token program parameter; call it at the top level before using the account |
73
73
  | `Address<A, "4Nd1…">` | exact address (2012; `address` in the IDL) |
@@ -77,7 +77,7 @@ The full reference. For a tour, start with the [README](../README.md).
77
77
  | `ProgramData` | this program's ProgramData (address derived from the program id, upgradeable-loader owner and state checked); `upgradeAuthority`, `deploySlot` — gate admin setup on `programData.upgradeAuthority === admin.key` |
78
78
  | `EventAuthority`, `SelfProgram` | Anchor's `["__event_authority"]` PDA, and this program's own address, for `emitCpi` |
79
79
  | `Zeroed<T>` | large accounts (up to 10 MiB) the client allocated with System `CreateAccount`: owned by this program, big enough, rent-exempt, discriminator still zero — then initialized; like Anchor's zero-copy `zero` |
80
- | `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (exactly one token program parameter, plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
80
+ | `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (exactly one token program parameter, plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. With `createIfNeeded` an existing mint must carry groupPointer / groupMemberPointer / metadataPointer / closeAuthority / permanentDelegate / transferHook with those keys (Anchor's 2024–2039). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
81
81
  | `Init<TokenAccount>`, `InitIfNeeded<TokenAccount>` (no `Ata<…>`) | Anchor `init` / `init_if_needed` with `token::mint` / `token::authority`: `vault.create(payer, ["vault", mint.key], mint, authority)` (the authority may be `vault.key` itself) creates a token account at that PDA (or a keypair account with `null`), sized for the mint's extensions under Token-2022. `createIfNeeded` checks an existing one has that mint and owner (2014 / 2015) |
82
82
  | `TokenAccount` / `Mut<TokenAccount>` | owned by SPL Token or Token-2022 (extensions allowed, multisig accounts refused), initialized; `mint`, `owner`, `amount`, `delegate`, `delegatedAmount`, `isFrozen`, `isNative`, `closeAuthority`, `tokenProgram` |
83
83
  | `Mint` / `Mut<Mint>` | owned by SPL Token or Token-2022, initialized; `decimals`, `supply`, `mintAuthority`, `freezeAuthority`, `tokenProgram` |
@@ -159,8 +159,7 @@ Growable `Vec`s anywhere but the last account field (use `FixedVec<T, N>`), Fixe
159
159
  the next instruction), `?.` on anything but optional accounts and Option<struct> fields, `!` on anything but an Option
160
160
  or an optional account, Core's external plugin adapters (oracles, app data, lifecycle hooks: use `invoke`), closures
161
161
  stored or returned (they are called or passed to list methods), arrow literals and closures as helper arguments (pass a
162
- named function), `map` / `filter` (they would build a new list), mint extensions on `InitIfNeeded<Mint>` (create the
163
- mint with `create(…, extensions)`), floating point (by design).
162
+ named function), `map` / `filter` (they would build a new list), floating point (by design).
164
163
  They are rejected at compile time rather than half-supported.
165
164
 
166
165
  `Init.create` handles a PDA address someone pre-funded: it tops up rent, then allocates and assigns instead of
@@ -49,7 +49,7 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
49
49
  | `Program<'info, System>` | `SystemProgram` |
50
50
  | `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
51
51
  | `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
52
- | `token::mint = m, token::authority = a` | `HasOne<TokenAccount, "mint" \| "owner">` against parameters with those names |
52
+ | `token::mint = m, token::authority = a` | `TokenOf<TokenAccount, "m", "a">` (2014 / 2015), or `HasOne<TokenAccount, "mint" \| "owner">` when the parameters have those names; `mint::authority = a, mint::freeze_authority = f` on an existing mint: `MintOf<Mint, "a", "f">` (2016 / 2017) |
53
53
  | `extensions::metadata_pointer::authority = a, …` (with `mint::…`) | `mint.create(p, …, { metadataPointer: { authority: a.key, metadataAddress: mint.key }, … })`: transferHook, closeAuthority, permanentDelegate, nonTransferable, groupPointer, groupMemberPointer and more |
54
54
  | `#[account(init, payer = p, seeds = […], bump, mint::decimals = 6, mint::authority = a, mint::freeze_authority = f)]` | `Init<Mint>`, then `const bump = mint.create(p, […], 6n, a.key, f.key)` (`null` seeds: a keypair mint; `InitIfNeeded<Mint>` + `createIfNeeded`) |
55
55
  | `#[account(init, payer = p, seeds = […], bump, token::mint = m, token::authority = a)]` | `Init<TokenAccount>`, then `vault.create(p, […], m, a.key)` (`InitIfNeeded<TokenAccount>` + `createIfNeeded`) |
@@ -91,6 +91,8 @@ Anchor instruction that takes or returns a struct with `String` / `Vec` / `Optio
91
91
  | `-> Result<u64>` / a struct | `: u64` / a struct: sent as return data, listed in the IDL |
92
92
  | `#[error_code] enum MyError { #[msg("…")] X }` | `export enum MyError { X = "…" }` (codes from 6000, in order) |
93
93
  | `require!(c, MyError::X)`, `err!(MyError::X)` | `require(c, MyError.X)`, `require(false, MyError.X)` |
94
+ | `require_keys_eq!(a, b)`, `err!(ErrorCode::AccountDidNotDeserialize)` (Anchor's own codes) | `require(a === b, AnchorError.RequireKeysEqViolated)`, `require(false, AnchorError.AccountDidNotDeserialize)` |
95
+ | `init_if_needed` + `extensions::…` on a mint | `mint.createIfNeeded(…, { metadataPointer: {…}, … })`: an existing mint is checked as Anchor checks it (2024–2039) |
94
96
  | `require_keys_eq!(a, b)` | `require(a === b, …)` |
95
97
  | `a.checked_add(b).ok_or(…)?` | `a + b` (every operation is checked; overflow aborts with 5100) |
96
98
  | `emit!(Ev { … })` / `emit_cpi!` | `emit<Ev>({ … })` / `emitCpi<Ev>({ … }, eventAuthority, self)` |