@typesol/cli 0.23.0 → 0.24.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +77 -2
  2. package/README.md +18 -15
  3. package/SECURITY.md +1 -1
  4. package/dist/clientgen.js +222 -61
  5. package/dist/compiler/analyze.js +132 -20
  6. package/dist/compiler/emit/base.d.ts +27 -4
  7. package/dist/compiler/emit/base.js +68 -15
  8. package/dist/compiler/emit/calls.d.ts +7 -0
  9. package/dist/compiler/emit/calls.js +99 -9
  10. package/dist/compiler/emit/cpi.d.ts +33 -1
  11. package/dist/compiler/emit/cpi.js +353 -110
  12. package/dist/compiler/emit/expressions.d.ts +9 -1
  13. package/dist/compiler/emit/expressions.js +16 -12
  14. package/dist/compiler/emit/function.js +3 -1
  15. package/dist/compiler/emit/shared.d.ts +11 -1
  16. package/dist/compiler/emit/statements.d.ts +4 -2
  17. package/dist/compiler/emit/statements.js +128 -27
  18. package/dist/compiler/emit/storage.d.ts +6 -2
  19. package/dist/compiler/emit/storage.js +36 -32
  20. package/dist/compiler/model.d.ts +8 -0
  21. package/dist/idlpublish.d.ts +6 -1
  22. package/dist/idlpublish.js +47 -5
  23. package/dist/portgen.js +1 -1
  24. package/dist/svmrpc.d.ts +7 -1
  25. package/dist/svmrpc.js +133 -19
  26. package/dist/testing.js +23 -3
  27. package/docs/architecture.md +26 -2
  28. package/docs/cli.md +2 -2
  29. package/docs/client.md +13 -10
  30. package/docs/errors.md +9 -3
  31. package/docs/language.md +18 -17
  32. package/docs/mainnet.md +1 -1
  33. package/docs/migrate-from-anchor.md +8 -9
  34. package/docs/runtime.md +59 -0
  35. package/docs/test-your-program.md +6 -2
  36. package/docs/testing.md +1 -1
  37. package/docs/troubleshooting.md +3 -2
  38. package/lib/typesol.d.ts +120 -21
  39. package/package.json +1 -1
  40. package/runtime/typesol_rt.h +106 -28
package/dist/svmrpc.js CHANGED
@@ -8,15 +8,81 @@
8
8
  * await counter.accounts.Counter.fetch(rpc, address);
9
9
  *
10
10
  * Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
11
- * the accounts transactions sent through this connection named, plus those passed to `track`.
11
+ * the accounts transactions sent through this connection named, plus those passed to `track`. Subscriptions see the
12
+ * transactions sent through this connection: `logsNotifications` replays those sent so far (failed ones too, with
13
+ * `err` set), then each new one, as `{ mentions: [address] }` filters them; `accountNotifications` yields the account
14
+ * after each later transaction that names it. Errors are in the JSON shape RPC nodes return
15
+ * (`{ InstructionError: [0, { Custom: 6000 }] }`).
12
16
  */
13
17
  import { getAddressDecoder, getBase58Decoder, getBase58Encoder, getBase64Decoder, getBase64Encoder, getCompiledTransactionMessageDecoder, getTransactionDecoder, } from "@solana/kit";
14
18
  import { FailedTransactionMetadata } from "litesvm";
15
19
  const b64 = (bytes) => getBase64Decoder().decode(bytes);
16
20
  const reply = (value) => ({ send: async () => value });
21
+ // LiteSVM's fieldless error enums by value (const enums: their names are not there at run time).
22
+ const TX_ERRORS = ("AccountInUse AccountLoadedTwice AccountNotFound ProgramAccountNotFound InsufficientFundsForFee InvalidAccountForFee " +
23
+ "AlreadyProcessed BlockhashNotFound CallChainTooDeep MissingSignatureForFee InvalidAccountIndex SignatureFailure " +
24
+ "InvalidProgramForExecution SanitizeFailure ClusterMaintenance AccountBorrowOutstanding WouldExceedMaxBlockCostLimit " +
25
+ "UnsupportedVersion InvalidWritableAccount WouldExceedMaxAccountCostLimit WouldExceedAccountDataBlockLimit " +
26
+ "TooManyAccountLocks AddressLookupTableNotFound InvalidAddressLookupTableOwner InvalidAddressLookupTableData " +
27
+ "InvalidAddressLookupTableIndex InvalidRentPayingAccount WouldExceedMaxVoteCostLimit WouldExceedAccountDataTotalLimit " +
28
+ "MaxLoadedAccountsDataSizeExceeded ResanitizationNeeded InvalidLoadedAccountsDataSizeLimit UnbalancedTransaction " +
29
+ "ProgramCacheHitMaxLimit CommitCancelled").split(" ");
30
+ const IX_ERRORS = ("GenericError InvalidArgument InvalidInstructionData InvalidAccountData AccountDataTooSmall InsufficientFunds " +
31
+ "IncorrectProgramId MissingRequiredSignature AccountAlreadyInitialized UninitializedAccount UnbalancedInstruction " +
32
+ "ModifiedProgramId ExternalAccountLamportSpend ExternalAccountDataModified ReadonlyLamportChange ReadonlyDataModified " +
33
+ "DuplicateAccountIndex ExecutableModified RentEpochModified NotEnoughAccountKeys AccountDataSizeChanged " +
34
+ "AccountNotExecutable AccountBorrowFailed AccountBorrowOutstanding DuplicateAccountOutOfSync InvalidError " +
35
+ "ExecutableDataModified ExecutableLamportChange ExecutableAccountNotRentExempt UnsupportedProgramId CallDepth " +
36
+ "MissingAccount ReentrancyNotAllowed MaxSeedLengthExceeded InvalidSeeds InvalidRealloc ComputationalBudgetExceeded " +
37
+ "PrivilegeEscalation ProgramEnvironmentSetupFailure ProgramFailedToComplete ProgramFailedToCompile Immutable " +
38
+ "IncorrectAuthority AccountNotRentExempt InvalidAccountOwner ArithmeticOverflow UnsupportedSysvar IllegalOwner " +
39
+ "MaxAccountsDataAllocationsExceeded MaxAccountsExceeded MaxInstructionTraceLengthExceeded " +
40
+ "BuiltinProgramsMustConsumeComputeUnits BorshIoError").split(" ");
41
+ /** A LiteSVM transaction error in the JSON shape RPC nodes return: `{ InstructionError: [0, { Custom: 6000 }] }`. */
42
+ export function rpcError(err) {
43
+ if (typeof err === "number")
44
+ return TX_ERRORS[err] ?? String(err);
45
+ if (typeof err !== "object" || err === null)
46
+ return String(err);
47
+ const o = err;
48
+ if (typeof o.err === "function" && typeof o.index === "number") {
49
+ const inner = o.err.call(err);
50
+ const fields = (typeof inner === "object" && inner !== null ? inner : {});
51
+ const ix = typeof inner === "number"
52
+ ? (IX_ERRORS[inner] ?? String(inner))
53
+ : typeof fields.code === "number"
54
+ ? { Custom: fields.code }
55
+ : typeof fields.msg === "string"
56
+ ? { BorshIoError: fields.msg }
57
+ : String(inner);
58
+ return { InstructionError: [o.index, ix] };
59
+ }
60
+ if (typeof o.index === "number")
61
+ return { DuplicateInstruction: o.index };
62
+ if (typeof o.accountIndex === "number")
63
+ return /Restricted/.test(String(err))
64
+ ? { ProgramExecutionTemporarilyRestricted: { account_index: o.accountIndex } }
65
+ : { InsufficientFundsForRent: { account_index: o.accountIndex } };
66
+ return String(err);
67
+ }
17
68
  export function svmConnection(svm) {
18
69
  const transactions = [];
19
70
  const known = new Set();
71
+ // Every transaction sent (failed ones too) for log subscriptions, the account keys of each, and live subscribers.
72
+ const sent = [];
73
+ const accountKeys = new Map();
74
+ const logListeners = new Set();
75
+ const accountListeners = new Map();
76
+ const record = (n) => {
77
+ sent.push(n);
78
+ for (const l of [...logListeners])
79
+ l(n);
80
+ // A failed transaction changed no account.
81
+ if (n.err === null)
82
+ for (const k of new Set(n.keys))
83
+ for (const l of [...(accountListeners.get(k) ?? [])])
84
+ l();
85
+ };
20
86
  const decode = (wire) => {
21
87
  const tx = getTransactionDecoder().decode(getBase64Encoder().encode(wire));
22
88
  return { tx, keys: getCompiledTransactionMessageDecoder().decode(tx.messageBytes).staticAccounts };
@@ -50,12 +116,17 @@ export function svmConnection(svm) {
50
116
  const { tx, keys } = decode(wire);
51
117
  const res = svm.sendTransaction(tx);
52
118
  svm.expireBlockhash();
53
- if (res instanceof FailedTransactionMetadata)
54
- throw new Error(`transaction failed: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
119
+ if (res instanceof FailedTransactionMetadata) {
120
+ const meta = res.meta();
121
+ record({ signature: getBase58Decoder().decode(meta.signature()), logs: meta.logs(), err: rpcError(res.err()), keys });
122
+ throw new Error(`transaction failed: ${String(res.err())}\n${meta.logs().join("\n")}`);
123
+ }
55
124
  for (const k of keys)
56
125
  known.add(k);
57
126
  const signature = getBase58Decoder().decode(res.signature());
58
127
  transactions.push({ signature, meta: res });
128
+ accountKeys.set(signature, keys);
129
+ record({ signature, logs: res.logs(), err: null, keys });
59
130
  return signature;
60
131
  },
61
132
  }),
@@ -77,7 +148,7 @@ export function svmConnection(svm) {
77
148
  if (res instanceof FailedTransactionMetadata)
78
149
  return {
79
150
  context: { slot: 0n },
80
- value: { err: String(res.err()), logs: res.meta().logs(), unitsConsumed: res.meta().computeUnitsConsumed(), returnData: null },
151
+ value: { err: rpcError(res.err()), logs: res.meta().logs(), unitsConsumed: res.meta().computeUnitsConsumed(), returnData: null },
81
152
  };
82
153
  const meta = res.meta();
83
154
  const ret = meta.returnData();
@@ -108,38 +179,81 @@ export function svmConnection(svm) {
108
179
  return want.every((b, i) => data[Number(memcmp.offset) + i] === b);
109
180
  });
110
181
  })),
111
- // The json shape: log messages, and inner instructions with base58 data (what `eventsOf` reads).
182
+ // The json shape: account keys, log messages, and inner instructions with their program and base58 data (what
183
+ // `eventsOf` reads).
112
184
  getTransaction: (signature) => {
113
185
  const t = transactions.find((x) => x.signature === signature);
114
186
  return reply(t
115
187
  ? {
116
188
  slot: 0n,
117
189
  blockTime: null,
190
+ transaction: { signatures: [signature], message: { accountKeys: accountKeys.get(signature) ?? [] } },
118
191
  meta: {
119
192
  err: null,
120
193
  logMessages: t.meta.logs(),
121
194
  computeUnitsConsumed: t.meta.computeUnitsConsumed(),
122
- innerInstructions: t.meta
123
- .innerInstructions()
124
- .map((group, index) => ({ index, instructions: group.map((i) => ({ data: getBase58Decoder().decode(i.instruction().data()) })) })),
195
+ innerInstructions: t.meta.innerInstructions().map((group, index) => ({
196
+ index,
197
+ instructions: group.map((i) => ({
198
+ programIdIndex: i.instruction().programIdIndex(),
199
+ accounts: [...i.instruction().accounts()],
200
+ data: getBase58Decoder().decode(i.instruction().data()),
201
+ stackHeight: i.stackHeight(),
202
+ })),
203
+ })),
125
204
  },
126
205
  }
127
206
  : null);
128
207
  },
129
208
  };
130
- /** Notifications that end when the subscriber aborts. */
131
- const stream = (items) => ({
132
- subscribe: async ({ abortSignal }) => (async function* () {
133
- for (const item of items())
134
- yield item;
135
- await new Promise((resolve) => abortSignal.addEventListener("abort", resolve, { once: true }));
136
- })(),
209
+ /**
210
+ * Notifications: `backlog` first, then each item `listen` pushes, until the subscriber aborts. The listener is in
211
+ * place once `subscribe` is called, so nothing sent after that call is missed.
212
+ */
213
+ const stream = (backlog, listen) => ({
214
+ subscribe: async ({ abortSignal }) => {
215
+ const queue = [...backlog()];
216
+ let wake;
217
+ const stop = listen((item) => {
218
+ queue.push(item);
219
+ wake?.();
220
+ });
221
+ const aborted = new Promise((resolve) => abortSignal.addEventListener("abort", () => resolve(), { once: true }));
222
+ void aborted.then(stop);
223
+ return (async function* () {
224
+ while (!abortSignal.aborted) {
225
+ if (queue.length) {
226
+ yield queue.shift();
227
+ continue;
228
+ }
229
+ await Promise.race([aborted, new Promise((resolve) => (wake = resolve))]);
230
+ wake = undefined;
231
+ }
232
+ })();
233
+ },
137
234
  });
235
+ const none = () => () => undefined;
138
236
  const rpcSubscriptions = {
139
- signatureNotifications: () => stream(() => [{ context: { slot: 0n }, value: { err: null } }]),
140
- slotNotifications: () => stream(() => []),
141
- logsNotifications: () => stream(() => transactions.map((t) => ({ context: { slot: 0n }, value: { signature: t.signature, logs: t.meta.logs(), err: null } }))),
142
- accountNotifications: (a) => stream(() => [{ context: { slot: 0n }, value: encoded(a) ?? { data: ["", "base64"], lamports: 0n } }]),
237
+ signatureNotifications: () => stream(() => [{ context: { slot: 0n }, value: { err: null } }], none),
238
+ slotNotifications: () => stream(() => [], none),
239
+ logsNotifications: (filter = "all") => {
240
+ const wanted = (n) => typeof filter !== "object" || filter.mentions.every((a) => n.keys.includes(a));
241
+ const shape = (n) => ({ context: { slot: 0n }, value: { signature: n.signature, logs: n.logs, err: n.err } });
242
+ return stream(() => sent.filter(wanted).map(shape), (push) => {
243
+ const l = (n) => {
244
+ if (wanted(n))
245
+ push(shape(n));
246
+ };
247
+ logListeners.add(l);
248
+ return () => logListeners.delete(l);
249
+ });
250
+ },
251
+ accountNotifications: (a) => stream(() => [], (push) => {
252
+ const l = () => push({ context: { slot: 0n }, value: encoded(a) ?? { data: ["", "base64"], lamports: 0n } });
253
+ const set = accountListeners.get(a) ?? new Set();
254
+ accountListeners.set(a, set.add(l));
255
+ return () => set.delete(l);
256
+ }),
143
257
  };
144
258
  return {
145
259
  rpc: rpc,
package/dist/testing.js CHANGED
@@ -313,13 +313,33 @@ export function programHarness(svm, programId, idl) {
313
313
  throw new Error(`${instruction} returns nothing`);
314
314
  const data = Buffer.from(harness.ok(res).returnData().data());
315
315
  const t = def.returns;
316
+ const short = (n) => {
317
+ if (data.length < n)
318
+ throw new Error(`${instruction} returned ${data.length} bytes; a ${String(t)} needs ${n}`);
319
+ };
316
320
  if (typeof t === "string") {
317
321
  if (t === "pubkey")
318
- return getAddressDecoder().decode(data);
322
+ return (short(32), getAddressDecoder().decode(data));
319
323
  if (t === "bool")
320
- return (data[0] !== 0);
324
+ return (short(1), data[0] !== 0);
325
+ if (t === "f32")
326
+ return (short(4), data.readFloatLE(0));
327
+ if (t === "f64")
328
+ return (short(8), data.readDoubleLE(0));
329
+ if (t === "string" || t === "bytes") {
330
+ // Borsh: a u32 length, then the bytes (UTF-8 for a string).
331
+ short(4);
332
+ const len = data.readUInt32LE(0);
333
+ short(4 + len);
334
+ const body = data.subarray(4, 4 + len);
335
+ return (t === "string" ? body.toString("utf8") : new Uint8Array(body));
336
+ }
337
+ const int = /^[ui](8|16|32|64|128)$/.exec(t);
338
+ if (!int)
339
+ throw new Error(`decode ${instruction}'s return data yourself (${t})`);
321
340
  const signed = t.startsWith("i");
322
- const bytes = Number(t.slice(1)) / 8;
341
+ const bytes = Number(int[1]) / 8;
342
+ short(bytes);
323
343
  let n = 0n;
324
344
  for (let i = bytes - 1; i >= 0; i--)
325
345
  n = (n << 8n) | BigInt(data[i]);
@@ -22,6 +22,21 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
22
22
  `compiler/index.ts` wires the stages together. `generate()` runs analyze, emit and IDL generation in memory, and
23
23
  `build()` also compiles and writes the output files.
24
24
 
25
+ ## Repository layout
26
+
27
+ | Path | Contents |
28
+ | --- | --- |
29
+ | `src/` | The compiler (`src/compiler/`) and the tools around it: CLI, workspace, client, CPI and port generators, testing, IDL publishing (source map below) |
30
+ | `runtime/` | `typesol_rt.h`, the C runtime every program includes ([runtime.md](runtime.md)) |
31
+ | `lib/` | `typesol.d.ts`, the language surface programs typecheck against |
32
+ | `ts-plugin/` | The editor plugin (`typesol/ts-plugin`) |
33
+ | `examples/` | Example programs; the `typesol init` templates are copies of three of them |
34
+ | `test/` | The test suite: `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated clients), `programs/` (third-party program binaries, see its SOURCES.md) |
35
+ | `bench/anchor/` | Anchor twins of the counter, vault and escrow examples, for the differential tests (`pnpm bench:anchor`) and the zero-copy client test; the benchmarks themselves are in [typesol/benchmarks](https://github.com/typesol/benchmarks) |
36
+ | `scripts/` | `smoke.ts` and `localnet.sh` (a local validator run), `fetch-programs.sh` (third-party binaries), `readme-bench.ts` (the README numbers) |
37
+ | `docs/` | User and contributor documentation (also published on typesol.com) |
38
+ | `assets/` | The CLI's logo and the README images (`readme/`, generated) |
39
+
25
40
  ## Source map
26
41
 
27
42
  | Path | Role |
@@ -34,11 +49,17 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
34
49
  | `src/compiler/idl.ts` | Turns the model into an Anchor IDL: accounts with `pda`, `relations`, `address` and `optional`, plus types, events, errors and docs. |
35
50
  | `src/compiler/toolchain.ts` | Finds the Agave SDK and runs clang and lld with the SDK's flags, plus per-function sections, `--gc-sections` and `--strip-all` so unused code and symbol tables stay out of the binary. |
36
51
  | `src/compiler/diagnostics.ts` | `Reporter` and `CompileError`: diagnostics with file, line and column. |
52
+ | `src/compiler/anchor-errors.ts` | Anchor's own error codes (`AnchorError.X`), shared by `require` and the generated clients. |
53
+ | `src/compiler/index.ts` | The compiler's API (`generate`, `build`, `verifyBinary`, the build manifest). |
37
54
  | `runtime/typesol_rt.h` | The C runtime: error codes, word-wise memory helpers, the entry point's input parser (`ts_deserialize`), account checks, checked math, Borsh helpers, CPIs (System, Token, Token-2022, ATA, Metaplex), oracles and the heap allocator. Header-only and `static inline`, so unused code is dropped. |
38
55
  | `src/workspace.ts` | `typesol.json` projects, program keypairs, `typesol init` templates and deploy arguments. |
39
56
  | `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
40
57
  | `src/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
41
- | `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program. |
58
+ | `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program, and reading a program's IDL back from the chain (Program Metadata or Anchor's IDL account) for `typesol client --address`. |
59
+ | `src/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
60
+ | `src/templates.ts` | The programs and tests `typesol init --template` writes (copies of `examples/`; a test checks they match). |
61
+ | `src/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
62
+ | `src/keys.ts` | Base58 and program keypair helpers. |
42
63
  | `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
43
64
  | `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
44
65
  | `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
@@ -54,7 +75,7 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
54
75
 
55
76
  | File | Layer | Contents |
56
77
  | --- | --- | --- |
57
- | `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`) |
78
+ | `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`), growable Vec placement (`vecPlace`, `vecLenCheck`) |
58
79
  | `base.ts` | `EmitterBase` | function state; `line()`, `tmp()`, `settle()`, scopes, `fail()`, account lookup |
59
80
  | `storage.ts` | `StorageEmitter` | references into account data and locals, reads and writes, indexing, aliasing, argument validation |
60
81
  | `expressions.ts` | `ExpressionEmitter` | literals, operators and checked arithmetic, property reads, conditionals, text, casts |
@@ -63,6 +84,9 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
63
84
  | `statements.ts` | `StatementEmitter` | variables, assignment, `if`/loops/`switch`, `return` |
64
85
  | `function.ts` | `FunctionEmitter` | the instruction prologue (account checks, constraints, argument decoding) and epilogue |
65
86
 
87
+ `borsh.ts` writes the Borsh pack/unpack codecs of `@layout borsh` structs, and `program.ts` assembles the C file
88
+ (typedefs, codecs, helpers, instructions, the entry point). `index.ts` is the emitter's export.
89
+
66
90
  ## Rules the emitter keeps
67
91
 
68
92
  These invariants are easy to break. Each one has tests.
package/docs/cli.md CHANGED
@@ -6,7 +6,7 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
6
6
 
7
7
  | Command | What it does |
8
8
  | --- | --- |
9
- | `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`. The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. |
9
+ | `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`, `programs/tsconfig.json` (with the editor plugin) and `.gitignore`. `<name>` must be snake_case; by default it is derived from `<dir>` (`my-app` → `my_app`). The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. |
10
10
  | `typesol build [<program>…] [--sbpf v0\|v1\|v2] [--profile] [--quiet]` | In a project: build every program (or those named) into `<out>/<name>/`: `<name>.so`, `<name>.json` (Anchor IDL), `<name>.client.ts` (TypeScript client, see [client.md](client.md)), `<name>.c`, `<name>.build.json`. The first build creates `<out>/deploy/<name>-keypair.json` and records the address in `typesol.json`; later builds refuse a missing or different keypair. |
11
11
  | `typesol build <file.ts> [--out <dir>] [--program-id <keypair.json>] [--sbpf …] [--profile]` | Outside a project (or a file that is not one of its programs): build that file. A project program's `.ts` builds as that program. |
12
12
  | `typesol check <file.ts> [--json] [--stdin]` | Type-check and validate without building. `--json` prints `{ ok, diagnostics: [{ file, line, column, message }] }`; `--stdin` checks the text given on stdin as that file (its imports come from disk). |
@@ -14,7 +14,7 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
14
14
  | `typesol deploy [<program>…] [--url <rpc>] [--keypair <payer.json>] [--config <cli config>] [--dry-run] [--sbpf …]` | Build, then `solana program deploy --use-rpc` at the program keypair's address. Deploying again upgrades (the payer must be the upgrade authority). `--dry-run` builds and prints the commands. Needs `solana` on PATH. |
15
15
  | `typesol idl publish [<program>…] [--url <rpc>] [--keypair <authority.json>] [--config <cli config>] [--non-canonical]` | Compile (the binary is not rebuilt), then write each IDL to the Program Metadata program (seed `idl`). Canonical IDLs must be signed by the upgrade authority. Defaults come from the Solana CLI config (`~/.config/solana/cli/config.yml`). |
16
16
  | `typesol cpi <idl.json> [--out <file.ts>] [--prefix <name>] [--address <base58>]` | Generate TypeSOL helpers that call another program from its IDL. Unsupported instructions are listed on stderr. |
17
- | `typesol port <idl.json> [--out <file.ts>]` | A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program: account structs, enums, errors and one function per instruction with its accounts and arguments, each body `require(false, …NotPorted)` until written. Instruction discriminators (an explicit `@discriminator` when the name derives another), account names and error codes are kept, so existing clients keep working where the layouts match; notes on stderr list where they do not (Borsh `String`/`Option` in account data, a `Vec` that is not the last field, custom discriminators or error codes). |
17
+ | `typesol port <idl.json> [--out <file.ts>]` | A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program: account structs, enums, errors, events and one function per instruction with its accounts and arguments, each body `require(false, …NotPorted)` until written. Instruction discriminators (an explicit `@discriminator` when the name derives another), account names and error codes are kept, so existing clients keep working where the layouts match; notes on stderr list where they do not (Borsh `String`/`Option` in account data, a `Vec` that is not the last field, custom discriminators or error codes). |
18
18
  | `typesol client <idl.json> [--out <file.ts>] [--address <base58>]`, `typesol client --address <program id> [--url <rpc>] [--config <cli config>] [--out <file.ts>]` | Generate a typed TypeScript client on `@solana/kit` from an Anchor IDL (0.30+/1.x or TypeSOL); the IDL is validated first, and what the client leaves out is printed as notes. Without a file, the IDL is read from the chain: the canonical Program Metadata IDL, else Anchor's IDL account. See [client.md](client.md). |
19
19
  | `typesol verify <program\|file.ts> [<deployed.so>] [--address <program id>] [--url <rpc>] [--config <cli config>] [--sbpf …]` | Rebuild and compare with what is deployed. Without a `.so`, reads the program from the cluster (`--url`, cluster monikers such as `mainnet-beta` included, else the Solana CLI config) at `--address` or the address `typesol.json` records, under any loader (v2, upgradeable v3, v4); prints the upgrade authority (or that the program is immutable) and the last deploy slot, and compares the IDL published with `typesol idl publish` (a published IDL that differs is a mismatch). With a `.so` (e.g. `solana program dump`), compares with that file. Trailing zero padding is ignored; the SBPF version is read from the deployed ELF unless `--sbpf` is given. |
20
20
  | `typesol profile <logs.txt\|-> [--program <id>]` | Compute units per TypeScript line from the logs of a `--profile` build's transaction (exit 1 when the logs have no markers). |
package/docs/client.md CHANGED
@@ -19,7 +19,11 @@ const [book] = await ledgerbook.pdas.book({ admin: signer.address, title: "grant
19
19
  const file = await ledgerbook.file({ who: signer, book }, { amount: 100n, note: "rent", memo: null });
20
20
 
21
21
  // Accounts: decode bytes you already have, or fetch through an RPC.
22
- const b = await ledgerbook.accounts.Book.fetch(rpc, book); // { title: string, approver: Address | null, entries: [...], ... }
22
+ const bookData = await ledgerbook.accounts.Book.fetch(rpc, book); // { title: string, approver: Address | null, entries: [...], ... }
23
+
24
+ // Send and confirm (one transaction, signers in the accounts sign too), or simulate
25
+ const signature = await ledgerbook.sendInstructions({ rpc, rpcSubscriptions }, signer, [create, file]);
26
+ const { err, logs, unitsConsumed, returnData } = await ledgerbook.simulate(rpc, signer, [file]);
23
27
 
24
28
  // Errors: by name, from a kit SolanaError, a LiteSVM result, a simulation message or a code.
25
29
  if (ledgerbook.isError(e, "NotApprover")) { /* ... */ }
@@ -32,13 +36,9 @@ for (const ev of ledgerbook.parseEvents(logs)) if (ev.name === "Filed") ev.data.
32
36
  ledgerbook.returns.total(returnDataBytes); // bigint
33
37
  await ledgerbook.views.total(rpc, signer, { book }, { status: Status.Approved }); // bigint, nothing sent
34
38
 
35
- // Send and confirm (one transaction, signers in the accounts sign too), or simulate
36
- const signature = await ledgerbook.sendInstructions({ rpc, rpcSubscriptions }, signer, [create, file]);
37
- const { err, logs, unitsConsumed, returnData } = await ledgerbook.simulate(rpc, signer, [file]);
38
-
39
39
  // Every Book of the program, or those with given bytes at an offset (after the 8-byte discriminator)
40
40
  const books = await ledgerbook.accounts.Book.fetchAll(rpc); // [{ address, data }]
41
- const several = await ledgerbook.accounts.Book.fetchMultiple(rpc, [a, b]); // (Book | null)[]
41
+ const several = await ledgerbook.accounts.Book.fetchMultiple(rpc, [book, otherBook]); // (Book | null)[]
42
42
 
43
43
  // Live updates, until the AbortSignal aborts
44
44
  for await (const book of ledgerbook.accounts.Book.watch(rpcSubscriptions, address, { abortSignal })) render(book);
@@ -51,7 +51,7 @@ await ledgerbook.approveMany({ approver: signer, book }, { indices: [0n, 1n] },
51
51
  const accounts = await ledgerbook.resolve.someInstruction(rpc, { book }, { signer }); // signers, and what book's fields name
52
52
 
53
53
  // Events of a fetched transaction (getTransaction, json): log events and emitCpi events from inner instructions
54
- ledgerbook.eventsOf(await rpc.getTransaction(signature, { maxSupportedTransactionVersion: 0 }).send());
54
+ ledgerbook.eventsOf(await rpc.getTransaction(signature, { encoding: "json", maxSupportedTransactionVersion: 0 }).send());
55
55
  ```
56
56
 
57
57
  Signer accounts take a `TransactionSigner`, which kit uses to sign; an `Address` works too when you sign separately.
@@ -87,8 +87,11 @@ Anchor IDLs are refused with a hint to convert them (`anchor idl convert`).
87
87
 
88
88
  ## Limits
89
89
 
90
- - **Errors and events are matched by code and discriminator, not by program.** A program this one calls can raise the
91
- same error code, and `parseEvents` reads every `Program data:` line that decodes as one of this program's events.
90
+ - **Errors are matched by code, not by program**: a program this one calls can raise the same error code. Events are
91
+ this program's own: `parseEvents` follows the `Program <id> invoke` / `success` / `failed` lines and reads a
92
+ `Program data:` line only while this program is the innermost one running, and `eventsOf` reads only the `emitCpi`
93
+ inner instructions that call this program (it needs `tx.transaction`'s account keys for that). `parseCpiEvents(data)`
94
+ on its own does not check the program: give it this program's inner instructions only.
92
95
  - **Not filled in automatically** (pass them): PDAs seeded from struct fields (Anchor cannot seed with them either),
93
96
  PDAs whose program is another account's field, and a PDA seeded from its own fields (derive it with its `pdas`
94
97
  helper from the values). `typesol client` prints a note for each.
@@ -98,5 +101,5 @@ Anchor IDLs are refused with a hint to convert them (`anchor idl convert`).
98
101
  Rust's own layout (`repr(Rust)`) or holding a non-zero-copy type, and the generic definitions themselves.
99
102
  - **In tests**: `svmConnection(svm)` from `typesol/testing` answers the client's RPC calls from LiteSVM, and
100
103
  `testProgram(...)` has it as `t.connection` ([test-your-program.md](test-your-program.md)).
101
- - `watchEvents` reads log events; with `rpc` it also fetches each transaction in which the program invoked itself and
104
+ - `watchEvents` reads log events of transactions that succeeded (a failed one is skipped); with `rpc` it also fetches each transaction in which the program invoked itself and
102
105
  reads its `emitCpi` events (log subscriptions do not carry inner instructions).
package/docs/errors.md CHANGED
@@ -6,12 +6,16 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
6
6
  | --- | --- |
7
7
  | 100 / 101 / 102 | instruction data missing / unknown instruction / malformed arguments (InstructionMissing / FallbackNotFound / DidNotDeserialize) |
8
8
  | 2001 | HasOne constraint failed (ConstraintHasOne) |
9
+ | 2002 | event authority did not sign (ConstraintSigner) |
10
+ | 2004 | Owner constraint failed (ConstraintOwner) |
9
11
  | 2005 | Zeroed account is not rent-exempt (ConstraintRentExempt) |
10
12
  | 2006 | PDA seeds mismatch (ConstraintSeeds) |
13
+ | 2007 | Executable constraint failed (ConstraintExecutable) |
11
14
  | 2009 | not the associated token account (ConstraintAssociated) |
12
15
  | 2012 | not the expected address (ConstraintAddress) |
13
16
  | 2014 / 2015 | token account mint / owner mismatch (ConstraintTokenMint / ConstraintTokenOwner) |
14
17
  | 2016 / 2017 / 2018 | mint authority / freeze authority / decimals mismatch (2018: an existing mint `createIfNeeded` found) |
18
+ | 2024–2039 | an existing mint `createIfNeeded` found lacks a Token-2022 extension it was given, or holds other keys in it: group pointer 2024–2026, group member pointer 2027–2029, metadata pointer 2030–2032, close authority 2033–2034, permanent delegate 2035–2036, transfer hook 2037–2039 (the first code of each: the extension is missing; the next: the authority / address differs) (ConstraintMint…Extension…) |
15
19
  | 2040 | the same account passed in two writable slots (ConstraintDuplicateMutableAccount) |
16
20
  | 3000 | account already exists / initialized (AccountDiscriminatorAlreadySet) |
17
21
  | 3002 | discriminator mismatch (AccountDiscriminatorMismatch) |
@@ -28,7 +32,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
28
32
  | 4102 | cast overflow (InvalidNumericConversion) |
29
33
  | 5100 / 5101 | arithmetic overflow (including shifts) / division by zero |
30
34
  | 5103 | sysvar unavailable |
31
- | 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec, `push` on a full FixedVec) |
35
+ | 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec, `push` on a full FixedVec, a stored `map` / `filter` result one element too small) |
32
36
  | 5105 | string longer than the FixedString capacity |
33
37
  | 5106 | missing or mismatched return data |
34
38
  | 5107 | event larger than 1024 bytes |
@@ -39,5 +43,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
39
43
  | 5113 | more than 32 accounts in one instruction |
40
44
  | 5114 | a function with a result ended without returning one |
41
45
  | 5115 | an optional account that was left out was used |
42
- | 2002 / 2004 / 2007 | event authority did not sign / Owner constraint / Executable constraint |
43
- | 6000+ | your `require` errors, in declaration order |
46
+ | 6000+ | your error enums' members, in declaration order (imported files first), then one `RequireFailed<N>` error (code 6000 + N) per distinct `require(cond, "message")` string |
47
+
48
+ Any of Anchor's own codes can be raised directly: `require(cond, AnchorError.RequireKeysEqViolated)` fails with 2502,
49
+ as `require_keys_eq!` does.
package/docs/language.md CHANGED
@@ -6,12 +6,12 @@ The full reference. For a tour, start with the [README](../README.md).
6
6
 
7
7
  | Concept | TypeSOL |
8
8
  | --- | --- |
9
- | Instruction | an exported function returning `void`, one integer / boolean / `Pubkey`, or a fixed-size struct (sent as return data, at most 1024 bytes; `returns` in the IDL, and the generated client decodes it) |
9
+ | Instruction | an exported function returning `void`, one integer / boolean / `Pubkey`, a `string` (a Borsh String), or a fixed-size struct (sent as return data, at most 1024 bytes; `returns` in the IDL, and the generated client decodes it) |
10
10
  | Account data | an exported `interface` with fixed-size fields; interfaces nest (`home: Point`, `trail: FixedArray<Point, 4>`) |
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+; `require(cond, AnchorError.RequireKeysEqViolated)` fails with one of Anchor's own codes (2502 here; every anchor-lang `ErrorCode`) |
14
+ | Errors | `export enum MyError { Name = "message" }` + `require(cond, MyError.Name)` → codes 6000+; `require(cond, "message")` makes an error of its own (`RequireFailed<N>`, numbered after the enums' errors; one per distinct message, listed in the IDL); `require(cond, AnchorError.RequireKeysEqViolated)` fails with one of Anchor's own codes (2502 here; any anchor-lang `ErrorCode` can be raised this way). See [errors.md](errors.md) |
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 |
@@ -27,7 +27,7 @@ The full reference. For a tour, start with the [README](../README.md).
27
27
  | Vecs | `entries: Vec<Entry>` in account data, any number of them, declared anywhere: they are stored after the fixed-size fields in the order declared (so every other field keeps its offset; the IDL lists that order), and a push, pop or clear moves the Vecs after it (a reference into one of those cannot be used across it). In a `@layout borsh` struct a Vec is the last field (Anchor's order) — `get`, `set`, `push(v, payer)`, `pop()`, `clear()`, `.length`; the account is resized (realloc) and rent topped up by `payer`; `Vec<u64>` arguments are read-only views; `Vec<string>` arguments (Anchor's `Vec<String>`) are views of the instruction data, each checked as UTF-8 on entry (102), and go back out as Borsh through `invoke`; `pop()` of a Vec of structs works as a statement |
28
28
  | Anchor's account bytes | `/** @layout borsh */` on an account struct stores it as Anchor does (Borsh: a `FixedString<N>` is a String, a `FixedVec<T, N>` a Vec, an `Option` one byte when empty, and the fields after them move), so a TypeSOL program can take over accounts an Anchor program wrote and the IDL lists Anchor's own types. The instruction unpacks it into the usual fixed layout when it starts (bad bytes fail with 3003) and packs it back when it returns (3004 when the account is too small for the new value), so everything else works on it unchanged. Programs invoked from the body (CPIs) see the account's bytes as they were when the instruction started. Costs more compute than the fixed layout (the whole account is read and written each time; see the benchmarks); nested structs and list elements must be fixed-size (FixedString elements allowed). The same struct is Borsh as a whole instruction argument (checked like Borsh deserialization, UTF-8 included), as return data and as an event; as those (not as account data) it may also hold `string` fields and data enums carrying strings, Anchor's `String` and enums in argument structs (account data takes data enums without strings); not inside list or enum arguments, `Zeroed<T>`, `loadAccount` or a growable `Vec<T>` |
29
29
  | Fixed-capacity lists | `owners: FixedVec<Pubkey, 8>` — any field, any number of them, in nested structs too: a u32 length and room for N elements (always 4 + N × size bytes), the same `get`, `set`, `push(v)`, `pop()`, `clear()`, `.length` and `for … of`; `push` on a full one fails (5104); `acct.owners = fixedVec([a, b])` writes a whole one (unused slots zeroed). In account data, returned structs and events; as an argument (`ids: FixedVec<u64, 8>`) it is a Borsh Vec of at most N elements, read like a `Vec` argument (more fail with 102) |
30
- | Struct arguments | `order: Order`, `ids: FixedArray<u8, 4>`, `orders: Vec<Order>` — fixed layouts, Borsh-identical, validated on entry (bools, enums, option tags, string lengths, enum tags; 102) and read-only |
30
+ | Struct arguments | `order: Order`, `ids: FixedArray<u8, 4>`, `orders: Vec<Order>` — fixed layouts, Borsh-identical, validated on entry (bools, enums, option tags, string lengths, enum tags; 102) and read-only. A struct holding a FixedVec that is only ever an argument is read as Anchor sends it (its FixedVec a Borsh Vec, as `@layout borsh`) |
31
31
  | Remaining accounts | `remainingAccounts()` — `.length`, `.get(i)` → `key`, `owner`, `lamports` (writes check writability), `isSigner`, `isWritable`; `loadAccount<T>` / `loadAccountMut<T>` give checked struct views; pass them to `invoke`, one at a time or spread: `...remainingAccounts()` (each with the flags it was passed with), `...readonly(rest)` / `...writable(rest)` (all with that role), `rest.slice(a, b)` for part of them; a CPI lists at most 32 accounts (5113) |
32
32
  | Struct literals | `book.entries.push({ who: who.key, amount, status: Status.Pending, note }, who)` — every field required |
33
33
  | Struct references | `const o = side === Side.Bid ? book.bids.get(i) : book.asks.get(i)` — aliases account data like a JS object reference (no copy); replacing the referenced place (`book.bids.set(i, …)`, `pop()`, `clear()`, `acct.p = …`) while the reference is still used afterwards is a compile error, because JS would keep the old object |
@@ -42,21 +42,21 @@ The full reference. For a tour, start with the [README](../README.md).
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">`) |
43
43
  | Multiple files | `import { Side, min } from "./math"` — structs, enums, constants and helpers can live in other files; only the entry file's exported functions are instructions; names are program-wide, so imports from TypeSOL files cannot be renamed; error codes number imported files first |
44
44
  | Loops | `for (let i = 0n; i < n; i++)`, `while`, `do … while`, `for (const x of list)` over a `Vec` or `FixedArray` (each `x` is what `list.get(i)` gives: a value, or a reference into account data for structs; the length is read again every step, as in JS), `break`, `continue` — bounded by the compute budget. Remaining accounts are iterated by index |
45
- | Helpers | non-exported functions (and every function of an imported file): take values, structs, Vec arguments, `SignerSeeds` and accounts the instruction already checked (`Signer`, `AccountInfo`/`AccountMeta`, `Mut<T>`, token accounts, programs); return `void`, a scalar or a fixed-size struct (`function at(x: u64, y: u64): Point { return { x, y }; }`: the caller gets its own copy, to bind, store with `acct.home = at(…)` / `arr.set(i, at(…))` or read as `at(…).x`); no recursion. A helper that takes a struct account as `Mut<T>` can grow its Vec (`push(value, payer)`: the account's size at instruction entry is passed along) |
46
- | Arrow functions and closures | `const fee = (x: u64): u64 => x / 100n` (or `const f = function (…) {…}`) at the top level or inside a function. One that reads or writes the enclosing function's variables (a closure: `let total = 0n; const add = (v: u64): void => { total += v; }`) is expanded where it is called: its names mean what they meant where it was declared, struct parameters are references, `return` leaves the closure; it cannot call itself. One that captures nothing becomes a helper. Parameter types may be left out on closures (taken from the arguments) |
47
- | Functions as arguments | a helper may take a function parameter (`function applyTwice(f: (x: u64) => u64, v: u64): u64`): it is compiled once per named function passed (a helper or a local function that captures nothing). An arrow written at the call works too (`applyTwice((x) => x * 3n, v)`: its types come from the parameter) when it reads none of the function's variables. Given a closure (one that reads them), the helper is expanded where it is called instead, like a closure, so it runs where the closure can; such a helper takes values, not accounts, and has no type parameters |
48
- | List callbacks | on a `Vec`, `FixedArray`, `FixedVec` or `Vec` argument: `forEach`, `some`, `every`, `find` (an Option of the element; scalar elements), `findIndex` (i64, -1 when absent), `reduce(fn, initial)`, `includes(value)`, `indexOf(value)` (i64), `map` and `filter`. The arrow written at the call is expanded into a loop (elements as `list.get(i)` gives them, structs by reference) and may read and write the function's variables. A `map` / `filter` chain used right away (`xs.filter(f).map(g).reduce(h, 0n)`, `.length`, `for … of`) is one loop that stores nothing; callbacks after a filter get the index among the elements it let through. Stored (`const ys = xs.filter(f)`), it is a FixedVec on the stack as big as the list it starts from; from a `Vec`, give the size: `const ys: FixedVec<u64, 16> = …` (at most 2 KiB; one element more aborts with 5104). `map` gives integers, booleans, Pubkeys or enums |
45
+ | Helpers | non-exported functions (and every function of an imported file): take values, structs, Vec arguments, `SignerSeeds` and accounts the instruction already checked (`Signer`, `AccountInfo`/`AccountMeta`, `Mut<T>`, token accounts, programs); return `void`, a scalar, a `string`, a function (see below) or a fixed-size struct (`function at(x: u64, y: u64): Point { return { x, y }; }`: the caller gets its own copy, to bind, store with `acct.home = at(…)` / `arr.set(i, at(…))` or read as `at(…).x`); no recursion. A helper that takes a struct account as `Mut<T>` can grow its Vec (`push(value, payer)`: the account's size at instruction entry is passed along) |
46
+ | Arrow functions and closures | `const fee = (x: u64): u64 => x / 100n` (or `const f = function (…) {…}`) at the top level or inside a function. One that reads or writes the enclosing function's variables (a closure: `let total = 0n; const add = (v: u64): void => { total += v; }`) is expanded where it is called: its names mean what they meant where it was declared, struct parameters are references, `return` leaves the closure; it cannot call itself. One that captures nothing becomes a helper. Parameter types may be left out on closures (taken from the arguments). Functions are values where they can be expanded: `const h = g` names the same function, and a function that returns one (`function adder(n: u64): (x: u64) => u64 { return (x) => x + n; }`) makes a closure where it is called (`const add5 = adder(5n)`, or `adder(5n)(x)`) |
47
+ | Functions as arguments | a helper may take a function parameter (`function applyTwice(f: (x: u64) => u64, v: u64): u64`): it is compiled once per named function passed (a helper or a local function that captures nothing). An arrow written at the call works too (`applyTwice((x) => x * 3n, v)`: its types come from the parameter) when it reads none of the function's variables. Given a closure (one that reads them), the helper is expanded where it is called instead, like a closure, so it runs where the closure can (its type parameters bound from the call, its accounts the caller's) |
48
+ | List callbacks | on a `Vec`, `FixedArray`, `FixedVec` or `Vec` argument: `forEach`, `some`, `every`, `find` (an Option of the element; scalar elements), `findIndex` (i64, -1 when absent), `reduce(fn, initial)`, `includes(value)`, `indexOf(value)` (i64), `map` and `filter`. The arrow written at the call is expanded into a loop (elements as `list.get(i)` gives them, structs by reference) and may read and write the function's variables. A `map` / `filter` chain used right away (`xs.filter(f).map(g).reduce(h, 0n)`, `.length`, `for … of`) is one loop that stores nothing; callbacks after a filter get the index among the elements it let through. Stored (`const ys = xs.filter(f)`), it is a FixedVec on the stack as big as the list it starts from; from a `Vec`, give the size: `const ys: FixedVec<u64, 16> = …` (at most 2 KiB; one element more aborts with 5104). `map` gives integers, booleans, Pubkeys or enums; stored with a `FixedVec<Struct, N>` or `FixedVec<FixedString<N>, M>` annotation it gives structs (`(o) => ({ qty: o.qty })`) or text |
49
49
  | Generic helpers | `function max<T extends bigint>(a: T, b: T): T` — compiled once per set of types it is called with (`max(a, b)` with u64 arguments, or `max<u128>(x, 1n)` when only literals tell); a type parameter stands for an integer, boolean, Pubkey, enum or fixed-size struct (`pick<Point>(…)`, or from struct arguments; a struct `T` is copied like any struct value). Each instance is checked like any helper; one that is never called is not compiled (TypeScript still type-checks it). Arithmetic on `T` is a `bigint` to TypeScript: `return (a + b) as T` (`as` names a type the value already has; it never converts) |
50
50
  | Evaluation order | operands and arguments are read left to right, as in JS, even when a later one changes account data (`v.length + v.pop()`) |
51
51
  | `++` `--` `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` `<<=` `>>=` | all checked |
52
52
  | Sysvars | `now()`, `currentSlot()`, `currentEpoch()` (clock), `minimumBalance(account)`, `rentExempt(space)` (rent) |
53
- | Account size | `realloc(account, newSize, payer)` on a `Mut<Struct>` — growing charges the payer the extra rent (at most 10 KiB per instruction, 3016); shrinking keeps the lamports in the account; never below the struct |
53
+ | Account size | `realloc(account, newSize, payer)` on a `Mut<Struct>` — growing charges the payer the extra rent (at most 10 KiB per instruction, 3016); shrinking keeps the lamports in the account; never below the struct. Refused on a struct with a growable `Vec` (its push / pop / clear size the account) |
54
54
  | Token-2022 extensions | `transferFee(mint, amount)` (current-epoch TransferFeeConfig, ceil + cap like spl-token-2022), `transferHookProgram(mint)`, `transferHookAuthority(mint)`, `permanentDelegate(mint)`, `mintCloseAuthority(mint)` (Option<Pubkey>), `hasExtension(account, type)`, `isTransferring(tokenAccount)` (inside a transfer hook: whether Token-2022 is transferring from it) |
55
55
  | Events through CPI | `emitCpi<Paid>({ … }, eventAuthority, self)` with `eventAuthority: EventAuthority, self: SelfProgram` parameters — Anchor `emit_cpi!`: the event rides in a self-CPI (inner instruction), not in logs; the program accepts Anchor's event instruction (2002 / 2006 otherwise) |
56
56
  | Heap | 32 KiB by default; `const HEAP_SIZE_KIB = 64n` (32–256) lets the program use more, when clients add ComputeBudget `requestHeapFrame` |
57
57
  | Docs | JSDoc on instructions, `@param`, structs, fields and data enums becomes `docs` in the IDL |
58
- | Metaplex Core | `core: MplCoreProgram` — `createCollection`, `createAsset` (in a collection, PDA authorities via signerSeeds), `transferAsset`, `burnAsset` (`null` leaves an optional account out); plugins as a literal list after the uri (`[{ kind: "Royalties", basisPoints: 500n, creators: [{ address, percentage: 100n }] }, { kind: "PermanentFreezeDelegate", frozen: false, authority: studio.key }]`), and `addPlugin` / `updatePlugin` / `removePlugin(…, "FreezeDelegate")` with their `…CollectionPlugin` forms. Plugins: Royalties (with an optional `ruleSet: { programAllowList \| programDenyList: [...] }`), FreezeDelegate, BurnDelegate, TransferDelegate, UpdateDelegate, PermanentFreezeDelegate, Attributes, PermanentTransferDelegate, PermanentBurnDelegate, Edition, MasterEdition (collections: `maxSupply?`, `name?`, `uri?`), AddBlocker, ImmutableMetadata, VerifiedCreators, Autograph, BubblegumV2, FreezeExecute, PermanentFreezeExecute (Groups is kept by Core's group instructions); `authority` is `"owner"`, `"updateAuthority"`, `"none"` or a Pubkey; `coreAssetOwner(asset)`, `coreAssetCollection(asset)` |
59
- | Compressed NFTs | `bubblegum: BubblegumProgram` — V1 (trees on SPL Account Compression): `createTree`, `mint` (MintV1, no collection/creators), `transfer`, `burn`, `delegate` (the leaf owner or delegate signs; a PDA owner signs with the seeds). V2 (trees on mpl-account-compression with mpl-noop): `createTreeV2`, `mintV2(…, { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable? }, { collection, authority, cpiSigner }?)` (into a Core collection that has the `BubblegumV2` plugin), `transferV2`, `burnV2`, `delegateV2`, `freezeV2` / `thawV2`, `setNonTransferableV2`, `updateMetadataV2(…, currentMetadata, { name?, symbol?, uri?, creators?, … })`, `verifyCreatorV2` / `unverifyCreatorV2`, `setCollectionV2`. Each takes the leaf (`{ root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index }`, or `{ root, assetDataHash?, flags?, nonce, index }` with the leaf's current metadata for the metadata instructions) and its proof as `remainingAccounts()` / `rest.slice(a)` |
58
+ | Metaplex Core | `core: MplCoreProgram` — `createCollection`, `createAsset` (in a collection, PDA authorities via signerSeeds), `transferAsset`, `burnAsset` (`null` leaves an optional account out); plugins as a literal list after the uri (`[{ kind: "Royalties", basisPoints: 500n, creators: [{ address, percentage: 100n }] }, { kind: "PermanentFreezeDelegate", frozen: false, authority: studio.key }]`), and `addPlugin` / `updatePlugin` / `removePlugin(…, "FreezeDelegate")` with their `…CollectionPlugin` forms. Plugins: Royalties (with an optional `ruleSet: { programAllowList \| programDenyList: [...] }`), FreezeDelegate, BurnDelegate, TransferDelegate, UpdateDelegate, PermanentFreezeDelegate, Attributes, PermanentTransferDelegate, PermanentBurnDelegate, Edition, MasterEdition (collections: `maxSupply?`, `name?`, `uri?`), AddBlocker, ImmutableMetadata, VerifiedCreators, Autograph, BubblegumV2, FreezeExecute, PermanentFreezeExecute (Groups is kept by Core's group instructions); `authority` is `"owner"`, `"updateAuthority"`, `"none"` or a Pubkey; `coreAssetOwner(asset)`, `coreAssetCollection(asset)`. External plugin adapters: `addExternalPlugin` / `removeExternalPlugin` / `writeExternalPluginData` (and the Collection forms) with `{ kind: "AppData", dataAuthority, schema? }`, `{ kind: "Oracle", baseAddress, checks: [{ event: "transfer", canReject: true }], resultsOffset?, baseAddressConfig? }`, `{ kind: "LifecycleHook", hookedProgram, checks, extraAccounts?, dataAuthority?, schema? }` and the Linked forms; a key names one (`{ kind: "AppData", dataAuthority }`, `{ kind: "Oracle", address }`); data is a string, a `Vec<u8>` argument or `null` (Core currently answers lifecycle hooks with NotAvailable, 23) |
59
+ | Compressed NFTs | `bubblegum: BubblegumProgram` — V1 (trees on SPL Account Compression): `createTree`, `mint` (MintV1, no collection/creators), `mintToCollection` (MintToCollectionV1: into a verified Token Metadata collection), `transfer`, `burn`, `delegate` (the leaf owner or delegate signs; a PDA owner signs with the seeds). V2 (trees on mpl-account-compression with mpl-noop): `createTreeV2`, `mintV2(…, { name, symbol, uri, sellerFeeBasisPoints, creators?, isMutable? }, { collection, authority, cpiSigner }?)` (into a Core collection that has the `BubblegumV2` plugin), `transferV2`, `burnV2`, `delegateV2`, `freezeV2` / `thawV2`, `setNonTransferableV2`, `updateMetadataV2(…, currentMetadata, { name?, symbol?, uri?, creators?, … })`, `verifyCreatorV2` / `unverifyCreatorV2`, `setCollectionV2`. Each takes the leaf (`{ root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index }`, or `{ root, assetDataHash?, flags?, nonce, index }` with the leaf's current metadata for the metadata instructions) and its proof as `remainingAccounts()` / `rest.slice(a)` |
60
60
 
61
61
  ## Account parameters and the checks they emit
62
62
 
@@ -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. 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) |
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 (its only token program parameter, or one passed as the last argument of `create` / `createIfNeeded`; 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` |
@@ -129,7 +129,9 @@ export function relayIncrement(authority: Signer, counter: Mut<AccountInfo>, cou
129
129
  }
130
130
 
131
131
  // Sign for a PDA of this program: raw System transfer (u32 tag 2, u64 lamports) out of a treasury PDA.
132
- invoke(systemProgram, [writableSigner(treasury), writable(recipient)], [u32(2n), amount], ["treasury", admin.key, bump]);
132
+ export function payOut(admin: Signer, treasury: Mut<SystemAccount>, recipient: Mut<SystemAccount>, systemProgram: SystemProgram, amount: u64, bump: u8): void {
133
+ invoke(systemProgram, [writableSigner(treasury), writable(recipient)], [u32(2n), amount], ["treasury", admin.key, bump]);
134
+ }
133
135
  ```
134
136
 
135
137
  - accounts: `readonly(x)`, `writable(x)`, `readonlySigner(x)`, `writableSigner(x)`. The compiler refuses `writable` on a
@@ -154,12 +156,11 @@ an integer literal, an argument, `acct.key` or a scalar field of an account, the
154
156
 
155
157
  ## Not supported yet
156
158
 
157
- A struct argument holding a FixedVec (use `@layout borsh` for the struct, or a FixedVec argument of its own), growing an account by more than 10 KiB in one instruction (Solana's limit: realloc again in
159
+ A struct holding a FixedVec that is both an argument and account data (declare one for each), growing an account by more than 10 KiB in one instruction (Solana's limit: realloc again in
158
160
  the next instruction), `?.` on anything but optional accounts and Option<struct> fields, `!` on anything but an Option
159
- or an optional account, Core's external plugin adapters (oracles, app data, lifecycle hooks: use `invoke`), closures
160
- stored or returned (they are called, passed to list methods or given to helpers), a helper with type parameters or
161
- account parameters given a closure, a stored `map` result of structs or
162
- strings, floating point (by design).
161
+ or an optional account, functions stored where they cannot be expanded (in a variable chosen at run time, a struct,
162
+ a list, or returned from an instruction), a fused `map` to structs or strings (store it, annotated), floating point
163
+ (by design).
163
164
  They are rejected at compile time rather than half-supported.
164
165
 
165
166
  `Init.create` handles a PDA address someone pre-funded: it tops up rent, then allocates and assigns instead of
package/docs/mainnet.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  From an empty directory to a program on Solana mainnet-beta: create, test, deploy, publish the IDL, then check that
4
4
  the deployed binary is the one your source builds. The program here is the counter that `typesol init` creates
5
- (13.5 KB); any TypeSOL project works the same way.
5
+ (13.2 KiB); any TypeSOL project works the same way.
6
6
 
7
7
  You need Node.js 22.12 or later and the Agave CLI with platform-tools (see the README's Requirements).
8
8