@typesol/cli 0.26.0 → 0.28.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.
@@ -46,7 +46,12 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
46
46
  | `Option<Account<'info, T>>` | `Mut<T> \| null` (the client passes the program id to leave it out) |
47
47
  | `UncheckedAccount` / `AccountInfo` | `AccountInfo` / `Mut<AccountInfo>`: check what you rely on yourself |
48
48
  | `SystemAccount<'info>` | `SystemAccount` / `Mut<SystemAccount>` |
49
+ | `Migration<'info, From, To>` | `Migrate<From, To>`, then `acct.migrate(payer, value)`; the old struct keeps its on-chain discriminator with `/** @discriminator account:<OldName> */` |
50
+ | `#[account(discriminator = …)]` | `/** @discriminator <preimage or 0x8 bytes> */` on the struct |
49
51
  | `AccountLoader<T>` with `#[account(zero)]` | `Zeroed<T>` (TypeSOL reads every account in place: no copy to avoid) |
52
+ | `Account<T>` with `#[account(zero)]` (Borsh) | `Zeroed<T>` of a `/** @layout borsh */` struct |
53
+ | `#[account(signer)]` on a non-`Signer` account | `Signed<A>`, e.g. `Signed<Zeroed<Multisig>>` |
54
+ | `seeds = [multisig.key().as_ref()], bump = multisig.nonce` | `Seeds<A, ["@multisig"], "@multisig.nonce">` |
50
55
  | `Program<'info, System>` | `SystemProgram` |
51
56
  | `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
52
57
  | `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
@@ -57,6 +62,8 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
57
62
  | `#[account(init, payer = p, seeds = […], bump, token::mint = m, token::authority = a)]` | `Init<TokenAccount>`, then `vault.create(p, […], m, a.key)` (`InitIfNeeded<TokenAccount>` + `createIfNeeded`) |
58
63
  | `associated_token::mint = m, associated_token::authority = a` | `Ata<TokenAccount, "a", "m">`; `Ata<Init<TokenAccount>, "a", "m">` creates it |
59
64
  | `ctx.remaining_accounts` | `remainingAccounts()` |
65
+ | `#[account(mut, dup)]` | `Dup<Mut<T>>` (in-place accounts only) |
66
+ | `sysvar::instructions` (`load_current_index_checked`, `load_instruction_at_checked`), `get_stack_height()` | `InstructionsSysvar`: `currentIndex()`, `get(i)`, `.length`; `stackHeight()` |
60
67
  | `ctx.bumps.vault` | the value `create` returns, or `acct.pdaBump` |
61
68
 
62
69
  ## Account data
@@ -104,6 +111,8 @@ Anchor instruction that takes or returns a struct with `String` / `Vec` / `Optio
104
111
  | `system_program::transfer(CpiContext::new(…), lamports)` | `systemProgram.transfer(from, to, lamports)` |
105
112
  | `token::transfer_checked(CpiContext::new_with_signer(…, seeds), amount, decimals)` | `tokenProgram.transferChecked(from, mint, to, authority, amount, ["seed", key, bump])` |
106
113
  | `invoke_signed(&ix, &accounts, &[seeds])` | `invoke(program, [writable(a), readonly(b)], [data…], seeds)` |
114
+ | `invoke_signed(&Instruction { program_id, accounts, data }, ctx.remaining_accounts, …)` built at run time | `invokeInstruction(programId, metas, data, remainingAccounts(), seeds)` |
115
+ | `multisig.owners = owners` (a `Vec` argument into account data) | the same, into a `FixedVec<Pubkey, N>` |
107
116
  | `declare_program!(other)` / CPI to another Anchor program | `typesol cpi other.json` generates typed helpers |
108
117
  | `for x in v.iter()` | `for (const x of v)` |
109
118
  | `v.iter().any(\|x\| …)`, `.all`, `.position`, `.find`, `.fold(init, …)`, `.contains(&x)` | `v.some((x) => …)`, `every`, `findIndex`, `find`, `reduce((acc, x) => …, init)`, `includes(x)` |
@@ -31,7 +31,8 @@ Each holds for every instruction of every program the compiler accepts. A break
31
31
  initialized account of that kind (3007 / 3003); program parameters at their fixed address and executable
32
32
  (3008 / 3009); `SystemAccount` owned by the System Program (3011); `Zeroed<T>` owned, all-zero discriminator and
33
33
  rent-exempt.
34
- 3. **No aliasing.** Two writable parameters are never the same account (2040), whatever slots the transaction uses.
34
+ 3. **No aliasing.** Two writable parameters are never the same account (2040), whatever slots the transaction uses,
35
+ unless the program marks one `Dup<…>`, and then only for accounts read and written in place (see below).
35
36
  4. **Arguments.** Every argument is length-checked before it is read; strings are valid UTF-8; booleans, enums and
36
37
  options have valid tags (102).
37
38
  5. **Constraints.** `Seeds` (2006, with the stored or the canonical bump), `Ata` (2015 / 2009 / 2014), `HasOne`
@@ -50,7 +51,9 @@ Each holds for every instruction of every program the compiler accepts. A break
50
51
  written before it (compile error). Creation refuses an account that already exists (3000), and an address someone
51
52
  pre-funded still gets created. `createIfNeeded` on an existing account re-derives the canonical PDA and checks
52
53
  owner, discriminator and size (token accounts: mint and owner; mints: decimals, authorities and the extensions it
53
- names).
54
+ names). A `Migrate<From, To>` account is checked as an existing `From`; `migrate(…)` is a top-level statement every
55
+ path reaches, builds the new value before rewriting the bytes, and nothing reads the old data after it (compile
56
+ errors).
54
57
  9. **Writes through references.** A place is never written after the code that computes its value may have moved it:
55
58
  replacing or resizing what a live reference points into is a compile error, as is writing a Vec element with a
56
59
  right side that may resize the Vec.
@@ -62,6 +65,10 @@ Each holds for every instruction of every program the compiler accepts. A break
62
65
  12. **CPIs.** A program signs only for its own PDAs (the runtime enforces it; the seeds are those written in the
63
66
  source). `invoke` marks an account writable or signer only when the parameter is `Mut` / `Signer` or the program
64
67
  signs for it; remaining accounts keep the flags they were passed with, or are checked for the role requested.
68
+ `invokeInstruction` is the exception by design: its program, account flags and data come from values (stored in
69
+ account data or passed in), so the compiler cannot check them; the runtime does, refusing any account the
70
+ transaction did not pass that way and any signer the program's seeds do not cover. Only programs that store
71
+ instructions to run (a multisig, a DAO) need it; review who can write those values.
65
72
  Return data is read only from the program named, at the expected size (5106).
66
73
  13. **Events.** `emitCpi` is accepted only from this program's event-authority PDA, signed (2002).
67
74
 
@@ -78,6 +85,10 @@ Each holds for every instruction of every program the compiler accepts. A break
78
85
  - **`AccountInfo` / `UncheckedProgram`.** Unchecked by design, as in Anchor: check what you rely on.
79
86
  - **`requirePda(account, seeds, bump)`.** Any valid bump passes (Anchor's `bump = expr`). Use a stored bump, never one
80
87
  from the arguments.
88
+ - **`Dup<…>`.** A parameter marked `Dup` may be the same account as another writable one. Reads see every write
89
+ (both are the same bytes), so a value read before a write through the other slot is stale: read, then write.
90
+ - **Instruction introspection.** The Instructions sysvar holds what the transaction's author put there. Require things
91
+ of the transaction with it (that a repay instruction follows), never treat it as proof of who did what.
81
92
  - **Oracles.** `pythPrice` checks verification, feed and age (pass the exponent you scale by); the raw `update.*`
82
93
  fields are unchecked. `switchboardValue` needs the expected feed; set `minSamples`.
83
94
  - **`merkleVerify`.** Leaves and inner nodes hash alike: build leaves as `keccak256(keccak256(data))` in the program.
@@ -87,7 +98,8 @@ Each holds for every instruction of every program the compiler accepts. A break
87
98
 
88
99
  ## Where TypeSOL is stricter than Anchor
89
100
 
90
- - Any two writable parameters naming one account are refused (Anchor compares only program and token accounts).
101
+ - Any two writable parameters naming one account are refused unless one is `Dup<…>` (Anchor compares only program
102
+ and token accounts, and `dup` lifts that).
91
103
  - Arithmetic overflow returns an error code (5100) in every build; Anchor aborts the program, and only when
92
104
  overflow checks are enabled.
93
105
 
package/lib/typesol.d.ts CHANGED
@@ -123,7 +123,8 @@ export interface FixedVec<T, N extends number> extends ListMethods<T> {
123
123
  readonly length: u64;
124
124
  get(index: bigint): T;
125
125
  set(index: bigint, value: T): void;
126
- push(value: T): void;
126
+ // `...rest: never[]` takes nothing, but lets a Vec (whose push takes a payer) be assigned to a FixedVec: `acct.owners = owners`.
127
+ push(value: T, ...rest: never[]): void;
127
128
  pop(): T;
128
129
  clear(): void;
129
130
  [Symbol.iterator](): Iterator<T>;
@@ -186,10 +187,43 @@ export declare function keccak256(...parts: (string | Pubkey | Hash | bigint | b
186
187
  */
187
188
  export declare function merkleVerify(leaf: Hash, proof: Vec<Hash>, root: Hash): boolean;
188
189
 
189
- /** The Instructions sysvar (Sysvar1nstructions…). Its address is checked. */
190
+ /**
191
+ * The Instructions sysvar (Sysvar1nstructions…): every instruction of the transaction being executed. Its address is
192
+ * checked. What it holds is the transaction author's: use it to require something of the transaction (a repay
193
+ * instruction after a flash loan, a fee instruction before), never as proof of anything else.
194
+ */
190
195
  export interface InstructionsSysvar extends AccountMeta {
191
196
  readonly __instructions_sysvar: void;
197
+ /** How many instructions the transaction has. */
198
+ readonly length: u64;
199
+ /** The index of the top-level instruction being executed (this one, or the one that CPI'd into this program). */
200
+ currentIndex(): u64;
201
+ /** The transaction's instruction at `index` (5104 when there is none). */
202
+ get(index: u64): TransactionInstruction;
192
203
  }
204
+ /** One instruction of the transaction, read from the Instructions sysvar. Reads past its end fail with 5104. */
205
+ export interface TransactionInstruction {
206
+ readonly __transaction_instruction: void;
207
+ readonly programId: Pubkey;
208
+ readonly accountsLength: u64;
209
+ readonly dataLength: u64;
210
+ accountKey(i: u64): Pubkey;
211
+ accountIsSigner(i: u64): boolean;
212
+ accountIsWritable(i: u64): boolean;
213
+ /** Little-endian integers and keys from the instruction data. */
214
+ readU8(offset: u64): u8;
215
+ readU16(offset: u64): u16;
216
+ readU32(offset: u64): u32;
217
+ readU64(offset: u64): u64;
218
+ readPubkey(offset: u64): Pubkey;
219
+ /** `program`'s instruction `name` (snake_case or camelCase): its program id and 8-byte Anchor discriminator match. */
220
+ is(program: Pubkey, name: string): boolean;
221
+ }
222
+ /**
223
+ * How deep in cross-program calls this program runs: 1 when the transaction calls it directly, more when another
224
+ * program invoked it. `require(stackHeight() === 1n, …)` refuses CPI callers.
225
+ */
226
+ export declare function stackHeight(): u64;
193
227
 
194
228
  /**
195
229
  * True when this transaction contains an Ed25519 precompile instruction that verified `message`
@@ -597,6 +631,32 @@ export type TokenOf<A, MintParam extends string, AuthorityParam extends string =
597
631
  export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
598
632
  readonly __mintOf?: [AuthorityParam, FreezeAuthorityParam, TokenProgramParam];
599
633
  };
634
+ /**
635
+ * Anchor's `dup`: this writable account may be the same account as another writable parameter (TypeSOL refuses that
636
+ * otherwise, 2040). Both are read and written in place, so a write through one is seen through the other: read
637
+ * what you need before writing. Not for accounts that are created, closed, a close destination or `@layout borsh`.
638
+ */
639
+ export type Dup<A> = A & { readonly __dup?: true };
640
+ /**
641
+ * The account must sign the transaction (Anchor's `signer` constraint on an account that is not a `Signer`; 2002), e.g.
642
+ * `Signed<Zeroed<Multisig>>` for an account the client creates with a fresh keypair in the same transaction.
643
+ */
644
+ export type Signed<A> = A & { readonly __signed?: true };
645
+ /**
646
+ * Anchor 1.0's `Migration<From, To>`: an existing `From` account (owner, discriminator and size checked) that this
647
+ * instruction rewrites as `To` with `acct.migrate(payer, value)`, a top-level statement every path must reach. Read
648
+ * the old data through `acct.from` before migrating (the new value may use it: it is built in full before the account
649
+ * is rewritten), and the new data through `acct.to` after. The account is resized to `To`: the payer tops up the rent
650
+ * when it grows (at most 10 KiB), and shrinking keeps the lamports. `From` usually keeps the old type's discriminator
651
+ * under a new name: `/** @discriminator account:Counter *\/ interface CounterV1 { … }`. An account already migrated
652
+ * fails with 3002, as under Anchor.
653
+ */
654
+ export interface Migrate<From, To> extends AccountMeta {
655
+ readonly __migrate: [From, To];
656
+ readonly from: Readonly<From>;
657
+ readonly to: To;
658
+ migrate(payer: Mut<Signer>, value: To): void;
659
+ }
600
660
  export type Seeds<A, Names extends readonly string[], Bump extends string = never, ProgramId extends string = never> = A & {
601
661
  readonly __seeds?: [Names, Bump, ProgramId];
602
662
  /** The PDA's bump (Anchor `ctx.bumps.<account>`): the stored bump field when one is named, else the canonical bump found. */
@@ -1325,9 +1385,10 @@ export type Init<T> = T extends TokenAccount
1325
1385
  };
1326
1386
 
1327
1387
  /**
1328
- * A large account (over 10 KiB, up to 10 MiB) the client already allocated with System CreateAccount,
1329
- * owned by this program and still zeroed. Owner, size, rent exemption and emptiness are checked, then
1330
- * the discriminator is written: from here on it is a `Mut<T>`. Same idea as Anchor's zero-copy `zero`.
1388
+ * An account the client already allocated with System CreateAccount (any size up to 10 MiB; over 10 KiB a program
1389
+ * cannot create it itself), owned by this program and still zeroed: Anchor's `zero`. Emptiness (2013), owner, size and
1390
+ * rent exemption are checked, then the discriminator is written: from here on it is a `Mut<T>`. A `@layout borsh`
1391
+ * struct starts empty (lists and strings empty, Options None) and is written as Borsh when the instruction returns.
1331
1392
  */
1332
1393
  export type Zeroed<T> = T & AccountMeta;
1333
1394
 
@@ -1556,6 +1617,32 @@ export declare function invoke(
1556
1617
  signerSeeds?: Seed[] | SignerSeeds,
1557
1618
  ): void;
1558
1619
 
1620
+ /** One account of an instruction built at run time (Rust's `AccountMeta`): any struct with these three fields. */
1621
+ export interface InstructionAccountMeta {
1622
+ readonly pubkey: Pubkey;
1623
+ readonly isSigner: boolean;
1624
+ readonly isWritable: boolean;
1625
+ }
1626
+ /** A list read by index: a Vec or FixedVec in account data, a Vec argument, or a stored local list. */
1627
+ export interface IndexedList<T> {
1628
+ readonly length: u64;
1629
+ get(index: bigint): T;
1630
+ }
1631
+ /**
1632
+ * A CPI decided at run time, e.g. one stored in account data (Rust's `invoke_signed(&Instruction { program_id,
1633
+ * accounts, data }, infos, seeds)`). Every account in `infos` (`remainingAccounts()` or a slice of it) is handed to the
1634
+ * runtime, which finds each meta's account there by key: one that is missing fails the CPI, as under Rust. A meta may
1635
+ * be a signer or writable only as the transaction passed that account, or as a PDA of this program `signerSeeds` sign
1636
+ * for (`signer([multisig.key, multisig.nonce])`).
1637
+ */
1638
+ export declare function invokeInstruction(
1639
+ programId: Pubkey,
1640
+ accounts: IndexedList<InstructionAccountMeta>,
1641
+ data: IndexedList<u8>,
1642
+ infos: RemainingAccounts | RemainingSlice,
1643
+ signerSeeds?: Seed[] | SignerSeeds,
1644
+ ): void;
1645
+
1559
1646
  /**
1560
1647
  * Anchor's own error codes (anchor-lang `ErrorCode`): `require(condition, AnchorError.AccountDidNotDeserialize)` fails
1561
1648
  * with that code, as `require!(condition, ErrorCode::AccountDidNotDeserialize)` does. Not listed in the IDL's errors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  #define TS_ERR_CONSTRAINT_EXECUTABLE 2007 /* ConstraintExecutable */
28
28
  #define TS_ERR_ASSOCIATED 2009 /* ConstraintAssociated */
29
29
  #define TS_ERR_ADDRESS 2012 /* ConstraintAddress */
30
+ #define TS_ERR_CONSTRAINT_ZERO 2013 /* ConstraintZero */
30
31
  #define TS_ERR_TOKEN_MINT 2014 /* ConstraintTokenMint */
31
32
  #define TS_ERR_TOKEN_OWNER 2015 /* ConstraintTokenOwner */
32
33
  #define TS_ERR_MINT_AUTHORITY 2016 /* ConstraintMintMintAuthority */
@@ -654,10 +655,15 @@ static inline uint64_t ts_now(int64_t *out) {
654
655
  * to this program. It must be writable, rent-exempt, big enough and still all-zero in its discriminator;
655
656
  * the discriminator is written here so the account can never be initialized twice.
656
657
  */
657
- static inline uint64_t ts_init_zeroed(TsAccount *a, const SolPubkey *program_id, const uint8_t disc[8], uint64_t size) {
658
+ /* Zeroed<T> (Anchor's `zero`), in Anchor's order: a discriminator already set (2013), then owner and size as the
659
+ * account type checks them; the rent check and the discriminator write come after the mut / signer checks. */
660
+ static inline uint64_t ts_check_zeroed(const TsAccount *a, const SolPubkey *program_id, uint64_t size) {
661
+ if (a->data_len >= 8 && ts_ld64(a->data) != 0) TS_FAIL(TS_ERR_CONSTRAINT_ZERO, "TypeSOL: account already initialized");
658
662
  if (!ts_key_eq(&a->owner, program_id)) TS_FAIL(TS_ERR_WRONG_OWNER, "TypeSOL: account not owned by this program");
659
663
  if (a->data_len < 8 + size) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account data too small");
660
- if (ts_ld64(a->data) != 0) TS_FAIL(TS_ERR_ALREADY_INITIALIZED, "TypeSOL: account already initialized");
664
+ return SUCCESS;
665
+ }
666
+ static inline uint64_t ts_init_zeroed(TsAccount *a, const uint8_t disc[8]) {
661
667
  uint64_t need;
662
668
  TS_TRY(ts_minimum_balance(a->data_len, &need));
663
669
  if (a->lamports < need) TS_FAIL(TS_ERR_NOT_RENT_EXEMPT, "TypeSOL: account is not rent-exempt");
@@ -1121,6 +1127,81 @@ static inline bool ts_merkle_verify(const SolPubkey *leaf, const uint8_t *proof,
1121
1127
  return ts_mem_eq(node, root, 32);
1122
1128
  }
1123
1129
 
1130
+ /*
1131
+ * The Instructions sysvar: u16 count, u16 offsets[count], then per instruction u16 num_accounts,
1132
+ * num_accounts * (u8 flags: bit 0 signer, bit 1 writable; 32-byte key), 32-byte program id, u16 data_len, data; the
1133
+ * last two bytes of the account are the index of the instruction being executed. Everything here is the transaction
1134
+ * author's data: bounds-checked, never trusted for anything but what it says.
1135
+ */
1136
+ typedef struct {
1137
+ const uint8_t *accounts; /* num_accounts * 33 bytes */
1138
+ const SolPubkey *program_id;
1139
+ const uint8_t *data;
1140
+ uint16_t num_accounts;
1141
+ uint16_t data_len;
1142
+ } TsIx;
1143
+
1144
+ /* Instruction `index` of the transaction; false when out of range or malformed. */
1145
+ static inline bool ts_ix_at(const TsAccount *ixs, uint64_t index, TsIx *out) {
1146
+ const uint8_t *d = ixs->data;
1147
+ uint64_t len = ixs->data_len;
1148
+ if (len < 2) return false;
1149
+ uint16_t count;
1150
+ ts_memcpy(&count, d, 2);
1151
+ if (index >= count || 2 + (uint64_t)count * 2 > len) return false;
1152
+ uint16_t at;
1153
+ ts_memcpy(&at, d + 2 + index * 2, 2);
1154
+ uint64_t p = at;
1155
+ if (p + 2 > len) return false;
1156
+ ts_memcpy(&out->num_accounts, d + p, 2);
1157
+ p += 2;
1158
+ out->accounts = d + p;
1159
+ p += (uint64_t)out->num_accounts * 33;
1160
+ if (p + 34 > len) return false;
1161
+ out->program_id = (const SolPubkey *)(d + p);
1162
+ p += 32;
1163
+ ts_memcpy(&out->data_len, d + p, 2);
1164
+ p += 2;
1165
+ if (p + out->data_len > len) return false;
1166
+ out->data = d + p;
1167
+ return true;
1168
+ }
1169
+ static inline uint64_t ts_ix_load(const TsAccount *ixs, uint64_t index, TsIx *out) {
1170
+ if (!ts_ix_at(ixs, index, out)) TS_FAIL(TS_ERR_INDEX, "TypeSOL: no such instruction in the transaction");
1171
+ return SUCCESS;
1172
+ }
1173
+ static inline uint64_t ts_ix_count(const TsAccount *ixs, uint64_t *out) {
1174
+ if (ixs->data_len < 2) TS_FAIL(TS_ERR_INDEX, "TypeSOL: empty instructions sysvar");
1175
+ uint16_t n;
1176
+ ts_memcpy(&n, ixs->data, 2);
1177
+ *out = n;
1178
+ return SUCCESS;
1179
+ }
1180
+ static inline uint64_t ts_ix_current(const TsAccount *ixs, uint64_t *out) {
1181
+ if (ixs->data_len < 4) TS_FAIL(TS_ERR_INDEX, "TypeSOL: empty instructions sysvar");
1182
+ uint16_t i;
1183
+ ts_memcpy(&i, ixs->data + ixs->data_len - 2, 2);
1184
+ *out = i;
1185
+ return SUCCESS;
1186
+ }
1187
+ static inline uint64_t ts_ix_account(const TsIx *ix, uint64_t i, const uint8_t **out) {
1188
+ if (i >= ix->num_accounts) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the instruction has no account at that index");
1189
+ *out = ix->accounts + i * 33;
1190
+ return SUCCESS;
1191
+ }
1192
+ /* `n` bytes of the instruction's data at `off` (little-endian integers, keys). */
1193
+ static inline uint64_t ts_ix_read(const TsIx *ix, uint64_t off, uint64_t n, void *out) {
1194
+ if (off > ix->data_len || n > ix->data_len - off) TS_FAIL(TS_ERR_INDEX, "TypeSOL: read past the end of the instruction's data");
1195
+ ts_memcpy(out, ix->data + off, n);
1196
+ return SUCCESS;
1197
+ }
1198
+ /* Is it `program`'s instruction with this 8-byte discriminator (Anchor's sha256("global:<name>")[..8])? */
1199
+ static inline bool ts_ix_is(const TsIx *ix, const SolPubkey *program, const uint8_t disc[8]) {
1200
+ return ts_key_eq(ix->program_id, program) && ix->data_len >= 8 && ts_mem_eq(ix->data, disc, 8);
1201
+ }
1202
+ /* How deep in CPIs the program runs: 1 when the transaction calls it directly. */
1203
+ extern uint64_t sol_get_stack_height(void);
1204
+
1124
1205
  /*
1125
1206
  * Did an Ed25519 precompile instruction in this transaction verify `message` signed by `signer`?
1126
1207
  * Reads the Instructions sysvar (data: u16 count, u16 offsets[count], then per instruction
@@ -1129,29 +1210,15 @@ static inline bool ts_merkle_verify(const SolPubkey *leaf, const uint8_t *proof,
1129
1210
  * message, with offsets pointing into the precompile's own data (instruction index 0xFFFF).
1130
1211
  */
1131
1212
  static inline bool ts_ed25519_verified(const TsAccount *ixs, const SolPubkey *signer, TS_Str message) {
1132
- const uint8_t *d = ixs->data;
1133
- uint64_t len = ixs->data_len;
1134
- if (len < 2) return false;
1213
+ if (ixs->data_len < 2) return false;
1135
1214
  uint16_t count;
1136
- ts_memcpy(&count, d, 2);
1137
- if (2 + (uint64_t)count * 2 > len) return false;
1215
+ ts_memcpy(&count, ixs->data, 2);
1138
1216
  for (uint16_t i = 0; i < count; i++) {
1139
- uint16_t at;
1140
- ts_memcpy(&at, d + 2 + (uint64_t)i * 2, 2);
1141
- uint64_t p = at;
1142
- if (p + 2 > len) return false;
1143
- uint16_t nacc;
1144
- ts_memcpy(&nacc, d + p, 2);
1145
- p += 2 + (uint64_t)nacc * 33;
1146
- if (p + 34 > len) return false;
1147
- bool is_ed = ts_mem_eq(d + p, &TS_ED25519_PROGRAM_ID, 32);
1148
- p += 32;
1149
- uint16_t dlen;
1150
- ts_memcpy(&dlen, d + p, 2);
1151
- p += 2;
1152
- if (p + dlen > len) return false;
1153
- if (!is_ed || dlen < 2) continue;
1154
- const uint8_t *ix = d + p;
1217
+ TsIx t;
1218
+ if (!ts_ix_at(ixs, i, &t)) return false;
1219
+ if (!ts_key_eq(t.program_id, &TS_ED25519_PROGRAM_ID) || t.data_len < 2) continue;
1220
+ uint16_t dlen = t.data_len;
1221
+ const uint8_t *ix = t.data;
1155
1222
  uint8_t nsig = ix[0];
1156
1223
  for (uint8_t k = 0; k < nsig; k++) {
1157
1224
  uint64_t o = 2 + (uint64_t)k * 14;