@typesol/cli 0.24.0 → 0.24.1
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 +16 -2
- package/README.md +7 -5
- package/dist/clientgen.js +33 -21
- package/dist/compiler/analyze.js +21 -1
- package/dist/compiler/emit/base.d.ts +10 -0
- package/dist/compiler/emit/base.js +8 -0
- package/dist/compiler/emit/cpi.d.ts +10 -1
- package/dist/compiler/emit/cpi.js +178 -166
- package/docs/architecture.md +26 -2
- package/docs/runtime.md +59 -0
- package/package.json +1 -1
- package/runtime/typesol_rt.h +52 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
TypeSOL follows [
|
|
4
|
-
until 1.0, a minor version may change the language, the IDL layout or error
|
|
3
|
+
TypeSOL follows the organization's [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md)
|
|
4
|
+
(compiler specifics in CONTRIBUTING.md): until 1.0, a minor version may change the language, the IDL layout or error
|
|
5
|
+
codes, and says so here.
|
|
5
6
|
|
|
6
7
|
## Unreleased
|
|
7
8
|
|
|
9
|
+
## 0.24.1 — 2026-10-28
|
|
10
|
+
|
|
11
|
+
Smaller programs and fewer compute units, with the same results; no change to the language, IDLs or error codes. Also
|
|
12
|
+
new contributor docs (repository layout, a guide to the C runtime). Not externally audited (see SECURITY.md).
|
|
13
|
+
|
|
14
|
+
### Performance
|
|
15
|
+
- Smaller programs, same results: the rent calculation no longer links soft-float (an exact integer equivalent,
|
|
16
|
+
checked against IEEE doubles), and every failure logs through one shared call. The benchmarked programs are 11.6%
|
|
17
|
+
smaller in total (counter 13.1 → 11.0 KB, escrow 23.9 → 21.3 KB). A failing instruction costs 2 more compute units;
|
|
18
|
+
successful ones are unchanged.
|
|
19
|
+
- An identical signer-seed list in the same scope reuses the array declared for the first CPI (escrow `take_offer`
|
|
20
|
+
7058 → 7044, `refund_offer` 4287 → 4271).
|
|
21
|
+
|
|
8
22
|
## 0.24.0 — 2026-10-28
|
|
9
23
|
|
|
10
24
|
Functions as values, closures for any helper, stored `map` to structs and text, Metaplex Core external plugins, and
|
package/README.md
CHANGED
|
@@ -232,7 +232,9 @@ Subpath exports: `typesol/compiler`, `typesol/workspace`, `typesol/cpi`, `typeso
|
|
|
232
232
|
|
|
233
233
|
The same counter, vault and escrow programs, written four ways. All four use the same instruction encoding, account
|
|
234
234
|
layout and checks. They get identical transactions in LiteSVM and must end in identical account state. The table
|
|
235
|
-
shows mean compute units per call (generated by `scripts/readme-bench.ts` from the benchmark run)
|
|
235
|
+
shows mean compute units per call (generated by `scripts/readme-bench.ts` from the benchmark run). The programs,
|
|
236
|
+
scenarios and harness are in [typesol/benchmarks](https://github.com/typesol/benchmarks), which pins the compiler and
|
|
237
|
+
toolchain and records them with every result:
|
|
236
238
|
|
|
237
239
|
<!-- bench:table -->
|
|
238
240
|
| Instruction | TypeSOL | Pinocchio 0.10 | Native Rust | Anchor 1.0.2 |
|
|
@@ -244,9 +246,9 @@ shows mean compute units per call (generated by `scripts/readme-bench.ts` from t
|
|
|
244
246
|
| vault.withdraw | **315** | 370 | 1,176 | 3,628 |
|
|
245
247
|
| vault.shut | **161** | 167 | 950 | 2,024 |
|
|
246
248
|
| escrow.make | **3,605** | 6,179 | 9,240 | 13,304 |
|
|
247
|
-
| escrow.take | **4,
|
|
248
|
-
| escrow.refund | **2,
|
|
249
|
-
| binary, counter / vault / escrow |
|
|
249
|
+
| escrow.take | **4,019** | 4,051 | 8,500 | 14,365 |
|
|
250
|
+
| escrow.refund | **2,696** | 2,769 | 5,695 | 8,449 |
|
|
251
|
+
| binary, counter / vault / escrow | 11.0 / 14.1 / 21.3 KB | 19.6 / 22.9 / 29.4 KB | 27.9 / 35.0 / 46.5 KB | 145.2 / 158.9 / 190.7 KB |
|
|
250
252
|
<!-- /bench:table -->
|
|
251
253
|
|
|
252
254
|
`initialize`, `open` and `make` include the PDA bump search. TypeSOL hashes PDA candidates with the sha256 and
|
|
@@ -307,7 +309,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
307
309
|
|
|
308
310
|
## Status
|
|
309
311
|
|
|
310
|
-
**TypeSOL is pre-1.0
|
|
312
|
+
**TypeSOL is pre-1.0.** Releases follow the organization's
|
|
311
313
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
312
314
|
change.
|
|
313
315
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
package/dist/clientgen.js
CHANGED
|
@@ -1102,7 +1102,10 @@ function generate(idl, opts) {
|
|
|
1102
1102
|
// A seed or program path names an account: in the same group first, then at the top level.
|
|
1103
1103
|
const byPath = (p, group) => flat.find((f) => `${f.group}${f.name}` === `${group}${p}`) ?? flat.find((f) => f.group === "" && f.name === p);
|
|
1104
1104
|
const argType = new Map(ix.args.map((a) => [a.name, a.type]));
|
|
1105
|
-
const
|
|
1105
|
+
const seedsProp = (k) => `seeds${NAME.test(k) && !JS_RESERVED.has(k) ? `.${k}` : `[${JSON.stringify(k)}]`}`;
|
|
1106
|
+
const BUILDER = { arg: (n) => `args.${n}`, account: (k) => `$${k}` };
|
|
1107
|
+
const HELPER = { arg: (n) => seedsProp(n), account: (k) => seedsProp(k) };
|
|
1108
|
+
const seedExpr = (s, group, refs = BUILDER) => {
|
|
1106
1109
|
if (s.kind === "const")
|
|
1107
1110
|
return bytes(s.value);
|
|
1108
1111
|
if (s.kind === "arg") {
|
|
@@ -1111,7 +1114,7 @@ function generate(idl, opts) {
|
|
|
1111
1114
|
let t = argType.get(head);
|
|
1112
1115
|
if (t === undefined)
|
|
1113
1116
|
return { why: `the seed argument ${head} does not exist` };
|
|
1114
|
-
let v =
|
|
1117
|
+
let v = refs.arg(camel(head));
|
|
1115
1118
|
for (const part of rest) {
|
|
1116
1119
|
const d = defs.get(definedName(t) ?? "");
|
|
1117
1120
|
const f = d?.type.kind === "struct" ? (d.type.fields ?? []).find((x) => isField(x) && x.name === part) : undefined;
|
|
@@ -1134,25 +1137,30 @@ function generate(idl, opts) {
|
|
|
1134
1137
|
if (!target)
|
|
1135
1138
|
return { why: `the seed account ${s.path} is not an account of the instruction` };
|
|
1136
1139
|
use("getAddressEncoder");
|
|
1137
|
-
return `getAddressEncoder().encode(key(
|
|
1140
|
+
return `getAddressEncoder().encode(key(${refs.account(target.key)}))`;
|
|
1138
1141
|
};
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if (!a.pda || a.address)
|
|
1142
|
-
continue;
|
|
1143
|
-
const seeds = a.pda.seeds.map((s) => seedExpr(s, a.group));
|
|
1144
|
-
let program;
|
|
1142
|
+
/** A PDA's program as the code being written names it (undefined: this program). */
|
|
1143
|
+
const pdaProgram = (a, refs) => {
|
|
1145
1144
|
const p = a.pda.program;
|
|
1146
1145
|
if (p?.kind === "const")
|
|
1147
|
-
|
|
1148
|
-
|
|
1146
|
+
return (use("getAddressDecoder"), `programFromBytes(${bytes(p.value)})`);
|
|
1147
|
+
if (p?.kind === "account") {
|
|
1149
1148
|
const target = p.path.includes(".") ? undefined : byPath(p.path, a.group);
|
|
1150
|
-
|
|
1149
|
+
return target ? `key(${refs.account(target.key)})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
|
|
1151
1150
|
}
|
|
1152
|
-
|
|
1151
|
+
if (p?.kind === "arg") {
|
|
1153
1152
|
const t = p.path.includes(".") ? undefined : argType.get(p.path);
|
|
1154
|
-
|
|
1153
|
+
return t === "pubkey" || t === "publicKey" ? refs.arg(camel(p.path)) : { why: "the PDA program comes from an argument that is not a Pubkey" };
|
|
1155
1154
|
}
|
|
1155
|
+
return undefined;
|
|
1156
|
+
};
|
|
1157
|
+
const resolvable = new Map();
|
|
1158
|
+
for (const a of flat) {
|
|
1159
|
+
if (!a.pda || a.address)
|
|
1160
|
+
continue;
|
|
1161
|
+
const seeds = a.pda.seeds.map((s) => seedExpr(s, a.group));
|
|
1162
|
+
const program = pdaProgram(a, BUILDER);
|
|
1163
|
+
const p = a.pda.program;
|
|
1156
1164
|
const why = [...seeds, ...(program === undefined ? [] : [program])].find((x) => typeof x === "object");
|
|
1157
1165
|
if (why) {
|
|
1158
1166
|
notes.push(`instruction ${ix.name}: ${a.name} is not filled in (${why.why}); pass it`);
|
|
@@ -1228,11 +1236,13 @@ function generate(idl, opts) {
|
|
|
1228
1236
|
return [k, `${k}: Address | TransactionSigner`];
|
|
1229
1237
|
})).values(),
|
|
1230
1238
|
];
|
|
1231
|
-
|
|
1239
|
+
// The same derivation, its inputs named as the helper's `seeds` parameter.
|
|
1240
|
+
const helperSeeds = a.pda.seeds.map((s) => seedExpr(s, a.group, HELPER));
|
|
1241
|
+
const helperProgram = pdaProgram(a, HELPER) ?? "PROGRAM_ADDRESS";
|
|
1232
1242
|
use("type ProgramDerivedAddress");
|
|
1233
1243
|
pdaFns.set(pn, {
|
|
1234
1244
|
json,
|
|
1235
|
-
source: ` /** The ${a.name} PDA (as ${ix.name} derives it). */\n ${prop(pn)}(seeds: { ${params.join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${
|
|
1245
|
+
source: ` /** The ${a.name} PDA (as ${ix.name} derives it). */\n ${prop(pn)}(seeds: { ${params.join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${helperProgram}, seeds: [${helperSeeds.join(", ")}] });\n },`,
|
|
1236
1246
|
});
|
|
1237
1247
|
}
|
|
1238
1248
|
const metas = flat.map((a) => {
|
|
@@ -1326,6 +1336,7 @@ function generate(idl, opts) {
|
|
|
1326
1336
|
}
|
|
1327
1337
|
const bases = new Map();
|
|
1328
1338
|
const seeds = [];
|
|
1339
|
+
const helperSeeds = []; // the same seeds, named as a `pdas` helper's `seeds` (for one seeded from its own fields)
|
|
1329
1340
|
const own = [];
|
|
1330
1341
|
// Inputs of a `pdas` helper for a PDA seeded from its own fields: its own field values, accounts and arguments.
|
|
1331
1342
|
const params = new Map();
|
|
@@ -1334,13 +1345,14 @@ function generate(idl, opts) {
|
|
|
1334
1345
|
let usesArgs = p?.kind === "arg";
|
|
1335
1346
|
for (const sd of a.pda.seeds) {
|
|
1336
1347
|
if (sd.kind === "const")
|
|
1337
|
-
seeds.push(bytes(sd.value));
|
|
1348
|
+
(seeds.push(bytes(sd.value)), helperSeeds.push(bytes(sd.value)));
|
|
1338
1349
|
else if (sd.kind === "arg") {
|
|
1339
1350
|
const e = seedExpr(sd, a.group);
|
|
1340
1351
|
if (typeof e !== "string")
|
|
1341
1352
|
ok = false;
|
|
1342
1353
|
else {
|
|
1343
1354
|
seeds.push(e);
|
|
1355
|
+
helperSeeds.push(seedExpr(sd, a.group, HELPER));
|
|
1344
1356
|
usesArgs = true;
|
|
1345
1357
|
const head = sd.path.split(".")[0];
|
|
1346
1358
|
params.set(camel(head), `${camel(head)}: ${tsType(argType.get(head), "in")}`);
|
|
@@ -1353,6 +1365,7 @@ function generate(idl, opts) {
|
|
|
1353
1365
|
else {
|
|
1354
1366
|
use("getAddressEncoder");
|
|
1355
1367
|
seeds.push(`getAddressEncoder().encode(key(${acc(t)}!))`);
|
|
1368
|
+
helperSeeds.push(`getAddressEncoder().encode(key(${seedsProp(t.key)}))`);
|
|
1356
1369
|
params.set(t.key, `${t.key}: Address | TransactionSigner`);
|
|
1357
1370
|
if (auto(t))
|
|
1358
1371
|
autoDep = true;
|
|
@@ -1370,6 +1383,7 @@ function generate(idl, opts) {
|
|
|
1370
1383
|
own.push({ field: parts[1], type: field.type });
|
|
1371
1384
|
params.set(camel(parts[1]), `${camel(parts[1])}: ${tsType(field.type, "in")}`);
|
|
1372
1385
|
seeds.push(enc);
|
|
1386
|
+
helperSeeds.push(enc);
|
|
1373
1387
|
}
|
|
1374
1388
|
else if (!consts || auto(base))
|
|
1375
1389
|
ok = false;
|
|
@@ -1399,13 +1413,11 @@ function generate(idl, opts) {
|
|
|
1399
1413
|
continue;
|
|
1400
1414
|
if (p?.kind === "arg")
|
|
1401
1415
|
params.set(camel(p.path), `${camel(p.path)}: Address`);
|
|
1402
|
-
const
|
|
1403
|
-
.replace(/\bargs\.([A-Za-z0-9_$]+)/g, "seeds.$1")
|
|
1404
|
-
.replace(/key\(ownProp\(acc, "([^"]+)"\)!\)/g, (_, k) => `key(seeds${NAME.test(k) && !JS_RESERVED.has(k) ? `.${k}` : `[${JSON.stringify(k)}]`})`);
|
|
1416
|
+
const helperProgram = p?.kind === "arg" ? seedsProp(camel(p.path)) : program;
|
|
1405
1417
|
use("type ProgramDerivedAddress", "getProgramDerivedAddress");
|
|
1406
1418
|
pdaFns.set(pn, {
|
|
1407
1419
|
json,
|
|
1408
|
-
source: ` /** The ${a.name} PDA (as ${ix.name} derives it), from the values of its own fields it is seeded with (${own.map((o) => camel(o.field)).join(", ")}). */\n ${prop(pn)}(seeds: { ${[...params.values()].join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${
|
|
1420
|
+
source: ` /** The ${a.name} PDA (as ${ix.name} derives it), from the values of its own fields it is seeded with (${own.map((o) => camel(o.field)).join(", ")}). */\n ${prop(pn)}(seeds: { ${[...params.values()].join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${helperProgram}, seeds: [${helperSeeds.join(", ")}] });\n },`,
|
|
1409
1421
|
});
|
|
1410
1422
|
continue;
|
|
1411
1423
|
}
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -680,7 +680,7 @@ export function analyze(entry) {
|
|
|
680
680
|
dataEnums,
|
|
681
681
|
options,
|
|
682
682
|
constants,
|
|
683
|
-
events:
|
|
683
|
+
events: collectEvents([...functions, ...helperNodes], structs),
|
|
684
684
|
accountStructs,
|
|
685
685
|
argStructs,
|
|
686
686
|
fstrings,
|
|
@@ -1164,6 +1164,26 @@ function fnParamType(arrow, checker) {
|
|
|
1164
1164
|
}
|
|
1165
1165
|
return undefined;
|
|
1166
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* The structs a program emits (`emit<T>(…)` / `emitCpi<T>(…)` anywhere in its functions and helpers), known before
|
|
1169
|
+
* any code is emitted, so the IDL and the event checks do not depend on the order code is emitted in.
|
|
1170
|
+
*/
|
|
1171
|
+
function collectEvents(sources, structs) {
|
|
1172
|
+
const events = new Set();
|
|
1173
|
+
for (const { node, imports } of sources) {
|
|
1174
|
+
const visit = (n) => {
|
|
1175
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
1176
|
+
const name = imports.get(n.expression.text);
|
|
1177
|
+
const t = n.typeArguments?.[0];
|
|
1178
|
+
if ((name === "emit" || name === "emitCpi") && t && ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && structs.has(t.typeName.text))
|
|
1179
|
+
events.add(t.typeName.text);
|
|
1180
|
+
}
|
|
1181
|
+
ts.forEachChild(n, visit);
|
|
1182
|
+
};
|
|
1183
|
+
visit(node);
|
|
1184
|
+
}
|
|
1185
|
+
return events;
|
|
1186
|
+
}
|
|
1167
1187
|
/** Whether a function returns a function: typed so, or its body is (or only returns) an arrow. */
|
|
1168
1188
|
function returnsFunction(fn) {
|
|
1169
1189
|
if (fn.type)
|
|
@@ -24,6 +24,16 @@ export declare abstract class EmitterBase {
|
|
|
24
24
|
protected readonly pendingInit: Set<string>;
|
|
25
25
|
/** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
|
|
26
26
|
protected readonly bumps: Map<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Seed arrays already declared (no spare slot, every seed read in place): the C entries, and where the array is.
|
|
29
|
+
* An identical list in the same or an inner block reuses it (see seeds()).
|
|
30
|
+
*/
|
|
31
|
+
protected readonly seedArrays: Map<string, {
|
|
32
|
+
name: string;
|
|
33
|
+
scope: Map<string, Local>;
|
|
34
|
+
at: number;
|
|
35
|
+
text: string;
|
|
36
|
+
}>;
|
|
27
37
|
/** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
|
|
28
38
|
protected inlineFrames: {
|
|
29
39
|
label: string;
|
|
@@ -23,6 +23,11 @@ export class EmitterBase {
|
|
|
23
23
|
pendingInit = new Set();
|
|
24
24
|
/** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
|
|
25
25
|
bumps = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Seed arrays already declared (no spare slot, every seed read in place): the C entries, and where the array is.
|
|
28
|
+
* An identical list in the same or an inner block reuses it (see seeds()).
|
|
29
|
+
*/
|
|
30
|
+
seedArrays = new Map();
|
|
26
31
|
/** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
|
|
27
32
|
inlineFrames = [];
|
|
28
33
|
/** Closures being expanded (a closure calling itself cannot be expanded). */
|
|
@@ -300,6 +305,7 @@ export class EmitterBase {
|
|
|
300
305
|
created: new Set(this.created),
|
|
301
306
|
pendingInit: new Set(this.pendingInit),
|
|
302
307
|
bumps: new Map(this.bumps),
|
|
308
|
+
seedArrays: new Map(this.seedArrays),
|
|
303
309
|
};
|
|
304
310
|
try {
|
|
305
311
|
return this.reporter.guard(fn);
|
|
@@ -322,6 +328,8 @@ export class EmitterBase {
|
|
|
322
328
|
}
|
|
323
329
|
this.bumps.clear();
|
|
324
330
|
saved.bumps.forEach((v, k) => this.bumps.set(k, v));
|
|
331
|
+
this.seedArrays.clear();
|
|
332
|
+
saved.seedArrays.forEach((v, k) => this.seedArrays.set(k, v));
|
|
325
333
|
}
|
|
326
334
|
}
|
|
327
335
|
fail(node, message) {
|
|
@@ -103,8 +103,17 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
103
103
|
protected corePlugin(n: ts.Expression, withAuthority: boolean): MplField[];
|
|
104
104
|
/** CreateV1 / CreateCollectionV1 plugins: Option<Vec<PluginAuthorityPair>>. */
|
|
105
105
|
protected corePluginList(n: ts.Expression | undefined): MplField[];
|
|
106
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Metaplex Core: assets and collections (with plugins), plugins and external plugin adapters added, updated, written
|
|
108
|
+
* and removed, transfer and burn. Each method is a row of CORE_OPS below: its accounts in order, its data.
|
|
109
|
+
*/
|
|
107
110
|
protected coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Core instructions: `args` fixed arguments (then an optional plugin list when `plugins`, then optional signer
|
|
113
|
+
* seeds), accounts in order (`[argument, label, writable, signer]`, or "self": the Core program, Core's "none" for an
|
|
114
|
+
* optional account), at most `max` bytes of data.
|
|
115
|
+
*/
|
|
116
|
+
private coreOps;
|
|
108
117
|
/** The fields of a Core object literal (`{ kind: "AppData", … }`) by name. */
|
|
109
118
|
private coreProps;
|
|
110
119
|
/** PluginAuthority: "none" | "owner" | "updateAuthority" (literals), or an address. */
|
|
@@ -207,6 +207,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
207
207
|
ts.isPostfixUnaryExpression(n) ||
|
|
208
208
|
ts.forEachChild(n, mutates) === true;
|
|
209
209
|
const inPlace = !node.elements.some(mutates);
|
|
210
|
+
const mark = this.lines.length;
|
|
210
211
|
for (const el of node.elements) {
|
|
211
212
|
const desc = this.describeSeed(el);
|
|
212
213
|
if (!desc)
|
|
@@ -252,9 +253,19 @@ export class CpiEmitter extends CallEmitter {
|
|
|
252
253
|
else
|
|
253
254
|
this.fail(el, "seeds can be strings, Pubkeys, integers or unit enum values");
|
|
254
255
|
}
|
|
256
|
+
// The same list again (escrow: the vault transfer, then its close) reuses the array declared for the first, when
|
|
257
|
+
// nothing was copied into temporaries (every entry points at a literal or at the place itself, so the array reads
|
|
258
|
+
// what a new one would), it has no spare slot (callers never write into those arrays) and it is still in scope.
|
|
259
|
+
const key = parts.join(", ");
|
|
260
|
+
const reusable = extra === 0 && parts.length > 0 && this.lines.length === mark;
|
|
261
|
+
const known = reusable ? this.seedArrays.get(key) : undefined;
|
|
262
|
+
if (known && this.scopes.includes(known.scope) && this.lines[known.at]?.trim() === known.text)
|
|
263
|
+
return { name: known.name, count: parts.length, idl };
|
|
255
264
|
const name = this.tmp("seeds");
|
|
256
265
|
const slots = parts.length + extra;
|
|
257
266
|
this.line(`SolSignerSeed ${name}[${Math.max(slots, 1)}] = {${parts.join(", ")}};`);
|
|
267
|
+
if (reusable)
|
|
268
|
+
this.seedArrays.set(key, { name, scope: this.scopes[this.scopes.length - 1], at: this.lines.length - 1, text: this.lines.at(-1).trim() });
|
|
258
269
|
return { name, count: parts.length, idl };
|
|
259
270
|
}
|
|
260
271
|
describeSeed(el) {
|
|
@@ -1715,179 +1726,180 @@ export class CpiEmitter extends CallEmitter {
|
|
|
1715
1726
|
});
|
|
1716
1727
|
return [{ bytes: [1, els.length & 0xff, 0, 0, 0] }, ...els.flatMap((e) => this.corePlugin(e, true))];
|
|
1717
1728
|
}
|
|
1718
|
-
/**
|
|
1729
|
+
/**
|
|
1730
|
+
* Metaplex Core: assets and collections (with plugins), plugins and external plugin adapters added, updated, written
|
|
1731
|
+
* and removed, transfer and burn. Each method is a row of CORE_OPS below: its accounts in order, its data.
|
|
1732
|
+
*/
|
|
1719
1733
|
coreOp(node, core, method) {
|
|
1720
1734
|
const a = node.arguments;
|
|
1721
1735
|
const p = `a_${core.name}`;
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1736
|
+
const op = this.coreOps()[method];
|
|
1737
|
+
if (!op)
|
|
1738
|
+
this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin, addExternalPlugin, removeExternalPlugin, writeExternalPluginData (and the Collection forms), transferAsset and burnAsset, not ${method}`);
|
|
1739
|
+
// An array literal of object literals right after the fixed arguments is the plugin list; the signer seeds follow.
|
|
1725
1740
|
const isPlugins = (n) => !!n && ts.isArrayLiteralExpression(n) && (n.elements.length === 0 || n.elements.every((e) => ts.isObjectLiteralExpression(e)));
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1741
|
+
const list = op.plugins && isPlugins(a[op.args]) ? a[op.args] : undefined;
|
|
1742
|
+
const seedsAt = op.args + (list ? 1 : 0);
|
|
1743
|
+
if (op.plugins ? a.length < op.args || a.length > seedsAt + 1 : a.length !== op.args && a.length !== op.args + 1)
|
|
1744
|
+
this.fail(node, op.usage(method));
|
|
1745
|
+
const metas = op.metas.map((m) => (m === "self" ? [p, false, false] : this.mplMeta(a[m[0]], core, m[1], m[2], m[3])));
|
|
1746
|
+
const data = this.mplData(node, op.max, op.data(a, list));
|
|
1747
|
+
this.cpi(p, metas, data.buf, data.len, a[seedsAt] ? this.signingSeeds(a[seedsAt]) : { name: "NULL", count: 0 });
|
|
1748
|
+
return undefined;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Core instructions: `args` fixed arguments (then an optional plugin list when `plugins`, then optional signer
|
|
1752
|
+
* seeds), accounts in order (`[argument, label, writable, signer]`, or "self": the Core program, Core's "none" for an
|
|
1753
|
+
* optional account), at most `max` bytes of data.
|
|
1754
|
+
*/
|
|
1755
|
+
coreOps() {
|
|
1756
|
+
const str = (n) => ({ value: n, type: STRING });
|
|
1757
|
+
const asset = [0, "the asset", true, false];
|
|
1758
|
+
const assetPlugin = {
|
|
1759
|
+
usage: (m) => `${m}(asset, collection | null, payer, authority | null, systemProgram, ${m === "removePlugin" ? "kind" : "plugin"}, signerSeeds?)`,
|
|
1760
|
+
args: 6,
|
|
1761
|
+
// Add/Remove/UpdatePluginV1: asset, collection, payer, authority, systemProgram, logWrapper.
|
|
1762
|
+
metas: [
|
|
1763
|
+
asset,
|
|
1764
|
+
[1, "the collection", true, false],
|
|
1765
|
+
[2, "the payer", true, true],
|
|
1766
|
+
[3, "the authority", false, true],
|
|
1767
|
+
[4, "the system program", false, false],
|
|
1768
|
+
"self",
|
|
1769
|
+
],
|
|
1770
|
+
max: 2048,
|
|
1771
|
+
};
|
|
1772
|
+
const collectionPlugin = {
|
|
1773
|
+
usage: (m) => `${m}(collection, payer, authority | null, systemProgram, ${m === "removeCollectionPlugin" ? "kind" : "plugin"}, signerSeeds?)`,
|
|
1774
|
+
args: 5,
|
|
1775
|
+
// Add/Remove/UpdateCollectionPluginV1: collection, payer, authority, systemProgram, logWrapper.
|
|
1776
|
+
metas: [
|
|
1777
|
+
[0, "the collection", true, false],
|
|
1778
|
+
[1, "the payer", true, true],
|
|
1779
|
+
[2, "the authority", false, true],
|
|
1780
|
+
[3, "the system program", false, false],
|
|
1781
|
+
"self",
|
|
1782
|
+
],
|
|
1783
|
+
max: 2048,
|
|
1784
|
+
};
|
|
1785
|
+
const assetPluginOp = (method, disc) => ({
|
|
1786
|
+
...assetPlugin,
|
|
1787
|
+
data: (a) => [
|
|
1788
|
+
{ bytes: [disc] },
|
|
1789
|
+
...(method === "removePlugin" ? [this.corePluginType(a[5])] : this.corePlugin(a[5], method === "addPlugin")),
|
|
1790
|
+
],
|
|
1791
|
+
});
|
|
1792
|
+
const collectionPluginOp = (method, disc) => ({
|
|
1793
|
+
...collectionPlugin,
|
|
1794
|
+
data: (a) => [
|
|
1795
|
+
{ bytes: [disc] },
|
|
1796
|
+
...(method === "removeCollectionPlugin" ? [this.corePluginType(a[4])] : this.corePlugin(a[4], method === "addCollectionPlugin")),
|
|
1797
|
+
],
|
|
1798
|
+
});
|
|
1799
|
+
// Add/RemoveExternalPluginAdapterV1: asset, collection, payer, authority, systemProgram, logWrapper; the write has a
|
|
1800
|
+
// buffer account (left out: the data is in the instruction) before systemProgram. The Collection forms drop the asset.
|
|
1801
|
+
const external = (method, disc, collection) => {
|
|
1802
|
+
const write = method.startsWith("write");
|
|
1803
|
+
const add = method.startsWith("add");
|
|
1804
|
+
const n = (collection ? 5 : 6) + (write ? 1 : 0);
|
|
1805
|
+
const at = collection ? 4 : 5;
|
|
1806
|
+
const head = collection ? "collection, payer, authority | null, systemProgram" : "asset, collection | null, payer, authority | null, systemProgram";
|
|
1807
|
+
const metas = collection
|
|
1808
|
+
? [
|
|
1809
|
+
[0, "the collection", true, false],
|
|
1810
|
+
[1, "the payer", true, true],
|
|
1811
|
+
[2, "the authority", false, true],
|
|
1812
|
+
]
|
|
1813
|
+
: [asset, [1, "the collection", true, false], [2, "the payer", true, true], [3, "the authority", false, true]];
|
|
1814
|
+
if (write)
|
|
1815
|
+
metas.push("self");
|
|
1816
|
+
metas.push([collection ? 3 : 4, "the system program", false, false], "self");
|
|
1817
|
+
return {
|
|
1818
|
+
usage: (m) => `${m}(${head}, ${add ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`,
|
|
1819
|
+
args: n,
|
|
1820
|
+
metas,
|
|
1821
|
+
max: 1024,
|
|
1822
|
+
data: (a) => [
|
|
1823
|
+
{ bytes: [disc] },
|
|
1824
|
+
...(add
|
|
1825
|
+
? this.coreExternalPlugin(a[at])
|
|
1826
|
+
: write
|
|
1827
|
+
? [...this.coreExternalKey(a[at]), ...this.coreExternalData(a[at + 1])]
|
|
1828
|
+
: this.coreExternalKey(a[at])),
|
|
1829
|
+
],
|
|
1830
|
+
};
|
|
1831
|
+
};
|
|
1832
|
+
return {
|
|
1833
|
+
createAsset: {
|
|
1834
|
+
usage: () => "createAsset(asset, collection | null, authority | null, payer, owner | null, systemProgram, name, uri, plugins?, signerSeeds?)",
|
|
1835
|
+
args: 8,
|
|
1836
|
+
plugins: true,
|
|
1837
|
+
// CreateV1: asset, collection, authority, payer, owner, updateAuthority (the collection's, or the payer by
|
|
1838
|
+
// default), systemProgram, logWrapper.
|
|
1839
|
+
metas: [
|
|
1840
|
+
[0, "the asset", true, true],
|
|
1841
|
+
[1, "the collection", true, false],
|
|
1842
|
+
[2, "the collection authority", false, true],
|
|
1843
|
+
[3, "the payer", true, true],
|
|
1844
|
+
[4, "the owner", false, false],
|
|
1845
|
+
"self",
|
|
1846
|
+
[5, "the system program", false, false],
|
|
1847
|
+
"self",
|
|
1848
|
+
],
|
|
1849
|
+
max: 2048,
|
|
1850
|
+
data: (a, plugins) => [{ bytes: [0, 0] }, str(a[6]), str(a[7]), ...this.corePluginList(plugins)],
|
|
1851
|
+
},
|
|
1852
|
+
createCollection: {
|
|
1853
|
+
usage: () => "createCollection(collection, updateAuthority | null, payer, systemProgram, name, uri, plugins?, signerSeeds?)",
|
|
1854
|
+
args: 6,
|
|
1855
|
+
plugins: true,
|
|
1748
1856
|
// CreateCollectionV1: collection, updateAuthority, payer, systemProgram.
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
if (a.length !== 6 && a.length !== 7)
|
|
1768
|
-
this.fail(node, `${method}(asset, collection | null, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
|
|
1769
|
-
const metas = [
|
|
1770
|
-
this.mplMeta(a[0], core, "the asset", true, false),
|
|
1771
|
-
this.mplMeta(a[1], core, "the collection", true, false),
|
|
1772
|
-
this.mplMeta(a[2], core, "the payer", true, true),
|
|
1773
|
-
this.mplMeta(a[3], core, "the authority", false, true),
|
|
1774
|
-
this.mplMeta(a[4], core, "the system program", false, false),
|
|
1775
|
-
self,
|
|
1776
|
-
];
|
|
1777
|
-
const body = method === "removePlugin" ? [this.corePluginType(a[5])] : this.corePlugin(a[5], method === "addPlugin");
|
|
1778
|
-
const data = this.mplData(node, 2048, [{ bytes: [{ addPlugin: 2, removePlugin: 4, updatePlugin: 6 }[method]] }, ...body]);
|
|
1779
|
-
this.cpi(p, metas, data.buf, data.len, seedsAt(6));
|
|
1780
|
-
return undefined;
|
|
1781
|
-
}
|
|
1782
|
-
case "addCollectionPlugin":
|
|
1783
|
-
case "updateCollectionPlugin":
|
|
1784
|
-
case "removeCollectionPlugin": {
|
|
1785
|
-
// Add/Remove/UpdateCollectionPluginV1: collection, payer, authority, systemProgram, logWrapper.
|
|
1786
|
-
const last = method === "removeCollectionPlugin" ? "kind" : "plugin";
|
|
1787
|
-
if (a.length !== 5 && a.length !== 6)
|
|
1788
|
-
this.fail(node, `${method}(collection, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
|
|
1789
|
-
const metas = [
|
|
1790
|
-
this.mplMeta(a[0], core, "the collection", true, false),
|
|
1791
|
-
this.mplMeta(a[1], core, "the payer", true, true),
|
|
1792
|
-
this.mplMeta(a[2], core, "the authority", false, true),
|
|
1793
|
-
this.mplMeta(a[3], core, "the system program", false, false),
|
|
1794
|
-
self,
|
|
1795
|
-
];
|
|
1796
|
-
const body = method === "removeCollectionPlugin" ? [this.corePluginType(a[4])] : this.corePlugin(a[4], method === "addCollectionPlugin");
|
|
1797
|
-
const data = this.mplData(node, 2048, [{ bytes: [{ addCollectionPlugin: 3, removeCollectionPlugin: 5, updateCollectionPlugin: 7 }[method]] }, ...body]);
|
|
1798
|
-
this.cpi(p, metas, data.buf, data.len, seedsAt(5));
|
|
1799
|
-
return undefined;
|
|
1800
|
-
}
|
|
1801
|
-
case "transferAsset": {
|
|
1857
|
+
metas: [
|
|
1858
|
+
[0, "the collection", true, true],
|
|
1859
|
+
[1, "the update authority", false, false],
|
|
1860
|
+
[2, "the payer", true, true],
|
|
1861
|
+
[3, "the system program", false, false],
|
|
1862
|
+
],
|
|
1863
|
+
max: 2048,
|
|
1864
|
+
data: (a, plugins) => [{ bytes: [1] }, str(a[4]), str(a[5]), ...this.corePluginList(plugins)],
|
|
1865
|
+
},
|
|
1866
|
+
addPlugin: assetPluginOp("addPlugin", 2),
|
|
1867
|
+
removePlugin: assetPluginOp("removePlugin", 4),
|
|
1868
|
+
updatePlugin: assetPluginOp("updatePlugin", 6),
|
|
1869
|
+
addCollectionPlugin: collectionPluginOp("addCollectionPlugin", 3),
|
|
1870
|
+
removeCollectionPlugin: collectionPluginOp("removeCollectionPlugin", 5),
|
|
1871
|
+
updateCollectionPlugin: collectionPluginOp("updateCollectionPlugin", 7),
|
|
1872
|
+
transferAsset: {
|
|
1873
|
+
usage: () => "transferAsset(asset, collection | null, payer, authority | null, newOwner, signerSeeds?)",
|
|
1874
|
+
args: 5,
|
|
1802
1875
|
// TransferV1: asset, collection, payer, authority, newOwner, systemProgram, logWrapper.
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
]
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
case "burnAsset": {
|
|
1876
|
+
metas: [
|
|
1877
|
+
asset,
|
|
1878
|
+
[1, "the collection", false, false],
|
|
1879
|
+
[2, "the payer", true, true],
|
|
1880
|
+
[3, "the authority", false, true],
|
|
1881
|
+
[4, "the new owner", false, false],
|
|
1882
|
+
"self",
|
|
1883
|
+
"self",
|
|
1884
|
+
],
|
|
1885
|
+
max: 8,
|
|
1886
|
+
data: () => [{ bytes: [14, 0] }],
|
|
1887
|
+
},
|
|
1888
|
+
burnAsset: {
|
|
1889
|
+
usage: () => "burnAsset(asset, collection | null, payer, authority | null, signerSeeds?)",
|
|
1890
|
+
args: 4,
|
|
1819
1891
|
// BurnV1: asset, collection, payer, authority, systemProgram, logWrapper.
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
this.cpi(p, metas, data.buf, data.len, seedsAt(4));
|
|
1832
|
-
return undefined;
|
|
1833
|
-
}
|
|
1834
|
-
case "addExternalPlugin":
|
|
1835
|
-
case "removeExternalPlugin":
|
|
1836
|
-
case "writeExternalPluginData": {
|
|
1837
|
-
// Add/RemoveExternalPluginAdapterV1: asset, collection, payer, authority, systemProgram, logWrapper;
|
|
1838
|
-
// WriteExternalPluginAdapterDataV1 has a buffer account (left out: the data is in the instruction) before systemProgram.
|
|
1839
|
-
const write = method === "writeExternalPluginData";
|
|
1840
|
-
const n = write ? 7 : 6;
|
|
1841
|
-
if (a.length !== n && a.length !== n + 1)
|
|
1842
|
-
this.fail(node, `${method}(asset, collection | null, payer, authority | null, systemProgram, ${method === "addExternalPlugin" ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`);
|
|
1843
|
-
const metas = [
|
|
1844
|
-
this.mplMeta(a[0], core, "the asset", true, false),
|
|
1845
|
-
this.mplMeta(a[1], core, "the collection", true, false),
|
|
1846
|
-
this.mplMeta(a[2], core, "the payer", true, true),
|
|
1847
|
-
this.mplMeta(a[3], core, "the authority", false, true),
|
|
1848
|
-
...(write ? [self] : []),
|
|
1849
|
-
this.mplMeta(a[4], core, "the system program", false, false),
|
|
1850
|
-
self,
|
|
1851
|
-
];
|
|
1852
|
-
const body = method === "addExternalPlugin"
|
|
1853
|
-
? this.coreExternalPlugin(a[5])
|
|
1854
|
-
: write
|
|
1855
|
-
? [...this.coreExternalKey(a[5]), ...this.coreExternalData(a[6])]
|
|
1856
|
-
: this.coreExternalKey(a[5]);
|
|
1857
|
-
const data = this.mplData(node, 1024, [{ bytes: [{ addExternalPlugin: 22, removeExternalPlugin: 24, writeExternalPluginData: 28 }[method]] }, ...body]);
|
|
1858
|
-
this.cpi(p, metas, data.buf, data.len, seedsAt(n));
|
|
1859
|
-
return undefined;
|
|
1860
|
-
}
|
|
1861
|
-
case "addCollectionExternalPlugin":
|
|
1862
|
-
case "removeCollectionExternalPlugin":
|
|
1863
|
-
case "writeCollectionExternalPluginData": {
|
|
1864
|
-
// The same on a collection: collection, payer, authority, (buffer,) systemProgram, logWrapper.
|
|
1865
|
-
const write = method === "writeCollectionExternalPluginData";
|
|
1866
|
-
const n = write ? 6 : 5;
|
|
1867
|
-
if (a.length !== n && a.length !== n + 1)
|
|
1868
|
-
this.fail(node, `${method}(collection, payer, authority | null, systemProgram, ${method === "addCollectionExternalPlugin" ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`);
|
|
1869
|
-
const metas = [
|
|
1870
|
-
this.mplMeta(a[0], core, "the collection", true, false),
|
|
1871
|
-
this.mplMeta(a[1], core, "the payer", true, true),
|
|
1872
|
-
this.mplMeta(a[2], core, "the authority", false, true),
|
|
1873
|
-
...(write ? [self] : []),
|
|
1874
|
-
this.mplMeta(a[3], core, "the system program", false, false),
|
|
1875
|
-
self,
|
|
1876
|
-
];
|
|
1877
|
-
const body = method === "addCollectionExternalPlugin"
|
|
1878
|
-
? this.coreExternalPlugin(a[4])
|
|
1879
|
-
: write
|
|
1880
|
-
? [...this.coreExternalKey(a[4]), ...this.coreExternalData(a[5])]
|
|
1881
|
-
: this.coreExternalKey(a[4]);
|
|
1882
|
-
const data = this.mplData(node, 1024, [
|
|
1883
|
-
{ bytes: [{ addCollectionExternalPlugin: 23, removeCollectionExternalPlugin: 25, writeCollectionExternalPluginData: 29 }[method]] },
|
|
1884
|
-
...body,
|
|
1885
|
-
]);
|
|
1886
|
-
this.cpi(p, metas, data.buf, data.len, seedsAt(n));
|
|
1887
|
-
return undefined;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin, addExternalPlugin, removeExternalPlugin, writeExternalPluginData (and the Collection forms), transferAsset and burnAsset, not ${method}`);
|
|
1892
|
+
metas: [asset, [1, "the collection", true, false], [2, "the payer", true, true], [3, "the authority", false, true], "self", "self"],
|
|
1893
|
+
max: 8,
|
|
1894
|
+
data: () => [{ bytes: [12, 0] }],
|
|
1895
|
+
},
|
|
1896
|
+
addExternalPlugin: external("addExternalPlugin", 22, false),
|
|
1897
|
+
removeExternalPlugin: external("removeExternalPlugin", 24, false),
|
|
1898
|
+
writeExternalPluginData: external("writeExternalPluginData", 28, false),
|
|
1899
|
+
addCollectionExternalPlugin: external("addCollectionExternalPlugin", 23, true),
|
|
1900
|
+
removeCollectionExternalPlugin: external("removeCollectionExternalPlugin", 25, true),
|
|
1901
|
+
writeCollectionExternalPluginData: external("writeCollectionExternalPluginData", 29, true),
|
|
1902
|
+
};
|
|
1891
1903
|
}
|
|
1892
1904
|
/** The fields of a Core object literal (`{ kind: "AppData", … }`) by name. */
|
|
1893
1905
|
coreProps(node, what) {
|
package/docs/architecture.md
CHANGED
|
@@ -22,6 +22,21 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
22
22
|
`compiler/index.ts` wires the stages together. `generate()` runs analyze, emit and IDL generation in memory, and
|
|
23
23
|
`build()` also compiles and writes the output files.
|
|
24
24
|
|
|
25
|
+
## Repository layout
|
|
26
|
+
|
|
27
|
+
| Path | Contents |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `src/` | The compiler (`src/compiler/`) and the tools around it: CLI, workspace, client, CPI and port generators, testing, IDL publishing (source map below) |
|
|
30
|
+
| `runtime/` | `typesol_rt.h`, the C runtime every program includes ([runtime.md](runtime.md)) |
|
|
31
|
+
| `lib/` | `typesol.d.ts`, the language surface programs typecheck against |
|
|
32
|
+
| `ts-plugin/` | The editor plugin (`typesol/ts-plugin`) |
|
|
33
|
+
| `examples/` | Example programs; the `typesol init` templates are copies of three of them |
|
|
34
|
+
| `test/` | The test suite: `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated clients), `programs/` (third-party program binaries, see its SOURCES.md) |
|
|
35
|
+
| `bench/anchor/` | Anchor twins of the counter, vault and escrow examples, for the differential tests (`pnpm bench:anchor`) and the zero-copy client test; the benchmarks themselves are in [typesol/benchmarks](https://github.com/typesol/benchmarks) |
|
|
36
|
+
| `scripts/` | `smoke.ts` and `localnet.sh` (a local validator run), `fetch-programs.sh` (third-party binaries), `readme-bench.ts` (the README numbers) |
|
|
37
|
+
| `docs/` | User and contributor documentation (also published on typesol.com) |
|
|
38
|
+
| `assets/` | The CLI's logo and the README images (`readme/`, generated) |
|
|
39
|
+
|
|
25
40
|
## Source map
|
|
26
41
|
|
|
27
42
|
| Path | Role |
|
|
@@ -34,11 +49,17 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
34
49
|
| `src/compiler/idl.ts` | Turns the model into an Anchor IDL: accounts with `pda`, `relations`, `address` and `optional`, plus types, events, errors and docs. |
|
|
35
50
|
| `src/compiler/toolchain.ts` | Finds the Agave SDK and runs clang and lld with the SDK's flags, plus per-function sections, `--gc-sections` and `--strip-all` so unused code and symbol tables stay out of the binary. |
|
|
36
51
|
| `src/compiler/diagnostics.ts` | `Reporter` and `CompileError`: diagnostics with file, line and column. |
|
|
52
|
+
| `src/compiler/anchor-errors.ts` | Anchor's own error codes (`AnchorError.X`), shared by `require` and the generated clients. |
|
|
53
|
+
| `src/compiler/index.ts` | The compiler's API (`generate`, `build`, `verifyBinary`, the build manifest). |
|
|
37
54
|
| `runtime/typesol_rt.h` | The C runtime: error codes, word-wise memory helpers, the entry point's input parser (`ts_deserialize`), account checks, checked math, Borsh helpers, CPIs (System, Token, Token-2022, ATA, Metaplex), oracles and the heap allocator. Header-only and `static inline`, so unused code is dropped. |
|
|
38
55
|
| `src/workspace.ts` | `typesol.json` projects, program keypairs, `typesol init` templates and deploy arguments. |
|
|
39
56
|
| `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
|
|
40
57
|
| `src/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
|
|
41
|
-
| `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program
|
|
58
|
+
| `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program, and reading a program's IDL back from the chain (Program Metadata or Anchor's IDL account) for `typesol client --address`. |
|
|
59
|
+
| `src/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
|
|
60
|
+
| `src/templates.ts` | The programs and tests `typesol init --template` writes (copies of `examples/`; a test checks they match). |
|
|
61
|
+
| `src/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
|
|
62
|
+
| `src/keys.ts` | Base58 and program keypair helpers. |
|
|
42
63
|
| `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
|
|
43
64
|
| `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
|
|
44
65
|
| `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
|
|
@@ -54,7 +75,7 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
|
|
|
54
75
|
|
|
55
76
|
| File | Layer | Contents |
|
|
56
77
|
| --- | --- | --- |
|
|
57
|
-
| `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`) |
|
|
78
|
+
| `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`), growable Vec placement (`vecPlace`, `vecLenCheck`) |
|
|
58
79
|
| `base.ts` | `EmitterBase` | function state; `line()`, `tmp()`, `settle()`, scopes, `fail()`, account lookup |
|
|
59
80
|
| `storage.ts` | `StorageEmitter` | references into account data and locals, reads and writes, indexing, aliasing, argument validation |
|
|
60
81
|
| `expressions.ts` | `ExpressionEmitter` | literals, operators and checked arithmetic, property reads, conditionals, text, casts |
|
|
@@ -63,6 +84,9 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
|
|
|
63
84
|
| `statements.ts` | `StatementEmitter` | variables, assignment, `if`/loops/`switch`, `return` |
|
|
64
85
|
| `function.ts` | `FunctionEmitter` | the instruction prologue (account checks, constraints, argument decoding) and epilogue |
|
|
65
86
|
|
|
87
|
+
`borsh.ts` writes the Borsh pack/unpack codecs of `@layout borsh` structs, and `program.ts` assembles the C file
|
|
88
|
+
(typedefs, codecs, helpers, instructions, the entry point). `index.ts` is the emitter's export.
|
|
89
|
+
|
|
66
90
|
## Rules the emitter keeps
|
|
67
91
|
|
|
68
92
|
These invariants are easy to break. Each one has tests.
|
package/docs/runtime.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# The C runtime
|
|
2
|
+
|
|
3
|
+
`runtime/typesol_rt.h` is included by every generated program. It holds what the emitted C calls: error codes, the
|
|
4
|
+
entry point's input parser, account checks, checked arithmetic, Borsh helpers, CPIs, sysvars, strings and the heap.
|
|
5
|
+
This page is for people changing it; programs never call it directly.
|
|
6
|
+
|
|
7
|
+
## Conventions
|
|
8
|
+
|
|
9
|
+
- **Header-only, `static inline`.** Each program compiles the header with its own code, and `--gc-sections` drops
|
|
10
|
+
what it does not use. `always_inline` marks the few helpers that must disappear into their caller (word loads and
|
|
11
|
+
stores, `ts_memcpy`, checked add/subtract); `noinline` marks the ones that should exist once per program because
|
|
12
|
+
many call sites would otherwise each grow the binary (`ts_fail`, the rent fallback `ts_rent_slow`, PDA search).
|
|
13
|
+
- **Errors are return values.** A function that can fail returns `uint64_t`: `SUCCESS`, or an error code. Fail with
|
|
14
|
+
`TS_FAIL(code, "TypeSOL: …")` (logs the message, then returns the code) and propagate with `TS_TRY(call)`.
|
|
15
|
+
Nothing aborts: the instruction returns the code, and the runtime reports it as `Custom(code)`.
|
|
16
|
+
- **Error codes follow Anchor.** Where Anchor has a framework error with the same meaning, its number is used (2006
|
|
17
|
+
ConstraintSeeds, 3003 AccountDidNotDeserialize, …), so Anchor and Codama clients name failures correctly.
|
|
18
|
+
TypeSOL-only conditions are 5100–5199; user errors start at 6000. Every code is listed in [errors.md](errors.md).
|
|
19
|
+
- **No byte loops from the SDK.** The SDK's `sol_memcpy`, `sol_memcmp` and `SolPubkey_same` move one byte per
|
|
20
|
+
iteration. The runtime uses `ts_ld64` / `ts_st64` (unaligned 8-byte loads and stores, which SBF allows) in
|
|
21
|
+
`ts_memcpy`, `ts_memzero`, `ts_mem_eq`, `ts_key_eq` and `ts_memmove`.
|
|
22
|
+
- **Same results as the reference.** Fast paths must give bit-identical results to what they replace: `ts_add_overflow_u64`
|
|
23
|
+
matches `__builtin_add_overflow`, the rent fast path and `ts_rent_slow` match the double computation, PDA search
|
|
24
|
+
matches `sol_try_find_program_address`. A change here needs a test against the reference, not only against itself.
|
|
25
|
+
|
|
26
|
+
## Sections
|
|
27
|
+
|
|
28
|
+
In file order:
|
|
29
|
+
|
|
30
|
+
| Section | What it provides |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| Error codes, `TS_FAIL`, `TS_TRY` | The codes above and the two macros |
|
|
33
|
+
| Word-wise memory helpers | `ts_ld64`, `ts_st64`, `ts_memcpy`, `ts_memmove`, `ts_memzero`, `ts_mem_eq`; checked u64 add/subtract/multiply |
|
|
34
|
+
| Well-known program ids | System, Token, Token-2022, ATA, Metaplex and the other programs the language names |
|
|
35
|
+
| Strings | `TS_Str` views (pointer, length), UTF-8 validation, comparison, `FixedString<N>` storage |
|
|
36
|
+
| Entry point input | `TsAccount` (the loader's serialized account, read in place), `TsParams`, `ts_deserialize` |
|
|
37
|
+
| Account checks | owner, discriminator, signer, writable, executable, address, size, uniqueness (`ts_check_*`), growable Vec lengths |
|
|
38
|
+
| Little-endian IO | `ts_read_le` and friends for argument decoding and Borsh |
|
|
39
|
+
| Sysvars | clock, rent (`ts_rent`, `ts_minimum_balance_of`, `ts_minimum_balance`), program data |
|
|
40
|
+
| PDA | `ts_pda_hash`, `ts_create_pda_address`, `ts_find_pda` (canonical bump), `ts_require_pda` |
|
|
41
|
+
| System program CPIs | create, assign, allocate, transfer, `ts_create_pda`, `ts_resize` / `ts_realloc`, close |
|
|
42
|
+
| Token-2022 extensions | TLV walking for the extensions the language reads |
|
|
43
|
+
| Metaplex Core and Bubblegum | account readers (`coreAssetOwner`, …) |
|
|
44
|
+
| SPL Token / Token-2022 CPIs | transfer, mint, burn, approve, close, freeze and their checked forms |
|
|
45
|
+
| Return data | `sol_set_return_data` wrappers |
|
|
46
|
+
| Crypto | sha256, keccak256, ed25519 / secp256k1 verification helpers, Merkle proofs |
|
|
47
|
+
| Heap | `ts_alloc`, a bump allocator (32 KiB by default, `HEAP_SIZE_KIB` up to 256), string building (`ts_str_join`, `ts_str_append`, number and base58 text) |
|
|
48
|
+
| Oracles | Pyth price updates, Switchboard |
|
|
49
|
+
| Associated token accounts, token metadata, transfer hooks | ATA derivation and creation, Token Metadata CPIs, transfer-hook extra accounts |
|
|
50
|
+
|
|
51
|
+
## Changing it
|
|
52
|
+
|
|
53
|
+
- Every runtime change can move compute units or binary size. Measure it before and after with
|
|
54
|
+
[typesol/benchmarks](https://github.com/typesol/benchmarks) pointed at your checkout, and put the difference in
|
|
55
|
+
the pull request (see CONTRIBUTING).
|
|
56
|
+
- A change that alters behavior (a result, an error code, a log line, account bytes) is a language change and goes
|
|
57
|
+
in the CHANGELOG as one. One that only changes cost says so: "same results".
|
|
58
|
+
- New error codes: add the `#define` here, the row in [errors.md](errors.md) and the name in the compiler's runtime
|
|
59
|
+
error list (`TYPESOL_ERRORS` in `src/clientgen.ts`), so clients name it (a test checks every code has a name).
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -62,11 +62,12 @@
|
|
|
62
62
|
#define TS_ERR_NO_RETURN 5114
|
|
63
63
|
#define TS_ERR_ACCOUNT_MISSING 5115
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
/* Every failure logs through this one sol_log_ call: each syscall call site costs a 16-byte relocation in the binary. */
|
|
66
|
+
static __attribute__((noinline)) uint64_t ts_fail(uint64_t code, const char *msg, uint64_t len) {
|
|
67
|
+
sol_log_(msg, len);
|
|
68
|
+
return code;
|
|
69
|
+
}
|
|
70
|
+
#define TS_FAIL(code, msg) return ts_fail((code), (msg), sol_strlen(msg))
|
|
70
71
|
|
|
71
72
|
#define TS_TRY(expr) \
|
|
72
73
|
do { \
|
|
@@ -503,6 +504,49 @@ static inline void ts_write_le(uint8_t *p, uint64_t v, int n) { ts_memcpy(p, &v,
|
|
|
503
504
|
uint64_t sol_get_rent_sysvar(void *ret);
|
|
504
505
|
uint64_t sol_get_clock_sysvar(void *ret);
|
|
505
506
|
|
|
507
|
+
/* The general case, in integers: exactly (uint64_t)((double)per_year * threshold) as soft-float computes it (the
|
|
508
|
+
conversion and the product rounded to nearest, ties to even; NaN, negative and < 1 give 0), or UINT64_MAX when that
|
|
509
|
+
product is >= 2^64 (no double below 2^64 converts to UINT64_MAX: the largest is 2^64 - 2048). No soft-float routine
|
|
510
|
+
is linked, which keeps about 1.7 KB out of every program that creates accounts. */
|
|
511
|
+
static __attribute__((noinline, minsize)) uint64_t ts_rent_slow(uint64_t per_year, uint64_t tbits) {
|
|
512
|
+
uint64_t e = (tbits >> 52) & 0x7ff, f = tbits & 0xfffffffffffffULL;
|
|
513
|
+
if (e == 0x7ff) return !f && per_year && !(tbits >> 63) ? UINT64_MAX : 0; /* +inf overflows; NaN, inf * 0, -inf: 0 */
|
|
514
|
+
if ((tbits >> 63) || !per_year || (!e && !f)) return 0; /* a negative or zero product */
|
|
515
|
+
/* Round v * 2^ex (with a sticky bit for bits already dropped) to 53 bits, nearest-even, twice: per_year to a double,
|
|
516
|
+
then the exact product of the two significands. */
|
|
517
|
+
uint64_t v = per_year, sticky = 0, mt = e ? f | (1ULL << 52) : f;
|
|
518
|
+
int64_t ex = (e ? (int64_t)e : 1) - 1075;
|
|
519
|
+
for (int pass = 0; pass < 2; pass++) {
|
|
520
|
+
int top = 63;
|
|
521
|
+
while (!(v >> top)) top--;
|
|
522
|
+
if (top > 52) {
|
|
523
|
+
int sh = top - 52;
|
|
524
|
+
uint64_t rem = v & ((1ULL << sh) - 1), half = 1ULL << (sh - 1);
|
|
525
|
+
v >>= sh;
|
|
526
|
+
ex += sh;
|
|
527
|
+
if (rem > half || (rem == half && (sticky || (v & 1)))) v++;
|
|
528
|
+
if (v >> 53) v >>= 1, ex++;
|
|
529
|
+
}
|
|
530
|
+
if (pass) break;
|
|
531
|
+
/* v (< 2^53) * mt (< 2^53) as hi:lo, folded to 64 bits with the dropped low bits as the sticky bit. */
|
|
532
|
+
uint64_t a0 = v & 0xffffffff, a1 = v >> 32, b0 = mt & 0xffffffff, b1 = mt >> 32;
|
|
533
|
+
uint64_t lo = a0 * b0, mid = a0 * b1 + a1 * b0, hi = a1 * b1;
|
|
534
|
+
uint64_t lo2 = lo + (mid << 32);
|
|
535
|
+
hi += (mid >> 32) + (lo2 < lo);
|
|
536
|
+
lo = lo2;
|
|
537
|
+
int s = 0;
|
|
538
|
+
while (hi >> s) s++;
|
|
539
|
+
v = s ? (hi << (64 - s)) | (lo >> s) : lo;
|
|
540
|
+
sticky = s ? (lo & ((1ULL << s) - 1)) != 0 : 0;
|
|
541
|
+
ex += s;
|
|
542
|
+
}
|
|
543
|
+
int top = 63;
|
|
544
|
+
while (!(v >> top)) top--;
|
|
545
|
+
if (top + ex >= 64) return UINT64_MAX;
|
|
546
|
+
if (ex >= 0) return v << ex;
|
|
547
|
+
return ex > -64 ? v >> -ex : 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
506
550
|
/* The minimum balance of `data_len` bytes under `rent`, the Rent sysvar
|
|
507
551
|
{ lamports_per_byte_year: u64, exemption_threshold: f64, burn_percent: u8 }. */
|
|
508
552
|
static inline uint64_t ts_minimum_balance_of(const uint64_t rent[3], uint64_t data_len, uint64_t *out) {
|
|
@@ -520,11 +564,9 @@ static inline uint64_t ts_minimum_balance_of(const uint64_t rent[3], uint64_t da
|
|
|
520
564
|
return SUCCESS;
|
|
521
565
|
}
|
|
522
566
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (lamports >= 18446744073709551615.0) TS_FAIL(TS_ERR_OVERFLOW, "TypeSOL: arithmetic overflow");
|
|
527
|
-
*out = (uint64_t)lamports;
|
|
567
|
+
uint64_t lamports = ts_rent_slow(per_year, rent[1]);
|
|
568
|
+
if (lamports == UINT64_MAX) TS_FAIL(TS_ERR_OVERFLOW, "TypeSOL: arithmetic overflow");
|
|
569
|
+
*out = lamports;
|
|
528
570
|
return SUCCESS;
|
|
529
571
|
}
|
|
530
572
|
|