@typesol/cli 0.23.0 → 0.24.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.
@@ -27,8 +27,9 @@ or declare `const HEAP_SIZE_KIB = 64n` and add a ComputeBudget `requestHeapFrame
27
27
  `solana-test-validator` (Agave 3.1 included) rejects a System instruction that the current Program Metadata program
28
28
  uses. Publishing works on mainnet-beta. The error shows the failing transaction's last log lines.
29
29
 
30
- **"instruction X needs N bytes of stack; SBF gives each function 4096"** — every function on Solana gets a 4 KiB
31
- stack frame, and going past it corrupts memory, so the build refuses it. Large structs and arrays copied into typed
30
+ **"instruction X (with the helpers inlined into it) needs N bytes of stack; SBPF v0 gives each function 4096"** — under
31
+ SBPF v0 (the default) every function gets a fixed 4 KiB stack frame, and going past it corrupts memory, so the build
32
+ refuses it. SBPF v1 and v2 (`--sbpf v1` / `v2`) grow frames dynamically, so this limit does not apply there. Large structs and arrays copied into typed
32
33
  locals (`const q: Big = acct.big`) are the usual cause: bind them without a type (`const q = acct.big`, a reference
33
34
  into the account data) or move the work into helpers. Helpers are often inlined into their instruction, so their
34
35
  locals count toward its frame.
package/lib/typesol.d.ts CHANGED
@@ -8,6 +8,10 @@
8
8
  * Integer types are all `bigint` to TypeScript. The compiler tracks the declared
9
9
  * width itself, rejects `number`, and checks every arithmetic operation for
10
10
  * overflow at runtime.
11
+ *
12
+ * Integer operators: + - * / % are checked (overflow and division by zero abort). & | ^ work on equal types;
13
+ * << aborts when bits would be lost or the amount is not below the width; >> is arithmetic on signed types;
14
+ * ~ flips the bits within the type's width.
11
15
  */
12
16
 
13
17
  export type u8 = bigint;
@@ -27,10 +31,6 @@ export interface Pubkey {
27
31
  equals(other: Pubkey): boolean;
28
32
  }
29
33
 
30
- /**
31
- * A fixed-length array stored inline in account data, e.g. `scores: FixedArray<u64, 8>`.
32
- * Indexes are bounds-checked; out of range aborts the instruction.
33
- */
34
34
  /**
35
35
  * Callback methods of Vec, FixedArray and FixedVec. Each expands into a loop over the elements (as `list.get(i)` gives
36
36
  * them: structs by reference) with the arrow written at the call; it may read and write the function's variables.
@@ -53,13 +53,19 @@ export interface ListMethods<T> {
53
53
  /**
54
54
  * The elements `fn` maps them to (integers, booleans, Pubkeys or enums). Use it right away (another list method,
55
55
  * `.length`, `for … of`: one loop, nothing stored) or store it (`const ys = xs.map(…)`: a FixedVec on the stack, as
56
- * big as `xs`; from a `Vec`, annotate it: `const ys: FixedVec<u64, 16> = …`).
56
+ * big as `xs`; from a `Vec`, annotate it: `const ys: FixedVec<u64, 16> = …`). Stored with a `FixedVec<Struct, N>` or
57
+ * `FixedVec<FixedString<N>, M>` annotation it gives structs (`(o) => ({ qty: o.qty })`) or text. A stored list holds
58
+ * at most 2 KiB; one element more than it holds aborts (5104).
57
59
  */
58
60
  map<U>(fn: (x: T, index: u64) => U): FixedVec<U, never>;
59
61
  /** The elements `fn` holds for, used or stored like `map`'s; callbacks after it get the index among those. */
60
62
  filter(fn: (x: T, index: u64) => boolean): FixedVec<T, never>;
61
63
  }
62
64
 
65
+ /**
66
+ * A fixed-length array stored inline in account data, e.g. `scores: FixedArray<u64, 8>`.
67
+ * Indexes are bounds-checked; out of range aborts the instruction.
68
+ */
63
69
  export interface FixedArray<T, N extends number> extends ListMethods<T> {
64
70
  readonly length: u64;
65
71
  get(index: bigint): T;
@@ -82,15 +88,17 @@ export type FixedString<N extends number> = string & { readonly __capacity?: N }
82
88
  export declare function byteLength(value: string): u64;
83
89
 
84
90
  /**
85
- * An optional scalar: `best: Option<u64>` (same as `u64 | null`). Compare with `=== null` / `!== null`;
91
+ * An optional value: `best: Option<u64>` (same as `u64 | null`), also of a struct (`home: Option<Point>`). Compare with `=== null` / `!== null`;
86
92
  * TypeScript narrows it, and using an empty option where a value is needed aborts (5108).
87
93
  * In account data it takes a fixed 1 + size(T) bytes.
88
94
  */
89
95
  export type Option<T> = T | null;
90
96
 
91
97
  /**
92
- * A growable list. As the last field of an account struct it is stored Borsh-style (u32 length + elements)
93
- * and the account is resized as it grows. As an instruction argument it is a read-only view.
98
+ * A growable list. As an account struct field it is stored Borsh-style (u32 length + elements) and the account is
99
+ * resized as it grows. An account may have any number of them, declared anywhere: they are stored after the
100
+ * fixed-size fields, in the order declared (in a `@layout borsh` struct, a Vec must be the last field). As an
101
+ * instruction argument it is a read-only view.
94
102
  */
95
103
  export interface Vec<T> extends ListMethods<T> {
96
104
  readonly length: u64;
@@ -108,8 +116,8 @@ export interface Vec<T> extends ListMethods<T> {
108
116
 
109
117
  /**
110
118
  * A list stored inline with room for N elements: a u32 length, then N slots (Anchor `Vec<T>` with `#[max_len(N)]`, but
111
- * always 4 + N × size(T) bytes). Unlike `Vec<T>` it can be any field, any number of times, and in nested structs;
112
- * `push` fails (5104) when it is full. Write a whole one with `fixedVec([a, b])`.
119
+ * always 4 + N × size(T) bytes). Unlike `Vec<T>` its size is fixed: it keeps its place among the fields, can be in
120
+ * nested structs, and `push` takes no payer; `push` fails (5104) when it is full. Write a whole one with `fixedVec([a, b])`.
113
121
  */
114
122
  export interface FixedVec<T, N extends number> extends ListMethods<T> {
115
123
  readonly length: u64;
@@ -611,11 +619,6 @@ export interface SelfProgram extends AccountMeta {
611
619
  */
612
620
  export declare function emitCpi<T>(event: T, eventAuthority: EventAuthority, self: SelfProgram): void;
613
621
 
614
- /**
615
- * Metaplex Core (CoREEN…), address checked. Assets and collections without plugins; `null` leaves an optional
616
- * account out. Accounts that sign (the new asset/collection, authorities) are keypair Signers, or PDAs of this
617
- * program covered by `signerSeeds`.
618
- */
619
622
  /**
620
623
  * Who manages a Metaplex Core plugin: `"owner"`, `"updateAuthority"`, `"none"` (nobody: fixed), or an address. Left out,
621
624
  * the plugin type's default (the owner for owner-managed plugins, the update authority for the others).
@@ -650,7 +653,49 @@ export type CorePlugin = (
650
653
  | { kind: "PermanentFreezeExecute"; frozen: boolean }
651
654
  ) & { authority?: CorePluginAuthority };
652
655
  export type CorePluginKind = CorePlugin["kind"] | "Groups";
656
+ /** When a Core lifecycle event reaches an external plugin, and what the plugin may do about it. */
657
+ export interface CoreLifecycleCheck {
658
+ event: "create" | "transfer" | "burn" | "update" | "execute";
659
+ canListen?: boolean;
660
+ canApprove?: boolean;
661
+ canReject?: boolean;
662
+ }
663
+ /** An account Core passes to an external plugin: one it already has, or an address. */
664
+ export type CoreExtraAccount =
665
+ | { kind: "program" | "collection" | "owner" | "recipient" | "asset"; isSigner?: boolean; isWritable?: boolean }
666
+ | { kind: "address"; address: Pubkey; isSigner?: boolean; isWritable?: boolean };
667
+ /**
668
+ * A Metaplex Core external plugin adapter, written as an object literal: data a third party writes (`AppData`), an
669
+ * account that approves or rejects lifecycle events (`Oracle`), or a program called on them (`LifecycleHook`); the
670
+ * `Linked` forms go on a collection, for each of its assets. `authority` manages the adapter itself.
671
+ */
672
+ export type CoreExternalPlugin = (
673
+ | { kind: "AppData" | "LinkedAppData"; dataAuthority: CorePluginAuthority; schema?: "binary" | "json" | "msgpack" }
674
+ | {
675
+ kind: "Oracle";
676
+ baseAddress: Pubkey;
677
+ checks: CoreLifecycleCheck[];
678
+ baseAddressConfig?: CoreExtraAccount;
679
+ resultsOffset?: "none" | "anchor" | u64;
680
+ }
681
+ | {
682
+ kind: "LifecycleHook" | "LinkedLifecycleHook";
683
+ hookedProgram: Pubkey;
684
+ checks: CoreLifecycleCheck[];
685
+ extraAccounts?: CoreExtraAccount[];
686
+ dataAuthority?: CorePluginAuthority;
687
+ schema?: "binary" | "json" | "msgpack";
688
+ }
689
+ ) & { authority?: CorePluginAuthority };
690
+ /** Which external plugin: an AppData by its data authority, an Oracle by its base address, a LifecycleHook by its program. */
691
+ export type CoreExternalPluginKey =
692
+ { kind: "AppData" | "LinkedAppData"; dataAuthority: CorePluginAuthority } | { kind: "Oracle" | "LifecycleHook" | "LinkedLifecycleHook"; address: Pubkey };
653
693
 
694
+ /**
695
+ * Metaplex Core (CoREEN…), address checked. Assets and collections, with plugins and external plugin adapters; `null`
696
+ * leaves an optional account out. Accounts that sign (the new asset/collection, authorities) are keypair Signers, or
697
+ * PDAs of this program covered by `signerSeeds`.
698
+ */
654
699
  export interface MplCoreProgram extends AccountMeta {
655
700
  readonly __mpl_core: void;
656
701
  /**
@@ -753,6 +798,61 @@ export interface MplCoreProgram extends AccountMeta {
753
798
  kind: CorePluginKind,
754
799
  signerSeeds?: Seed[] | SignerSeeds,
755
800
  ): void;
801
+ /** AddExternalPluginAdapterV1, signed by the update authority as `authority` (`null`: the payer). */
802
+ addExternalPlugin(
803
+ asset: AccountMeta,
804
+ collection: AccountMeta | null,
805
+ payer: Mut<Signer>,
806
+ authority: AccountMeta | null,
807
+ systemProgram: SystemProgram,
808
+ plugin: CoreExternalPlugin,
809
+ signerSeeds?: Seed[] | SignerSeeds,
810
+ ): void;
811
+ removeExternalPlugin(
812
+ asset: AccountMeta,
813
+ collection: AccountMeta | null,
814
+ payer: Mut<Signer>,
815
+ authority: AccountMeta | null,
816
+ systemProgram: SystemProgram,
817
+ key: CoreExternalPluginKey,
818
+ signerSeeds?: Seed[] | SignerSeeds,
819
+ ): void;
820
+ /** WriteExternalPluginAdapterDataV1, signed by the plugin's data authority: `data` replaces what it holds (`null` clears it). */
821
+ writeExternalPluginData(
822
+ asset: AccountMeta,
823
+ collection: AccountMeta | null,
824
+ payer: Mut<Signer>,
825
+ authority: AccountMeta | null,
826
+ systemProgram: SystemProgram,
827
+ key: CoreExternalPluginKey,
828
+ data: string | Vec<u8> | null,
829
+ signerSeeds?: Seed[] | SignerSeeds,
830
+ ): void;
831
+ addCollectionExternalPlugin(
832
+ collection: AccountMeta,
833
+ payer: Mut<Signer>,
834
+ authority: AccountMeta | null,
835
+ systemProgram: SystemProgram,
836
+ plugin: CoreExternalPlugin,
837
+ signerSeeds?: Seed[] | SignerSeeds,
838
+ ): void;
839
+ removeCollectionExternalPlugin(
840
+ collection: AccountMeta,
841
+ payer: Mut<Signer>,
842
+ authority: AccountMeta | null,
843
+ systemProgram: SystemProgram,
844
+ key: CoreExternalPluginKey,
845
+ signerSeeds?: Seed[] | SignerSeeds,
846
+ ): void;
847
+ writeCollectionExternalPluginData(
848
+ collection: AccountMeta,
849
+ payer: Mut<Signer>,
850
+ authority: AccountMeta | null,
851
+ systemProgram: SystemProgram,
852
+ key: CoreExternalPluginKey,
853
+ data: string | Vec<u8> | null,
854
+ signerSeeds?: Seed[] | SignerSeeds,
855
+ ): void;
756
856
  /** TransferV1, signed by the owner (or a delegate) as `authority` (`null`: the payer). */
757
857
  transferAsset(
758
858
  asset: AccountMeta,
@@ -1437,7 +1537,6 @@ export declare function invoke(
1437
1537
  signerSeeds?: Seed[] | SignerSeeds,
1438
1538
  ): void;
1439
1539
 
1440
- /** Abort with the given error unless `condition` holds. Use a string enum member or a string literal. */
1441
1540
  /**
1442
1541
  * Anchor's own error codes (anchor-lang `ErrorCode`): `require(condition, AnchorError.AccountDidNotDeserialize)` fails
1443
1542
  * with that code, as `require!(condition, ErrorCode::AccountDidNotDeserialize)` does. Not listed in the IDL's errors.
@@ -1521,6 +1620,11 @@ export declare enum AnchorError {
1521
1620
  InvalidNumericConversion = 4102,
1522
1621
  Deprecated = 5000,
1523
1622
  }
1623
+ /**
1624
+ * Abort with the given error unless `condition` holds. The error is a member of an exported string enum, any
1625
+ * `AnchorError` member, or a string literal: each distinct literal becomes an error named `RequireFailed<N>` with code
1626
+ * 6000 + N, numbered after the program's error enums and listed in the IDL.
1627
+ */
1524
1628
  export declare function require(condition: boolean, error: string | AnchorError): asserts condition;
1525
1629
 
1526
1630
  /** Write a string, integer or pubkey to the program log. */
@@ -1562,11 +1666,6 @@ export declare function i16(value: bigint): i16;
1562
1666
  export declare function i32(value: bigint): i32;
1563
1667
  export declare function i128(value: bigint): i128;
1564
1668
 
1565
- /**
1566
- * Integer operators: + - * / % are checked (overflow and division by zero abort). & | ^ work on equal types;
1567
- * << aborts when bits would be lost or the amount is not below the width; >> is arithmetic on signed types;
1568
- * ~ flips the bits within the type's width.
1569
- */
1570
1669
  /** floor(a * b / c) with a double-width intermediate (all u64, or all u128); aborts if c is 0 or the result does not fit. */
1571
1670
  export declare function mulDiv(a: u64, b: u64, c: u64): u64;
1572
1671
  /** ceil(a * b / c), like mulDiv. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -369,9 +369,19 @@ static inline uint64_t ts_create_pda_address(const SolSignerSeed *seeds, int n,
369
369
  /* sol_try_find_program_address: the canonical (highest) bump. */
370
370
  static inline uint64_t ts_find_pda(const SolSignerSeed *seeds, int n, const SolPubkey *program, SolPubkey *out, uint8_t *bump_out) {
371
371
  if (!ts_pda_seeds_ok(seeds, n, TS_MAX_PDA_SEEDS - 1)) return 1; /* not SUCCESS: the PDA syscalls fail the same inputs */
372
+ /* The seed list is built once; each attempt only changes the bump byte it points at. */
373
+ SolBytes parts[TS_MAX_PDA_SEEDS + 2];
374
+ int k = 0;
375
+ for (int i = 0; i < n; i++) parts[k++] = (SolBytes){seeds[i].addr, seeds[i].len};
376
+ uint8_t bump = 255;
377
+ parts[k++] = (SolBytes){&bump, 1};
378
+ parts[k++] = (SolBytes){program->x, 32};
379
+ parts[k++] = (SolBytes){(const uint8_t *)"ProgramDerivedAddress", 21};
372
380
  for (int b = 255; b >= 0; b--) {
373
- uint8_t bump = (uint8_t)b;
374
- if (ts_pda_hash(seeds, n, &bump, program, out)) {
381
+ bump = (uint8_t)b;
382
+ sol_sha256(parts, k, out->x);
383
+ uint8_t ignored;
384
+ if (sol_curve_validate_point(0 /* CURVE25519_EDWARDS */, out->x, &ignored) != 0) {
375
385
  *bump_out = bump;
376
386
  return SUCCESS;
377
387
  }
@@ -493,10 +503,9 @@ static inline void ts_write_le(uint8_t *p, uint64_t v, int n) { ts_memcpy(p, &v,
493
503
  uint64_t sol_get_rent_sysvar(void *ret);
494
504
  uint64_t sol_get_clock_sysvar(void *ret);
495
505
 
496
- static inline uint64_t ts_minimum_balance(uint64_t data_len, uint64_t *out) {
497
- /* Rent { lamports_per_byte_year: u64, exemption_threshold: f64, burn_percent: u8 } */
498
- uint64_t rent[3] = {0};
499
- if (sol_get_rent_sysvar(rent) != SUCCESS) TS_FAIL(TS_ERR_SYSVAR, "TypeSOL: rent sysvar unavailable");
506
+ /* The minimum balance of `data_len` bytes under `rent`, the Rent sysvar
507
+ { lamports_per_byte_year: u64, exemption_threshold: f64, burn_percent: u8 }. */
508
+ static inline uint64_t ts_minimum_balance_of(const uint64_t rent[3], uint64_t data_len, uint64_t *out) {
500
509
  uint64_t bytes, per_year;
501
510
  if (ts_add_overflow_u64(data_len, 128, &bytes) || ts_mul_overflow_u64(bytes, rent[0], &per_year))
502
511
  TS_FAIL(TS_ERR_OVERFLOW, "TypeSOL: arithmetic overflow");
@@ -519,6 +528,18 @@ static inline uint64_t ts_minimum_balance(uint64_t data_len, uint64_t *out) {
519
528
  return SUCCESS;
520
529
  }
521
530
 
531
+ static inline uint64_t ts_rent(uint64_t rent[3]) {
532
+ rent[0] = rent[1] = rent[2] = 0;
533
+ if (sol_get_rent_sysvar(rent) != SUCCESS) TS_FAIL(TS_ERR_SYSVAR, "TypeSOL: rent sysvar unavailable");
534
+ return SUCCESS;
535
+ }
536
+
537
+ static inline uint64_t ts_minimum_balance(uint64_t data_len, uint64_t *out) {
538
+ uint64_t rent[3];
539
+ TS_TRY(ts_rent(rent));
540
+ return ts_minimum_balance_of(rent, data_len, out);
541
+ }
542
+
522
543
  /* Clock sysvar word `i`: 0 slot, 2 epoch (u64). */
523
544
  static inline uint64_t ts_clock_u64(int i, uint64_t *out) {
524
545
  uint64_t clock[5] = {0};
@@ -791,9 +812,10 @@ static inline uint64_t ts_resize(TsParams *params, TsAccount *payer, TsAccount *
791
812
  if (new_len > old_len) {
792
813
  /* The payer always funds the extra rent, even when the account already holds enough lamports: those may be
793
814
  deposits, which must not become rent (and shrinking never pays anything back, see ts_realloc). */
794
- uint64_t old_rent, new_rent;
795
- TS_TRY(ts_minimum_balance(old_len, &old_rent));
796
- TS_TRY(ts_minimum_balance(new_len, &new_rent));
815
+ uint64_t rent[3], old_rent, new_rent;
816
+ TS_TRY(ts_rent(rent)); /* read once: the sysvar cannot change within the instruction */
817
+ TS_TRY(ts_minimum_balance_of(rent, old_len, &old_rent));
818
+ TS_TRY(ts_minimum_balance_of(rent, new_len, &new_rent));
797
819
  if (new_rent > old_rent) {
798
820
  if (payer == NULL) TS_FAIL(TS_ERR_REALLOC, "TypeSOL: growing an account needs a payer");
799
821
  TS_TRY(ts_system_transfer(params, payer, acct, new_rent - old_rent));
@@ -1340,18 +1362,32 @@ static inline uint64_t ts_str_i128(__int128 v, TS_Str *out) {
1340
1362
  return ts_str_u128(mag, v < 0, out);
1341
1363
  }
1342
1364
 
1365
+ /* "00" … "99": two digits per division. */
1366
+ static const char TS_DIGIT_PAIRS[201] =
1367
+ "00010203040506070809101112131415161718192021222324252627282930313233343536373839"
1368
+ "40414243444546474849505152535455565758596061626364656667686970717273747576777879"
1369
+ "8081828384858687888990919293949596979899";
1343
1370
  static inline uint64_t ts_str_u64(uint64_t v, bool negative, TS_Str *out) {
1371
+ /* Written from the end, two digits at a time, then copied out in one go. */
1344
1372
  uint8_t tmp[21];
1345
- int n = 0;
1346
- do {
1347
- tmp[n++] = (uint8_t)('0' + v % 10);
1348
- v /= 10;
1349
- } while (v);
1350
- if (negative) tmp[n++] = '-';
1373
+ int i = 21;
1374
+ while (v >= 100) {
1375
+ uint64_t q = v / 100, r = v - q * 100;
1376
+ tmp[--i] = (uint8_t)TS_DIGIT_PAIRS[2 * r + 1];
1377
+ tmp[--i] = (uint8_t)TS_DIGIT_PAIRS[2 * r];
1378
+ v = q;
1379
+ }
1380
+ if (v >= 10) {
1381
+ tmp[--i] = (uint8_t)TS_DIGIT_PAIRS[2 * v + 1];
1382
+ tmp[--i] = (uint8_t)TS_DIGIT_PAIRS[2 * v];
1383
+ } else
1384
+ tmp[--i] = (uint8_t)('0' + v);
1385
+ if (negative) tmp[--i] = '-';
1386
+ uint64_t n = (uint64_t)(21 - i);
1351
1387
  uint8_t *p;
1352
- TS_TRY(ts_alloc((uint64_t)n, &p));
1353
- for (int i = 0; i < n; i++) p[i] = tmp[n - 1 - i];
1354
- *out = ts_str(p, (uint64_t)n);
1388
+ TS_TRY(ts_alloc(n, &p));
1389
+ ts_memcpy(p, tmp + i, n);
1390
+ *out = ts_str(p, n);
1355
1391
  return SUCCESS;
1356
1392
  }
1357
1393
  static inline uint64_t ts_str_i64(int64_t v, TS_Str *out) {