@typesol/cli 0.21.0 → 0.23.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.
- package/CHANGELOG.md +43 -0
- package/README.md +1 -1
- package/dist/clientgen.js +357 -53
- package/dist/compiler/analyze.js +93 -15
- package/dist/compiler/emit/base.d.ts +11 -1
- package/dist/compiler/emit/base.js +21 -0
- package/dist/compiler/emit/calls.d.ts +11 -1
- package/dist/compiler/emit/calls.js +68 -6
- package/dist/compiler/emit/cpi.js +42 -4
- package/dist/compiler/emit/expressions.js +9 -0
- package/dist/compiler/emit/function.js +5 -3
- package/dist/compiler/emit/program.js +9 -2
- package/dist/compiler/emit/shared.d.ts +32 -2
- package/dist/compiler/emit/shared.js +43 -0
- package/dist/compiler/emit/statements.d.ts +35 -0
- package/dist/compiler/emit/statements.js +237 -79
- package/dist/compiler/emit/storage.js +39 -13
- package/dist/compiler/model.d.ts +18 -1
- package/dist/svmrpc.d.ts +26 -0
- package/dist/svmrpc.js +150 -0
- package/dist/testing.d.ts +4 -1
- package/dist/testing.js +3 -0
- package/docs/client.md +14 -12
- package/docs/language.md +9 -9
- package/docs/test-your-program.md +4 -1
- package/lib/typesol.d.ts +8 -0
- package/package.json +1 -1
- package/runtime/typesol_rt.h +19 -0
package/dist/svmrpc.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests as they run against a
|
|
3
|
+
* cluster: `sendInstructions`, `simulate`, `views`, `accounts.<Name>.fetch*` and `watch`, `resolve`, `eventsOf` and
|
|
4
|
+
* `watchEvents`. Only the methods those helpers call are answered.
|
|
5
|
+
*
|
|
6
|
+
* const { rpc, rpcSubscriptions } = svmConnection(svm);
|
|
7
|
+
* await counter.sendInstructions({ rpc, rpcSubscriptions }, alice, [await counter.initialize({ authority: alice })]);
|
|
8
|
+
* await counter.accounts.Counter.fetch(rpc, address);
|
|
9
|
+
*
|
|
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`.
|
|
12
|
+
*/
|
|
13
|
+
import { type Address, type Rpc, type RpcSubscriptions, type SolanaRpcApi, type SolanaRpcSubscriptionsApi } from "@solana/kit";
|
|
14
|
+
import { type LiteSVM, type TransactionMetadata } from "litesvm";
|
|
15
|
+
export interface SvmConnection {
|
|
16
|
+
rpc: Rpc<SolanaRpcApi>;
|
|
17
|
+
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
|
|
18
|
+
/** Every transaction sent through `rpc` that succeeded, in order. */
|
|
19
|
+
transactions: {
|
|
20
|
+
signature: string;
|
|
21
|
+
meta: TransactionMetadata;
|
|
22
|
+
}[];
|
|
23
|
+
/** Add accounts for `getProgramAccounts` to scan (accounts the transactions named are scanned already). */
|
|
24
|
+
track(...addresses: Address[]): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function svmConnection(svm: LiteSVM): SvmConnection;
|
package/dist/svmrpc.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests as they run against a
|
|
3
|
+
* cluster: `sendInstructions`, `simulate`, `views`, `accounts.<Name>.fetch*` and `watch`, `resolve`, `eventsOf` and
|
|
4
|
+
* `watchEvents`. Only the methods those helpers call are answered.
|
|
5
|
+
*
|
|
6
|
+
* const { rpc, rpcSubscriptions } = svmConnection(svm);
|
|
7
|
+
* await counter.sendInstructions({ rpc, rpcSubscriptions }, alice, [await counter.initialize({ authority: alice })]);
|
|
8
|
+
* await counter.accounts.Counter.fetch(rpc, address);
|
|
9
|
+
*
|
|
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`.
|
|
12
|
+
*/
|
|
13
|
+
import { getAddressDecoder, getBase58Decoder, getBase58Encoder, getBase64Decoder, getBase64Encoder, getCompiledTransactionMessageDecoder, getTransactionDecoder, } from "@solana/kit";
|
|
14
|
+
import { FailedTransactionMetadata } from "litesvm";
|
|
15
|
+
const b64 = (bytes) => getBase64Decoder().decode(bytes);
|
|
16
|
+
const reply = (value) => ({ send: async () => value });
|
|
17
|
+
export function svmConnection(svm) {
|
|
18
|
+
const transactions = [];
|
|
19
|
+
const known = new Set();
|
|
20
|
+
const decode = (wire) => {
|
|
21
|
+
const tx = getTransactionDecoder().decode(getBase64Encoder().encode(wire));
|
|
22
|
+
return { tx, keys: getCompiledTransactionMessageDecoder().decode(tx.messageBytes).staticAccounts };
|
|
23
|
+
};
|
|
24
|
+
const encoded = (a) => {
|
|
25
|
+
const acct = svm.getAccount(a);
|
|
26
|
+
if (!acct.exists)
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
data: [b64(acct.data), "base64"],
|
|
30
|
+
executable: acct.executable,
|
|
31
|
+
lamports: acct.lamports,
|
|
32
|
+
owner: acct.programAddress,
|
|
33
|
+
space: BigInt(acct.data.length),
|
|
34
|
+
rentEpoch: 0n,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const rpc = {
|
|
38
|
+
getLatestBlockhash: () => reply({ context: { slot: 0n }, value: { blockhash: svm.latestBlockhash(), lastValidBlockHeight: 1000000n } }),
|
|
39
|
+
getEpochInfo: () => reply({ absoluteSlot: 0n, blockHeight: 0n, epoch: 0n, slotIndex: 0n, slotsInEpoch: 432000n, transactionCount: null }),
|
|
40
|
+
getSignatureStatuses: (sigs) => reply({
|
|
41
|
+
context: { slot: 0n },
|
|
42
|
+
value: sigs.map((s) => transactions.some((t) => t.signature === s)
|
|
43
|
+
? { confirmationStatus: "confirmed", confirmations: null, err: null, slot: 0n, status: { Ok: null } }
|
|
44
|
+
: null),
|
|
45
|
+
}),
|
|
46
|
+
getMinimumBalanceForRentExemption: (space) => reply(svm.minimumBalanceForRentExemption(space)),
|
|
47
|
+
getBalance: (a) => reply({ context: { slot: 0n }, value: svm.getBalance(a) ?? 0n }),
|
|
48
|
+
sendTransaction: (wire) => ({
|
|
49
|
+
send: async () => {
|
|
50
|
+
const { tx, keys } = decode(wire);
|
|
51
|
+
const res = svm.sendTransaction(tx);
|
|
52
|
+
svm.expireBlockhash();
|
|
53
|
+
if (res instanceof FailedTransactionMetadata)
|
|
54
|
+
throw new Error(`transaction failed: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
|
|
55
|
+
for (const k of keys)
|
|
56
|
+
known.add(k);
|
|
57
|
+
const signature = getBase58Decoder().decode(res.signature());
|
|
58
|
+
transactions.push({ signature, meta: res });
|
|
59
|
+
return signature;
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
simulateTransaction: (wire, config = {}) => ({
|
|
63
|
+
send: async () => {
|
|
64
|
+
// Simulations are usually unsigned (sigVerify: false): turn LiteSVM's check off for this one only.
|
|
65
|
+
const inner = svm.inner;
|
|
66
|
+
const restore = config.sigVerify === false && inner?.getSigverify?.() === true;
|
|
67
|
+
if (restore)
|
|
68
|
+
svm.withSigverify(false);
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
res = svm.simulateTransaction(decode(wire).tx);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (restore)
|
|
75
|
+
svm.withSigverify(true);
|
|
76
|
+
}
|
|
77
|
+
if (res instanceof FailedTransactionMetadata)
|
|
78
|
+
return {
|
|
79
|
+
context: { slot: 0n },
|
|
80
|
+
value: { err: String(res.err()), logs: res.meta().logs(), unitsConsumed: res.meta().computeUnitsConsumed(), returnData: null },
|
|
81
|
+
};
|
|
82
|
+
const meta = res.meta();
|
|
83
|
+
const ret = meta.returnData();
|
|
84
|
+
return {
|
|
85
|
+
context: { slot: 0n },
|
|
86
|
+
value: {
|
|
87
|
+
err: null,
|
|
88
|
+
logs: meta.logs(),
|
|
89
|
+
unitsConsumed: meta.computeUnitsConsumed(),
|
|
90
|
+
returnData: ret.data().length ? { programId: getAddressDecoder().decode(ret.programId()), data: [b64(ret.data()), "base64"] } : null,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
getAccountInfo: (a) => reply({ context: { slot: 0n }, value: encoded(a) }),
|
|
96
|
+
getMultipleAccounts: (addresses) => reply({ context: { slot: 0n }, value: addresses.map(encoded) }),
|
|
97
|
+
getProgramAccounts: (program, config = {}) => reply([...known]
|
|
98
|
+
.map((a) => ({ pubkey: a, account: encoded(a) }))
|
|
99
|
+
.filter(({ account }) => account && account.owner === program)
|
|
100
|
+
.filter(({ account }) => {
|
|
101
|
+
const data = getBase64Encoder().encode(account.data[0]);
|
|
102
|
+
return (config.filters ?? []).every(({ memcmp, dataSize }) => {
|
|
103
|
+
if (dataSize !== undefined)
|
|
104
|
+
return BigInt(data.length) === BigInt(dataSize);
|
|
105
|
+
if (!memcmp)
|
|
106
|
+
return true;
|
|
107
|
+
const want = getBase58Encoder().encode(memcmp.bytes);
|
|
108
|
+
return want.every((b, i) => data[Number(memcmp.offset) + i] === b);
|
|
109
|
+
});
|
|
110
|
+
})),
|
|
111
|
+
// The json shape: log messages, and inner instructions with base58 data (what `eventsOf` reads).
|
|
112
|
+
getTransaction: (signature) => {
|
|
113
|
+
const t = transactions.find((x) => x.signature === signature);
|
|
114
|
+
return reply(t
|
|
115
|
+
? {
|
|
116
|
+
slot: 0n,
|
|
117
|
+
blockTime: null,
|
|
118
|
+
meta: {
|
|
119
|
+
err: null,
|
|
120
|
+
logMessages: t.meta.logs(),
|
|
121
|
+
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()) })) })),
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
: null);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
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
|
+
})(),
|
|
137
|
+
});
|
|
138
|
+
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 } }]),
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
rpc: rpc,
|
|
146
|
+
rpcSubscriptions: rpcSubscriptions,
|
|
147
|
+
transactions,
|
|
148
|
+
track: (...addresses) => addresses.forEach((a) => known.add(a)),
|
|
149
|
+
};
|
|
150
|
+
}
|
package/dist/testing.d.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import anchor, { type Idl } from "@anchor-lang/core";
|
|
20
20
|
import { type Address, type Instruction, type KeyPairSigner } from "@solana/kit";
|
|
21
21
|
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
|
|
22
|
+
import { type SvmConnection, svmConnection } from "./svmrpc.js";
|
|
23
|
+
export { type SvmConnection, svmConnection };
|
|
22
24
|
declare const BorshCoder: typeof anchor.BorshCoder;
|
|
23
25
|
type BorshCoder = InstanceType<typeof BorshCoder>;
|
|
24
26
|
export type Result = TransactionMetadata | FailedTransactionMetadata;
|
|
@@ -58,9 +60,10 @@ export interface TestProgram {
|
|
|
58
60
|
returned<T = unknown>(instruction: string, res: Result): T;
|
|
59
61
|
/** Lamports of any account (0 when it does not exist). */
|
|
60
62
|
balance(address: Address): bigint;
|
|
63
|
+
/** A kit RPC and subscriptions pair over this LiteSVM, for the generated client (`svmConnection`). */
|
|
64
|
+
connection: SvmConnection;
|
|
61
65
|
}
|
|
62
66
|
/** Build `program` of the project around `from` (a file URL such as import.meta.url, or a directory) and load it. */
|
|
63
67
|
export declare function testProgram(program: string, from: string | URL): Promise<TestProgram>;
|
|
64
68
|
/** The same helpers for a program already loaded into `svm` at `programId` with this IDL. */
|
|
65
69
|
export declare function programHarness(svm: LiteSVM, programId: Address, idl: Idl): TestProgram;
|
|
66
|
-
export {};
|
package/dist/testing.js
CHANGED
|
@@ -22,7 +22,9 @@ import { FailedTransactionMetadata, LiteSVM } from "litesvm";
|
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { svmConnection } from "./svmrpc.js";
|
|
25
26
|
import { buildWorkspace, findWorkspace } from "./workspace.js";
|
|
27
|
+
export { svmConnection };
|
|
26
28
|
// @anchor-lang/core is CommonJS: Node 22 cannot import its classes by name from an installed ES module.
|
|
27
29
|
const { BN, BorshCoder } = anchor;
|
|
28
30
|
const BIG = new Set(["u64", "i64", "u128", "i128"]);
|
|
@@ -332,6 +334,7 @@ export function programHarness(svm, programId, idl) {
|
|
|
332
334
|
balance(address) {
|
|
333
335
|
return svm.getBalance(address) ?? 0n;
|
|
334
336
|
},
|
|
337
|
+
connection: svmConnection(svm),
|
|
335
338
|
};
|
|
336
339
|
return harness;
|
|
337
340
|
}
|
package/docs/client.md
CHANGED
|
@@ -48,7 +48,7 @@ for await (const { signature, event } of ledgerbook.watchEvents(rpcSubscriptions
|
|
|
48
48
|
await ledgerbook.approveMany({ approver: signer, book }, { indices: [0n, 1n] }, [{ address: extra, role: AccountRole.WRITABLE }]);
|
|
49
49
|
|
|
50
50
|
// has_one accounts that do not sign, read from the account that names them (Anchor resolves `relations` the same way)
|
|
51
|
-
const accounts = await ledgerbook.resolve.someInstruction(rpc, { book }); //
|
|
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
54
|
ledgerbook.eventsOf(await rpc.getTransaction(signature, { maxSupportedTransactionVersion: 0 }).send());
|
|
@@ -64,9 +64,9 @@ Kit's own transaction functions work with the instructions as well; `sendInstruc
|
|
|
64
64
|
| `<program>.<instruction>(accounts, args)` | Builds the `Instruction`. Accounts with a fixed address and PDAs whose seeds are constants, arguments or other accounts' keys are optional and filled in; pass one to override. An optional account passed as `null` is left out (sent as the program id, the convention TypeSOL and Anchor share); left undefined, it is filled in like any other. |
|
|
65
65
|
| `<program>.<instruction>(accounts, args, remaining?)` | The same, with remaining accounts appended in order (kit `AccountMeta`s). |
|
|
66
66
|
| `<program>.accounts.<Name>` | `decode(bytes)`, `encode(value)`, `fetch(rpc, address)`, `fetchMaybe(rpc, address)`, `fetchMultiple(rpc, addresses)`, `fetchAll(rpc, filters?)` (getProgramAccounts by discriminator, plus `{ offset, bytes }` filters), `watch(rpcSubscriptions, address, { abortSignal })`, `discriminator`. `decode` checks the discriminator. |
|
|
67
|
-
| `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives. |
|
|
67
|
+
| `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives, also one seeded from its own fields (from the field values). |
|
|
68
68
|
| `<program>.returns.<instruction>(bytes)` | Decodes return data. |
|
|
69
|
-
| `<program>.resolve.<instruction>(rpc, accounts, args?)` | Fills in what needs the chain: `has_one` accounts that do not sign (read from the account that names them) and PDAs seeded from another account's fields (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived). Given accounts are kept. |
|
|
69
|
+
| `<program>.resolve.<instruction>(rpc, accounts, args?, { signer? })` | Fills in what needs the chain or the wallet: signer accounts left out (`options.signer`, the default signer, like Anchor's provider wallet), `has_one` accounts that do not sign (read from the account that names them), and PDAs seeded from another account's fields, scalars, strings or bytes (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived). Given accounts are kept. |
|
|
70
70
|
| `<program>.views.<instruction>(rpc, feePayer, accounts, args)` | Simulates an instruction that returns a value and decodes it (throws with the logs if it fails). |
|
|
71
71
|
| `<program>.sendInstructions(connection, feePayer, instructions)`, `simulate(rpc, feePayer, instructions)` | One transaction sent and confirmed (kit's sendAndConfirm), or simulated without signatures (error, logs, units, this program's return data). |
|
|
72
72
|
| `<program>.errors`, `errorCode(e)`, `isError(e, name)` | The program's errors (6000+), and for TypeSOL programs also every Anchor framework error (`AnchorError` and the constraints raise them) and TypeSOL's runtime errors. |
|
|
@@ -89,12 +89,14 @@ Anchor IDLs are refused with a hint to convert them (`anchor idl convert`).
|
|
|
89
89
|
|
|
90
90
|
- **Errors and events are matched by code and discriminator, not by program.** A program this one calls can raise the
|
|
91
91
|
same error code, and `parseEvents` reads every `Program data:` line that decodes as one of this program's events.
|
|
92
|
-
- **Not filled in automatically** (pass them):
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
- **Not filled in automatically** (pass them): PDAs seeded from struct fields (Anchor cannot seed with them either),
|
|
93
|
+
PDAs whose program is another account's field, and a PDA seeded from its own fields (derive it with its `pdas`
|
|
94
|
+
helper from the values). `typesol client` prints a note for each.
|
|
95
|
+
- **Anchor IDLs**: zero-copy (`bytemuck`) accounts decode and encode at their `#[repr(C)]` offsets as the program sees
|
|
96
|
+
them on SBF (padding included; `packed` and `align` honored; 128-bit integers align to 8). Generic types become one
|
|
97
|
+
type per use with concrete arguments (`Pair<u64, 2>` is `PairU64N2`). Left out, with a note: zero-copy types with
|
|
98
|
+
Rust's own layout (`repr(Rust)`) or holding a non-zero-copy type, and the generic definitions themselves.
|
|
99
|
+
- **In tests**: `svmConnection(svm)` from `typesol/testing` answers the client's RPC calls from LiteSVM, and
|
|
100
|
+
`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
|
|
102
|
+
reads its `emitCpi` events (log subscriptions do not carry inner instructions).
|
package/docs/language.md
CHANGED
|
@@ -21,12 +21,12 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
21
21
|
| Destructuring | `const { owner, pos: p } = acct` (or `let` for scalars to change) from a name or a field path — one declaration per name, read like `acct.owner`: scalars are copies, nested structs are references. No rest, defaults or nesting |
|
|
22
22
|
| Struct values | `const p: Point = { x, y }` (or a copy of another Point) — a local struct; fields can be updated, store it with `acct.home = p` |
|
|
23
23
|
| Pubkey constants | `const ADMIN = pubkey("4Nd1…")` (checked at compile time) or inline `pubkey("…")`; usable as seeds |
|
|
24
|
-
| Events | `emit<Deposited>({ owner: owner.key, amount })` — Anchor-format `Program data:` logs, listed under `events` in the IDL; event structs may hold `string` and `
|
|
24
|
+
| Events | `emit<Deposited>({ owner: owner.key, amount })` — Anchor-format `Program data:` logs, listed under `events` in the IDL; event structs may hold `string`, `Option`, `FixedVec<T, N>` (its fixed layout, as in account data) and `Vec<T>` (Borsh: from a Vec argument or a list in account data) |
|
|
25
25
|
| Enums | `export enum Status { Pending, Approved }` — unit variants stored as the Borsh u8 index; `Status.Approved`, `===` |
|
|
26
26
|
| Options | `best: Option<u64>` (or `u64 \| null`) — `null`, `=== null`, TypeScript narrowing; `===` between options or with a value follows JS (null equals null, empty never equals a value); empty use aborts (5108); `acct?.field` on an optional account and `x.home?.field` on an `Option<struct>` give an Option (empty when the account was left out / the option is empty), `a ?? b` takes b only when a is empty (b runs only then; the result is an Option when b can be null), `x!` is the value or aborts with 5108; `${opt}` prints the value or `null`. Borsh `option` in arguments/events, fixed `{ is_some, value }` in account data |
|
|
27
|
-
| Vecs | `entries: Vec<Entry>`
|
|
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
|
-
| 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).
|
|
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
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 |
|
|
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 |
|
|
@@ -44,8 +44,8 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
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
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
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).
|
|
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)
|
|
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 |
|
|
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 |
|
|
@@ -154,12 +154,12 @@ an integer literal, an argument, `acct.key` or a scalar field of an account, the
|
|
|
154
154
|
|
|
155
155
|
## Not supported yet
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
`@layout borsh` structs, growing an account by more than 10 KiB in one instruction (Solana's limit: realloc again in
|
|
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
158
|
the next instruction), `?.` on anything but optional accounts and Option<struct> fields, `!` on anything but an Option
|
|
160
159
|
or an optional account, Core's external plugin adapters (oracles, app data, lifecycle hooks: use `invoke`), closures
|
|
161
|
-
stored or returned (they are called
|
|
162
|
-
|
|
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).
|
|
163
163
|
They are rejected at compile time rather than half-supported.
|
|
164
164
|
|
|
165
165
|
`Init.create` handles a PDA address someone pre-funded: it tops up rent, then allocates and assigns instead of
|
|
@@ -44,9 +44,12 @@ it("counts", async () => {
|
|
|
44
44
|
| `t.account(type, address)` | the account decoded by its IDL type (bigints, address strings), or `null` once closed. TypeSOL fixed layouts come back as their IDL structs: a `FixedString<N>` as `{ len, bytes }`, a `FixedVec<T, N>` as `{ len, items }` (the generated client gives strings and arrays) |
|
|
45
45
|
| `t.returned(instruction, res)` | the instruction's return data: an integer, boolean, Pubkey or struct |
|
|
46
46
|
| `t.balance(address)` | lamports |
|
|
47
|
+
| `t.connection` | `{ rpc, rpcSubscriptions }` answered by this LiteSVM, for the generated client: `counter.sendInstructions(t.connection, alice, [ix])`, `counter.accounts.Counter.fetch(t.connection.rpc, address)`, views, `resolve`, `watch` and `watchEvents` |
|
|
47
48
|
|
|
48
49
|
`t.svm`, `t.programId`, `t.idl` and `t.coder` are there for anything else, e.g. writing a mint straight into the VM
|
|
49
50
|
with `t.svm.setAccount` (see the escrow template). `programHarness(svm, programId, idl)` gives the same helpers for a
|
|
50
|
-
program you loaded yourself.
|
|
51
|
+
program you loaded yourself, and `svmConnection(svm)` the RPC pair alone (for any LiteSVM, e.g. one with an Anchor
|
|
52
|
+
program loaded). Transactions sent through it run at once; `getProgramAccounts` (`fetchAll`) sees the accounts those
|
|
53
|
+
transactions named, plus any you `track(...)`.
|
|
51
54
|
|
|
52
55
|
The module needs `litesvm` and `@anchor-lang/core`, which `typesol init` adds to `devDependencies`.
|
package/lib/typesol.d.ts
CHANGED
|
@@ -50,6 +50,14 @@ export interface ListMethods<T> {
|
|
|
50
50
|
includes(value: T): boolean;
|
|
51
51
|
/** The index of the first element equal to `value`, or -1. */
|
|
52
52
|
indexOf(value: T): i64;
|
|
53
|
+
/**
|
|
54
|
+
* The elements `fn` maps them to (integers, booleans, Pubkeys or enums). Use it right away (another list method,
|
|
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> = …`).
|
|
57
|
+
*/
|
|
58
|
+
map<U>(fn: (x: T, index: u64) => U): FixedVec<U, never>;
|
|
59
|
+
/** The elements `fn` holds for, used or stored like `map`'s; callbacks after it get the index among those. */
|
|
60
|
+
filter(fn: (x: T, index: u64) => boolean): FixedVec<T, never>;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
export interface FixedArray<T, N extends number> extends ListMethods<T> {
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -104,6 +104,25 @@ static inline __attribute__((always_inline)) void ts_memcpy(void *dst, const voi
|
|
|
104
104
|
for (; n; n--) *d++ = *s++;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/* Copy that may overlap (a growable Vec's followers moving when it grows or shrinks): forward or backward as needed. */
|
|
108
|
+
static inline void ts_memmove(void *dst, const void *src, uint64_t n) {
|
|
109
|
+
uint8_t *d = (uint8_t *)dst;
|
|
110
|
+
const uint8_t *s = (const uint8_t *)src;
|
|
111
|
+
if (d == s || n == 0) return;
|
|
112
|
+
if (d < s) {
|
|
113
|
+
ts_memcpy(d, s, n); /* forward: each 8-byte load happens before the store that could overlap it */
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
d += n;
|
|
117
|
+
s += n;
|
|
118
|
+
for (; n >= 8; n -= 8) {
|
|
119
|
+
d -= 8;
|
|
120
|
+
s -= 8;
|
|
121
|
+
ts_st64(d, ts_ld64(s));
|
|
122
|
+
}
|
|
123
|
+
while (n--) *--d = *--s;
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
static inline __attribute__((always_inline)) void ts_memzero(void *dst, uint64_t n) {
|
|
108
127
|
uint8_t *d = (uint8_t *)dst;
|
|
109
128
|
for (; n >= 8; n -= 8, d += 8) ts_st64(d, 0);
|