@typesol/cli 0.24.1 → 0.26.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 +51 -0
- package/README.md +35 -16
- package/SECURITY.md +2 -0
- package/dist/cli.js +52 -37
- package/dist/compiler/analyze.js +11 -1
- package/dist/compiler/emit/calls.js +10 -10
- package/dist/compiler/emit/cpi.js +4 -0
- package/dist/compiler/emit/function.d.ts +9 -0
- package/dist/compiler/emit/function.js +169 -139
- package/dist/compiler/emit/program.js +3 -0
- package/dist/compiler/emit/shared.d.ts +1 -0
- package/dist/compiler/emit/shared.js +1 -0
- package/dist/compiler/emit/statements.d.ts +6 -0
- package/dist/compiler/emit/statements.js +35 -5
- package/dist/templates.js +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{clientgen.js → tools/clientgen.js} +4 -1
- package/dist/tools/doctor.d.ts +45 -0
- package/dist/tools/doctor.js +229 -0
- package/dist/{portgen.js → tools/portgen.js} +1 -1
- package/dist/workspace.js +1 -1
- package/docs/architecture.md +17 -17
- package/docs/audit.md +90 -0
- package/docs/case-study-escrow.md +224 -0
- package/docs/cli.md +3 -2
- package/docs/errors.md +26 -1
- package/docs/language.md +12 -8
- package/docs/migrate-from-anchor.md +3 -1
- package/docs/runtime.md +1 -1
- package/docs/security-model.md +97 -0
- package/docs/testing.md +5 -4
- package/lib/typesol.d.ts +35 -13
- package/package.json +13 -13
- package/runtime/typesol_rt.h +33 -4
- /package/dist/{clientgen.d.ts → tools/clientgen.d.ts} +0 -0
- /package/dist/{cpigen.d.ts → tools/cpigen.d.ts} +0 -0
- /package/dist/{cpigen.js → tools/cpigen.js} +0 -0
- /package/dist/{idlpublish.d.ts → tools/idlpublish.d.ts} +0 -0
- /package/dist/{idlpublish.js → tools/idlpublish.js} +0 -0
- /package/dist/{onchain.d.ts → tools/onchain.d.ts} +0 -0
- /package/dist/{onchain.js → tools/onchain.js} +0 -0
- /package/dist/{portgen.d.ts → tools/portgen.d.ts} +0 -0
- /package/dist/{profile.d.ts → tools/profile.d.ts} +0 -0
- /package/dist/{profile.js → tools/profile.js} +0 -0
- /package/dist/{svmrpc.d.ts → tools/svmrpc.d.ts} +0 -0
- /package/dist/{svmrpc.js → tools/svmrpc.js} +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// `typesol doctor` and the checks `init` and `deploy` run before they need something: Node, the platform-tools the
|
|
2
|
+
// compiler builds with, the Solana CLI, its keypair and RPC, the project, and (with an RPC) whether it answers and the
|
|
3
|
+
// payer can afford a deploy. Each problem comes with the command that fixes it.
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { findToolchain } from "../compiler/toolchain.js";
|
|
9
|
+
import { base58, readKeypair } from "../keys.js";
|
|
10
|
+
import { findWorkspace } from "../workspace.js";
|
|
11
|
+
export const INSTALL_AGAVE = 'sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"';
|
|
12
|
+
const MIN_NODE = [22, 12];
|
|
13
|
+
/** The platform-tools the compiler needs: found, or what to run. */
|
|
14
|
+
export function toolchainCheck() {
|
|
15
|
+
try {
|
|
16
|
+
const tc = findToolchain();
|
|
17
|
+
let version = "";
|
|
18
|
+
try {
|
|
19
|
+
version = execFileSync(tc.clang, ["--version"], { encoding: "utf8" })
|
|
20
|
+
.split("\n")[0]
|
|
21
|
+
.replace(/\s*\(.*$/, "");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* the path is enough */
|
|
25
|
+
}
|
|
26
|
+
return { ok: true, what: "platform-tools", detail: version || tc.sdk };
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
what: "platform-tools",
|
|
32
|
+
detail: e.message.split(". ")[0],
|
|
33
|
+
fix: [INSTALL_AGAVE, "cargo-build-sbf --version # once: downloads the platform-tools TypeSOL compiles with"],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** json_rpc_url / keypair_path from a Solana CLI config (the given file, or the CLI's default one). */
|
|
38
|
+
export function solanaConfig(file) {
|
|
39
|
+
const configPath = file ?? path.join(os.homedir(), ".config/solana/cli/config.yml");
|
|
40
|
+
if (!existsSync(configPath))
|
|
41
|
+
return { file: configPath, exists: false };
|
|
42
|
+
const text = readFileSync(configPath, "utf8");
|
|
43
|
+
const get = (k) => new RegExp(`^${k}:\\s*["']?([^"'\\n]+?)["']?\\s*$`, "m").exec(text)?.[1];
|
|
44
|
+
const expand = (p) => (p?.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p);
|
|
45
|
+
return { url: get("json_rpc_url"), keypair: expand(get("keypair_path")), file: configPath, exists: true };
|
|
46
|
+
}
|
|
47
|
+
/** The Solana CLI's cluster monikers, as `solana --url` accepts them; anything else is used as given. */
|
|
48
|
+
export function clusterUrl(url) {
|
|
49
|
+
const monikers = {
|
|
50
|
+
"mainnet-beta": "https://api.mainnet-beta.solana.com",
|
|
51
|
+
m: "https://api.mainnet-beta.solana.com",
|
|
52
|
+
devnet: "https://api.devnet.solana.com",
|
|
53
|
+
d: "https://api.devnet.solana.com",
|
|
54
|
+
testnet: "https://api.testnet.solana.com",
|
|
55
|
+
t: "https://api.testnet.solana.com",
|
|
56
|
+
localhost: "http://127.0.0.1:8899",
|
|
57
|
+
l: "http://127.0.0.1:8899",
|
|
58
|
+
};
|
|
59
|
+
return monikers[url] ?? url;
|
|
60
|
+
}
|
|
61
|
+
/** Only the host: an RPC URL can carry an API key in its path or query, and these messages may be shared. */
|
|
62
|
+
export function rpcHost(url) {
|
|
63
|
+
try {
|
|
64
|
+
return new URL(url).host;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return "the RPC";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The cluster an RPC belongs to, for the airdrop hint (mainnet has none). */
|
|
71
|
+
function clusterOf(url) {
|
|
72
|
+
const host = rpcHost(url);
|
|
73
|
+
if (/devnet/.test(host))
|
|
74
|
+
return "devnet";
|
|
75
|
+
if (/testnet/.test(host))
|
|
76
|
+
return "testnet";
|
|
77
|
+
if (/^(127\.0\.0\.1|localhost|0\.0\.0\.0)(:|$)/.test(host))
|
|
78
|
+
return "localhost";
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
export async function rpc(url, method, params = []) {
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
86
|
+
signal: AbortSignal.timeout(10_000),
|
|
87
|
+
});
|
|
88
|
+
const body = (await res.json());
|
|
89
|
+
if (body.error)
|
|
90
|
+
throw new Error(body.error.message);
|
|
91
|
+
return body.result;
|
|
92
|
+
}
|
|
93
|
+
/** fetch's "fetch failed" says little: the cause's code (ECONNREFUSED, ENOTFOUND, a timeout) says more. */
|
|
94
|
+
export const errText = (e) => {
|
|
95
|
+
const err = e;
|
|
96
|
+
return err.cause?.code ?? err.cause?.message ?? (err.name === "TimeoutError" ? "no answer in 10 s" : err.message);
|
|
97
|
+
};
|
|
98
|
+
const sol = (lamports) => lamports === 0 ? "0 SOL" : lamports < 100_000 ? `${lamports} lamports` : `${(lamports / 1e9).toFixed(lamports < 1e7 ? 4 : 2)} SOL`;
|
|
99
|
+
/** A cost, rounded up so the advice is enough. */
|
|
100
|
+
const solUp = (lamports) => `${(Math.ceil(lamports / 1e7) / 100).toFixed(2)} SOL`;
|
|
101
|
+
/**
|
|
102
|
+
* Before `solana program deploy`: the RPC answers, and the payer holds enough for the program's rent (a first deploy
|
|
103
|
+
* holds about twice the binary's rent while the buffer exists; an upgrade, the buffer's). Throws with what to do.
|
|
104
|
+
*/
|
|
105
|
+
export async function deployPreflight(opts) {
|
|
106
|
+
const host = rpcHost(opts.url);
|
|
107
|
+
try {
|
|
108
|
+
await rpc(opts.url, "getVersion");
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
const local = clusterOf(opts.url) === "localhost";
|
|
112
|
+
throw new Error(`cannot reach ${host} (${errText(e)}). ` +
|
|
113
|
+
(local
|
|
114
|
+
? "Start a local validator in another terminal: `solana-test-validator`, or deploy to devnet with --url devnet."
|
|
115
|
+
: "Check --url (or json_rpc_url in the Solana CLI config); for a test deploy use --url devnet or a local `solana-test-validator`."));
|
|
116
|
+
}
|
|
117
|
+
if (!opts.payer || !existsSync(opts.payer))
|
|
118
|
+
return; // the Solana CLI reports a missing keypair itself
|
|
119
|
+
const payer = base58(readKeypair(opts.payer).subarray(32));
|
|
120
|
+
let need = 0;
|
|
121
|
+
for (const p of opts.programs) {
|
|
122
|
+
const size = statSync(p.soPath).size;
|
|
123
|
+
const exists = (await rpc(opts.url, "getAccountInfo", [p.address, { encoding: "base64", dataSlice: { offset: 0, length: 0 } }])).value;
|
|
124
|
+
const rent = await rpc(opts.url, "getMinimumBalanceForRentExemption", [size + 45]);
|
|
125
|
+
need += exists ? rent : 2 * rent;
|
|
126
|
+
}
|
|
127
|
+
need += 5_000_000; // fees for the buffer writes
|
|
128
|
+
const balance = (await rpc(opts.url, "getBalance", [payer])).value;
|
|
129
|
+
if (balance >= need)
|
|
130
|
+
return;
|
|
131
|
+
const cluster = clusterOf(opts.url);
|
|
132
|
+
const airdrop = cluster ? ` Get test SOL: \`solana airdrop ${Math.max(2, Math.ceil((need - balance) / 1e9))} ${payer} --url ${cluster}\`.` : "";
|
|
133
|
+
throw new Error(`the payer ${payer} has ${sol(balance)} on ${host}; deploying needs about ${solUp(need)} (rent, mostly returned when a buffer closes).${airdrop}`);
|
|
134
|
+
}
|
|
135
|
+
/** Everything `typesol doctor` checks, in order. */
|
|
136
|
+
export async function doctorChecks(opts) {
|
|
137
|
+
const checks = [];
|
|
138
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
139
|
+
const nodeOk = major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
|
|
140
|
+
checks.push({
|
|
141
|
+
ok: nodeOk,
|
|
142
|
+
what: "Node.js",
|
|
143
|
+
detail: process.versions.node,
|
|
144
|
+
fix: nodeOk ? undefined : [`install Node.js ${MIN_NODE.join(".")} or later (https://nodejs.org)`],
|
|
145
|
+
});
|
|
146
|
+
checks.push(toolchainCheck());
|
|
147
|
+
let solanaVersion;
|
|
148
|
+
try {
|
|
149
|
+
solanaVersion = execFileSync("solana", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
150
|
+
.split(" (")[0]
|
|
151
|
+
.trim();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* not installed */
|
|
155
|
+
}
|
|
156
|
+
checks.push(solanaVersion
|
|
157
|
+
? { ok: true, what: "Solana CLI", detail: solanaVersion }
|
|
158
|
+
: { ok: false, optional: true, what: "Solana CLI", detail: "not on PATH (needed to deploy, not to build or test)", fix: [INSTALL_AGAVE] });
|
|
159
|
+
const cfg = solanaConfig(opts.config);
|
|
160
|
+
const keypair = cfg.keypair ?? path.join(os.homedir(), ".config/solana/id.json");
|
|
161
|
+
if (existsSync(keypair)) {
|
|
162
|
+
let address = "";
|
|
163
|
+
try {
|
|
164
|
+
address = base58(readKeypair(keypair).subarray(32));
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
checks.push({ ok: false, optional: true, what: "keypair", detail: e.message });
|
|
168
|
+
}
|
|
169
|
+
if (address)
|
|
170
|
+
checks.push({ ok: true, what: "keypair", detail: `${address} (${keypair.replace(os.homedir(), "~")})` });
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
checks.push({
|
|
174
|
+
ok: false,
|
|
175
|
+
optional: true,
|
|
176
|
+
what: "keypair",
|
|
177
|
+
detail: `none at ${keypair.replace(os.homedir(), "~")} (the deploy payer)`,
|
|
178
|
+
fix: ["solana-keygen new"],
|
|
179
|
+
});
|
|
180
|
+
const url = opts.url ? clusterUrl(opts.url) : cfg.url;
|
|
181
|
+
if (!url)
|
|
182
|
+
checks.push({ ok: false, optional: true, what: "RPC", detail: "none set (deploy takes --url)", fix: ["solana config set --url devnet"] });
|
|
183
|
+
else {
|
|
184
|
+
const cluster = clusterOf(url);
|
|
185
|
+
try {
|
|
186
|
+
const v = await rpc(url, "getVersion");
|
|
187
|
+
checks.push({ ok: true, what: "RPC", detail: `${rpcHost(url)} (solana-core ${v["solana-core"]})` });
|
|
188
|
+
if (existsSync(keypair)) {
|
|
189
|
+
const balance = (await rpc(url, "getBalance", [base58(readKeypair(keypair).subarray(32))])).value;
|
|
190
|
+
checks.push(balance > 0
|
|
191
|
+
? { ok: true, what: "balance", detail: sol(balance) }
|
|
192
|
+
: {
|
|
193
|
+
ok: false,
|
|
194
|
+
optional: true,
|
|
195
|
+
what: "balance",
|
|
196
|
+
detail: `0 SOL on ${rpcHost(url)}`,
|
|
197
|
+
fix: [
|
|
198
|
+
cluster && cluster !== "localhost"
|
|
199
|
+
? `solana airdrop 2 --url ${cluster}`
|
|
200
|
+
: cluster
|
|
201
|
+
? "solana airdrop 2 --url localhost"
|
|
202
|
+
: "fund the keypair above",
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
checks.push({
|
|
209
|
+
ok: false,
|
|
210
|
+
optional: true,
|
|
211
|
+
what: "RPC",
|
|
212
|
+
detail: `${rpcHost(url)} does not answer (${errText(e)})`,
|
|
213
|
+
fix: cluster === "localhost" ? ["solana-test-validator # in another terminal"] : ["pass --url devnet, or fix json_rpc_url in the Solana CLI config"],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const ws = findWorkspace(opts.cwd);
|
|
218
|
+
checks.push(ws
|
|
219
|
+
? { ok: true, what: "project", detail: `${path.relative(opts.cwd, ws.file) || ws.file}: ${Object.keys(ws.programs).join(", ")}` }
|
|
220
|
+
: { ok: true, optional: true, what: "project", detail: "none here (typesol init <dir> creates one)" });
|
|
221
|
+
return checks;
|
|
222
|
+
}
|
|
223
|
+
export function formatChecks(checks) {
|
|
224
|
+
const mark = (c) => (c.ok ? "ok " : c.optional ? " ! " : "ERR");
|
|
225
|
+
const width = Math.max(...checks.map((c) => c.what.length));
|
|
226
|
+
return checks
|
|
227
|
+
.map((c) => ` ${mark(c)} ${c.what.padEnd(width)} ${c.detail}${(c.fix ?? []).map((f) => `\n${" ".repeat(width + 9)}${f}`).join("")}`)
|
|
228
|
+
.join("\n");
|
|
229
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* data enum, a COption) it says so in a note and a comment.
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
|
-
import { cNameProblem } from "
|
|
12
|
+
import { cNameProblem } from "../compiler/analyze.js";
|
|
13
13
|
const RESERVED = new Set("break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "));
|
|
14
14
|
const camel = (s) => s.replace(/[_-]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
15
15
|
const pascal = (s) => camel(s).replace(/^[a-z]/, (c) => c.toUpperCase());
|
package/dist/workspace.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import { writeClient } from "./clientgen.js";
|
|
16
|
+
import { writeClient } from "./tools/clientgen.js";
|
|
17
17
|
import { build, COMPILER_VERSION, SBPF_VERSIONS } from "./compiler/index.js";
|
|
18
18
|
import { loadOrCreateKeypair } from "./keys.js";
|
|
19
19
|
import { TEMPLATES } from "./templates.js";
|
package/docs/architecture.md
CHANGED
|
@@ -26,13 +26,13 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
26
26
|
|
|
27
27
|
| Path | Contents |
|
|
28
28
|
| --- | --- |
|
|
29
|
-
| `src/` | The compiler (`src/compiler/`) and the
|
|
29
|
+
| `src/` | The compiler (`src/compiler/`); the CLI, projects, init templates and `typesol/testing` at the top; generators and chain tooling in `src/tools/` (client, CPI and port generators, IDL publishing, on-chain reads, profiling, the LiteSVM RPC). Source map below |
|
|
30
30
|
| `runtime/` | `typesol_rt.h`, the C runtime every program includes ([runtime.md](runtime.md)) |
|
|
31
31
|
| `lib/` | `typesol.d.ts`, the language surface programs typecheck against |
|
|
32
32
|
| `ts-plugin/` | The editor plugin (`typesol/ts-plugin`) |
|
|
33
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
|
-
| `
|
|
34
|
+
| `test/` | The test suite, by subject: `language/`, `security/`, `examples/`, `integrations/` (token, Metaplex, oracle and other CPIs), `client/`, `cli/`, `porting/`, `verification/` (differential and fuzz tests). Shared: `harness.ts` and the `*-helpers.ts`, `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated C and clients), `programs/` (third-party program binaries, see its SOURCES.md), `anchor-twins/` (below) |
|
|
35
|
+
| `test/anchor-twins/` | Anchor twins of the counter, vault and escrow examples, for the differential tests (`pnpm build:twins`) and the zero-copy client test; the benchmarks themselves are in [typesol/benchmarks](https://github.com/typesol/benchmarks) |
|
|
36
36
|
| `scripts/` | `smoke.ts` and `localnet.sh` (a local validator run), `fetch-programs.sh` (third-party binaries), `readme-bench.ts` (the README numbers) |
|
|
37
37
|
| `docs/` | User and contributor documentation (also published on typesol.com) |
|
|
38
38
|
| `assets/` | The CLI's logo and the README images (`readme/`, generated) |
|
|
@@ -53,17 +53,17 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
53
53
|
| `src/compiler/index.ts` | The compiler's API (`generate`, `build`, `verifyBinary`, the build manifest). |
|
|
54
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. |
|
|
55
55
|
| `src/workspace.ts` | `typesol.json` projects, program keypairs, `typesol init` templates and deploy arguments. |
|
|
56
|
-
| `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
|
|
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. |
|
|
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). |
|
|
56
|
+
| `src/tools/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
|
|
57
|
+
| `src/tools/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. |
|
|
58
|
+
| `src/tools/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/tools/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
|
|
60
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`). |
|
|
61
|
+
| `src/tools/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
|
|
62
62
|
| `src/keys.ts` | Base58 and program keypair helpers. |
|
|
63
63
|
| `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
|
|
64
64
|
| `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
|
|
65
|
-
| `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
|
|
66
|
-
| `src/profile.ts` | Turns `--profile` logs into compute units per source line. |
|
|
65
|
+
| `src/tools/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
|
|
66
|
+
| `src/tools/profile.ts` | Turns `--profile` logs into compute units per source line. |
|
|
67
67
|
| `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). |
|
|
68
68
|
|
|
69
69
|
## The emitter
|
|
@@ -123,7 +123,7 @@ These invariants are easy to break. Each one has tests.
|
|
|
123
123
|
3. Put any C helper in `runtime/typesol_rt.h` as `static inline`, returning `uint64_t` (`SUCCESS` or an error code)
|
|
124
124
|
so that callers can wrap it in `TS_TRY`.
|
|
125
125
|
4. Add an execution test that compiles a fixture under `test/fixtures/` and runs it in LiteSVM. Add a diagnostics
|
|
126
|
-
case in `test/diagnostics.test.ts` for each misuse you reject.
|
|
126
|
+
case in `test/language/diagnostics.test.ts` for each misuse you reject.
|
|
127
127
|
5. Document it in `docs/language.md` and add a line to `CHANGELOG.md`.
|
|
128
128
|
|
|
129
129
|
### Add an account constraint
|
|
@@ -145,7 +145,7 @@ These invariants are easy to break. Each one has tests.
|
|
|
145
145
|
### Change generated C
|
|
146
146
|
|
|
147
147
|
`test/golden/counter.c` pins the output for the counter example. Run
|
|
148
|
-
`UPDATE_GOLDEN=1 pnpm test test/diagnostics.test.ts`, review the diff, and commit it with the change.
|
|
148
|
+
`UPDATE_GOLDEN=1 pnpm test test/language/diagnostics.test.ts`, review the diff, and commit it with the change.
|
|
149
149
|
|
|
150
150
|
For refactors that should not change behaviour, compare binaries: build every example and fixture before and after
|
|
151
151
|
the change. The sha256 values in the `*.build.json` manifests must not change.
|
|
@@ -154,12 +154,12 @@ the change. The sha256 values in the `*.build.json` manifests must not change.
|
|
|
154
154
|
|
|
155
155
|
| Files | What they cover |
|
|
156
156
|
| --- | --- |
|
|
157
|
-
| `test/<feature>.test.ts` | Execution tests. Each compiles fixtures and runs them in LiteSVM through `test/harness.ts`. |
|
|
158
|
-
| `test/diagnostics.test.ts` | Every rejected construct, plus the golden C. |
|
|
159
|
-
| `test/hardening.test.ts`, `test/regressions.test.ts` | Attacks and edge cases that once failed. Keep each as a negative test. |
|
|
160
|
-
| `test/fuzz-*.test.ts`, `test/differential.test.ts` | Randomized checks against BigInt, a TypeScript state model, and the Anchor twins in `
|
|
157
|
+
| `test/<subject>/<feature>.test.ts` | Execution tests. Each compiles fixtures and runs them in LiteSVM through `test/harness.ts`. |
|
|
158
|
+
| `test/language/diagnostics.test.ts` | Every rejected construct, plus the golden C. |
|
|
159
|
+
| `test/security/hardening.test.ts`, `test/security/regressions.test.ts` | Attacks and edge cases that once failed. Keep each as a negative test. |
|
|
160
|
+
| `test/verification/fuzz-*.test.ts`, `test/verification/differential.test.ts` | Randomized checks against BigInt, a TypeScript state model, and the Anchor twins in `test/anchor-twins`. See [testing.md](testing.md). |
|
|
161
161
|
| `scripts/smoke.ts` | End to end against a real `solana-test-validator` (`scripts/localnet.sh`). |
|
|
162
162
|
|
|
163
163
|
Some tests need optional inputs and are skipped without them:
|
|
164
164
|
- `pnpm fetch:programs` downloads the Metaplex programs, which cannot be redistributed.
|
|
165
|
-
- `pnpm
|
|
165
|
+
- `pnpm build:twins` builds the Anchor twins and needs Rust and Anchor.
|
package/docs/audit.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Audit package
|
|
2
|
+
|
|
3
|
+
For a security firm reviewing TypeSOL. TypeSOL compiles a subset of TypeScript to C and then to Solana SBF; every
|
|
4
|
+
program it builds includes one C runtime header. The question for an audit is whether a program compiled by TypeSOL
|
|
5
|
+
can be made to do something its source and [the security model](security-model.md) say it cannot. Programs written
|
|
6
|
+
with TypeSOL are out of scope; their authors audit them.
|
|
7
|
+
|
|
8
|
+
## Scope
|
|
9
|
+
|
|
10
|
+
In order of priority. Line counts at 0.25.0.
|
|
11
|
+
|
|
12
|
+
| Priority | Component | Files | Lines | Why |
|
|
13
|
+
| --- | --- | --- | ---: | --- |
|
|
14
|
+
| 1 | C runtime | `runtime/typesol_rt.h` | 1,740 | In every program: input parsing, account checks, checked arithmetic, PDAs, system and token CPIs, account creation, resizing and closing, oracles, signature checks, the heap |
|
|
15
|
+
| 1 | Instruction prologue and constraints | `src/compiler/emit/function.ts` | 584 | Which checks each account parameter gets, and their order |
|
|
16
|
+
| 1 | Account data, Vecs and assignment | `src/compiler/emit/storage.ts`, `statements.ts`, `base.ts` | 2,296 | Reads and writes of account data, growable Vec layout and resizing, references and aliasing, closure and helper expansion |
|
|
17
|
+
| 1 | Calls and CPIs | `src/compiler/emit/calls.ts`, `cpi.ts` | 4,276 | Built-ins, remaining accounts, `invoke` and its account flags, signer seeds, token and Metaplex CPIs, account creation |
|
|
18
|
+
| 1 | Expressions | `src/compiler/emit/expressions.ts`, `shared.ts`, `borsh.ts` | 1,512 | Checked arithmetic and casts, Borsh encoding of arguments and `@layout borsh` accounts |
|
|
19
|
+
| 2 | Front end | `src/compiler/analyze.ts`, `model.ts` | 2,202 | What the language accepts; constraints and account types as parsed from the source |
|
|
20
|
+
| 2 | Interface | `src/compiler/idl.ts`, `src/tools/clientgen.ts` | 2,211 | The IDL and the generated client must describe the program exactly (a mismatch can make clients sign the wrong thing) |
|
|
21
|
+
| 3 | Build and tooling | `src/compiler/toolchain.ts`, `src/cli.ts`, `src/workspace.ts`, `src/tools/` | | Build flags, deploy and verification commands |
|
|
22
|
+
|
|
23
|
+
The language surface is `lib/typesol.d.ts` (1,711 lines): every type and built-in a program can use, with its
|
|
24
|
+
documented behavior. [docs/language.md](language.md) is the reference; [docs/architecture.md](architecture.md) maps
|
|
25
|
+
the code, [docs/runtime.md](runtime.md) the runtime's conventions.
|
|
26
|
+
|
|
27
|
+
Out of scope: the Solana runtime, platform-tools (clang, lld), the TypeScript compiler, and programs written with
|
|
28
|
+
TypeSOL.
|
|
29
|
+
|
|
30
|
+
## What to check
|
|
31
|
+
|
|
32
|
+
The guarantees in [security-model.md](security-model.md), each of which should hold for every program the compiler
|
|
33
|
+
accepts. Areas where a bug would be most damaging, and where earlier internal reviews found the issues listed below:
|
|
34
|
+
|
|
35
|
+
- **Account checks**: every parameter type and constraint, also when an account only reaches a helper, a closure or a
|
|
36
|
+
CPI; optional accounts; the duplicate-writable check.
|
|
37
|
+
- **Aliasing**: two views of one account (parameters, remaining accounts, references held across a call), writes
|
|
38
|
+
through a place the right side moved.
|
|
39
|
+
- **Account data layout**: growable Vecs after the fixed fields, their lengths against the account size, resizing and
|
|
40
|
+
the Vecs that follow, `@layout borsh` accounts.
|
|
41
|
+
- **Arithmetic**: every operator and cast at every width, compound assignment, `mulDiv`, rent.
|
|
42
|
+
- **CPIs and signing**: signer-seed arrays (including their reuse across statements), account flags in `invoke`,
|
|
43
|
+
account creation on pre-funded addresses, closing.
|
|
44
|
+
- **The runtime's parsing**: instruction data, the serialized accounts, Token-2022 extension data, oracle accounts and
|
|
45
|
+
the instructions sysvar.
|
|
46
|
+
|
|
47
|
+
## Earlier reviews
|
|
48
|
+
|
|
49
|
+
Internal reviews, by the maintainers, in October 2026. Their findings and fixes are in the CHANGELOG:
|
|
50
|
+
|
|
51
|
+
- **0.25.0** (runtime and emitted checks): two Medium issues fixed, a remaining account loaded mutably twice through
|
|
52
|
+
the same slot, and a Vec element written after its right side resized the Vec; plus a token program ambiguity,
|
|
53
|
+
`mint::token_program`, oracle options and documentation of `requirePda`, Merkle leaves and `createIfNeeded`.
|
|
54
|
+
- **0.24.0** (compiler review): helper and closure expansion scoped to their own module, stale struct references
|
|
55
|
+
across Vec moves refused, account-shadowing parameters refused; client SDK hardening.
|
|
56
|
+
- Earlier releases: see `test/security/regressions.test.ts` and `test/security/hardening.test.ts`, which keep every
|
|
57
|
+
attack that once worked as a failing transaction.
|
|
58
|
+
|
|
59
|
+
## Evidence
|
|
60
|
+
|
|
61
|
+
- **Tests**: 570, by subject under `test/` (`docs/architecture.md`). `test/security/` holds the attacks.
|
|
62
|
+
- **Differential tests**: the same transactions to TypeSOL and to Anchor twins (`test/anchor-twins`), comparing
|
|
63
|
+
outcomes, account contents and balances after every step.
|
|
64
|
+
- **Model-based fuzzing** (`test/verification/`): arithmetic against BigInt at every width; honest and hostile action
|
|
65
|
+
sequences against state models with value conservation; account layout with two growable Vecs and aliasing through
|
|
66
|
+
remaining accounts. Each run prints its seed (`FUZZ_SEED`), `pnpm fuzz` runs them wide.
|
|
67
|
+
- **Ported programs**: all 46 programs of solana-developers/program-examples, run through their original tests' flows
|
|
68
|
+
and failure paths ([test/ports/README.md](../test/ports/README.md)).
|
|
69
|
+
- **Four-way comparison**: [typesol/benchmarks](https://github.com/typesol/benchmarks) runs 17 of those programs in
|
|
70
|
+
TypeSOL, Anchor, native Rust and Pinocchio, requires the same outcome, and records how each refuses the hostile calls
|
|
71
|
+
and with which error.
|
|
72
|
+
|
|
73
|
+
## Running it
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
git clone https://github.com/typesol/typesol && cd typesol
|
|
77
|
+
pnpm install && pnpm build
|
|
78
|
+
pnpm fetch:programs # Metaplex binaries for the token-metadata, Core and Bubblegum tests
|
|
79
|
+
pnpm build:twins # the Anchor twins (Rust + Anchor 1.0.2)
|
|
80
|
+
pnpm test # everything; CI runs it on Ubuntu (Node 22, 24) per change, macOS daily
|
|
81
|
+
pnpm fuzz # the randomized checks, wide
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`node dist/cli.js build <program.ts> --out <dir>` writes the generated C next to the binary, which is the most direct
|
|
85
|
+
way to review what a construct compiles to; `test/golden/counter.c` is a pinned example.
|
|
86
|
+
|
|
87
|
+
## Contact
|
|
88
|
+
|
|
89
|
+
Coordinate through a private advisory on https://github.com/typesol/typesol/security, or report findings the same way
|
|
90
|
+
(SECURITY.md).
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Case study: porting Anchor's escrow
|
|
2
|
+
|
|
3
|
+
This page follows one complete Anchor program into TypeSOL: the token escrow from
|
|
4
|
+
[solana-developers/program-examples](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow/anchor)
|
|
5
|
+
(`tokens/escrow`, commit `de09019`). It shows the original and the port side by side, what changed, how the two were
|
|
6
|
+
checked against each other, where they still differ, and what the port costs to run. The port is
|
|
7
|
+
[`test/ports/tokens/escrow.ts`](../test/ports/tokens/escrow.ts); [Migrate from Anchor](migrate-from-anchor.md) is the
|
|
8
|
+
general reference.
|
|
9
|
+
|
|
10
|
+
## The program
|
|
11
|
+
|
|
12
|
+
A maker offers an amount of token A for an amount of token B.
|
|
13
|
+
|
|
14
|
+
- `make_offer(id, token_a_offered_amount, token_b_wanted_amount)` creates the offer account, a PDA at
|
|
15
|
+
`["offer", maker, id]`, creates the vault (the offer's associated token account for mint A) and moves the offered
|
|
16
|
+
tokens into it.
|
|
17
|
+
- `take_offer()` sends token B from the taker to the maker, releases the vault to the taker, closes the vault and the
|
|
18
|
+
offer, and returns their rent to the maker. It creates the taker's mint A account and the maker's mint B account if
|
|
19
|
+
they do not exist.
|
|
20
|
+
- `refund_offer()` returns the vault to the maker and closes both accounts.
|
|
21
|
+
|
|
22
|
+
Upstream, this is 398 lines of Rust in ten files. The port is one 95-line TypeScript file.
|
|
23
|
+
|
|
24
|
+
## Side by side
|
|
25
|
+
|
|
26
|
+
### State
|
|
27
|
+
|
|
28
|
+
```rust
|
|
29
|
+
#[account]
|
|
30
|
+
#[derive(InitSpace)]
|
|
31
|
+
pub struct Offer {
|
|
32
|
+
pub id: u64,
|
|
33
|
+
pub maker: Pubkey,
|
|
34
|
+
pub token_mint_a: Pubkey,
|
|
35
|
+
pub token_mint_b: Pubkey,
|
|
36
|
+
pub token_b_wanted_amount: u64,
|
|
37
|
+
pub bump: u8,
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export interface Offer {
|
|
43
|
+
id: u64;
|
|
44
|
+
maker: Pubkey;
|
|
45
|
+
tokenMintA: Pubkey;
|
|
46
|
+
tokenMintB: Pubkey;
|
|
47
|
+
tokenBWantedAmount: u64;
|
|
48
|
+
bump: u8;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The account has the same discriminator and the same Borsh bytes: an Anchor client decodes a TypeSOL offer unchanged.
|
|
53
|
+
Field names are camelCase in TypeScript and snake_case in the IDL, as Anchor writes them.
|
|
54
|
+
|
|
55
|
+
### make_offer
|
|
56
|
+
|
|
57
|
+
Anchor declares the accounts in a struct with constraint attributes, then splits the work into helper functions:
|
|
58
|
+
|
|
59
|
+
```rust
|
|
60
|
+
#[derive(Accounts)]
|
|
61
|
+
#[instruction(id: u64)]
|
|
62
|
+
pub struct MakeOffer<'info> {
|
|
63
|
+
#[account(mut)]
|
|
64
|
+
pub maker: Signer<'info>,
|
|
65
|
+
#[account(mint::token_program = token_program)]
|
|
66
|
+
pub token_mint_a: InterfaceAccount<'info, Mint>,
|
|
67
|
+
#[account(mint::token_program = token_program)]
|
|
68
|
+
pub token_mint_b: InterfaceAccount<'info, Mint>,
|
|
69
|
+
#[account(mut, associated_token::mint = token_mint_a, associated_token::authority = maker,
|
|
70
|
+
associated_token::token_program = token_program)]
|
|
71
|
+
pub maker_token_account_a: InterfaceAccount<'info, TokenAccount>,
|
|
72
|
+
#[account(init, payer = maker, space = ANCHOR_DISCRIMINATOR + Offer::INIT_SPACE,
|
|
73
|
+
seeds = [b"offer", maker.key().as_ref(), id.to_le_bytes().as_ref()], bump)]
|
|
74
|
+
pub offer: Account<'info, Offer>,
|
|
75
|
+
#[account(init, payer = maker, associated_token::mint = token_mint_a, associated_token::authority = offer,
|
|
76
|
+
associated_token::token_program = token_program)]
|
|
77
|
+
pub vault: InterfaceAccount<'info, TokenAccount>,
|
|
78
|
+
pub associated_token_program: Program<'info, AssociatedToken>,
|
|
79
|
+
pub token_program: Interface<'info, TokenInterface>,
|
|
80
|
+
pub system_program: Program<'info, System>,
|
|
81
|
+
}
|
|
82
|
+
// + send_offered_tokens_to_vault (a TransferChecked CPI through a shared transfer_tokens helper)
|
|
83
|
+
// + save_offer (offer.set_inner(Offer { … bump: context.bumps.offer }))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In TypeSOL the accounts are the function's parameters, in the same order, and each constraint is part of the
|
|
87
|
+
parameter's type:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export function makeOffer(
|
|
91
|
+
maker: Mut<Signer>,
|
|
92
|
+
tokenMintA: MintOf<Mint, never, never, "tokenProgram">,
|
|
93
|
+
tokenMintB: MintOf<Mint, never, never, "tokenProgram">,
|
|
94
|
+
makerTokenAccountA: Ata<Mut<TokenAccount>, "maker", "tokenMintA">,
|
|
95
|
+
offer: Init<Offer>,
|
|
96
|
+
vault: Ata<Init<TokenAccount>, "offer", "tokenMintA">,
|
|
97
|
+
associatedTokenProgram: AssociatedTokenProgram,
|
|
98
|
+
tokenProgram: TokenInterface,
|
|
99
|
+
systemProgram: SystemProgram,
|
|
100
|
+
id: u64,
|
|
101
|
+
tokenAOfferedAmount: u64,
|
|
102
|
+
tokenBWantedAmount: u64,
|
|
103
|
+
): void {
|
|
104
|
+
offer.bump = offer.create(maker, ["offer", maker.key, id]);
|
|
105
|
+
vault.create(maker);
|
|
106
|
+
tokenProgram.transferChecked(makerTokenAccountA, tokenMintA, vault, maker, tokenAOfferedAmount);
|
|
107
|
+
offer.id = id;
|
|
108
|
+
offer.maker = maker.key;
|
|
109
|
+
offer.tokenMintA = tokenMintA.key;
|
|
110
|
+
offer.tokenMintB = tokenMintB.key;
|
|
111
|
+
offer.tokenBWantedAmount = tokenBWantedAmount;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`MintOf<Mint, never, never, "tokenProgram">` is `mint::token_program = token_program`: the mint must be owned by
|
|
116
|
+
the token program passed (2022 otherwise). `offer.create` finds the canonical bump, creates the PDA with the computed space and returns the bump, which Anchor
|
|
117
|
+
exposes as `context.bumps.offer`. `transferChecked` reads the mint's decimals itself.
|
|
118
|
+
|
|
119
|
+
### take_offer and refund_offer
|
|
120
|
+
|
|
121
|
+
The offer is checked declaratively, as in Anchor: the seeds come from the maker account and the offer's own `id`
|
|
122
|
+
field, and the stored bump is used.
|
|
123
|
+
|
|
124
|
+
```rust
|
|
125
|
+
#[account(mut, close = maker, has_one = maker, has_one = token_mint_a, has_one = token_mint_b,
|
|
126
|
+
seeds = [b"offer", maker.key().as_ref(), offer.id.to_le_bytes().as_ref()], bump = offer.bump)]
|
|
127
|
+
offer: Account<'info, Offer>,
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
offer: Seeds<CloseTo<HasOne<Mut<Offer>, "maker" | "tokenMintA" | "tokenMintB">, "maker">, ["offer", "@maker", "@offer.id"], "bump">,
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The body signs for the vault with the offer's seeds, where Anchor builds a `signer_seeds` array and a
|
|
135
|
+
`CpiContext::new_with_signer` for each call:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
takerTokenAccountA.createIfNeeded(taker);
|
|
139
|
+
makerTokenAccountB.createIfNeeded(taker);
|
|
140
|
+
tokenProgram.transferChecked(takerTokenAccountB, tokenMintB, makerTokenAccountB, taker, offer.tokenBWantedAmount);
|
|
141
|
+
tokenProgram.transferChecked(vault, tokenMintA, takerTokenAccountA, offer, vault.amount, ["offer", maker.key, offer.id, offer.bump]);
|
|
142
|
+
tokenProgram.closeAccount(vault, maker, offer, ["offer", maker.key, offer.id, offer.bump]);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`CloseTo<…, "maker">` closes the offer after the body returns, as Anchor's `close = maker` does.
|
|
146
|
+
|
|
147
|
+
## What changed
|
|
148
|
+
|
|
149
|
+
| Anchor | Port |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `Signer`, `SystemAccount` (`#[account(mut)]`) | `Mut<Signer>`, `Mut<SystemAccount>` |
|
|
152
|
+
| `InterfaceAccount<Mint>`, `InterfaceAccount<TokenAccount>` | `Mint`, `TokenAccount` |
|
|
153
|
+
| `mint::token_program = token_program` | `MintOf<Mint, never, never, "tokenProgram">` |
|
|
154
|
+
| `associated_token::mint = m, associated_token::authority = a` | `Ata<TokenAccount, "a", "m">` |
|
|
155
|
+
| `init` / `init_if_needed` on an associated token account | `Ata<Init<…>>` + `vault.create(payer)` / `Ata<InitIfNeeded<…>>` + `createIfNeeded(payer)` |
|
|
156
|
+
| `init, seeds, bump` + `context.bumps.offer` | `Init<Offer>` + `offer.bump = offer.create(maker, seeds)` |
|
|
157
|
+
| `seeds = […], bump = offer.bump` | `Seeds<…, ["offer", "@maker", "@offer.id"], "bump">` |
|
|
158
|
+
| `has_one`, `close` | `HasOne<…>`, `CloseTo<…>` |
|
|
159
|
+
| `transfer_checked(CpiContext::new_with_signer(…), amount, decimals)` | `tokenProgram.transferChecked(from, mint, to, authority, amount, seeds)` |
|
|
160
|
+
| `ErrorCode::CustomError` (unused upstream) | `enum ErrorCode`, the same code 6000 |
|
|
161
|
+
|
|
162
|
+
Nothing in the program needed a construct TypeSOL lacks, and every account constraint has its TypeSOL form; the port
|
|
163
|
+
is marked PORTED in [the ports table](../test/ports/README.md).
|
|
164
|
+
|
|
165
|
+
## Same inputs, same results
|
|
166
|
+
|
|
167
|
+
Two independent checks run the original and the port against each other.
|
|
168
|
+
|
|
169
|
+
- **The port's own test** ([`test/porting/ports-tokens.test.ts`](../test/porting/ports-tokens.test.ts)) runs the upstream flow with SPL
|
|
170
|
+
Token mints and again with Token-2022 mints: make, take (creating the taker's and the maker's missing token accounts),
|
|
171
|
+
make again, a refund attempt by someone other than the maker, and the refund. It checks balances, the decoded offer
|
|
172
|
+
and that offer and vault are closed.
|
|
173
|
+
- **The four-way comparison** in [typesol/benchmarks](https://github.com/typesol/benchmarks) builds the upstream Anchor,
|
|
174
|
+
native and Pinocchio versions unmodified and sends all four the same 21 offers (ten taken, ten refunded, one left
|
|
175
|
+
open). The decoded open offer, every token balance, the vault's authority and which accounts were closed must be
|
|
176
|
+
identical before any number is reported. They are.
|
|
177
|
+
|
|
178
|
+
Both implementations refuse the hostile calls the comparison makes:
|
|
179
|
+
|
|
180
|
+
| Hostile call | Anchor | TypeSOL |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| refund_offer signed by someone other than the maker | 2006 ConstraintSeeds | 2006 ConstraintSeeds |
|
|
183
|
+
| take_offer naming the taker as the maker | 2015 ConstraintTokenOwner | 2040 ConstraintDuplicateMutableAccount |
|
|
184
|
+
|
|
185
|
+
TypeSOL checks each account's constraints in Anchor's order, so a refused transaction usually gets the same error
|
|
186
|
+
code (see [which error a transaction gets](errors.md#which-error-a-transaction-gets)). The second call differs on
|
|
187
|
+
purpose: taker and maker are then the same writable account, which TypeSOL refuses for any two writable parameters,
|
|
188
|
+
where Anchor only compares program and token accounts and fails later, on the maker's token account. A client that
|
|
189
|
+
matches on this code needs to accept either.
|
|
190
|
+
|
|
191
|
+
## Cost
|
|
192
|
+
|
|
193
|
+
Mean compute units per instruction in LiteSVM (21 make_offer, 10 take_offer and 10 refund_offer calls), and the
|
|
194
|
+
program size, from typesol/benchmarks (TypeSOL at this version; Anchor 1.0.2). Lower is better.
|
|
195
|
+
|
|
196
|
+
| | TypeSOL | Anchor | Native Rust | Pinocchio |
|
|
197
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
198
|
+
| make_offer | **19,697** | 35,726 | 33,206 | 20,581 |
|
|
199
|
+
| take_offer | **7,053** | 29,235 | 24,995 | 21,606 |
|
|
200
|
+
| refund_offer | **4,271** | 15,326 | 12,477 | 8,137 |
|
|
201
|
+
| Binary | 32.0 KB | 244.5 KB | 120.4 KB | **25.1 KB** |
|
|
202
|
+
|
|
203
|
+
Most of `make_offer` in every version is the Associated Token Account program creating the vault. The native and
|
|
204
|
+
Pinocchio versions are teaching code with fewer checks (no owner or type check on the offer, `Transfer` instead of
|
|
205
|
+
`TransferChecked`; Pinocchio takes the offer's bump from the client).
|
|
206
|
+
|
|
207
|
+
The same program, built with an earlier TypeSOL, runs on mainnet next to the other three; the transactions are listed
|
|
208
|
+
with their Solscan links in the [performance report](https://typesol.com/report/) and the
|
|
209
|
+
[README](../README.md#on-mainnet).
|
|
210
|
+
|
|
211
|
+
## Porting your own program
|
|
212
|
+
|
|
213
|
+
The steps this port followed, in order:
|
|
214
|
+
|
|
215
|
+
1. Generate the skeleton from the Anchor IDL: `npx typesol port target/idl/escrow.json --out programs/escrow.ts`. It
|
|
216
|
+
keeps instruction names, discriminators, account order and error codes.
|
|
217
|
+
2. Turn each `#[account(...)]` into the parameter's type, using the table above and
|
|
218
|
+
[Accounts and constraints](migrate-from-anchor.md#accounts-and-constraints). Keep the account order: it is the
|
|
219
|
+
wire format.
|
|
220
|
+
3. Port the bodies. Helpers like `transfer_tokens` usually disappear into one call; `signer_seeds` arrays become the
|
|
221
|
+
last argument of the CPI.
|
|
222
|
+
4. Run the upstream test flow against the port with `typesol/testing`, including the failure paths, and compare the
|
|
223
|
+
decoded accounts.
|
|
224
|
+
5. Compare the IDLs, and check the error codes your clients match on (the table above shows how they can differ).
|