@typesol/cli 0.14.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 +20 -0
- package/README.md +27 -1
- package/dist/cli.js +14 -4
- 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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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
|
+
|
|
8
28
|
## 0.14.0 — 2026-10-27
|
|
9
29
|
|
|
10
30
|
PDAs are derived without the PDA syscalls, so instructions that create, check or search for PDAs use up to a few
|
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
|
```
|
|
@@ -246,6 +251,27 @@ CPI account records only for the accounts a call lists. It uses fewer compute un
|
|
|
246
251
|
instruction here, while also refusing duplicate writable accounts as Anchor does. Fewer compute
|
|
247
252
|
units are not a safety claim. The differential tests also run the escrow against its Anchor twin ([docs/testing.md](docs/testing.md)).
|
|
248
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
|
+
|
|
249
275
|
## How it is tested
|
|
250
276
|
|
|
251
277
|
- **Execution tests.** Every feature is compiled and run in LiteSVM.
|
|
@@ -276,7 +302,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
276
302
|
|
|
277
303
|
## Status
|
|
278
304
|
|
|
279
|
-
**TypeSOL is pre-1.0 (0.
|
|
305
|
+
**TypeSOL is pre-1.0 (0.15).** Releases follow the organization's
|
|
280
306
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
281
307
|
change.
|
|
282
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;
|
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.
|