@typesol/cli 0.24.0 → 0.25.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 +46 -2
- package/README.md +10 -7
- package/dist/clientgen.js +36 -21
- package/dist/compiler/analyze.js +32 -2
- package/dist/compiler/emit/base.d.ts +10 -0
- package/dist/compiler/emit/base.js +8 -0
- package/dist/compiler/emit/calls.js +10 -10
- package/dist/compiler/emit/cpi.d.ts +10 -1
- package/dist/compiler/emit/cpi.js +182 -166
- package/dist/compiler/emit/function.d.ts +9 -0
- package/dist/compiler/emit/function.js +169 -139
- package/dist/compiler/emit/program.js +3 -0
- package/dist/compiler/emit/shared.d.ts +1 -0
- package/dist/compiler/emit/shared.js +1 -0
- package/dist/compiler/emit/statements.d.ts +6 -0
- package/dist/compiler/emit/statements.js +35 -5
- package/docs/architecture.md +26 -2
- package/docs/case-study-escrow.md +224 -0
- package/docs/errors.md +26 -1
- package/docs/language.md +12 -8
- package/docs/migrate-from-anchor.md +3 -1
- package/docs/runtime.md +59 -0
- package/lib/typesol.d.ts +35 -13
- package/package.json +1 -1
- package/runtime/typesol_rt.h +85 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,54 @@
|
|
|
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.25.0 — 2026-10-29
|
|
10
|
+
|
|
11
|
+
Security review (October 2026) fixes, and error codes aligned with Anchor. Some refused transactions now report a
|
|
12
|
+
different error code (below): a minor version.
|
|
13
|
+
|
|
14
|
+
- Fixed: `loadAccountMut` on the same remaining account twice (`rest.get(i)`, `rest.get(j)` with `i == j`) gave two
|
|
15
|
+
writable views of one account; a program reading both before writing both could be made to create value. The second
|
|
16
|
+
load is now refused with 2040.
|
|
17
|
+
- Fixed: a Vec element written with a right side that resizes the Vec (`book.bids.get(i).qty += settle(book)` where the
|
|
18
|
+
helper pops, a closure, or a `push` / `pop` / `clear` in the expression) wrote through the element's old position,
|
|
19
|
+
into the bytes of the next Vec. Such an assignment is now a compile error ("store its result in a local first"),
|
|
20
|
+
as it already was for a plain `=` with a helper.
|
|
21
|
+
- Fixed: `Ata<Init<…>>` / `Ata<InitIfNeeded<…>>` creation and `Ata<AccountInfo, …>` with two token program parameters
|
|
22
|
+
used the first one; they now need exactly one (use `TokenInterface`).
|
|
23
|
+
- New: `TokenOf<…, …, …, "tokenProgram">` / `MintOf<…, …, …, "tokenProgram">`, Anchor's `token::token_program` /
|
|
24
|
+
`mint::token_program` (2021 / 2022). The escrow port uses it.
|
|
25
|
+
- New: `pythPrice(update, feedId, maxAge, exponent)` refuses a feed whose exponent is not the one given;
|
|
26
|
+
`switchboardValue(…, minSamples)` refuses a result resting on fewer oracle samples.
|
|
27
|
+
- Changed: each account's constraints are checked in Anchor's order (seeds, associated token account, has_one, owner,
|
|
28
|
+
executable, address, token::, mint::), so a transaction breaking several fails with Anchor's code: e.g. a stranger's
|
|
29
|
+
call on a `Seeds<HasOne<…>>` account now fails with 2006 (ConstraintSeeds) instead of 2001, and an `Ata<…>` of
|
|
30
|
+
another wallet with 2015 instead of 2009. A `Mut<…>` account passed read-only fails with 2000 (ConstraintMut), as
|
|
31
|
+
Anchor's `#[account(mut)]`, instead of 3006. docs/errors.md lists the order and the differences left.
|
|
32
|
+
- Docs: unchecked raw Pyth fields, `requirePda` with a bump from the arguments, Merkle leaves, and what
|
|
33
|
+
`createIfNeeded` leaves unchecked on an existing account.
|
|
34
|
+
- Docs: [Case study: porting Anchor's escrow](docs/case-study-escrow.md), one Anchor program ported end to end, with the
|
|
35
|
+
differences, the checks run against the original and the cost.
|
|
36
|
+
- Releases: the release workflow checks that CHANGELOG.md has the version, and after publishing installs the package
|
|
37
|
+
from npm, scaffolds a project from each template and runs its typecheck, build and test.
|
|
38
|
+
|
|
39
|
+
## 0.24.1 — 2026-10-28
|
|
40
|
+
|
|
41
|
+
Smaller programs and fewer compute units, with the same results; no change to the language, IDLs or error codes. Also
|
|
42
|
+
new contributor docs (repository layout, a guide to the C runtime). Not externally audited (see SECURITY.md).
|
|
43
|
+
|
|
44
|
+
### Performance
|
|
45
|
+
- Smaller programs, same results: the rent calculation no longer links soft-float (an exact integer equivalent,
|
|
46
|
+
checked against IEEE doubles), and every failure logs through one shared call. The benchmarked programs are 11.6%
|
|
47
|
+
smaller in total (counter 13.1 → 11.0 KB, escrow 23.9 → 21.3 KB). A failing instruction costs 2 more compute units;
|
|
48
|
+
successful ones are unchanged.
|
|
49
|
+
- An identical signer-seed list in the same scope reuses the array declared for the first CPI (escrow `take_offer`
|
|
50
|
+
7058 → 7044, `refund_offer` 4287 → 4271).
|
|
51
|
+
|
|
8
52
|
## 0.24.0 — 2026-10-28
|
|
9
53
|
|
|
10
54
|
Functions as values, closures for any helper, stored `map` to structs and text, Metaplex Core external plugins, and
|
package/README.md
CHANGED
|
@@ -162,8 +162,9 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
|
|
|
162
162
|
Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
|
|
163
163
|
program's address.
|
|
164
164
|
|
|
165
|
-
Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct,
|
|
166
|
-
starts from your program's IDL
|
|
165
|
+
Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct, `typesol port`
|
|
166
|
+
starts from your program's IDL, and [docs/case-study-escrow.md](docs/case-study-escrow.md) follows one complete Anchor
|
|
167
|
+
program through the port. Tests use `typesol/testing` (accounts by name, plain values; [docs/test-your-program.md](docs/test-your-program.md)). To go to
|
|
167
168
|
mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
|
|
168
169
|
|
|
169
170
|
## How it works
|
|
@@ -232,7 +233,9 @@ Subpath exports: `typesol/compiler`, `typesol/workspace`, `typesol/cpi`, `typeso
|
|
|
232
233
|
|
|
233
234
|
The same counter, vault and escrow programs, written four ways. All four use the same instruction encoding, account
|
|
234
235
|
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)
|
|
236
|
+
shows mean compute units per call (generated by `scripts/readme-bench.ts` from the benchmark run). The programs,
|
|
237
|
+
scenarios and harness are in [typesol/benchmarks](https://github.com/typesol/benchmarks), which pins the compiler and
|
|
238
|
+
toolchain and records them with every result:
|
|
236
239
|
|
|
237
240
|
<!-- bench:table -->
|
|
238
241
|
| Instruction | TypeSOL | Pinocchio 0.10 | Native Rust | Anchor 1.0.2 |
|
|
@@ -244,9 +247,9 @@ shows mean compute units per call (generated by `scripts/readme-bench.ts` from t
|
|
|
244
247
|
| vault.withdraw | **315** | 370 | 1,176 | 3,628 |
|
|
245
248
|
| vault.shut | **161** | 167 | 950 | 2,024 |
|
|
246
249
|
| escrow.make | **3,605** | 6,179 | 9,240 | 13,304 |
|
|
247
|
-
| escrow.take | **4,
|
|
248
|
-
| escrow.refund | **2,
|
|
249
|
-
| binary, counter / vault / escrow |
|
|
250
|
+
| escrow.take | **4,019** | 4,051 | 8,500 | 14,365 |
|
|
251
|
+
| escrow.refund | **2,696** | 2,769 | 5,695 | 8,449 |
|
|
252
|
+
| 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
253
|
<!-- /bench:table -->
|
|
251
254
|
|
|
252
255
|
`initialize`, `open` and `make` include the PDA bump search. TypeSOL hashes PDA candidates with the sha256 and
|
|
@@ -307,7 +310,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
307
310
|
|
|
308
311
|
## Status
|
|
309
312
|
|
|
310
|
-
**TypeSOL is pre-1.0
|
|
313
|
+
**TypeSOL is pre-1.0.** Releases follow the organization's
|
|
311
314
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
312
315
|
change.
|
|
313
316
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
package/dist/clientgen.js
CHANGED
|
@@ -32,6 +32,7 @@ export const TYPESOL_ERRORS = {
|
|
|
32
32
|
100: "InstructionMissing",
|
|
33
33
|
101: "InstructionFallbackNotFound",
|
|
34
34
|
102: "InstructionDidNotDeserialize",
|
|
35
|
+
2000: "ConstraintMut",
|
|
35
36
|
2001: "ConstraintHasOne",
|
|
36
37
|
2002: "ConstraintSigner",
|
|
37
38
|
2004: "ConstraintOwner",
|
|
@@ -45,6 +46,8 @@ export const TYPESOL_ERRORS = {
|
|
|
45
46
|
2016: "ConstraintMintMintAuthority",
|
|
46
47
|
2017: "ConstraintMintFreezeAuthority",
|
|
47
48
|
2018: "ConstraintMintDecimals",
|
|
49
|
+
2021: "ConstraintTokenTokenProgram",
|
|
50
|
+
2022: "ConstraintMintTokenProgram",
|
|
48
51
|
2040: "ConstraintDuplicateMutableAccount",
|
|
49
52
|
3000: "AccountDiscriminatorAlreadySet",
|
|
50
53
|
3002: "AccountDiscriminatorMismatch",
|
|
@@ -1102,7 +1105,10 @@ function generate(idl, opts) {
|
|
|
1102
1105
|
// A seed or program path names an account: in the same group first, then at the top level.
|
|
1103
1106
|
const byPath = (p, group) => flat.find((f) => `${f.group}${f.name}` === `${group}${p}`) ?? flat.find((f) => f.group === "" && f.name === p);
|
|
1104
1107
|
const argType = new Map(ix.args.map((a) => [a.name, a.type]));
|
|
1105
|
-
const
|
|
1108
|
+
const seedsProp = (k) => `seeds${NAME.test(k) && !JS_RESERVED.has(k) ? `.${k}` : `[${JSON.stringify(k)}]`}`;
|
|
1109
|
+
const BUILDER = { arg: (n) => `args.${n}`, account: (k) => `$${k}` };
|
|
1110
|
+
const HELPER = { arg: (n) => seedsProp(n), account: (k) => seedsProp(k) };
|
|
1111
|
+
const seedExpr = (s, group, refs = BUILDER) => {
|
|
1106
1112
|
if (s.kind === "const")
|
|
1107
1113
|
return bytes(s.value);
|
|
1108
1114
|
if (s.kind === "arg") {
|
|
@@ -1111,7 +1117,7 @@ function generate(idl, opts) {
|
|
|
1111
1117
|
let t = argType.get(head);
|
|
1112
1118
|
if (t === undefined)
|
|
1113
1119
|
return { why: `the seed argument ${head} does not exist` };
|
|
1114
|
-
let v =
|
|
1120
|
+
let v = refs.arg(camel(head));
|
|
1115
1121
|
for (const part of rest) {
|
|
1116
1122
|
const d = defs.get(definedName(t) ?? "");
|
|
1117
1123
|
const f = d?.type.kind === "struct" ? (d.type.fields ?? []).find((x) => isField(x) && x.name === part) : undefined;
|
|
@@ -1134,25 +1140,30 @@ function generate(idl, opts) {
|
|
|
1134
1140
|
if (!target)
|
|
1135
1141
|
return { why: `the seed account ${s.path} is not an account of the instruction` };
|
|
1136
1142
|
use("getAddressEncoder");
|
|
1137
|
-
return `getAddressEncoder().encode(key(
|
|
1143
|
+
return `getAddressEncoder().encode(key(${refs.account(target.key)}))`;
|
|
1138
1144
|
};
|
|
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;
|
|
1145
|
+
/** A PDA's program as the code being written names it (undefined: this program). */
|
|
1146
|
+
const pdaProgram = (a, refs) => {
|
|
1145
1147
|
const p = a.pda.program;
|
|
1146
1148
|
if (p?.kind === "const")
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
+
return (use("getAddressDecoder"), `programFromBytes(${bytes(p.value)})`);
|
|
1150
|
+
if (p?.kind === "account") {
|
|
1149
1151
|
const target = p.path.includes(".") ? undefined : byPath(p.path, a.group);
|
|
1150
|
-
|
|
1152
|
+
return target ? `key(${refs.account(target.key)})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
|
|
1151
1153
|
}
|
|
1152
|
-
|
|
1154
|
+
if (p?.kind === "arg") {
|
|
1153
1155
|
const t = p.path.includes(".") ? undefined : argType.get(p.path);
|
|
1154
|
-
|
|
1156
|
+
return t === "pubkey" || t === "publicKey" ? refs.arg(camel(p.path)) : { why: "the PDA program comes from an argument that is not a Pubkey" };
|
|
1155
1157
|
}
|
|
1158
|
+
return undefined;
|
|
1159
|
+
};
|
|
1160
|
+
const resolvable = new Map();
|
|
1161
|
+
for (const a of flat) {
|
|
1162
|
+
if (!a.pda || a.address)
|
|
1163
|
+
continue;
|
|
1164
|
+
const seeds = a.pda.seeds.map((s) => seedExpr(s, a.group));
|
|
1165
|
+
const program = pdaProgram(a, BUILDER);
|
|
1166
|
+
const p = a.pda.program;
|
|
1156
1167
|
const why = [...seeds, ...(program === undefined ? [] : [program])].find((x) => typeof x === "object");
|
|
1157
1168
|
if (why) {
|
|
1158
1169
|
notes.push(`instruction ${ix.name}: ${a.name} is not filled in (${why.why}); pass it`);
|
|
@@ -1228,11 +1239,13 @@ function generate(idl, opts) {
|
|
|
1228
1239
|
return [k, `${k}: Address | TransactionSigner`];
|
|
1229
1240
|
})).values(),
|
|
1230
1241
|
];
|
|
1231
|
-
|
|
1242
|
+
// The same derivation, its inputs named as the helper's `seeds` parameter.
|
|
1243
|
+
const helperSeeds = a.pda.seeds.map((s) => seedExpr(s, a.group, HELPER));
|
|
1244
|
+
const helperProgram = pdaProgram(a, HELPER) ?? "PROGRAM_ADDRESS";
|
|
1232
1245
|
use("type ProgramDerivedAddress");
|
|
1233
1246
|
pdaFns.set(pn, {
|
|
1234
1247
|
json,
|
|
1235
|
-
source: ` /** The ${a.name} PDA (as ${ix.name} derives it). */\n ${prop(pn)}(seeds: { ${params.join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${
|
|
1248
|
+
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
1249
|
});
|
|
1237
1250
|
}
|
|
1238
1251
|
const metas = flat.map((a) => {
|
|
@@ -1326,6 +1339,7 @@ function generate(idl, opts) {
|
|
|
1326
1339
|
}
|
|
1327
1340
|
const bases = new Map();
|
|
1328
1341
|
const seeds = [];
|
|
1342
|
+
const helperSeeds = []; // the same seeds, named as a `pdas` helper's `seeds` (for one seeded from its own fields)
|
|
1329
1343
|
const own = [];
|
|
1330
1344
|
// Inputs of a `pdas` helper for a PDA seeded from its own fields: its own field values, accounts and arguments.
|
|
1331
1345
|
const params = new Map();
|
|
@@ -1334,13 +1348,14 @@ function generate(idl, opts) {
|
|
|
1334
1348
|
let usesArgs = p?.kind === "arg";
|
|
1335
1349
|
for (const sd of a.pda.seeds) {
|
|
1336
1350
|
if (sd.kind === "const")
|
|
1337
|
-
seeds.push(bytes(sd.value));
|
|
1351
|
+
(seeds.push(bytes(sd.value)), helperSeeds.push(bytes(sd.value)));
|
|
1338
1352
|
else if (sd.kind === "arg") {
|
|
1339
1353
|
const e = seedExpr(sd, a.group);
|
|
1340
1354
|
if (typeof e !== "string")
|
|
1341
1355
|
ok = false;
|
|
1342
1356
|
else {
|
|
1343
1357
|
seeds.push(e);
|
|
1358
|
+
helperSeeds.push(seedExpr(sd, a.group, HELPER));
|
|
1344
1359
|
usesArgs = true;
|
|
1345
1360
|
const head = sd.path.split(".")[0];
|
|
1346
1361
|
params.set(camel(head), `${camel(head)}: ${tsType(argType.get(head), "in")}`);
|
|
@@ -1353,6 +1368,7 @@ function generate(idl, opts) {
|
|
|
1353
1368
|
else {
|
|
1354
1369
|
use("getAddressEncoder");
|
|
1355
1370
|
seeds.push(`getAddressEncoder().encode(key(${acc(t)}!))`);
|
|
1371
|
+
helperSeeds.push(`getAddressEncoder().encode(key(${seedsProp(t.key)}))`);
|
|
1356
1372
|
params.set(t.key, `${t.key}: Address | TransactionSigner`);
|
|
1357
1373
|
if (auto(t))
|
|
1358
1374
|
autoDep = true;
|
|
@@ -1370,6 +1386,7 @@ function generate(idl, opts) {
|
|
|
1370
1386
|
own.push({ field: parts[1], type: field.type });
|
|
1371
1387
|
params.set(camel(parts[1]), `${camel(parts[1])}: ${tsType(field.type, "in")}`);
|
|
1372
1388
|
seeds.push(enc);
|
|
1389
|
+
helperSeeds.push(enc);
|
|
1373
1390
|
}
|
|
1374
1391
|
else if (!consts || auto(base))
|
|
1375
1392
|
ok = false;
|
|
@@ -1399,13 +1416,11 @@ function generate(idl, opts) {
|
|
|
1399
1416
|
continue;
|
|
1400
1417
|
if (p?.kind === "arg")
|
|
1401
1418
|
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)}]`})`);
|
|
1419
|
+
const helperProgram = p?.kind === "arg" ? seedsProp(camel(p.path)) : program;
|
|
1405
1420
|
use("type ProgramDerivedAddress", "getProgramDerivedAddress");
|
|
1406
1421
|
pdaFns.set(pn, {
|
|
1407
1422
|
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: ${
|
|
1423
|
+
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
1424
|
});
|
|
1410
1425
|
continue;
|
|
1411
1426
|
}
|
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)
|
|
@@ -1453,6 +1473,13 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1453
1473
|
if (entry.includes("=")) {
|
|
1454
1474
|
if (!accounts.some((o) => o.name === param))
|
|
1455
1475
|
reporter.fail(a.node, `add an account parameter named "${param}"`);
|
|
1476
|
+
if (f === "tokenProgram") {
|
|
1477
|
+
if (a.kind.kind !== "token" && a.kind.kind !== "mint")
|
|
1478
|
+
reporter.fail(a.node, "TokenOf / MintOf apply to a TokenAccount / Mint");
|
|
1479
|
+
if (accounts.find((o) => o.name === param).kind.kind !== "tokenProgram")
|
|
1480
|
+
reporter.fail(a.node, `"${param}" must be a TokenProgram, Token2022Program or TokenInterface parameter`);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1456
1483
|
if (a.kind.kind === "token" ? f !== "mint" && f !== "owner" : a.kind.kind === "mint" ? false : true)
|
|
1457
1484
|
reporter.fail(a.node, a.kind.kind === "token" || a.kind.kind === "mint" ? "TokenOf<…> applies to a TokenAccount" : "TokenOf / MintOf apply to a TokenAccount / Mint");
|
|
1458
1485
|
if (a.kind.kind === "mint" && f !== "mintAuthority" && f !== "freezeAuthority")
|
|
@@ -1590,12 +1617,15 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1590
1617
|
const one = (t) => (t && t.kind !== ts.SyntaxKind.NeverKeyword ? lit(t)[0] : undefined);
|
|
1591
1618
|
const p1 = one(args[1]);
|
|
1592
1619
|
const p2 = one(args[2]);
|
|
1593
|
-
|
|
1620
|
+
const p3 = one(args[3]);
|
|
1621
|
+
if (!p1 && !p2 && !p3)
|
|
1594
1622
|
reporter.fail(node, `${name}<…> names at least one parameter`);
|
|
1595
1623
|
if (p1)
|
|
1596
1624
|
out.hasOne.push(`${first}=${p1}`);
|
|
1597
1625
|
if (p2)
|
|
1598
1626
|
out.hasOne.push(`${second}=${p2}`);
|
|
1627
|
+
if (p3)
|
|
1628
|
+
out.hasOne.push(`tokenProgram=${p3}`);
|
|
1599
1629
|
}
|
|
1600
1630
|
if (name === "CloseTo")
|
|
1601
1631
|
out.closeTo = lit(args[1])[0];
|
|
@@ -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) {
|
|
@@ -3,7 +3,7 @@ import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
|
|
|
3
3
|
import { ANCHOR_ERRORS } from "../anchor-errors.js";
|
|
4
4
|
import { decodeAddress } from "../../keys.js";
|
|
5
5
|
import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
|
|
6
|
-
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
|
|
6
|
+
import { U64, U8, I64, I32, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
|
|
7
7
|
import { ExpressionEmitter } from "./expressions.js";
|
|
8
8
|
/**
|
|
9
9
|
* Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
|
|
@@ -344,29 +344,29 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
344
344
|
return { c: `ts_secp256k1_verify(${h.c}, (uint64_t)${id.c}, ${r.c}, ${sv.c}, ${eth.c})`, type: BOOL };
|
|
345
345
|
}
|
|
346
346
|
case "pythPrice": {
|
|
347
|
-
if (args.length !== 3)
|
|
348
|
-
this.fail(node, "pythPrice(update, feedId, maxAgeSeconds) takes three arguments");
|
|
347
|
+
if (args.length !== 3 && args.length !== 4)
|
|
348
|
+
this.fail(node, "pythPrice(update, feedId, maxAgeSeconds, exponent?) takes three or four arguments");
|
|
349
349
|
const u = this.accountParam(args[0], "the price update");
|
|
350
350
|
if (u.kind.kind !== "pyth")
|
|
351
351
|
this.fail(args[0], "pass a PythPriceUpdate parameter");
|
|
352
|
-
const [feed, age] = this.lowerInOrder(args.slice(1), [HASH, U64]);
|
|
352
|
+
const [feed, age, expo] = this.lowerInOrder(args.slice(1), [HASH, U64, I32]);
|
|
353
353
|
const t = this.tmp("px");
|
|
354
354
|
this.line(`int64_t ${t};`);
|
|
355
|
-
this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, &${t}));`);
|
|
355
|
+
this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, ${expo ? `true, ${expo.c}` : "false, 0"}, &${t}));`);
|
|
356
356
|
return { c: t, type: I64 };
|
|
357
357
|
}
|
|
358
358
|
case "switchboardValue": {
|
|
359
359
|
// Anyone can create a Switchboard feed: like pythPrice's feed id, the expected feed address is required.
|
|
360
|
-
if (args.length !== 4)
|
|
361
|
-
this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals) takes four arguments");
|
|
360
|
+
if (args.length !== 4 && args.length !== 5)
|
|
361
|
+
this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals, minSamples?) takes four or five arguments");
|
|
362
362
|
const f = this.accountParam(args[0], "the feed");
|
|
363
363
|
if (f.kind.kind !== "switchboard")
|
|
364
364
|
this.fail(args[0], "pass a SwitchboardFeed parameter");
|
|
365
|
-
const [expected, stale, dec] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8]);
|
|
365
|
+
const [expected, stale, dec, min] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8, U8]);
|
|
366
366
|
const t = this.tmp("sb");
|
|
367
367
|
this.line(`int64_t ${t};`);
|
|
368
368
|
this.line(`if (!ts_key_eq(&a_${f.name}->key, ${expected.c})) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: not the expected Switchboard feed");`);
|
|
369
|
-
this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, &${t}));`);
|
|
369
|
+
this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, ${min ? min.c : "1"}, &${t}));`);
|
|
370
370
|
return { c: t, type: I64 };
|
|
371
371
|
}
|
|
372
372
|
case "associatedTokenAddress": {
|
|
@@ -980,7 +980,7 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
980
980
|
this.line(l);
|
|
981
981
|
if (mut) {
|
|
982
982
|
this.line(`TS_TRY(ts_check_writable(${h}));`);
|
|
983
|
-
this.line(`TS_TRY(
|
|
983
|
+
this.line(`TS_TRY(ts_claim_mut(params, ${h}));`);
|
|
984
984
|
}
|
|
985
985
|
return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
|
|
986
986
|
}
|
|
@@ -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. */
|