@typesol/cli 0.15.0 → 0.17.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 +107 -0
- package/README.md +11 -7
- package/dist/cli.js +147 -20
- package/dist/clientgen.js +37 -0
- package/dist/compiler/analyze.d.ts +11 -1
- package/dist/compiler/analyze.js +307 -37
- package/dist/compiler/emit/base.d.ts +7 -0
- package/dist/compiler/emit/base.js +62 -2
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +232 -0
- package/dist/compiler/emit/calls.js +50 -15
- package/dist/compiler/emit/cpi.d.ts +32 -8
- package/dist/compiler/emit/cpi.js +706 -25
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +126 -4
- package/dist/compiler/emit/function.js +45 -12
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +12 -0
- package/dist/compiler/emit/shared.js +22 -4
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +141 -7
- package/dist/compiler/emit/storage.d.ts +6 -1
- package/dist/compiler/emit/storage.js +82 -17
- package/dist/compiler/idl.js +61 -18
- package/dist/compiler/model.d.ts +56 -4
- package/dist/compiler/model.js +8 -2
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +367 -0
- package/dist/templates.js +49 -154
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.js +11 -1
- package/docs/architecture.md +3 -0
- package/docs/cli.md +16 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +19 -14
- package/docs/mainnet.md +5 -4
- package/docs/migrate-from-anchor.md +113 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +7 -0
- package/lib/typesol.d.ts +325 -13
- package/package.json +19 -1
- package/runtime/typesol_rt.h +54 -9
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `typesol/testing`: test a project's program in LiteSVM (an in-process Solana runtime) without the boilerplate.
|
|
3
|
+
*
|
|
4
|
+
* const t = await testProgram("counter", import.meta.url);
|
|
5
|
+
* const alice = await t.wallet();
|
|
6
|
+
* const counter = await t.pda("counter", alice);
|
|
7
|
+
* t.ok(await t.send(alice, t.ix("initialize", {}, { authority: alice, counter })));
|
|
8
|
+
* t.ok(await t.send(alice, t.ix("increment", { by: 5n }, { authority: alice, counter })));
|
|
9
|
+
* expect((await t.account("Counter", counter))?.count).toBe(5n);
|
|
10
|
+
*
|
|
11
|
+
* Accounts are passed by name; their signer / writable flags and fixed addresses (system program, token programs…)
|
|
12
|
+
* come from the IDL, as Anchor's `.accounts({...})` does, and PDAs seeded by constants, arguments or other accounts'
|
|
13
|
+
* keys are derived when left out. Arguments take plain values: bigints or numbers for
|
|
14
|
+
* integers, address strings for Pubkeys, null for an empty Option. Decoded accounts and return data come back the
|
|
15
|
+
* same way (bigints, address strings).
|
|
16
|
+
*
|
|
17
|
+
* Needs `litesvm` and `@anchor-lang/core`, which `typesol init` projects have.
|
|
18
|
+
*/
|
|
19
|
+
import anchor, { type Idl } from "@anchor-lang/core";
|
|
20
|
+
import { type Address, type Instruction, type KeyPairSigner } from "@solana/kit";
|
|
21
|
+
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
|
|
22
|
+
declare const BorshCoder: typeof anchor.BorshCoder;
|
|
23
|
+
type BorshCoder = InstanceType<typeof BorshCoder>;
|
|
24
|
+
export type Result = TransactionMetadata | FailedTransactionMetadata;
|
|
25
|
+
/** An account for an instruction: an address, a keypair (which then signs), or null to leave an optional one out. */
|
|
26
|
+
export type AccountInput = Address | KeyPairSigner | null;
|
|
27
|
+
/**
|
|
28
|
+
* A PDA seed: text, an address (or keypair), raw bytes, or a bigint as a little-endian u64. A string that is a valid
|
|
29
|
+
* address is taken as one; pass `new TextEncoder().encode(s)` for text that happens to look like an address.
|
|
30
|
+
*/
|
|
31
|
+
export type SeedInput = string | Address | KeyPairSigner | Uint8Array | bigint;
|
|
32
|
+
export interface TestProgram {
|
|
33
|
+
svm: LiteSVM;
|
|
34
|
+
programId: Address;
|
|
35
|
+
idl: Idl;
|
|
36
|
+
coder: BorshCoder;
|
|
37
|
+
/** A new keypair funded with `sol` SOL (default 10). */
|
|
38
|
+
wallet(sol?: number): Promise<KeyPairSigner>;
|
|
39
|
+
/** The program's PDA for these seeds. */
|
|
40
|
+
pda(...seeds: SeedInput[]): Promise<Address>;
|
|
41
|
+
/** The PDA and its bump. */
|
|
42
|
+
pdaWithBump(...seeds: SeedInput[]): Promise<[Address, number]>;
|
|
43
|
+
/** An instruction of this program, accounts by name (`authority` or `authority`'s snake_case), args as plain values. */
|
|
44
|
+
ix(name: string, args?: Record<string, unknown>, accounts?: Record<string, AccountInput>, remaining?: {
|
|
45
|
+
address: Address;
|
|
46
|
+
writable?: boolean;
|
|
47
|
+
signer?: KeyPairSigner;
|
|
48
|
+
}[]): Instruction;
|
|
49
|
+
/** Send instructions in one transaction paid by `payer` (keypairs passed as accounts sign too). */
|
|
50
|
+
send(payer: KeyPairSigner, ...ixs: Instruction[]): Promise<Result>;
|
|
51
|
+
/** The successful result, or a thrown error with the program's logs. */
|
|
52
|
+
ok(res: Result): TransactionMetadata;
|
|
53
|
+
/** The custom error code of a failed transaction (6000+ for the program's error enums); throws if it succeeded. */
|
|
54
|
+
errorCode(res: Result): number;
|
|
55
|
+
/** An account of this program decoded by its IDL type, or null when it does not exist. */
|
|
56
|
+
account<T = Record<string, unknown>>(type: string, address: Address): Promise<T | null>;
|
|
57
|
+
/** A successful instruction's return data, decoded by its IDL `returns` type. */
|
|
58
|
+
returned<T = unknown>(instruction: string, res: Result): T;
|
|
59
|
+
/** Lamports of any account (0 when it does not exist). */
|
|
60
|
+
balance(address: Address): bigint;
|
|
61
|
+
}
|
|
62
|
+
/** Build `program` of the project around `from` (a file URL such as import.meta.url, or a directory) and load it. */
|
|
63
|
+
export declare function testProgram(program: string, from: string | URL): Promise<TestProgram>;
|
|
64
|
+
/** The same helpers for a program already loaded into `svm` at `programId` with this IDL. */
|
|
65
|
+
export declare function programHarness(svm: LiteSVM, programId: Address, idl: Idl): TestProgram;
|
|
66
|
+
export {};
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `typesol/testing`: test a project's program in LiteSVM (an in-process Solana runtime) without the boilerplate.
|
|
3
|
+
*
|
|
4
|
+
* const t = await testProgram("counter", import.meta.url);
|
|
5
|
+
* const alice = await t.wallet();
|
|
6
|
+
* const counter = await t.pda("counter", alice);
|
|
7
|
+
* t.ok(await t.send(alice, t.ix("initialize", {}, { authority: alice, counter })));
|
|
8
|
+
* t.ok(await t.send(alice, t.ix("increment", { by: 5n }, { authority: alice, counter })));
|
|
9
|
+
* expect((await t.account("Counter", counter))?.count).toBe(5n);
|
|
10
|
+
*
|
|
11
|
+
* Accounts are passed by name; their signer / writable flags and fixed addresses (system program, token programs…)
|
|
12
|
+
* come from the IDL, as Anchor's `.accounts({...})` does, and PDAs seeded by constants, arguments or other accounts'
|
|
13
|
+
* keys are derived when left out. Arguments take plain values: bigints or numbers for
|
|
14
|
+
* integers, address strings for Pubkeys, null for an empty Option. Decoded accounts and return data come back the
|
|
15
|
+
* same way (bigints, address strings).
|
|
16
|
+
*
|
|
17
|
+
* Needs `litesvm` and `@anchor-lang/core`, which `typesol init` projects have.
|
|
18
|
+
*/
|
|
19
|
+
import anchor from "@anchor-lang/core";
|
|
20
|
+
import { AccountRole, appendTransactionMessageInstructions, createTransactionMessage, generateKeyPairSigner, getAddressDecoder, getAddressEncoder, getProgramDerivedAddress, isAddress, isOffCurveAddress, lamports, pipe, setTransactionMessageFeePayerSigner, signTransactionMessageWithSigners, } from "@solana/kit";
|
|
21
|
+
import { FailedTransactionMetadata, LiteSVM } from "litesvm";
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { buildWorkspace, findWorkspace } from "./workspace.js";
|
|
26
|
+
// @anchor-lang/core is CommonJS: Node 22 cannot import its classes by name from an installed ES module.
|
|
27
|
+
const { BN, BorshCoder } = anchor;
|
|
28
|
+
const BIG = new Set(["u64", "i64", "u128", "i128"]);
|
|
29
|
+
const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
30
|
+
const camel = (s) => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
31
|
+
const isSigner = (v) => typeof v === "object" && v !== null && "address" in v && "signTransactions" in v;
|
|
32
|
+
/** Build `program` of the project around `from` (a file URL such as import.meta.url, or a directory) and load it. */
|
|
33
|
+
export async function testProgram(program, from) {
|
|
34
|
+
const start = typeof from === "string" && !from.startsWith("file:") ? from : path.dirname(fileURLToPath(from));
|
|
35
|
+
const ws = findWorkspace(start);
|
|
36
|
+
if (!ws)
|
|
37
|
+
throw new Error(`no typesol.json at or above ${start}`);
|
|
38
|
+
if (!ws.programs[program])
|
|
39
|
+
throw new Error(`typesol.json has no program "${program}" (it has ${Object.keys(ws.programs).join(", ")})`);
|
|
40
|
+
const [built] = buildWorkspace(ws, [program]);
|
|
41
|
+
const svm = new LiteSVM();
|
|
42
|
+
const programId = built.address;
|
|
43
|
+
svm.addProgramFromFile(programId, built.result.soPath);
|
|
44
|
+
return programHarness(svm, programId, built.result.idl);
|
|
45
|
+
}
|
|
46
|
+
/** The same helpers for a program already loaded into `svm` at `programId` with this IDL. */
|
|
47
|
+
export function programHarness(svm, programId, idl) {
|
|
48
|
+
const coder = new BorshCoder(idl);
|
|
49
|
+
const pidl = idl;
|
|
50
|
+
const types = new Map((pidl.types ?? []).map((t) => [t.name, t]));
|
|
51
|
+
const enc = getAddressEncoder();
|
|
52
|
+
/** A plain value → what the Anchor coder expects for `type`. */
|
|
53
|
+
const toCoder = (v, type, where) => {
|
|
54
|
+
if (typeof type === "string") {
|
|
55
|
+
if (BIG.has(type))
|
|
56
|
+
return new BN(String(v));
|
|
57
|
+
if (/^[ui](8|16|32)$/.test(type))
|
|
58
|
+
return Number(v);
|
|
59
|
+
if (type === "pubkey") {
|
|
60
|
+
const a = isSigner(v) ? v.address : v;
|
|
61
|
+
if (typeof a !== "string" || !isAddress(a))
|
|
62
|
+
throw new Error(`${where}: expected an address`);
|
|
63
|
+
return { toBuffer: () => Buffer.from(enc.encode(a)) };
|
|
64
|
+
}
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
if ("option" in type)
|
|
68
|
+
return v === null || v === undefined ? null : toCoder(v, type.option, where);
|
|
69
|
+
if ("vec" in type)
|
|
70
|
+
return v.map((x, i) => toCoder(x, type.vec, `${where}[${i}]`));
|
|
71
|
+
if ("array" in type)
|
|
72
|
+
return v.map((x, i) => toCoder(x, type.array[0], `${where}[${i}]`));
|
|
73
|
+
const def = types.get(type.defined.name);
|
|
74
|
+
if (def?.type.kind === "struct" && def.type.fields && typeof v === "object" && v !== null) {
|
|
75
|
+
const o = v;
|
|
76
|
+
return Object.fromEntries(def.type.fields.map((f) => [f.name, toCoder(o[f.name] ?? o[camel(f.name)], f.type, `${where}.${f.name}`)]));
|
|
77
|
+
}
|
|
78
|
+
// A unit enum as its index or variant name, as well as Anchor's { Variant: {} }.
|
|
79
|
+
const variants = def?.type?.variants;
|
|
80
|
+
const unitObject = typeof v === "object" && v !== null && Object.keys(v).length === 1 && !Object.values(v)[0];
|
|
81
|
+
const emptyObject = typeof v === "object" && v !== null && Object.keys(v).length === 1 && Object.keys(Object.values(v)[0]).length === 0;
|
|
82
|
+
if (def?.type.kind === "enum" && variants && (typeof v === "number" || typeof v === "bigint" || typeof v === "string" || unitObject || emptyObject)) {
|
|
83
|
+
const key = typeof v === "object" ? Object.keys(v)[0] : v;
|
|
84
|
+
const same = (x) => x.name === key || camel(x.name) === key || x.name.toLowerCase() === String(key).toLowerCase();
|
|
85
|
+
const i = typeof key === "string" ? variants.findIndex(same) : Number(key);
|
|
86
|
+
if (!variants[i])
|
|
87
|
+
throw new Error(`${where}: ${String(v)} is not a variant of ${type.defined.name}`);
|
|
88
|
+
return { [variants[i].name]: {} };
|
|
89
|
+
}
|
|
90
|
+
// A variant with fields, { Variant: { field: value } }: its fields converted like a struct's.
|
|
91
|
+
if (def?.type.kind === "enum" && variants && typeof v === "object" && v !== null && Object.keys(v).length === 1) {
|
|
92
|
+
const [key, body] = Object.entries(v)[0];
|
|
93
|
+
const variant = variants.find((x) => x.name === key || camel(x.name) === key || x.name.toLowerCase() === key.toLowerCase());
|
|
94
|
+
if (!variant)
|
|
95
|
+
throw new Error(`${where}: ${key} is not a variant of ${type.defined.name}`);
|
|
96
|
+
const fields = (variant.fields ?? []);
|
|
97
|
+
if (fields.length && fields.every((f) => typeof f === "object" && f?.name && f.type) && typeof body === "object" && body !== null) {
|
|
98
|
+
const o = body;
|
|
99
|
+
return {
|
|
100
|
+
[variant.name]: Object.fromEntries(fields.map((f) => [f.name, toCoder(o[f.name] ?? o[camel(f.name)], f.type, `${where}.${variant.name}.${f.name}`)])),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { [variant.name]: body };
|
|
104
|
+
}
|
|
105
|
+
return v;
|
|
106
|
+
};
|
|
107
|
+
/** What the coder decodes → plain values (bigints, address strings), recursively. */
|
|
108
|
+
const plain = (v) => {
|
|
109
|
+
if (v === null || v === undefined)
|
|
110
|
+
return v;
|
|
111
|
+
if (BN.isBN(v))
|
|
112
|
+
return BigInt(v.toString());
|
|
113
|
+
if (typeof v === "object" && typeof v.toBase58 === "function")
|
|
114
|
+
return v.toBase58();
|
|
115
|
+
if (Array.isArray(v))
|
|
116
|
+
return v.map(plain);
|
|
117
|
+
if (v instanceof Uint8Array)
|
|
118
|
+
return v;
|
|
119
|
+
if (typeof v === "object") {
|
|
120
|
+
const o = v;
|
|
121
|
+
const keys = Object.keys(o).sort().join(",");
|
|
122
|
+
// Account data keeps Options and FixedStrings in a fixed layout; give them back as the plain value / string.
|
|
123
|
+
if (keys === "is_some,value")
|
|
124
|
+
return o.is_some ? plain(o.value) : null;
|
|
125
|
+
if (keys === "bytes,len" && Array.isArray(o.bytes))
|
|
126
|
+
return Buffer.from(o.bytes.slice(0, Number(o.len))).toString("utf8");
|
|
127
|
+
return Object.fromEntries(Object.entries(o).map(([k, x]) => [k, plain(x)]));
|
|
128
|
+
}
|
|
129
|
+
return v;
|
|
130
|
+
};
|
|
131
|
+
/** find_program_address, synchronously (sha256 + the curve check), so ix() can fill PDAs in. */
|
|
132
|
+
const findPda = (seeds, program) => {
|
|
133
|
+
for (let bump = 255; bump >= 0; bump--) {
|
|
134
|
+
const h = createHash("sha256");
|
|
135
|
+
for (const s of seeds)
|
|
136
|
+
h.update(s);
|
|
137
|
+
h.update(Uint8Array.of(bump));
|
|
138
|
+
h.update(new Uint8Array(enc.encode(program)));
|
|
139
|
+
h.update("ProgramDerivedAddress");
|
|
140
|
+
const a = getAddressDecoder().decode(h.digest());
|
|
141
|
+
if (isOffCurveAddress(a))
|
|
142
|
+
return a;
|
|
143
|
+
}
|
|
144
|
+
throw new Error("no off-curve bump for these seeds");
|
|
145
|
+
};
|
|
146
|
+
/** An argument's bytes as a seed: little-endian integers at their width, bool as one byte, Pubkey, UTF-8 text. */
|
|
147
|
+
const argSeed = (v, type) => {
|
|
148
|
+
if (typeof type !== "string" || v === undefined || v === null)
|
|
149
|
+
return undefined;
|
|
150
|
+
const int = /^([ui])(8|16|32|64|128)$/.exec(type);
|
|
151
|
+
if (int) {
|
|
152
|
+
const bytes = Number(int[2]) / 8;
|
|
153
|
+
let n = BigInt.asUintN(bytes * 8, BigInt(v));
|
|
154
|
+
const out = new Uint8Array(bytes);
|
|
155
|
+
for (let i = 0; i < bytes; i++, n >>= 8n)
|
|
156
|
+
out[i] = Number(n & 0xffn);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
if (type === "bool")
|
|
160
|
+
return Uint8Array.of(v ? 1 : 0);
|
|
161
|
+
if (type === "pubkey")
|
|
162
|
+
return new Uint8Array(enc.encode((isSigner(v) ? v.address : v)));
|
|
163
|
+
if (type === "string")
|
|
164
|
+
return new TextEncoder().encode(v);
|
|
165
|
+
return undefined;
|
|
166
|
+
};
|
|
167
|
+
/** A unit-enum argument as a seed: one byte, its variant index (given as the index or as Anchor's { Variant: {} }). */
|
|
168
|
+
const enumSeed = (v, type) => {
|
|
169
|
+
if (typeof type !== "object" || !("defined" in type) || v === undefined || v === null)
|
|
170
|
+
return undefined;
|
|
171
|
+
const def = types.get(type.defined.name);
|
|
172
|
+
const variants = def?.type.kind === "enum" ? def.type.variants : undefined;
|
|
173
|
+
if (!variants || variants.some((x) => x.fields?.length))
|
|
174
|
+
return undefined;
|
|
175
|
+
if (typeof v === "number" || typeof v === "bigint")
|
|
176
|
+
return Uint8Array.of(Number(v));
|
|
177
|
+
const name = typeof v === "object" ? Object.keys(v)[0] : String(v);
|
|
178
|
+
const i = variants.findIndex((x) => x.name === name || camel(x.name) === name || x.name.toLowerCase() === String(name).toLowerCase());
|
|
179
|
+
return i < 0 ? undefined : Uint8Array.of(i);
|
|
180
|
+
};
|
|
181
|
+
const seedBytes = (s) => {
|
|
182
|
+
if (s instanceof Uint8Array)
|
|
183
|
+
return s;
|
|
184
|
+
if (typeof s === "bigint") {
|
|
185
|
+
const b = new Uint8Array(8);
|
|
186
|
+
new DataView(b.buffer).setBigUint64(0, s, true);
|
|
187
|
+
return b;
|
|
188
|
+
}
|
|
189
|
+
if (isSigner(s))
|
|
190
|
+
return new Uint8Array(enc.encode(s.address));
|
|
191
|
+
if (isAddress(s))
|
|
192
|
+
return new Uint8Array(enc.encode(s));
|
|
193
|
+
return new TextEncoder().encode(s);
|
|
194
|
+
};
|
|
195
|
+
const harness = {
|
|
196
|
+
svm,
|
|
197
|
+
programId,
|
|
198
|
+
idl,
|
|
199
|
+
coder,
|
|
200
|
+
async wallet(sol = 10) {
|
|
201
|
+
const k = await generateKeyPairSigner();
|
|
202
|
+
svm.airdrop(k.address, lamports(BigInt(Math.round(sol * 1e9))));
|
|
203
|
+
return k;
|
|
204
|
+
},
|
|
205
|
+
async pda(...seeds) {
|
|
206
|
+
return (await harness.pdaWithBump(...seeds))[0];
|
|
207
|
+
},
|
|
208
|
+
async pdaWithBump(...seeds) {
|
|
209
|
+
const [a, bump] = await getProgramDerivedAddress({ programAddress: programId, seeds: seeds.map(seedBytes) });
|
|
210
|
+
return [a, bump];
|
|
211
|
+
},
|
|
212
|
+
ix(name, args = {}, accounts = {}, remaining = []) {
|
|
213
|
+
const def = pidl.instructions.find((i) => i.name === name || i.name === snake(name));
|
|
214
|
+
if (!def)
|
|
215
|
+
throw new Error(`the program has no instruction "${name}" (it has ${pidl.instructions.map((i) => i.name).join(", ")})`);
|
|
216
|
+
const given = new Map(Object.entries(accounts).map(([k, v]) => [snake(k), v]));
|
|
217
|
+
for (const k of given.keys())
|
|
218
|
+
if (!def.accounts.some((a) => a.name === k))
|
|
219
|
+
throw new Error(`${def.name} has no account "${k}"`);
|
|
220
|
+
const argsIn = args;
|
|
221
|
+
// PDAs whose seeds are constants, arguments or other accounts' keys are filled in, as the generated client does.
|
|
222
|
+
const addressOf = (name) => {
|
|
223
|
+
const v = given.get(name);
|
|
224
|
+
if (v === undefined || v === null)
|
|
225
|
+
return def.accounts.find((x) => x.name === name)?.address; // fixed in the IDL
|
|
226
|
+
return isSigner(v) ? v.address : v;
|
|
227
|
+
};
|
|
228
|
+
for (let progress = true; progress;) {
|
|
229
|
+
progress = false;
|
|
230
|
+
for (const a of def.accounts) {
|
|
231
|
+
if (given.has(a.name) || !a.pda || a.address)
|
|
232
|
+
continue;
|
|
233
|
+
const seed = (s) => {
|
|
234
|
+
if (s.kind === "const")
|
|
235
|
+
return Uint8Array.from(s.value);
|
|
236
|
+
if (s.kind === "account") {
|
|
237
|
+
const at = s.path.includes(".") ? undefined : addressOf(s.path);
|
|
238
|
+
return at && new Uint8Array(enc.encode(at));
|
|
239
|
+
}
|
|
240
|
+
const arg = def.args.find((x) => x.name === s.path);
|
|
241
|
+
const value = arg && (argsIn[arg.name] ?? argsIn[camel(arg.name)]);
|
|
242
|
+
return arg && (argSeed(value, arg.type) ?? enumSeed(value, arg.type));
|
|
243
|
+
};
|
|
244
|
+
const seeds = a.pda.seeds.map(seed);
|
|
245
|
+
const program = a.pda.program ? seed(a.pda.program) : new Uint8Array(enc.encode(programId));
|
|
246
|
+
if (seeds.some((x) => x === undefined) || !program || program.length !== 32)
|
|
247
|
+
continue;
|
|
248
|
+
given.set(a.name, findPda(seeds, getAddressDecoder().decode(program)));
|
|
249
|
+
progress = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const metas = def.accounts.map((a) => {
|
|
253
|
+
let v = given.has(a.name) ? given.get(a.name) : undefined;
|
|
254
|
+
if (v === undefined && a.address)
|
|
255
|
+
v = a.address;
|
|
256
|
+
if (v === undefined || v === null) {
|
|
257
|
+
if (!a.optional)
|
|
258
|
+
throw new Error(`${def.name}: account "${camel(a.name)}" is required`);
|
|
259
|
+
return { address: programId, role: AccountRole.READONLY }; // left out: the program's own id stands in
|
|
260
|
+
}
|
|
261
|
+
if (a.signer && !isSigner(v))
|
|
262
|
+
throw new Error(`${def.name}: account "${camel(a.name)}" signs; pass its keypair, not its address`);
|
|
263
|
+
const address = isSigner(v) ? v.address : v;
|
|
264
|
+
const role = a.signer
|
|
265
|
+
? a.writable
|
|
266
|
+
? AccountRole.WRITABLE_SIGNER
|
|
267
|
+
: AccountRole.READONLY_SIGNER
|
|
268
|
+
: a.writable
|
|
269
|
+
? AccountRole.WRITABLE
|
|
270
|
+
: AccountRole.READONLY;
|
|
271
|
+
return { address, role, ...(isSigner(v) && a.signer ? { signer: v } : {}) };
|
|
272
|
+
});
|
|
273
|
+
for (const r of remaining)
|
|
274
|
+
metas.push({
|
|
275
|
+
address: r.address,
|
|
276
|
+
role: r.signer ? (r.writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER) : r.writable ? AccountRole.WRITABLE : AccountRole.READONLY,
|
|
277
|
+
...(r.signer ? { signer: r.signer } : {}),
|
|
278
|
+
});
|
|
279
|
+
const a = args;
|
|
280
|
+
const encoded = Object.fromEntries(def.args.map((x) => [x.name, toCoder(a[x.name] ?? a[camel(x.name)], x.type, `${def.name}(${camel(x.name)})`)]));
|
|
281
|
+
return { programAddress: programId, accounts: metas, data: new Uint8Array(coder.instruction.encode(def.name, encoded)) };
|
|
282
|
+
},
|
|
283
|
+
async send(payer, ...ixs) {
|
|
284
|
+
const tx = await pipe(createTransactionMessage({ version: 0 }), (t) => setTransactionMessageFeePayerSigner(payer, t), (t) => svm.setTransactionMessageLifetimeUsingLatestBlockhash(t), (t) => appendTransactionMessageInstructions(ixs, t), (t) => signTransactionMessageWithSigners(t));
|
|
285
|
+
const res = svm.sendTransaction(tx);
|
|
286
|
+
svm.expireBlockhash();
|
|
287
|
+
return res;
|
|
288
|
+
},
|
|
289
|
+
ok(res) {
|
|
290
|
+
if (res instanceof FailedTransactionMetadata)
|
|
291
|
+
throw new Error(`transaction failed: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
|
|
292
|
+
return res;
|
|
293
|
+
},
|
|
294
|
+
errorCode(res) {
|
|
295
|
+
if (!(res instanceof FailedTransactionMetadata))
|
|
296
|
+
throw new Error("expected the transaction to fail, but it succeeded");
|
|
297
|
+
const m = /Custom\D*(\d+)/.exec(String(res.err()));
|
|
298
|
+
if (!m)
|
|
299
|
+
throw new Error(`not a custom program error: ${String(res.err())}\n${res.meta().logs().join("\n")}`);
|
|
300
|
+
return Number(m[1]);
|
|
301
|
+
},
|
|
302
|
+
async account(type, address) {
|
|
303
|
+
const a = svm.getAccount(address);
|
|
304
|
+
if (!a.exists)
|
|
305
|
+
return null;
|
|
306
|
+
return plain(coder.accounts.decode(type, Buffer.from(a.data)));
|
|
307
|
+
},
|
|
308
|
+
returned(instruction, res) {
|
|
309
|
+
const def = pidl.instructions.find((i) => i.name === instruction || i.name === snake(instruction));
|
|
310
|
+
if (!def?.returns)
|
|
311
|
+
throw new Error(`${instruction} returns nothing`);
|
|
312
|
+
const data = Buffer.from(harness.ok(res).returnData().data());
|
|
313
|
+
const t = def.returns;
|
|
314
|
+
if (typeof t === "string") {
|
|
315
|
+
if (t === "pubkey")
|
|
316
|
+
return getAddressDecoder().decode(data);
|
|
317
|
+
if (t === "bool")
|
|
318
|
+
return (data[0] !== 0);
|
|
319
|
+
const signed = t.startsWith("i");
|
|
320
|
+
const bytes = Number(t.slice(1)) / 8;
|
|
321
|
+
let n = 0n;
|
|
322
|
+
for (let i = bytes - 1; i >= 0; i--)
|
|
323
|
+
n = (n << 8n) | BigInt(data[i]);
|
|
324
|
+
if (signed && n >= 1n << BigInt(bytes * 8 - 1))
|
|
325
|
+
n -= 1n << BigInt(bytes * 8);
|
|
326
|
+
return (bytes <= 4 ? Number(n) : n);
|
|
327
|
+
}
|
|
328
|
+
if ("defined" in t)
|
|
329
|
+
return plain(coder.types.decode(t.defined.name, data));
|
|
330
|
+
throw new Error(`decode ${instruction}'s return data yourself (${JSON.stringify(t)})`);
|
|
331
|
+
},
|
|
332
|
+
balance(address) {
|
|
333
|
+
return svm.getBalance(address) ?? 0n;
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
return harness;
|
|
337
|
+
}
|
package/dist/workspace.js
CHANGED
|
@@ -114,7 +114,17 @@ export function scaffold(dir, name, template = "counter") {
|
|
|
114
114
|
[CONFIG]: JSON.stringify({ programs: { [name]: { path: `programs/${name}.ts` } }, out: "target", sbpf: "v0" }, null, 2) + "\n",
|
|
115
115
|
[`programs/${name}.ts`]: t.program,
|
|
116
116
|
"programs/tsconfig.json": JSON.stringify({
|
|
117
|
-
compilerOptions: {
|
|
117
|
+
compilerOptions: {
|
|
118
|
+
target: "ES2022",
|
|
119
|
+
module: "ESNext",
|
|
120
|
+
moduleResolution: "Bundler",
|
|
121
|
+
strict: true,
|
|
122
|
+
noEmit: true,
|
|
123
|
+
types: [],
|
|
124
|
+
lib: ["ES2022"],
|
|
125
|
+
// TypeSOL's own errors in the editor (with the workspace's TypeScript; see docs/cli.md).
|
|
126
|
+
plugins: [{ name: "typesol/ts-plugin" }],
|
|
127
|
+
},
|
|
118
128
|
include: ["**/*.ts"],
|
|
119
129
|
}, null, 2) + "\n",
|
|
120
130
|
[`tests/${name}.test.ts`]: t.test(name),
|
package/docs/architecture.md
CHANGED
|
@@ -39,6 +39,9 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
39
39
|
| `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
|
|
40
40
|
| `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
41
|
| `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program. |
|
|
42
|
+
| `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
|
|
43
|
+
| `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
|
|
44
|
+
| `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
|
|
42
45
|
| `src/profile.ts` | Turns `--profile` logs into compute units per source line. |
|
|
43
46
|
| `src/cli.ts` | Command parsing. Every command has strict options, `--help`, and exit codes 0 (success), 1 (failure), 2 (usage error) and 3 (verify mismatch). |
|
|
44
47
|
|
package/docs/cli.md
CHANGED
|
@@ -9,13 +9,14 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
|
|
|
9
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. |
|
|
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
|
-
| `typesol check <file.ts
|
|
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). |
|
|
13
13
|
| `typesol emit-c <file.ts> [--out <file.c>] [--profile]` | Print or write the generated C. |
|
|
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
18
|
| `typesol client <idl.json> [--out <file.ts>] [--address <base58>]` | 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. See [client.md](client.md). |
|
|
18
|
-
| `typesol verify <program\|file.ts> <deployed.so> [--sbpf …]` | Rebuild and compare with
|
|
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. |
|
|
19
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). |
|
|
20
21
|
| `typesol help <command>` | Same as `typesol <command> --help`. |
|
|
21
22
|
| `typesol --version` | Print the version. |
|
|
@@ -32,3 +33,16 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
|
|
|
32
33
|
|
|
33
34
|
`"programs": { "vault": "programs/vault.ts" }` (a plain path) is accepted too. Keep `target/deploy/*-keypair.json`
|
|
34
35
|
safe and out of git: it is each program's address and its first upgrade authority must not lose it.
|
|
36
|
+
|
|
37
|
+
## Editor
|
|
38
|
+
|
|
39
|
+
`typesol init` projects load `typesol/ts-plugin` from `programs/tsconfig.json`: besides TypeScript's errors, the editor
|
|
40
|
+
shows what the TypeSOL compiler refuses (an unsupported construct, a constraint that cannot hold…) at its line, for the
|
|
41
|
+
text being edited, once TypeScript itself has nothing to report. It checks the program entries listed in
|
|
42
|
+
`typesol.json`. Editors load tsconfig plugins from the project's own TypeScript: in VS Code, run "TypeScript: Select
|
|
43
|
+
TypeScript Version" → "Use Workspace Version". To add it to an existing project:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{ "compilerOptions": { "plugins": [{ "name": "typesol/ts-plugin" }] } }
|
|
47
|
+
```
|
|
48
|
+
|
package/docs/errors.md
CHANGED
|
@@ -15,7 +15,8 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
15
15
|
| 2040 | the same account passed in two writable slots (ConstraintDuplicateMutableAccount) |
|
|
16
16
|
| 3000 | account already exists / initialized (AccountDiscriminatorAlreadySet) |
|
|
17
17
|
| 3002 | discriminator mismatch (AccountDiscriminatorMismatch) |
|
|
18
|
-
| 3003 | data too small, not an initialized token account / mint,
|
|
18
|
+
| 3003 | data too small, not an initialized token account / mint, a read past the end, or a `@layout borsh` account whose bytes are not a valid Borsh value (AccountDidNotDeserialize) |
|
|
19
|
+
| 3004 | a `@layout borsh` account too small for the value it would now hold (AccountDidNotSerialize) |
|
|
19
20
|
| 3005 | not enough accounts (AccountNotEnoughKeys) |
|
|
20
21
|
| 3006 | account must be writable (AccountNotMutable) |
|
|
21
22
|
| 3007 | wrong owner program (AccountOwnedByWrongProgram) |
|
|
@@ -26,7 +27,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
26
27
|
| 4102 | cast overflow (InvalidNumericConversion) |
|
|
27
28
|
| 5100 / 5101 | arithmetic overflow (including shifts) / division by zero |
|
|
28
29
|
| 5103 | sysvar unavailable |
|
|
29
|
-
| 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec) |
|
|
30
|
+
| 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec, `push` on a full FixedVec) |
|
|
30
31
|
| 5105 | string longer than the FixedString capacity |
|
|
31
32
|
| 5106 | missing or mismatched return data |
|
|
32
33
|
| 5107 | event larger than 1024 bytes |
|
package/docs/language.md
CHANGED
|
@@ -6,9 +6,9 @@ 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`,
|
|
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) |
|
|
10
10
|
| Account data | an exported `interface` with fixed-size fields; interfaces nest (`home: Point`, `trail: FixedArray<Point, 4>`) |
|
|
11
|
-
| Integers | `u8` `u16` `u32` `u64` `u128` `i8` `i16` `i32` `i64` `i128` — bigint literals (`1n`), every `+ - * /
|
|
11
|
+
| Integers | `u8` `u16` `u32` `u64` `u128` `i8` `i16` `i32` `i64` `i128` — bigint literals (`1n`), every `+ - * / % **` 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), `isqrt(x)` (up to u128), `min(a, b)`, `max(a, b)` |
|
|
13
13
|
| Other scalars | `boolean`, `Pubkey` (compare with `===` or `.equals`) |
|
|
14
14
|
| Errors | `export enum MyError { Name = "message" }` + `require(cond, MyError.Name)` → codes 6000+ |
|
|
@@ -18,12 +18,15 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
18
18
|
| Local arrays | `const xs = fixedArray<i64, 5>(0n)` — up to 256 elements on the stack |
|
|
19
19
|
| Strings | `string` arguments and literals; `name: FixedString<32>` in account data (u32 length + 32 bytes); `===`, `byteLength(s)`, `log(s)`, `+=`, as PDA seeds; too-long writes abort (5105). A local bound to account text keeps its own copy |
|
|
20
20
|
| Copies | `a.home = b.home`, `a.trail.set(0n, a.home)` copy whole structs / arrays of the same type: assignment copies (like Rust/Anchor), binding with `const` references |
|
|
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 |
|
|
21
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` |
|
|
22
23
|
| Pubkey constants | `const ADMIN = pubkey("4Nd1…")` (checked at compile time) or inline `pubkey("…")`; usable as seeds |
|
|
23
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 `Option` |
|
|
24
25
|
| Enums | `export enum Status { Pending, Approved }` — unit variants stored as the Borsh u8 index; `Status.Approved`, `===` |
|
|
25
|
-
| 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); `${opt}` prints the value or `null`. Borsh `option` in arguments/events, fixed `{ is_some, value }` in account data |
|
|
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); `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 |
|
|
26
27
|
| Vecs | `entries: Vec<Entry>` as the last account field — `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; `pop()` of a Vec of structs works as a statement |
|
|
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); not for arguments, events, `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). Account data and returned structs only: arguments use `Vec<T>` |
|
|
27
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 |
|
|
28
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) |
|
|
29
32
|
| Struct literals | `book.entries.push({ who: who.key, amount, status: Status.Pending, note }, who)` — every field required |
|
|
@@ -31,15 +34,17 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
31
34
|
| Other programs' accounts | `const pool = loadForeign<Pool>(account, AMM_PROGRAM)` — owner, `account:Pool` discriminator and size checked (3007 / 3002 / 3003); `readData<u64>(account, offset)` for raw little-endian reads (bounds-checked, 3003) |
|
|
32
35
|
| Ternaries | `const fee = vip ? 0n : 30n` — only the chosen branch runs |
|
|
33
36
|
| Constants | top-level `const SEED = "vault"`, `const FEE: u16 = 30n`, `const ON = true` — inlined, usable as seeds (and described in the IDL) |
|
|
34
|
-
| Data enums | `export type Action = { kind: "Deposit"; amount: u64 } \| { kind: "Pause" }` — `a.kind === "Deposit"`, `switch (a.kind)`, TypeScript narrowing picks the variant (checked again at runtime, 5112); Borsh enums in arguments/events, fixed `{ tag, payload }` in account data |
|
|
37
|
+
| Data enums | `export type Action = { kind: "Deposit"; amount: u64 } \| { kind: "Pause" }` — `a.kind === "Deposit"`, `switch (a.kind)`, TypeScript narrowing picks the variant (checked again at runtime, 5112); Borsh enums in arguments/events, fixed `{ tag, payload }` in account data. A variant may hold a `string` (`{ kind: "Say"; text: string }`): such an enum works in arguments, events and locals (Borsh, as Anchor encodes it) but not in account data |
|
|
35
38
|
| Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
|
|
36
39
|
| Strings at runtime | `"hello, " + name`, `` `#${n}` `` — built on the 32 KiB heap (5110 when full) |
|
|
37
40
|
| `switch` | on integers (up to 64 bits), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
|
|
38
41
|
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
39
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">`) |
|
|
40
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 |
|
|
41
|
-
| Loops | `for (let i = 0n; i < n; i++)`, `while`, `break`, `continue` — bounded by the compute budget |
|
|
42
|
-
| 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 a scalar or `
|
|
44
|
+
| Loops | `for (let i = 0n; i < n; i++)`, `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. Helpers cannot grow Vecs (only instructions know the entry size) |
|
|
46
|
+
| Arrow functions | `const fee = (x: u64): u64 => x / 100n` (or `const f = function (…) {…}`) at the top level or inside a function is a helper (parameter and return types written out). A local one capturing nothing: it reads its parameters, top-level names and other functions, not the enclosing function's variables (pass them in). Functions are called, not stored or passed |
|
|
47
|
+
| 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 or enum. 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) |
|
|
43
48
|
| 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()`) |
|
|
44
49
|
| `++` `--` `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` `<<=` `>>=` | all checked |
|
|
45
50
|
| Sysvars | `now()`, `currentSlot()`, `currentEpoch()` (clock), `minimumBalance(account)`, `rentExempt(space)` (rent) |
|
|
@@ -48,8 +53,8 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
48
53
|
| 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) |
|
|
49
54
|
| Heap | 32 KiB by default; `const HEAP_SIZE_KIB = 64n` (32–256) lets the program use more, when clients add ComputeBudget `requestHeapFrame` |
|
|
50
55
|
| Docs | JSDoc on instructions, `@param`, structs, fields and data enums becomes `docs` in the IDL |
|
|
51
|
-
| Metaplex Core | `core: MplCoreProgram` — `createCollection`, `createAsset` (in a collection, PDA authorities via signerSeeds), `transferAsset`, `burnAsset` (
|
|
52
|
-
| Compressed NFTs | `bubblegum: BubblegumProgram` — `createTree` (client-allocated tree on SPL Account Compression), `mint` (MintV1, no collection/creators) |
|
|
56
|
+
| 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 (no rule set), FreezeDelegate, BurnDelegate, TransferDelegate, UpdateDelegate, PermanentFreezeDelegate, Attributes, PermanentTransferDelegate, PermanentBurnDelegate, Edition, AddBlocker, ImmutableMetadata; `authority` is `"owner"`, `"updateAuthority"`, `"none"` or a Pubkey; `coreAssetOwner(asset)`, `coreAssetCollection(asset)` |
|
|
57
|
+
| Compressed NFTs | `bubblegum: BubblegumProgram` — V1: `createTree` (client-allocated tree on SPL Account Compression), `mint` (MintV1, no collection/creators). 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` and `burnV2` with the leaf `{ root, dataHash, creatorHash, assetDataHash?, flags?, nonce, index }` and its proof as `remainingAccounts()` / `rest.slice(a)` |
|
|
53
58
|
|
|
54
59
|
## Account parameters and the checks they emit
|
|
55
60
|
|
|
@@ -75,7 +80,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
75
80
|
| `SystemProgram`, `TokenProgram`, `Token2022Program` | exact program address |
|
|
76
81
|
| `TokenInterface` | either token program |
|
|
77
82
|
| `AssociatedTokenProgram` | `create` / `createIdempotent` CPIs; `associatedTokenAddress(owner, mint, tokenProgram)` |
|
|
78
|
-
| `TokenMetadataProgram` | Metaplex `createV3(...)` (CreateMetadataAccountV3); `metadataAddress(mint)` |
|
|
83
|
+
| `TokenMetadataProgram` | Metaplex `createV3(...)` (CreateMetadataAccountV3), optionally with `{ creators: [{ address, verified, share }], collection: mint }` after `isMutable` (1–5 creators, shares adding up to 100; only the update authority, signing, can be `verified: true`; the collection is written unverified; `isCollection: true` makes the NFT a sized collection); `createMasterEditionV3(edition, mint, updateAuthority, mintAuthority, payer, metadata, tokenProgram, systemProgram, maxSupply \| null)`; `verifyCollection(authority, metadata, collectionMint, collectionMetadata, collectionMasterEdition, systemProgram, instructionsSysvar)` (Verify with CollectionV1); `metadataAddress(mint)`, `masterEditionAddress(mint)` |
|
|
79
84
|
| `InstructionsSysvar`, `PythPriceUpdate`, `SwitchboardFeed` | exact address / owner program + discriminator |
|
|
80
85
|
| `Program<"Address…">` | exact program address + executable |
|
|
81
86
|
| `UncheckedProgram` | executable only — check `.key` yourself |
|
|
@@ -94,7 +99,7 @@ account written through `.lamports` or `loadAccountMut` is another account of th
|
|
|
94
99
|
`transfer`, `transferChecked(from, mint, to, authority, amount)` (required by Token-2022 mints with fees/hooks), `mintTo`, `burn`, `closeAccount`,
|
|
95
100
|
`approve(source, delegate, owner, amount)`, `revoke`, `setAuthority(target, current, AuthorityType.MintTokens, next | null)`,
|
|
96
101
|
`freezeAccount` / `thawAccount(account, mint, freezeAuthority)`, `syncNative(account)`, `initializeMint2(mint, decimals, mintAuthority, freezeAuthority | null)`,
|
|
97
|
-
`initializeAccount3(account, mint, owner)`, and on Token-2022 `initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri)` (metadata extension)
|
|
102
|
+
`initializeAccount3(account, mint, owner)`, and on Token-2022 `initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri)` (metadata extension) and the extension initializers to call between `createAccount` and `initializeMint2` / `initializeAccount3`: `initializeTransferFeeConfig(mint, configAuthority | null, withdrawAuthority | null, basisPoints, maximumFee)`, `initializeMintCloseAuthority(mint, authority | null)`, `initializeNonTransferableMint(mint)`, `initializePermanentDelegate(mint, delegate)`, `initializeDefaultAccountState(mint, frozen)`, `initializeMetadataPointer` / `initializeGroupPointer` / `initializeGroupMemberPointer` / `initializeTransferHook(mint, authority | null, address | null)`, `initializeInterestBearingMint(mint, rateAuthority | null, rate)`, `initializeImmutableOwner(account)`
|
|
98
103
|
- `account.lamports` read/write on `Mut` accounts, `close(account, destination)` for program accounts
|
|
99
104
|
- `requirePda(account, seeds, bump)`
|
|
100
105
|
|
|
@@ -144,10 +149,10 @@ an integer literal, an argument, `acct.key` or a scalar field of an account, the
|
|
|
144
149
|
|
|
145
150
|
## Not supported yet
|
|
146
151
|
|
|
147
|
-
|
|
148
|
-
inside a helper, data-enum arguments carrying
|
|
149
|
-
128-bit integers, `mulDiv` beyond u64,
|
|
150
|
-
|
|
152
|
+
Growable `Vec`s anywhere but the last account field (use `FixedVec<T, N>`), FixedVec in arguments and events, growing an account by more than 10 KiB in one instruction, growing a Vec
|
|
153
|
+
inside a helper, data-enum arguments carrying structs, Pubkeys in templates, `?.`, `**=`, `!` on anything but an Option, `switch` on
|
|
154
|
+
128-bit integers, `mulDiv` beyond u64, Core's MasterEdition / VerifiedCreators / Autograph plugins, royalty rule sets and external plugin adapters, Bubblegum V2 delegates / freezes / metadata updates / V1 transfers, closures (local functions reading the enclosing function's variables), functions as values (callbacks,
|
|
155
|
+
`.map`), type parameters standing for structs, floating point (by design), `do … while`.
|
|
151
156
|
They are rejected at compile time rather than half-supported.
|
|
152
157
|
|
|
153
158
|
`Init.create` handles a PDA address someone pre-funded: it tops up rent, then allocates and assigns instead of
|
package/docs/mainnet.md
CHANGED
|
@@ -77,12 +77,13 @@ explorers look for it. It costs a few thousandths of a SOL of rent.
|
|
|
77
77
|
## 6. Verify
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
|
-
|
|
81
|
-
npx typesol verify my_app deployed.so
|
|
80
|
+
npx typesol verify my_app
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
`verify` rebuilds the program from `programs/my_app.ts
|
|
85
|
-
|
|
83
|
+
`verify` rebuilds the program from `programs/my_app.ts`, reads the deployed one at the address in `typesol.json` from
|
|
84
|
+
the cluster in your Solana CLI config, and compares the bytes. `match` means anyone with your source and the same
|
|
85
|
+
TypeSOL version gets exactly the program on chain. It also prints the upgrade authority and checks the IDL you
|
|
86
|
+
published in step 5. Anyone can run it against your program with `--address <program id> --url mainnet-beta`.
|
|
86
87
|
|
|
87
88
|
## 7. Call it
|
|
88
89
|
|