@typesol/cli 0.13.0 → 0.15.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 +34 -0
- package/README.md +33 -6
- package/dist/cli.js +14 -4
- package/dist/compiler/emit/calls.js +1 -1
- package/dist/compiler/emit/function.js +1 -1
- package/dist/idlpublish.js +43 -9
- package/dist/templates.d.ts +9 -0
- package/dist/templates.js +429 -0
- package/dist/workspace.d.ts +3 -2
- package/dist/workspace.js +7 -105
- package/docs/cli.md +1 -1
- package/docs/mainnet.md +99 -0
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -1
- package/runtime/typesol_rt.h +57 -8
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ until 1.0, a minor version may change the language, the IDL layout or error code
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.15.0 — 2026-10-27
|
|
9
|
+
|
|
10
|
+
Project templates for `typesol init`, a mainnet deployment guide, and clearer `typesol idl publish` failures. No
|
|
11
|
+
language, IDL or error-code changes. Not externally audited (see SECURITY.md).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `typesol init --template counter|vault|escrow`: start from a PDA counter (the default, as before), a SOL vault or an
|
|
15
|
+
SPL token escrow. Each template is the matching `examples/` program with a LiteSVM test of its main flow and of a
|
|
16
|
+
refused call; the escrow project also gets `@solana-program/token`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `typesol idl publish` names the reason a write failed (the transaction's error and its last log lines) instead of
|
|
20
|
+
"The provided transaction plan failed to execute".
|
|
21
|
+
|
|
22
|
+
### Docs
|
|
23
|
+
- docs/mainnet.md: from `typesol init` to a verified program on mainnet-beta (wallet, cost, deploy, IDL, `verify`,
|
|
24
|
+
freezing and closing).
|
|
25
|
+
- README: the official escrow on mainnet-beta in all four versions, with each transaction's compute units linked to
|
|
26
|
+
Solscan; the TypeSOL program's IDL is published on chain.
|
|
27
|
+
|
|
28
|
+
## 0.14.0 — 2026-10-27
|
|
29
|
+
|
|
30
|
+
PDAs are derived without the PDA syscalls, so instructions that create, check or search for PDAs use up to a few
|
|
31
|
+
thousand fewer compute units. No language, IDL or error-code changes. Not externally audited (see SECURITY.md).
|
|
32
|
+
|
|
33
|
+
### Performance
|
|
34
|
+
- PDAs are derived with the `sol_sha256` and `sol_curve_validate_point` syscalls instead of
|
|
35
|
+
`sol_try_find_program_address` / `sol_create_program_address` (1,500 CU per bump tried): the
|
|
36
|
+
same addresses, bumps and limits (16 seeds to check, 15 to search, 32 bytes each). `init` with seeds, `Seeds<…>`,
|
|
37
|
+
`create`, `signer` and `findProgramAddress` all use it. Benchmarks: counter.initialize 3,030 → 1,872 CU,
|
|
38
|
+
escrow.make 5,974 → 3,638; in the official examples escrow take_offer 14,519 → 7,117 and close_user 1,728 → 586.
|
|
39
|
+
Binaries grow 0.4–1.2 KB. New tests compare it with @solana/kit's derivation over random seeds, canonical bumps
|
|
40
|
+
below 255 and on-curve bumps. Needs the curve25519 syscalls (active on mainnet-beta since epoch 637).
|
|
41
|
+
|
|
8
42
|
## 0.13.0 — 2026-10-27
|
|
9
43
|
|
|
10
44
|
Every program in solana-developers/program-examples now ports to TypeSOL: PDA seeds from account fields, remaining
|
package/README.md
CHANGED
|
@@ -152,6 +152,9 @@ npx typesol build # target/counter/{counter.so, coun
|
|
|
152
152
|
npx typesol deploy --url http://127.0.0.1:8899 --keypair ~/.config/solana/id.json
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
+
`--template vault` or `--template escrow` starts from a SOL vault or an SPL token escrow instead of the counter, each
|
|
156
|
+
with a passing LiteSVM test.
|
|
157
|
+
|
|
155
158
|
The npm package is [`@typesol/cli`](https://www.npmjs.com/package/@typesol/cli). Projects install it under the name
|
|
156
159
|
`typesol`, which is what programs import (`import { … } from "typesol"`); `typesol init` sets that up. To add it to an
|
|
157
160
|
existing project: `npm install -D typesol@npm:@typesol/cli`.
|
|
@@ -159,6 +162,8 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
|
|
|
159
162
|
Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
|
|
160
163
|
program's address.
|
|
161
164
|
|
|
165
|
+
To go to mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
|
|
166
|
+
|
|
162
167
|
## How it works
|
|
163
168
|
|
|
164
169
|
```
|
|
@@ -227,24 +232,46 @@ shows mean compute units per call (generated by `scripts/readme-bench.ts` from t
|
|
|
227
232
|
<!-- bench:table -->
|
|
228
233
|
| Instruction | TypeSOL | Pinocchio 0.10 | Native Rust | Anchor 1.0.2 |
|
|
229
234
|
| --- | ---: | ---: | ---: | ---: |
|
|
230
|
-
| counter.initialize | **
|
|
235
|
+
| counter.initialize | **1,872** | 3,186 | 4,601 | 6,573 |
|
|
231
236
|
| counter.increment | **110** | 113 | 857 | 2,323 |
|
|
232
|
-
| vault.open | **
|
|
237
|
+
| vault.open | **1,891** | 3,185 | 4,613 | 6,627 |
|
|
233
238
|
| vault.deposit | **1,314** | 1,377 | 2,832 | 4,355 |
|
|
234
239
|
| vault.withdraw | **315** | 370 | 1,176 | 3,628 |
|
|
235
240
|
| vault.shut | **161** | 167 | 950 | 2,024 |
|
|
236
|
-
| escrow.make | **
|
|
241
|
+
| escrow.make | **3,638** | 6,179 | 9,240 | 13,304 |
|
|
237
242
|
| escrow.take | **4,035** | 4,051 | 8,500 | 14,365 |
|
|
238
243
|
| escrow.refund | **2,716** | 2,769 | 5,695 | 8,449 |
|
|
239
|
-
| binary, counter / vault / escrow |
|
|
244
|
+
| binary, counter / vault / escrow | 13.2 / 16.5 / 24.0 KB | 19.6 / 22.9 / 29.4 KB | 27.9 / 35.0 / 46.5 KB | 145.2 / 158.9 / 190.7 KB |
|
|
240
245
|
<!-- /bench:table -->
|
|
241
246
|
|
|
242
|
-
`initialize`, `open` and `make` include the PDA bump search
|
|
247
|
+
`initialize`, `open` and `make` include the PDA bump search. TypeSOL hashes PDA candidates with the sha256 and
|
|
248
|
+
curve-check syscalls instead of the 1,500-CU PDA syscalls the others call, with the same results. The
|
|
243
249
|
escrow moves SPL tokens through CPIs (`take` makes three). Like Pinocchio, TypeSOL reads accounts in place and builds
|
|
244
250
|
CPI account records only for the accounts a call lists. It uses fewer compute units than Pinocchio on every
|
|
245
251
|
instruction here, while also refusing duplicate writable accounts as Anchor does. Fewer compute
|
|
246
252
|
units are not a safety claim. The differential tests also run the escrow against its Anchor twin ([docs/testing.md](docs/testing.md)).
|
|
247
253
|
|
|
254
|
+
### On mainnet
|
|
255
|
+
|
|
256
|
+
The official escrow ([tokens/escrow](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow)),
|
|
257
|
+
deployed to Solana mainnet-beta in all four versions on 2026-10-27 and called with the same mints, amounts and accounts,
|
|
258
|
+
one instruction per transaction. Each figure is the compute units of that transaction, linked to Solscan; the lowest per row is in bold. The deployed
|
|
259
|
+
programs match these builds byte for byte (`typesol verify` for TypeSOL), and the TypeSOL program's IDL is published
|
|
260
|
+
on chain with `typesol idl publish`.
|
|
261
|
+
|
|
262
|
+
| Transaction | TypeSOL | Pinocchio | Native Rust | Anchor |
|
|
263
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
264
|
+
| make_offer (offer 1) | [20,304](https://solscan.io/tx/32PVva77dT1rSWrMWX2sHTMPZiwztdEZYtpANaxrsuJxPcd3hnV6pDgJJaV4LkL3bX6ALDoHMNF8fjngtodYHBS1) | [**19,645**](https://solscan.io/tx/528BdMTrnLWauSXVh2vaFfy4XwKFpbyt95hQ6w3fwjtpkT85RPkRhGydunAizNDRqTTLPxcEVMrdcDvRer4H9LRE) | [30,698](https://solscan.io/tx/4MQgt75MvojaAkfxnvrE2yJWabGpE8bsZhNq2CeGJxHZg6uD6A64Kv71JmrsgiGnaEovvv8K34DYL49owLjhs4QJ) | [38,648](https://solscan.io/tx/9T1ZY9JaSMb1GMqv1i22AuZuJQN6B5XcgqeDv5tGqEm5Uox3MBTdXsECt59GwcXkFG9QKgXJyqndTEzpSXhbAVM) |
|
|
265
|
+
| take_offer (offer 1) | [**7,998**](https://solscan.io/tx/49EQwgnKShFdsAs37AEzYmv6tLY8DipcCjmRgqwj4fVTFEgVVWdEXw5AvuEufL5ymJYs4T4hVFdkENW9XK9een5m) | [25,206](https://solscan.io/tx/21MGkXpKRe4c7EeA1hC6JHevsk78xUdbcWmyqG47EbLeCNKSMTzTjkJQqtydnQmV8uHPmr6bJHnvQmmQTSZB7Am2) | [28,612](https://solscan.io/tx/B1hu1EL4Y5vWhBVWdyZEpzRNJb8kd6G9CgN7KtyNNGpQ84o9dfn1sdy9Z1UCHECNtp24Mw8bEH2iYz2YADdEUjx) | [32,835](https://solscan.io/tx/3ZLuQQz7Bx5YdgmFXpVQWHn5kyc8v39jr2BnfofH6sqenzfJpAdVm4h38uYcHwYygnfso92kJe7HRHeASsABoKPQ) |
|
|
266
|
+
| make_offer (offer 2) | [21,804](https://solscan.io/tx/xuu9KXbZvzGDpTEEcDE9LYvBqmPen81y6ty92e6crRxT8m4Mc4XyxhjWHbjVNsr9RE3QY4M1j1Sw9cH7PRMKKuc) | [**19,645**](https://solscan.io/tx/LLzS13AJrqACGoT1VHZN278ij39pNkttb1Z4Wh2AYXknWxwjaiaiMeD7WZomjP6TBv18RHfQHUfogQzApvXr2mC) | [35,198](https://solscan.io/tx/4xiKfzyccVatef1T52tZ3VAJwcx5R7iWgHWezBzZ3p2WpvoLzMp1PSUgr7x3sspE53mDE5JaghaNPKuUohVgPuwc) | [47,648](https://solscan.io/tx/4mxW8D5zpY8t4fL8JRbvhp6ejU6CUvB3iWmFjACipzexW6tZfFeatfnEnDTX3Y97BxsvAftoX9eQauiVu84xrLxh) |
|
|
267
|
+
| refund_offer (offer 2) | [**5,961**](https://solscan.io/tx/4kY7s3y95Lrd2WGCWGPWW3zFBVtEKH6EskdscuYmRCRRgPt2rJTZ1CmZMGUUkHs7aai9AygEiTSUG23EMUrFr7rg) | [13,387](https://solscan.io/tx/QUXWwdbKRcT6WhZvSe4U8nxr4GAMaPNV6pZ1fwrC28kRxdJm7HKXVr2DFRuKD9bEzoS6hFdiA2rLr1mQWyrN3oz) | [17,731](https://solscan.io/tx/5zj7E4c61j1JzUsUPC1fNs9ECFLVmCug9rsdYsVLn7YD1RSxdbAJUQDam1BX3epTzW6ed2uFGwTiNhwaYo7vqdwm) | [23,576](https://solscan.io/tx/5Kjwnq5tN8mzj5s67VGxSry4kRzZk6HLdgXW65FyTvpUeC1D7aQoDGTwzrA7kqyYEvV9y9znnZmLaqU98zFrJJo4) |
|
|
268
|
+
| program | [65ybkcg6…](https://solscan.io/account/65ybkcg6mQa9t5EuHtirRdvf2qAu12qRiK6wYBbGk16P) | [FrQwq3iU…](https://solscan.io/account/FrQwq3iUa8ZkWEne1y27webXTAyLgH4Hj6t986t6VS2G) | [5pnmi1Md…](https://solscan.io/account/5pnmi1MdxzXKmgX7ooRSE212rzZK8rb7nNpRTSMqP8V1) | [EG7M3sLL…](https://solscan.io/account/EG7M3sLLwB7MhmtzwYsaxbhZaFDbshL9hmVK79bQVyVq) |
|
|
269
|
+
|
|
270
|
+
Most of make_offer is the Associated Token Account program creating the vault (about 13,500 CU in every version); it
|
|
271
|
+
searches for the vault address's bump itself at 1,500 CU a try, which is why TypeSOL's offer 2 cost 1,500 more than
|
|
272
|
+
offer 1 (its own part was 6,719 CU both times). The Pinocchio version takes the offer PDA's bump from the client instead
|
|
273
|
+
of searching for it (its own part: 6,051 CU). Mainnet figures differ from the LiteSVM means above.
|
|
274
|
+
|
|
248
275
|
## How it is tested
|
|
249
276
|
|
|
250
277
|
- **Execution tests.** Every feature is compiled and run in LiteSVM.
|
|
@@ -275,7 +302,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
275
302
|
|
|
276
303
|
## Status
|
|
277
304
|
|
|
278
|
-
**TypeSOL is pre-1.0 (0.
|
|
305
|
+
**TypeSOL is pre-1.0 (0.15).** Releases follow the organization's
|
|
279
306
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
280
307
|
change.
|
|
281
308
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { generateClient, writeClient } from "./clientgen.js";
|
|
|
11
11
|
import { buildIdl } from "./compiler/idl.js";
|
|
12
12
|
import { decodeAddress, loadOrCreateKeypair, readKeypair } from "./keys.js";
|
|
13
13
|
import { formatProfile, profileLogs } from "./profile.js";
|
|
14
|
+
import { TEMPLATE_NAMES } from "./templates.js";
|
|
14
15
|
import { buildWorkspace, CONFIG, deployArgs, findWorkspace, programAddress, programForEntry, programPaths, scaffold } from "./workspace.js";
|
|
15
16
|
const ASSETS = fileURLToPath(new URL("../assets/", import.meta.url));
|
|
16
17
|
/** A usage problem: printed with the command's help, exit code 2. */
|
|
@@ -38,14 +39,23 @@ function banner() {
|
|
|
38
39
|
const SBPF = `--sbpf v0|v1|v2 SBPF version (default v0, or "sbpf" in ${CONFIG}); v1/v2 need clusters that enabled them`;
|
|
39
40
|
const COMMANDS = {
|
|
40
41
|
init: {
|
|
41
|
-
usage: "typesol init [<dir>] [--name <program>]",
|
|
42
|
-
help:
|
|
43
|
-
|
|
42
|
+
usage: "typesol init [<dir>] [--name <program>] [--template counter|vault|escrow]",
|
|
43
|
+
help: [
|
|
44
|
+
"Create a project: typesol.json, programs/<name>.ts, tests/<name>.test.ts (LiteSVM), package.json, tsconfig.json, .gitignore.",
|
|
45
|
+
"Refuses to overwrite files. <name> must be snake_case (default: derived from <dir>).",
|
|
46
|
+
"--template counter a PDA counter per user (default)",
|
|
47
|
+
"--template vault a SOL vault at a PDA: deposit, withdraw above rent, close",
|
|
48
|
+
"--template escrow an SPL token escrow: make, take, refund, with the offer PDA signing for its vault",
|
|
49
|
+
].join("\n "),
|
|
50
|
+
options: { name: { type: "string" }, template: { type: "string" } },
|
|
44
51
|
run(v, args) {
|
|
45
52
|
if (args.length > 1)
|
|
46
53
|
throw new UsageError("init takes at most one directory");
|
|
47
54
|
if (v.name !== undefined && !/^[a-z][a-z0-9_]*$/.test(v.name))
|
|
48
55
|
throw new UsageError(`--name "${v.name}" must be snake_case (a-z, 0-9, _; starting with a letter)`);
|
|
56
|
+
const template = v.template ?? "counter";
|
|
57
|
+
if (!TEMPLATE_NAMES.includes(template))
|
|
58
|
+
throw new UsageError(`--template "${template}" is not one of ${TEMPLATE_NAMES.join(", ")}`);
|
|
49
59
|
const root = path.resolve(args[0] ?? ".");
|
|
50
60
|
const derived = path
|
|
51
61
|
.basename(root)
|
|
@@ -53,7 +63,7 @@ const COMMANDS = {
|
|
|
53
63
|
.replace(/^_+|_+$/g, "")
|
|
54
64
|
.toLowerCase();
|
|
55
65
|
const name = v.name ?? (/^[a-z]/.test(derived) ? derived : `p_${derived || "program"}`);
|
|
56
|
-
for (const f of scaffold(root, name))
|
|
66
|
+
for (const f of scaffold(root, name, template))
|
|
57
67
|
log(` created ${path.join(root, f)}`);
|
|
58
68
|
log(`\n next: cd ${path.relative(process.cwd(), root) || "."} && npm install && npm test`);
|
|
59
69
|
return 0;
|
|
@@ -361,7 +361,7 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
361
361
|
const t = this.tmp("pda");
|
|
362
362
|
const b = this.tmp("bump");
|
|
363
363
|
this.line(`SolPubkey ${t}; uint8_t ${b};`);
|
|
364
|
-
this.line(`if (
|
|
364
|
+
this.line(`if (ts_find_pda(${seeds.name}, ${seeds.count}, ${program.c}, &${t}, &${b}) != SUCCESS) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: no program address for these seeds");`);
|
|
365
365
|
return { c: `(&${t})`, type: PUBKEY };
|
|
366
366
|
}
|
|
367
367
|
case "metadataAddress": {
|
|
@@ -444,7 +444,7 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
444
444
|
else {
|
|
445
445
|
bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
|
|
446
446
|
out.push(` { SolPubkey pda_${a.name};`);
|
|
447
|
-
out.push(` if (
|
|
447
|
+
out.push(` if (ts_find_pda(${seeds}, ${parts.length}, ${program}, &pda_${a.name}, &sb_${a.name}) != SUCCESS || !ts_key_eq(&pda_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: ${a.name} is not the expected PDA"); }`);
|
|
448
448
|
this.bumps.set(a.name, `sb_${a.name}`);
|
|
449
449
|
}
|
|
450
450
|
}
|
package/dist/idlpublish.js
CHANGED
|
@@ -62,17 +62,51 @@ export async function publishIdl(chain, opts) {
|
|
|
62
62
|
seeds: [getAddressEncoder().encode(opts.program)],
|
|
63
63
|
}))[0]
|
|
64
64
|
: undefined;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
try {
|
|
66
|
+
await writeMetadata(client, {
|
|
67
|
+
payer: opts.payer,
|
|
68
|
+
authority,
|
|
69
|
+
program: opts.program,
|
|
70
|
+
...(programData ? { programData } : {}),
|
|
71
|
+
seed: "idl",
|
|
72
|
+
format: Format.Json,
|
|
73
|
+
...packed,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
throw new Error(planFailure(e));
|
|
78
|
+
}
|
|
74
79
|
return metadataAddress(opts.program, canonical ? null : authority.address);
|
|
75
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The transaction planner reports a failed write as "The provided transaction plan failed to execute" and keeps the
|
|
83
|
+
* reason inside: name the first failed transaction's own error instead.
|
|
84
|
+
*/
|
|
85
|
+
function planFailure(e) {
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
const messages = [];
|
|
88
|
+
let logs = [];
|
|
89
|
+
const walk = (v, depth) => {
|
|
90
|
+
if (!v || typeof v !== "object" || seen.has(v) || depth > 8)
|
|
91
|
+
return;
|
|
92
|
+
seen.add(v);
|
|
93
|
+
const o = v;
|
|
94
|
+
if (Array.isArray(o.logs) && !logs.length)
|
|
95
|
+
logs = o.logs.map(String);
|
|
96
|
+
if (v instanceof Error && v.message && !/transaction plan failed to execute/.test(v.message))
|
|
97
|
+
messages.push(v.message);
|
|
98
|
+
for (const k of ["transactionPlanResult", "plans", "error", "cause", "context"]) {
|
|
99
|
+
const c = o[k];
|
|
100
|
+
if (Array.isArray(c))
|
|
101
|
+
c.forEach((x) => walk(x, depth + 1));
|
|
102
|
+
else
|
|
103
|
+
walk(c, depth + 1);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
walk(e, 0);
|
|
107
|
+
const tail = logs.length ? `\n ${logs.slice(-6).join("\n ")}` : "";
|
|
108
|
+
return messages.length ? `IDL write failed: ${messages[0]}${tail}` : e instanceof Error ? e.message : String(e);
|
|
109
|
+
}
|
|
76
110
|
export async function metadataAddress(program, authority) {
|
|
77
111
|
return (await findMetadataPda({ program, authority, seed: "idl" }))[0];
|
|
78
112
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const TEMPLATE_NAMES: readonly ["counter", "vault", "escrow"];
|
|
2
|
+
export type TemplateName = (typeof TEMPLATE_NAMES)[number];
|
|
3
|
+
export interface Template {
|
|
4
|
+
program: string;
|
|
5
|
+
test: (name: string) => string;
|
|
6
|
+
/** devDependencies beyond the ones every project gets. */
|
|
7
|
+
devDependencies?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const TEMPLATES: Record<TemplateName, Template>;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// Programs and tests that `typesol init --template <name>` writes. Each test runs its program in LiteSVM and covers its
|
|
2
|
+
// main flow and at least one refused call, so a new project starts from something that passes and shows how to test.
|
|
3
|
+
// The programs are copies of examples/ (test/workspace.test.ts checks they still match).
|
|
4
|
+
export const TEMPLATE_NAMES = ["counter", "vault", "escrow"];
|
|
5
|
+
/** The part every test shares: build the project's program into a LiteSVM, encode instructions with its IDL, send. */
|
|
6
|
+
const testFile = (name, o) => `// Runs the program in LiteSVM (an in-process Solana runtime): \`npm test\`.
|
|
7
|
+
import { BN, BorshCoder, type Idl } from "@anchor-lang/core";
|
|
8
|
+
${o.imports}import {
|
|
9
|
+
${[
|
|
10
|
+
"AccountRole",
|
|
11
|
+
"appendTransactionMessageInstructions",
|
|
12
|
+
"createTransactionMessage",
|
|
13
|
+
...o.kit,
|
|
14
|
+
"pipe",
|
|
15
|
+
"setTransactionMessageFeePayerSigner",
|
|
16
|
+
"signTransactionMessageWithSigners",
|
|
17
|
+
"type Address",
|
|
18
|
+
"type Instruction",
|
|
19
|
+
"type KeyPairSigner",
|
|
20
|
+
]
|
|
21
|
+
.map((x) => ` ${x},`)
|
|
22
|
+
.join("\n")}
|
|
23
|
+
} from "@solana/kit";
|
|
24
|
+
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { buildWorkspace, findWorkspace } from "typesol/workspace";
|
|
28
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
29
|
+
|
|
30
|
+
const SYSTEM = "11111111111111111111111111111111" as Address;
|
|
31
|
+
const ws = findWorkspace(path.dirname(fileURLToPath(import.meta.url)))!;
|
|
32
|
+
|
|
33
|
+
describe("${name}", () => {
|
|
34
|
+
const svm = new LiteSVM();
|
|
35
|
+
let programId: Address;
|
|
36
|
+
let coder: BorshCoder;
|
|
37
|
+
beforeAll(() => {
|
|
38
|
+
const [built] = buildWorkspace(ws, ["${name}"]);
|
|
39
|
+
programId = built.address as Address;
|
|
40
|
+
svm.addProgramFromFile(programId, built.result.soPath);
|
|
41
|
+
coder = new BorshCoder(built.result.idl as unknown as Idl);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const ix = (name: string, args: object, accounts: { address: Address; signer?: KeyPairSigner; writable?: boolean }[]): Instruction => ({
|
|
45
|
+
programAddress: programId,
|
|
46
|
+
accounts: accounts.map((a) => ({
|
|
47
|
+
address: a.address,
|
|
48
|
+
role: a.signer ? (a.writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER) : a.writable ? AccountRole.WRITABLE : AccountRole.READONLY,
|
|
49
|
+
...(a.signer ? { signer: a.signer } : {}),
|
|
50
|
+
})) as Instruction["accounts"],
|
|
51
|
+
data: new Uint8Array(coder.instruction.encode(name, args)),
|
|
52
|
+
});
|
|
53
|
+
const send = async (payer: KeyPairSigner, ixs: Instruction[]) => {
|
|
54
|
+
const tx = await pipe(
|
|
55
|
+
createTransactionMessage({ version: 0 }),
|
|
56
|
+
(t) => setTransactionMessageFeePayerSigner(payer, t),
|
|
57
|
+
(t) => svm.setTransactionMessageLifetimeUsingLatestBlockhash(t),
|
|
58
|
+
(t) => appendTransactionMessageInstructions(ixs, t),
|
|
59
|
+
(t) => signTransactionMessageWithSigners(t),
|
|
60
|
+
);
|
|
61
|
+
const res = svm.sendTransaction(tx);
|
|
62
|
+
svm.expireBlockhash();
|
|
63
|
+
return res;
|
|
64
|
+
};
|
|
65
|
+
/** The custom error code of a failed transaction: 6000 + the member's index in the program's error enum. */
|
|
66
|
+
const errorCode = (res: TransactionMetadata | FailedTransactionMetadata) => {
|
|
67
|
+
if (!(res instanceof FailedTransactionMetadata)) throw new Error("expected the transaction to fail");
|
|
68
|
+
return Number(/Custom\\D*(\\d+)/.exec(String(res.err()))?.[1]);
|
|
69
|
+
};
|
|
70
|
+
${o.body}});
|
|
71
|
+
`;
|
|
72
|
+
export const TEMPLATES = {
|
|
73
|
+
counter: {
|
|
74
|
+
program: `import { type Account, type Init, type Mut, type Pubkey, type Signer, type SystemProgram, type u8, type u64, require } from "typesol";
|
|
75
|
+
|
|
76
|
+
export interface Counter {
|
|
77
|
+
authority: Pubkey;
|
|
78
|
+
count: u64;
|
|
79
|
+
bump: u8;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export enum CounterError {
|
|
83
|
+
Unauthorized = "Only the counter's authority can change it",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Create the caller's counter at PDA ["counter", authority]. */
|
|
87
|
+
export function initialize(authority: Mut<Signer>, counter: Init<Counter>, systemProgram: SystemProgram): void {
|
|
88
|
+
counter.bump = counter.create(authority, ["counter", authority.key]);
|
|
89
|
+
counter.authority = authority.key;
|
|
90
|
+
counter.count = 0n;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function increment(authority: Signer, counter: Mut<Counter>, by: u64): void {
|
|
94
|
+
require(counter.authority === authority.key, CounterError.Unauthorized);
|
|
95
|
+
counter.count += by;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Read-only check that callers can simulate. */
|
|
99
|
+
export function assertAtLeast(counter: Account<Counter>, min: u64): void {
|
|
100
|
+
require(counter.count >= min, "counter is below the expected minimum");
|
|
101
|
+
}
|
|
102
|
+
`,
|
|
103
|
+
test: (name) => testFile(name, {
|
|
104
|
+
imports: "",
|
|
105
|
+
kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
|
|
106
|
+
body: `
|
|
107
|
+
it("initializes and increments the caller's counter; refuses anyone else", async () => {
|
|
108
|
+
const alice = await generateKeyPairSigner();
|
|
109
|
+
const mallory = await generateKeyPairSigner();
|
|
110
|
+
svm.airdrop(alice.address, lamports(1_000_000_000n));
|
|
111
|
+
svm.airdrop(mallory.address, lamports(1_000_000_000n));
|
|
112
|
+
const [counter] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["counter", getAddressEncoder().encode(alice.address)] });
|
|
113
|
+
const me = { address: alice.address, signer: alice, writable: true };
|
|
114
|
+
expect(await send(alice, [ix("initialize", {}, [me, { address: counter, writable: true }, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
|
|
115
|
+
expect(await send(alice, [ix("increment", { by: new BN(5) }, [{ address: alice.address, signer: alice }, { address: counter, writable: true }])])).toBeInstanceOf(TransactionMetadata);
|
|
116
|
+
const account = svm.getAccount(counter);
|
|
117
|
+
if (!account.exists) throw new Error("counter was not created");
|
|
118
|
+
expect(coder.accounts.decode("Counter", Buffer.from(account.data)).count.toString()).toBe("5");
|
|
119
|
+
|
|
120
|
+
// CounterError.Unauthorized is the enum's first member: 6000.
|
|
121
|
+
const stranger = ix("increment", { by: new BN(1) }, [{ address: mallory.address, signer: mallory }, { address: counter, writable: true }]);
|
|
122
|
+
expect(errorCode(await send(mallory, [stranger]))).toBe(6000);
|
|
123
|
+
});
|
|
124
|
+
`,
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
vault: {
|
|
128
|
+
program: `import {
|
|
129
|
+
type Init,
|
|
130
|
+
type Mut,
|
|
131
|
+
type Pubkey,
|
|
132
|
+
type Signer,
|
|
133
|
+
type SystemProgram,
|
|
134
|
+
type u8,
|
|
135
|
+
type u64,
|
|
136
|
+
close,
|
|
137
|
+
minimumBalance,
|
|
138
|
+
require,
|
|
139
|
+
} from "typesol";
|
|
140
|
+
|
|
141
|
+
/** A personal SOL vault at PDA ["vault", owner]. Lamports above rent are the balance. */
|
|
142
|
+
export interface Vault {
|
|
143
|
+
owner: Pubkey;
|
|
144
|
+
deposited: u64;
|
|
145
|
+
withdrawn: u64;
|
|
146
|
+
bump: u8;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export enum VaultError {
|
|
150
|
+
NotOwner = "Only the vault owner can do this",
|
|
151
|
+
ZeroAmount = "Amount must be greater than zero",
|
|
152
|
+
InsufficientFunds = "Withdrawal would dip below the rent-exempt minimum",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function open(owner: Mut<Signer>, vault: Init<Vault>, systemProgram: SystemProgram): void {
|
|
156
|
+
vault.bump = vault.create(owner, ["vault", owner.key]);
|
|
157
|
+
vault.owner = owner.key;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function deposit(owner: Mut<Signer>, vault: Mut<Vault>, systemProgram: SystemProgram, amount: u64): void {
|
|
161
|
+
require(amount > 0n, VaultError.ZeroAmount);
|
|
162
|
+
require(vault.owner === owner.key, VaultError.NotOwner);
|
|
163
|
+
systemProgram.transfer(owner, vault, amount);
|
|
164
|
+
vault.deposited += amount;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function withdraw(owner: Mut<Signer>, vault: Mut<Vault>, amount: u64): void {
|
|
168
|
+
require(amount > 0n, VaultError.ZeroAmount);
|
|
169
|
+
require(vault.owner === owner.key, VaultError.NotOwner);
|
|
170
|
+
const floor = minimumBalance(vault);
|
|
171
|
+
require(vault.lamports - amount >= floor, VaultError.InsufficientFunds);
|
|
172
|
+
// The vault is owned by this program, so it can move lamports directly.
|
|
173
|
+
vault.lamports -= amount;
|
|
174
|
+
owner.lamports += amount;
|
|
175
|
+
vault.withdrawn += amount;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Close the vault and return everything, rent included, to the owner. */
|
|
179
|
+
export function shut(owner: Mut<Signer>, vault: Mut<Vault>): void {
|
|
180
|
+
require(vault.owner === owner.key, VaultError.NotOwner);
|
|
181
|
+
close(vault, owner);
|
|
182
|
+
}
|
|
183
|
+
`,
|
|
184
|
+
test: (name) => testFile(name, {
|
|
185
|
+
imports: "",
|
|
186
|
+
kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
|
|
187
|
+
body: `
|
|
188
|
+
it("deposits and withdraws SOL, keeps the rent reserve, then closes", async () => {
|
|
189
|
+
const owner = await generateKeyPairSigner();
|
|
190
|
+
svm.airdrop(owner.address, lamports(10_000_000_000n));
|
|
191
|
+
const [vault] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["vault", getAddressEncoder().encode(owner.address)] });
|
|
192
|
+
const me = { address: owner.address, signer: owner, writable: true };
|
|
193
|
+
const v = { address: vault, writable: true };
|
|
194
|
+
|
|
195
|
+
expect(await send(owner, [ix("open", {}, [me, v, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
|
|
196
|
+
const rent = svm.getBalance(vault)!;
|
|
197
|
+
expect(await send(owner, [ix("deposit", { amount: new BN(3_000_000_000) }, [me, v, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
|
|
198
|
+
expect(await send(owner, [ix("withdraw", { amount: new BN(1_000_000_000) }, [me, v])])).toBeInstanceOf(TransactionMetadata);
|
|
199
|
+
expect(svm.getBalance(vault)).toBe(rent + 2_000_000_000n);
|
|
200
|
+
|
|
201
|
+
// VaultError.InsufficientFunds (third member, 6002): the rent reserve stays.
|
|
202
|
+
expect(errorCode(await send(owner, [ix("withdraw", { amount: new BN(2_000_000_001) }, [me, v])]))).toBe(6002);
|
|
203
|
+
|
|
204
|
+
expect(await send(owner, [ix("shut", {}, [me, v])])).toBeInstanceOf(TransactionMetadata);
|
|
205
|
+
expect(svm.getAccount(vault).exists).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("refuses a stranger", async () => {
|
|
209
|
+
const owner = await generateKeyPairSigner();
|
|
210
|
+
const thief = await generateKeyPairSigner();
|
|
211
|
+
svm.airdrop(owner.address, lamports(2_000_000_000n));
|
|
212
|
+
svm.airdrop(thief.address, lamports(1_000_000_000n));
|
|
213
|
+
const [vault] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["vault", getAddressEncoder().encode(owner.address)] });
|
|
214
|
+
const v = { address: vault, writable: true };
|
|
215
|
+
expect(await send(owner, [ix("open", {}, [{ address: owner.address, signer: owner, writable: true }, v, { address: SYSTEM }])])).toBeInstanceOf(
|
|
216
|
+
TransactionMetadata,
|
|
217
|
+
);
|
|
218
|
+
// VaultError.NotOwner: 6000.
|
|
219
|
+
expect(errorCode(await send(thief, [ix("shut", {}, [{ address: thief.address, signer: thief, writable: true }, v])]))).toBe(6000);
|
|
220
|
+
});
|
|
221
|
+
`,
|
|
222
|
+
}),
|
|
223
|
+
},
|
|
224
|
+
escrow: {
|
|
225
|
+
program: `import {
|
|
226
|
+
type AccountInfo,
|
|
227
|
+
type Init,
|
|
228
|
+
type Mut,
|
|
229
|
+
type Pubkey,
|
|
230
|
+
type Signer,
|
|
231
|
+
type SystemProgram,
|
|
232
|
+
type TokenAccount,
|
|
233
|
+
type TokenProgram,
|
|
234
|
+
type u8,
|
|
235
|
+
type u64,
|
|
236
|
+
close,
|
|
237
|
+
require,
|
|
238
|
+
} from "typesol";
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A maker escrows \`amountA\` of mint A and asks for \`wantB\` of mint B.
|
|
242
|
+
* Anyone can take the offer atomically; the maker can refund while it is open.
|
|
243
|
+
*
|
|
244
|
+
* The client creates \`vault\` beforehand as a token account for mint A whose
|
|
245
|
+
* owner is the offer PDA ["offer", maker, seed].
|
|
246
|
+
*/
|
|
247
|
+
export interface Offer {
|
|
248
|
+
maker: Pubkey;
|
|
249
|
+
mintA: Pubkey;
|
|
250
|
+
mintB: Pubkey;
|
|
251
|
+
vault: Pubkey;
|
|
252
|
+
amountA: u64;
|
|
253
|
+
wantB: u64;
|
|
254
|
+
seed: u64;
|
|
255
|
+
bump: u8;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export enum EscrowError {
|
|
259
|
+
ZeroAmount = "Amounts must be greater than zero",
|
|
260
|
+
BadVault = "Vault must be an empty token account owned by the offer",
|
|
261
|
+
WrongMaker = "Maker does not match the offer",
|
|
262
|
+
WrongVault = "Vault does not match the offer",
|
|
263
|
+
WrongMint = "Token account has the wrong mint",
|
|
264
|
+
WrongRecipient = "Maker's token account is not owned by the maker",
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function make(
|
|
268
|
+
maker: Mut<Signer>,
|
|
269
|
+
offer: Init<Offer>,
|
|
270
|
+
makerTokenA: Mut<TokenAccount>,
|
|
271
|
+
vault: Mut<TokenAccount>,
|
|
272
|
+
systemProgram: SystemProgram,
|
|
273
|
+
tokenProgram: TokenProgram,
|
|
274
|
+
seed: u64,
|
|
275
|
+
amountA: u64,
|
|
276
|
+
wantB: u64,
|
|
277
|
+
mintB: Pubkey,
|
|
278
|
+
): void {
|
|
279
|
+
offer.bump = offer.create(maker, ["offer", maker.key, seed]);
|
|
280
|
+
require(amountA > 0n && wantB > 0n, EscrowError.ZeroAmount);
|
|
281
|
+
require(vault.owner === offer.key && vault.mint === makerTokenA.mint && vault.amount === 0n, EscrowError.BadVault);
|
|
282
|
+
|
|
283
|
+
tokenProgram.transfer(makerTokenA, vault, maker, amountA);
|
|
284
|
+
|
|
285
|
+
offer.maker = maker.key;
|
|
286
|
+
offer.mintA = makerTokenA.mint;
|
|
287
|
+
offer.mintB = mintB;
|
|
288
|
+
offer.vault = vault.key;
|
|
289
|
+
offer.amountA = amountA;
|
|
290
|
+
offer.wantB = wantB;
|
|
291
|
+
offer.seed = seed;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function take(
|
|
295
|
+
taker: Mut<Signer>,
|
|
296
|
+
maker: Mut<AccountInfo>,
|
|
297
|
+
offer: Mut<Offer>,
|
|
298
|
+
takerTokenA: Mut<TokenAccount>,
|
|
299
|
+
takerTokenB: Mut<TokenAccount>,
|
|
300
|
+
makerTokenB: Mut<TokenAccount>,
|
|
301
|
+
vault: Mut<TokenAccount>,
|
|
302
|
+
tokenProgram: TokenProgram,
|
|
303
|
+
): void {
|
|
304
|
+
require(offer.maker === maker.key, EscrowError.WrongMaker);
|
|
305
|
+
require(offer.vault === vault.key, EscrowError.WrongVault);
|
|
306
|
+
require(takerTokenA.mint === offer.mintA && takerTokenB.mint === offer.mintB, EscrowError.WrongMint);
|
|
307
|
+
require(makerTokenB.mint === offer.mintB, EscrowError.WrongMint);
|
|
308
|
+
require(makerTokenB.owner === offer.maker, EscrowError.WrongRecipient);
|
|
309
|
+
|
|
310
|
+
tokenProgram.transfer(takerTokenB, makerTokenB, taker, offer.wantB);
|
|
311
|
+
tokenProgram.transfer(vault, takerTokenA, offer, offer.amountA, ["offer", offer.maker, offer.seed, offer.bump]);
|
|
312
|
+
tokenProgram.closeAccount(vault, maker, offer, ["offer", offer.maker, offer.seed, offer.bump]);
|
|
313
|
+
close(offer, maker);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function refund(
|
|
317
|
+
maker: Mut<Signer>,
|
|
318
|
+
offer: Mut<Offer>,
|
|
319
|
+
makerTokenA: Mut<TokenAccount>,
|
|
320
|
+
vault: Mut<TokenAccount>,
|
|
321
|
+
tokenProgram: TokenProgram,
|
|
322
|
+
): void {
|
|
323
|
+
require(offer.maker === maker.key, EscrowError.WrongMaker);
|
|
324
|
+
require(offer.vault === vault.key, EscrowError.WrongVault);
|
|
325
|
+
require(makerTokenA.mint === offer.mintA, EscrowError.WrongMint);
|
|
326
|
+
|
|
327
|
+
tokenProgram.transfer(vault, makerTokenA, offer, offer.amountA, ["offer", offer.maker, offer.seed, offer.bump]);
|
|
328
|
+
tokenProgram.closeAccount(vault, maker, offer, ["offer", offer.maker, offer.seed, offer.bump]);
|
|
329
|
+
close(offer, maker);
|
|
330
|
+
}
|
|
331
|
+
`,
|
|
332
|
+
test: (name) => testFile(name, {
|
|
333
|
+
imports: `import { AccountState, getMintEncoder, getMintSize, getTokenDecoder, getTokenEncoder, getTokenSize } from "@solana-program/token";\n`,
|
|
334
|
+
kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
|
|
335
|
+
body: `
|
|
336
|
+
const TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;
|
|
337
|
+
/** A mint, written straight into the VM (a real client sends System CreateAccount + InitializeMint2). */
|
|
338
|
+
const mint = async (authority: Address) => {
|
|
339
|
+
const address = (await generateKeyPairSigner()).address;
|
|
340
|
+
const data = getMintEncoder().encode({ mintAuthority: authority, supply: 0n, decimals: 6, isInitialized: true, freezeAuthority: null });
|
|
341
|
+
svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(1_461_600n), programAddress: TOKEN, space: BigInt(getMintSize()) });
|
|
342
|
+
return address;
|
|
343
|
+
};
|
|
344
|
+
/** A token account of \`owner\` for \`mint\` holding \`amount\`, written straight into the VM. */
|
|
345
|
+
const tokenAccount = async (owner: Address, mintAddress: Address, amount: bigint) => {
|
|
346
|
+
const address = (await generateKeyPairSigner()).address;
|
|
347
|
+
const data = getTokenEncoder().encode({
|
|
348
|
+
mint: mintAddress,
|
|
349
|
+
owner,
|
|
350
|
+
amount,
|
|
351
|
+
delegate: null,
|
|
352
|
+
state: AccountState.Initialized,
|
|
353
|
+
isNative: null,
|
|
354
|
+
delegatedAmount: 0n,
|
|
355
|
+
closeAuthority: null,
|
|
356
|
+
});
|
|
357
|
+
svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(2_039_280n), programAddress: TOKEN, space: BigInt(getTokenSize()) });
|
|
358
|
+
return address;
|
|
359
|
+
};
|
|
360
|
+
const balance = (address: Address) => {
|
|
361
|
+
const account = svm.getAccount(address);
|
|
362
|
+
return account.exists ? getTokenDecoder().decode(account.data).amount : null;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
it("escrows mint A for mint B; the taker swaps, the maker can refund", async () => {
|
|
366
|
+
const maker = await generateKeyPairSigner();
|
|
367
|
+
const taker = await generateKeyPairSigner();
|
|
368
|
+
svm.airdrop(maker.address, lamports(1_000_000_000n));
|
|
369
|
+
svm.airdrop(taker.address, lamports(1_000_000_000n));
|
|
370
|
+
const mintA = await mint(maker.address);
|
|
371
|
+
const mintB = await mint(taker.address);
|
|
372
|
+
const makerA = await tokenAccount(maker.address, mintA, 1_000n);
|
|
373
|
+
const makerB = await tokenAccount(maker.address, mintB, 0n);
|
|
374
|
+
const takerA = await tokenAccount(taker.address, mintA, 0n);
|
|
375
|
+
const takerB = await tokenAccount(taker.address, mintB, 500n);
|
|
376
|
+
|
|
377
|
+
const open = async (seed: number) => {
|
|
378
|
+
const [offer] = await getProgramDerivedAddress({
|
|
379
|
+
programAddress: programId,
|
|
380
|
+
seeds: ["offer", getAddressEncoder().encode(maker.address), new Uint8Array(new BN(seed).toArray("le", 8))],
|
|
381
|
+
});
|
|
382
|
+
// The client creates the vault: an empty token account for mint A owned by the offer PDA.
|
|
383
|
+
const vault = await tokenAccount(offer, mintA, 0n);
|
|
384
|
+
const make = ix("make", { seed: new BN(seed), amount_a: new BN(400), want_b: new BN(300), mint_b: { toBuffer: () => Buffer.from(getAddressEncoder().encode(mintB)) } }, [
|
|
385
|
+
{ address: maker.address, signer: maker, writable: true },
|
|
386
|
+
{ address: offer, writable: true },
|
|
387
|
+
{ address: makerA, writable: true },
|
|
388
|
+
{ address: vault, writable: true },
|
|
389
|
+
{ address: SYSTEM },
|
|
390
|
+
{ address: TOKEN },
|
|
391
|
+
]);
|
|
392
|
+
expect(await send(maker, [make])).toBeInstanceOf(TransactionMetadata);
|
|
393
|
+
return { offer, vault };
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const first = await open(1);
|
|
397
|
+
expect(balance(first.vault)).toBe(400n);
|
|
398
|
+
const take = ix("take", {}, [
|
|
399
|
+
{ address: taker.address, signer: taker, writable: true },
|
|
400
|
+
{ address: maker.address, writable: true },
|
|
401
|
+
{ address: first.offer, writable: true },
|
|
402
|
+
{ address: takerA, writable: true },
|
|
403
|
+
{ address: takerB, writable: true },
|
|
404
|
+
{ address: makerB, writable: true },
|
|
405
|
+
{ address: first.vault, writable: true },
|
|
406
|
+
{ address: TOKEN },
|
|
407
|
+
]);
|
|
408
|
+
expect(await send(taker, [take])).toBeInstanceOf(TransactionMetadata);
|
|
409
|
+
expect([balance(takerA), balance(makerB), balance(first.vault)]).toEqual([400n, 300n, null]);
|
|
410
|
+
|
|
411
|
+
const second = await open(2);
|
|
412
|
+
const refund = (signer: KeyPairSigner) =>
|
|
413
|
+
ix("refund", {}, [
|
|
414
|
+
{ address: signer.address, signer, writable: true },
|
|
415
|
+
{ address: second.offer, writable: true },
|
|
416
|
+
{ address: makerA, writable: true },
|
|
417
|
+
{ address: second.vault, writable: true },
|
|
418
|
+
{ address: TOKEN },
|
|
419
|
+
]);
|
|
420
|
+
// EscrowError.WrongMaker (third member, 6002): only the maker refunds.
|
|
421
|
+
expect(errorCode(await send(taker, [refund(taker)]))).toBe(6002);
|
|
422
|
+
expect(await send(maker, [refund(maker)])).toBeInstanceOf(TransactionMetadata);
|
|
423
|
+
expect([balance(makerA), svm.getAccount(second.offer).exists]).toEqual([600n, false]);
|
|
424
|
+
});
|
|
425
|
+
`,
|
|
426
|
+
}),
|
|
427
|
+
devDependencies: { "@solana-program/token": "~0.18.0" },
|
|
428
|
+
},
|
|
429
|
+
};
|
package/dist/workspace.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BuildResult, type Sbpf } from "./compiler/index.js";
|
|
2
|
+
import { type TemplateName } from "./templates.js";
|
|
2
3
|
export declare const CONFIG = "typesol.json";
|
|
3
4
|
/** A program entry in typesol.json: its entry file and (recorded on first build) its address. */
|
|
4
5
|
export interface ProgramEntry {
|
|
@@ -47,5 +48,5 @@ export declare function deployArgs(soPath: string, keypair: string, opts: {
|
|
|
47
48
|
payer?: string;
|
|
48
49
|
config?: string;
|
|
49
50
|
}): string[];
|
|
50
|
-
/** Create a project: typesol.json, one program
|
|
51
|
-
export declare function scaffold(dir: string, name: string): string[];
|
|
51
|
+
/** Create a project: typesol.json, one program from a template, its LiteSVM test, configs. Refuses to overwrite files. */
|
|
52
|
+
export declare function scaffold(dir: string, name: string, template?: TemplateName): string[];
|
package/dist/workspace.js
CHANGED
|
@@ -16,6 +16,7 @@ import path from "node:path";
|
|
|
16
16
|
import { writeClient } from "./clientgen.js";
|
|
17
17
|
import { build, COMPILER_VERSION, SBPF_VERSIONS } from "./compiler/index.js";
|
|
18
18
|
import { loadOrCreateKeypair } from "./keys.js";
|
|
19
|
+
import { TEMPLATES } from "./templates.js";
|
|
19
20
|
export const CONFIG = "typesol.json";
|
|
20
21
|
export function findWorkspace(dir) {
|
|
21
22
|
for (let d = path.resolve(dir);; d = path.dirname(d)) {
|
|
@@ -104,119 +105,19 @@ export function deployArgs(soPath, keypair, opts) {
|
|
|
104
105
|
args.push("--keypair", opts.payer);
|
|
105
106
|
return args;
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
authority: Pubkey;
|
|
111
|
-
count: u64;
|
|
112
|
-
bump: u8;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export enum CounterError {
|
|
116
|
-
Unauthorized = "Only the counter's authority can change it",
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Create the caller's counter at PDA ["counter", authority]. */
|
|
120
|
-
export function initialize(authority: Mut<Signer>, counter: Init<Counter>, systemProgram: SystemProgram): void {
|
|
121
|
-
counter.bump = counter.create(authority, ["counter", authority.key]);
|
|
122
|
-
counter.authority = authority.key;
|
|
123
|
-
counter.count = 0n;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function increment(authority: Signer, counter: Mut<Counter>, by: u64): void {
|
|
127
|
-
require(counter.authority === authority.key, CounterError.Unauthorized);
|
|
128
|
-
counter.count += by;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function assertAtLeast(counter: Account<Counter>, min: u64): void {
|
|
132
|
-
require(counter.count >= min, "counter is below the expected minimum");
|
|
133
|
-
}
|
|
134
|
-
`;
|
|
135
|
-
const TEST = (name) => `// Runs the program in LiteSVM (an in-process Solana runtime): \`npm test\`.
|
|
136
|
-
import { BN, BorshCoder, type Idl } from "@anchor-lang/core";
|
|
137
|
-
import {
|
|
138
|
-
AccountRole,
|
|
139
|
-
appendTransactionMessageInstructions,
|
|
140
|
-
createTransactionMessage,
|
|
141
|
-
generateKeyPairSigner,
|
|
142
|
-
getAddressEncoder,
|
|
143
|
-
getProgramDerivedAddress,
|
|
144
|
-
lamports,
|
|
145
|
-
pipe,
|
|
146
|
-
setTransactionMessageFeePayerSigner,
|
|
147
|
-
signTransactionMessageWithSigners,
|
|
148
|
-
type Address,
|
|
149
|
-
type Instruction,
|
|
150
|
-
type KeyPairSigner,
|
|
151
|
-
} from "@solana/kit";
|
|
152
|
-
import { FailedTransactionMetadata, LiteSVM } from "litesvm";
|
|
153
|
-
import path from "node:path";
|
|
154
|
-
import { fileURLToPath } from "node:url";
|
|
155
|
-
import { buildWorkspace, findWorkspace } from "typesol/workspace";
|
|
156
|
-
import { beforeAll, describe, expect, it } from "vitest";
|
|
157
|
-
|
|
158
|
-
const SYSTEM = "11111111111111111111111111111111" as Address;
|
|
159
|
-
const ws = findWorkspace(path.dirname(fileURLToPath(import.meta.url)))!;
|
|
160
|
-
|
|
161
|
-
describe("${name}", () => {
|
|
162
|
-
const svm = new LiteSVM();
|
|
163
|
-
let programId: Address;
|
|
164
|
-
let coder: BorshCoder;
|
|
165
|
-
beforeAll(() => {
|
|
166
|
-
const [built] = buildWorkspace(ws, ["${name}"]);
|
|
167
|
-
programId = built.address as Address;
|
|
168
|
-
svm.addProgramFromFile(programId, built.result.soPath);
|
|
169
|
-
coder = new BorshCoder(built.result.idl as unknown as Idl);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const ix = (name: string, args: object, accounts: { address: Address; signer?: KeyPairSigner; writable?: boolean }[]): Instruction => ({
|
|
173
|
-
programAddress: programId,
|
|
174
|
-
accounts: accounts.map((a) => ({
|
|
175
|
-
address: a.address,
|
|
176
|
-
role: a.signer ? (a.writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER) : a.writable ? AccountRole.WRITABLE : AccountRole.READONLY,
|
|
177
|
-
...(a.signer ? { signer: a.signer } : {}),
|
|
178
|
-
})) as Instruction["accounts"],
|
|
179
|
-
data: new Uint8Array(coder.instruction.encode(name, args)),
|
|
180
|
-
});
|
|
181
|
-
const send = async (payer: KeyPairSigner, ixs: Instruction[]) => {
|
|
182
|
-
const tx = await pipe(
|
|
183
|
-
createTransactionMessage({ version: 0 }),
|
|
184
|
-
(t) => setTransactionMessageFeePayerSigner(payer, t),
|
|
185
|
-
(t) => svm.setTransactionMessageLifetimeUsingLatestBlockhash(t),
|
|
186
|
-
(t) => appendTransactionMessageInstructions(ixs, t),
|
|
187
|
-
(t) => signTransactionMessageWithSigners(t),
|
|
188
|
-
);
|
|
189
|
-
const res = svm.sendTransaction(tx);
|
|
190
|
-
svm.expireBlockhash();
|
|
191
|
-
return res;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
it("initializes and increments the caller's counter", async () => {
|
|
195
|
-
const alice = await generateKeyPairSigner();
|
|
196
|
-
svm.airdrop(alice.address, lamports(1_000_000_000n));
|
|
197
|
-
const [counter] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["counter", getAddressEncoder().encode(alice.address)] });
|
|
198
|
-
const me = { address: alice.address, signer: alice, writable: true };
|
|
199
|
-
expect(await send(alice, [ix("initialize", {}, [me, { address: counter, writable: true }, { address: SYSTEM }])])).not.toBeInstanceOf(FailedTransactionMetadata);
|
|
200
|
-
expect(await send(alice, [ix("increment", { by: new BN(5) }, [{ address: alice.address, signer: alice }, { address: counter, writable: true }])])).not.toBeInstanceOf(FailedTransactionMetadata);
|
|
201
|
-
const account = svm.getAccount(counter);
|
|
202
|
-
if (!account.exists) throw new Error("counter was not created");
|
|
203
|
-
const state = coder.accounts.decode("Counter", Buffer.from(account.data));
|
|
204
|
-
expect(state.count.toString()).toBe("5");
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
`;
|
|
208
|
-
/** Create a project: typesol.json, one program, a LiteSVM test, configs. Refuses to overwrite files. */
|
|
209
|
-
export function scaffold(dir, name) {
|
|
108
|
+
/** Create a project: typesol.json, one program from a template, its LiteSVM test, configs. Refuses to overwrite files. */
|
|
109
|
+
export function scaffold(dir, name, template = "counter") {
|
|
110
|
+
const t = TEMPLATES[template];
|
|
210
111
|
if (!/^[a-z][a-z0-9_]*$/.test(name))
|
|
211
112
|
throw new Error(`program name "${name}" must be snake_case`);
|
|
212
113
|
const files = {
|
|
213
114
|
[CONFIG]: JSON.stringify({ programs: { [name]: { path: `programs/${name}.ts` } }, out: "target", sbpf: "v0" }, null, 2) + "\n",
|
|
214
|
-
[`programs/${name}.ts`]:
|
|
115
|
+
[`programs/${name}.ts`]: t.program,
|
|
215
116
|
"programs/tsconfig.json": JSON.stringify({
|
|
216
117
|
compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", strict: true, noEmit: true, types: [], lib: ["ES2022"] },
|
|
217
118
|
include: ["**/*.ts"],
|
|
218
119
|
}, null, 2) + "\n",
|
|
219
|
-
[`tests/${name}.test.ts`]:
|
|
120
|
+
[`tests/${name}.test.ts`]: t.test(name),
|
|
220
121
|
"package.json": JSON.stringify({
|
|
221
122
|
name,
|
|
222
123
|
private: true,
|
|
@@ -232,6 +133,7 @@ export function scaffold(dir, name) {
|
|
|
232
133
|
"@solana/kit": "^8.5.0",
|
|
233
134
|
litesvm: "^1.6.1",
|
|
234
135
|
vitest: "^5.0.5",
|
|
136
|
+
...t.devDependencies,
|
|
235
137
|
},
|
|
236
138
|
}, null, 2) + "\n",
|
|
237
139
|
// target/deploy/*-keypair.json are the programs' addresses: never commit them, but keep a backup.
|
package/docs/cli.md
CHANGED
|
@@ -6,7 +6,7 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
|
|
|
6
6
|
|
|
7
7
|
| Command | What it does |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| `typesol init [<dir>] [--name <program>]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`. Refuses to overwrite files. |
|
|
9
|
+
| `typesol init [<dir>] [--name <program>] [--template counter\|vault\|escrow]` | Create a project: `typesol.json`, `programs/<name>.ts`, `tests/<name>.test.ts` (LiteSVM), `package.json`, `tsconfig.json`. The template picks the program and its test: `counter` (default, a PDA counter per user), `vault` (SOL at a PDA: deposit, withdraw above rent, close) or `escrow` (SPL tokens: make, take, refund, the offer PDA signing for its vault). Refuses to overwrite files. |
|
|
10
10
|
| `typesol build [<program>…] [--sbpf v0\|v1\|v2] [--profile] [--quiet]` | In a project: build every program (or those named) into `<out>/<name>/`: `<name>.so`, `<name>.json` (Anchor IDL), `<name>.client.ts` (TypeScript client, see [client.md](client.md)), `<name>.c`, `<name>.build.json`. The first build creates `<out>/deploy/<name>-keypair.json` and records the address in `typesol.json`; later builds refuse a missing or different keypair. |
|
|
11
11
|
| `typesol build <file.ts> [--out <dir>] [--program-id <keypair.json>] [--sbpf …] [--profile]` | Outside a project (or a file that is not one of its programs): build that file. A project program's `.ts` builds as that program. |
|
|
12
12
|
| `typesol check <file.ts>` | Type-check and validate without building. |
|
package/docs/mainnet.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Deploy to mainnet
|
|
2
|
+
|
|
3
|
+
From an empty directory to a program on Solana mainnet-beta: create, test, deploy, publish the IDL, then check that
|
|
4
|
+
the deployed binary is the one your source builds. The program here is the counter that `typesol init` creates
|
|
5
|
+
(13.5 KB); any TypeSOL project works the same way.
|
|
6
|
+
|
|
7
|
+
You need Node.js 22.12 or later and the Agave CLI with platform-tools (see the README's Requirements).
|
|
8
|
+
|
|
9
|
+
## 1. Create and test
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx @typesol/cli init my-app
|
|
13
|
+
cd my-app
|
|
14
|
+
npm install
|
|
15
|
+
npm test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`npm test` runs `tests/my_app.test.ts` in LiteSVM: it compiles the program and calls it, with no validator or SOL.
|
|
19
|
+
Write your program in `programs/my_app.ts` and keep the tests passing before you spend anything.
|
|
20
|
+
|
|
21
|
+
## 2. Build
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx typesol build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This writes `target/my_app/`: the program (`my_app.so`), its Anchor IDL (`my_app.json`), a TypeScript client
|
|
28
|
+
(`my_app.client.ts`) and the generated C. The first build also creates `target/deploy/my_app-keypair.json`, the
|
|
29
|
+
program's address, and records the address in `typesol.json`.
|
|
30
|
+
|
|
31
|
+
**Back up `target/deploy/my_app-keypair.json` now.** It is not committed (`.gitignore`), and without it you cannot
|
|
32
|
+
redeploy to that address.
|
|
33
|
+
|
|
34
|
+
## 3. A wallet with SOL
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
solana-keygen new -o ~/.config/solana/deployer.json
|
|
38
|
+
solana config set --url mainnet-beta --keypair ~/.config/solana/deployer.json
|
|
39
|
+
solana address
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Send SOL to that address. Most of what a deploy costs is rent for the program's bytes, which you get back if you
|
|
43
|
+
ever close the program:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
solana rent $(wc -c < target/my_app/my_app.so)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The counter needs 0.069 SOL of rent. During the deploy the program is first written to a buffer account that holds
|
|
50
|
+
the same amount again; it is returned when the deploy finishes. Fund about twice the rent plus 0.01 SOL for fees.
|
|
51
|
+
|
|
52
|
+
The public mainnet RPC is rate-limited; for deploys, set `--url` (or `solana config set --url`) to your RPC
|
|
53
|
+
provider's endpoint.
|
|
54
|
+
|
|
55
|
+
## 4. Deploy
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx typesol deploy --dry-run # builds and prints the solana command it will run
|
|
59
|
+
npx typesol deploy
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It runs `solana program deploy` at the address in `target/deploy/my_app-keypair.json`, with your wallet as payer and
|
|
63
|
+
upgrade authority, and prints the program id. Deploying again upgrades the program in place.
|
|
64
|
+
|
|
65
|
+
If a deploy stops part-way, the buffer stays behind with its SOL. The error prints the buffer's address; recover it
|
|
66
|
+
with `solana program close <buffer address>`, or list yours with `solana program show --buffers`.
|
|
67
|
+
|
|
68
|
+
## 5. Publish the IDL
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npx typesol idl publish
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It writes the IDL to the Program Metadata program, signed by the upgrade authority, where Anchor clients and
|
|
75
|
+
explorers look for it. It costs a few thousandths of a SOL of rent.
|
|
76
|
+
|
|
77
|
+
## 6. Verify
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
solana program dump <program id> deployed.so
|
|
81
|
+
npx typesol verify my_app deployed.so
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`verify` rebuilds the program from `programs/my_app.ts` and compares it with the deployed bytes. `match` means anyone
|
|
85
|
+
with your source and the same TypeSOL version gets exactly the program on chain.
|
|
86
|
+
|
|
87
|
+
## 7. Call it
|
|
88
|
+
|
|
89
|
+
`target/my_app/my_app.client.ts` builds the program's instructions on `@solana/kit`; see [client.md](client.md).
|
|
90
|
+
|
|
91
|
+
## Later
|
|
92
|
+
|
|
93
|
+
- **Freeze it.** `solana program set-upgrade-authority <program id> --final` makes the program immutable. It cannot
|
|
94
|
+
be undone.
|
|
95
|
+
- **Close it.** `solana program close <program id> --bypass-warning` returns the rent to the authority. The program
|
|
96
|
+
id can never be used again.
|
|
97
|
+
|
|
98
|
+
A worked example: the official escrow, deployed to mainnet in TypeSOL, Pinocchio, native Rust and Anchor, with every
|
|
99
|
+
transaction linked, is in the README's [On mainnet](../README.md#on-mainnet) section.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -23,5 +23,6 @@ compiler's checks log their TypeScript location, e.g. `TypeSOL: arithmetic overf
|
|
|
23
23
|
**Heap errors (5110) building strings** — strings are allocated on a 32 KiB heap and never freed. Build less text,
|
|
24
24
|
or declare `const HEAP_SIZE_KIB = 64n` and add a ComputeBudget `requestHeapFrame` instruction to the transaction.
|
|
25
25
|
|
|
26
|
-
**`typesol idl publish` fails on a local validator with "invalid instruction data"** —
|
|
27
|
-
System instruction the current Program Metadata program
|
|
26
|
+
**`typesol idl publish` fails on a local validator with "invalid instruction data"** — the System Program of
|
|
27
|
+
`solana-test-validator` (Agave 3.1 included) rejects a System instruction that the current Program Metadata program
|
|
28
|
+
uses. Publishing works on mainnet-beta. The error shows the failing transaction's last log lines.
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -305,6 +305,55 @@ static inline __attribute__((always_inline)) uint64_t ts_invoke(TsParams *params
|
|
|
305
305
|
return sol_invoke_signed(ix, infos, ix->account_len, signers, nsigners);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/*
|
|
309
|
+
* ---- Program derived addresses ------------------------------------------------
|
|
310
|
+
* The same addresses sol_create_program_address / sol_try_find_program_address compute — sha256 of the seeds, the
|
|
311
|
+
* program id and "ProgramDerivedAddress", valid only off the ed25519 curve — from the sha256 and curve-validation
|
|
312
|
+
* syscalls, which cost a fraction of the 1500 compute units the PDA syscalls charge per attempt. The seed limits are the
|
|
313
|
+
* syscalls' (at most 16 seeds of at most 32 bytes, the bump included), and so is the result for every input.
|
|
314
|
+
*/
|
|
315
|
+
uint64_t sol_curve_validate_point(uint64_t curve_id, const uint8_t *point, uint8_t *result);
|
|
316
|
+
#define TS_MAX_PDA_SEEDS 16
|
|
317
|
+
|
|
318
|
+
/* The address of `seeds` (and `bump`, when given) under `program`; false when it lies on the curve (no PDA). */
|
|
319
|
+
static inline bool ts_pda_hash(const SolSignerSeed *seeds, int n, const uint8_t *bump, const SolPubkey *program, SolPubkey *out) {
|
|
320
|
+
SolBytes parts[TS_MAX_PDA_SEEDS + 2];
|
|
321
|
+
int k = 0;
|
|
322
|
+
for (int i = 0; i < n; i++) parts[k++] = (SolBytes){seeds[i].addr, seeds[i].len};
|
|
323
|
+
if (bump) parts[k++] = (SolBytes){bump, 1};
|
|
324
|
+
parts[k++] = (SolBytes){program->x, 32};
|
|
325
|
+
parts[k++] = (SolBytes){(const uint8_t *)"ProgramDerivedAddress", 21};
|
|
326
|
+
sol_sha256(parts, k, out->x);
|
|
327
|
+
uint8_t ignored;
|
|
328
|
+
return sol_curve_validate_point(0 /* CURVE25519_EDWARDS */, out->x, &ignored) != 0; /* 0: a valid curve point */
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
static inline bool ts_pda_seeds_ok(const SolSignerSeed *seeds, int n, int max) {
|
|
332
|
+
if (n > max) return false;
|
|
333
|
+
for (int i = 0; i < n; i++)
|
|
334
|
+
if (seeds[i].len > 32) return false;
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* sol_create_program_address: the bump is the last of `seeds`. */
|
|
339
|
+
static inline uint64_t ts_create_pda_address(const SolSignerSeed *seeds, int n, const SolPubkey *program, SolPubkey *out) {
|
|
340
|
+
if (!ts_pda_seeds_ok(seeds, n, TS_MAX_PDA_SEEDS) || !ts_pda_hash(seeds, n, NULL, program, out)) return 1; /* not SUCCESS: the PDA syscalls fail the same inputs */
|
|
341
|
+
return SUCCESS;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* sol_try_find_program_address: the canonical (highest) bump. */
|
|
345
|
+
static inline uint64_t ts_find_pda(const SolSignerSeed *seeds, int n, const SolPubkey *program, SolPubkey *out, uint8_t *bump_out) {
|
|
346
|
+
if (!ts_pda_seeds_ok(seeds, n, TS_MAX_PDA_SEEDS - 1)) return 1; /* not SUCCESS: the PDA syscalls fail the same inputs */
|
|
347
|
+
for (int b = 255; b >= 0; b--) {
|
|
348
|
+
uint8_t bump = (uint8_t)b;
|
|
349
|
+
if (ts_pda_hash(seeds, n, &bump, program, out)) {
|
|
350
|
+
*bump_out = bump;
|
|
351
|
+
return SUCCESS;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return 1; /* not SUCCESS: the PDA syscalls fail the same inputs */
|
|
355
|
+
}
|
|
356
|
+
|
|
308
357
|
/* ---- Account checks ------------------------------------------------------ */
|
|
309
358
|
/* Copy between two places of the same type: distinct, or the very same place (then nothing to do). */
|
|
310
359
|
static inline void ts_copy(void *dst, const void *src, uint64_t n) {
|
|
@@ -461,7 +510,7 @@ static inline uint64_t ts_check_program_data(const TsAccount *a, const SolPubkey
|
|
|
461
510
|
SolSignerSeed seed = {(const uint8_t *)program_id, 32};
|
|
462
511
|
SolPubkey expected;
|
|
463
512
|
uint8_t bump;
|
|
464
|
-
if (
|
|
513
|
+
if (ts_find_pda(&seed, 1, &TS_BPF_UPGRADEABLE_LOADER_ID, &expected, &bump) != SUCCESS || !ts_key_eq(&expected, &a->key))
|
|
465
514
|
TS_FAIL(TS_ERR_ADDRESS, "TypeSOL: not this program's ProgramData account");
|
|
466
515
|
return SUCCESS;
|
|
467
516
|
}
|
|
@@ -471,7 +520,7 @@ static const uint8_t TS_EVENT_IX_TAG[8] = {0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0
|
|
|
471
520
|
static inline uint64_t ts_check_event_authority(const TsAccount *a, const SolPubkey *program_id, uint8_t *bump) {
|
|
472
521
|
SolSignerSeed seed = {(const uint8_t *)"__event_authority", 17};
|
|
473
522
|
SolPubkey expected;
|
|
474
|
-
if (
|
|
523
|
+
if (ts_find_pda(&seed, 1, program_id, &expected, bump) != SUCCESS || !ts_key_eq(&expected, &a->key))
|
|
475
524
|
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: not this program's event authority");
|
|
476
525
|
return SUCCESS;
|
|
477
526
|
}
|
|
@@ -510,7 +559,7 @@ static inline uint64_t ts_init_zeroed(TsAccount *a, const SolPubkey *program_id,
|
|
|
510
559
|
static inline uint64_t ts_require_pda(const TsAccount *a, const SolSignerSeed *seeds, int n,
|
|
511
560
|
const SolPubkey *program_id) {
|
|
512
561
|
SolPubkey derived;
|
|
513
|
-
if (
|
|
562
|
+
if (ts_create_pda_address(seeds, n, program_id, &derived) != SUCCESS || !ts_key_eq(&derived, &a->key))
|
|
514
563
|
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: PDA seeds do not match account");
|
|
515
564
|
return SUCCESS;
|
|
516
565
|
}
|
|
@@ -536,7 +585,7 @@ static inline uint64_t ts_create_pda(TsParams *params, TsAccount *payer, TsAccou
|
|
|
536
585
|
uint8_t *bump_out) {
|
|
537
586
|
SolPubkey derived;
|
|
538
587
|
uint8_t bump;
|
|
539
|
-
if (
|
|
588
|
+
if (ts_find_pda(seeds, n, params->program_id, &derived, &bump) != SUCCESS ||
|
|
540
589
|
!ts_key_eq(&derived, &acct->key))
|
|
541
590
|
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: PDA seeds do not match account");
|
|
542
591
|
if (acct->data_len != 0 || !ts_key_eq(&acct->owner, &TS_SYSTEM_PROGRAM_ID))
|
|
@@ -627,7 +676,7 @@ static inline uint64_t ts_create_pda_if_needed(TsParams *params, TsAccount *paye
|
|
|
627
676
|
if (ts_key_eq(&acct->owner, params->program_id)) {
|
|
628
677
|
SolPubkey derived;
|
|
629
678
|
uint8_t bump;
|
|
630
|
-
if (
|
|
679
|
+
if (ts_find_pda(seeds, n, params->program_id, &derived, &bump) != SUCCESS ||
|
|
631
680
|
!ts_key_eq(&derived, &acct->key))
|
|
632
681
|
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: PDA seeds do not match account");
|
|
633
682
|
if (acct->data_len < space) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account data too small");
|
|
@@ -1105,7 +1154,7 @@ static const SolPubkey TS_MPL_METADATA_ID = {{11,112,101,177,227,209,124,69,56,1
|
|
|
1105
1154
|
static inline uint64_t ts_ata_address(const SolPubkey *owner, const SolPubkey *mint, const SolPubkey *token_program, SolPubkey *out) {
|
|
1106
1155
|
SolSignerSeed seeds[3] = {{(const uint8_t *)owner, 32}, {(const uint8_t *)token_program, 32}, {(const uint8_t *)mint, 32}};
|
|
1107
1156
|
uint8_t bump;
|
|
1108
|
-
if (
|
|
1157
|
+
if (ts_find_pda(seeds, 3, &TS_ATA_PROGRAM_ID, out, &bump) != SUCCESS)
|
|
1109
1158
|
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: cannot derive the associated token address");
|
|
1110
1159
|
return SUCCESS;
|
|
1111
1160
|
}
|
|
@@ -1155,7 +1204,7 @@ static inline uint64_t ts_t22_init_metadata(TsParams *params, const TsAccount *p
|
|
|
1155
1204
|
static inline uint64_t ts_metadata_address(const SolPubkey *mint, SolPubkey *out) {
|
|
1156
1205
|
SolSignerSeed seeds[3] = {{(const uint8_t *)"metadata", 8}, {(const uint8_t *)&TS_MPL_METADATA_ID, 32}, {(const uint8_t *)mint, 32}};
|
|
1157
1206
|
uint8_t bump;
|
|
1158
|
-
if (
|
|
1207
|
+
if (ts_find_pda(seeds, 3, &TS_MPL_METADATA_ID, out, &bump) != SUCCESS)
|
|
1159
1208
|
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: cannot derive the metadata address");
|
|
1160
1209
|
return SUCCESS;
|
|
1161
1210
|
}
|
|
@@ -1225,7 +1274,7 @@ static inline uint64_t ts_update_extra_metas(TsParams *params, TsAccount *payer,
|
|
|
1225
1274
|
SolSignerSeed seeds[2] = {{(const uint8_t *)"extra-account-metas", 19}, {(const uint8_t *)mint, 32}};
|
|
1226
1275
|
SolPubkey pda;
|
|
1227
1276
|
uint8_t bump;
|
|
1228
|
-
if (
|
|
1277
|
+
if (ts_find_pda(seeds, 2, params->program_id, &pda, &bump) != SUCCESS || !ts_key_eq(&pda, &acct->key))
|
|
1229
1278
|
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: not the mint's extra-account-metas PDA");
|
|
1230
1279
|
if (!ts_key_eq(&acct->owner, params->program_id)) TS_FAIL(TS_ERR_WRONG_OWNER, "TypeSOL: the extra-account-metas account is not this program's");
|
|
1231
1280
|
if (acct->data_len < 16 || ts_ld64(acct->data) != ts_ld64(TS_TRANSFER_HOOK_EXECUTE_DISC))
|