@typesol/cli 0.26.0 → 0.28.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 +39 -0
- package/README.md +3 -1
- package/dist/compiler/analyze.d.ts +2 -0
- package/dist/compiler/analyze.js +93 -12
- package/dist/compiler/emit/base.d.ts +3 -0
- package/dist/compiler/emit/base.js +4 -1
- package/dist/compiler/emit/calls.d.ts +16 -1
- package/dist/compiler/emit/calls.js +131 -1
- package/dist/compiler/emit/cpi.d.ts +9 -0
- package/dist/compiler/emit/cpi.js +63 -0
- package/dist/compiler/emit/expressions.js +18 -0
- package/dist/compiler/emit/function.js +41 -5
- package/dist/compiler/emit/shared.js +2 -0
- package/dist/compiler/emit/storage.js +40 -1
- package/dist/compiler/idl.js +1 -1
- package/dist/compiler/model.d.ts +14 -0
- package/dist/compiler/model.js +3 -0
- package/docs/architecture.md +2 -1
- package/docs/audit.md +1 -0
- package/docs/case-study-escrow.md +3 -3
- package/docs/case-study-multisig.md +167 -0
- package/docs/errors.md +4 -3
- package/docs/language.md +26 -6
- package/docs/migrate-from-anchor.md +9 -0
- package/docs/security-model.md +15 -3
- package/lib/typesol.d.ts +92 -5
- package/package.json +1 -1
- package/runtime/typesol_rt.h +90 -23
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ codes, and says so here.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.28.0 — 2026-10-29
|
|
10
|
+
|
|
11
|
+
A port of coral-xyz/multisig, checked against the program deployed on mainnet, and the features it needed. The
|
|
12
|
+
benchmark programs compile to the same numbers as in 0.27.0.
|
|
13
|
+
|
|
14
|
+
- New: `invokeInstruction(programId, metas, data, infos, signerSeeds?)`, a CPI decided at run time (Rust's
|
|
15
|
+
`invoke_signed` of an `Instruction` built from data): metas are structs with `pubkey`, `isSigner`, `isWritable`, the
|
|
16
|
+
data a list of u8, and every account in `infos` (the remaining accounts or a slice) goes to the runtime.
|
|
17
|
+
- New: `Signed<A>`, Anchor's `signer` constraint on an account that is not a `Signer` (2002); the IDL marks it a signer.
|
|
18
|
+
- New: `Zeroed<T>` of a `@layout borsh` struct (Anchor's `#[account(zero)]` on a Borsh account): it starts empty and is
|
|
19
|
+
written as Borsh when the instruction returns.
|
|
20
|
+
- New: a Seeds bump stored in another struct account, `Seeds<A, ["@multisig"], "@multisig.nonce">` (Anchor's
|
|
21
|
+
`bump = multisig.nonce`).
|
|
22
|
+
- New: `list = vecArgument` copies a `Vec` argument (or another list of the same elements) into a `FixedVec` (5104 when
|
|
23
|
+
it does not fit).
|
|
24
|
+
- Changed: a `Zeroed<T>` account whose discriminator is already set fails with 2013 (ConstraintZero), checked before its
|
|
25
|
+
owner, as under Anchor; it was 3000 after the owner check.
|
|
26
|
+
- Changed: `key`, `lamports`, `dataLen`, `isSigner`, `isWritable` and `pdaBump` are refused as field names only in
|
|
27
|
+
structs held in an account (where they would shadow `acct.key` and the others), no longer in every struct.
|
|
28
|
+
- Docs: `docs/case-study-multisig.md`; the mainnet binary is in `test/programs/coral_multisig.so`.
|
|
29
|
+
|
|
30
|
+
## 0.27.0 — 2026-10-29
|
|
31
|
+
|
|
32
|
+
Anchor 1.0 parity: instruction introspection, `Dup<…>` and account migration. Existing programs compile to the same
|
|
33
|
+
binaries as in 0.26.0.
|
|
34
|
+
|
|
35
|
+
- New: instruction introspection. `InstructionsSysvar` has `length`, `currentIndex()` and `get(i)`, a
|
|
36
|
+
`TransactionInstruction` with `programId`, its accounts (key, signer, writable), its data
|
|
37
|
+
(`readU8/U16/U32/U64/readPubkey`) and `is(program, "name")` (an instruction by its Anchor discriminator); every read
|
|
38
|
+
is bounds-checked (5104). `stackHeight()` is 1 at the top level, more under a CPI.
|
|
39
|
+
- New: `Dup<…>`, Anchor's `dup`: a writable parameter that may be the same account as another, for accounts read and
|
|
40
|
+
written in place (not created, closed, a close destination or `@layout borsh`, which are refused at compile time).
|
|
41
|
+
- New: `Migrate<From, To>`, Anchor 1.0's `Migration`: an existing account rewritten from one struct to another with
|
|
42
|
+
`acct.migrate(payer, value)`, the old data readable through `acct.from` until then (the new value is built before
|
|
43
|
+
the bytes are rewritten), the new through `acct.to` after; resized with the payer topping up rent. For now:
|
|
44
|
+
fixed-layout structs without growable Vecs, no constraints on the account, and it cannot be passed to a helper.
|
|
45
|
+
- New: `/** @discriminator … */` on an account struct (a sha256 preimage such as `account:Counter`, or 0x and 8
|
|
46
|
+
bytes), Anchor's `#[account(discriminator = …)]`: keeps an old type's on-chain discriminator under a new name.
|
|
47
|
+
|
|
9
48
|
## 0.26.0 — 2026-10-29
|
|
10
49
|
|
|
11
50
|
A first-run experience pass, the security model and audit package, and more fuzzing. The compiler's output is the
|
package/README.md
CHANGED
|
@@ -175,7 +175,8 @@ program's address.
|
|
|
175
175
|
|
|
176
176
|
Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct, `typesol port`
|
|
177
177
|
starts from your program's IDL, and [docs/case-study-escrow.md](docs/case-study-escrow.md) follows one complete Anchor
|
|
178
|
-
program through the port
|
|
178
|
+
program through the port; [docs/case-study-multisig.md](docs/case-study-multisig.md) ports coral-xyz/multisig and
|
|
179
|
+
checks it against the binary deployed on mainnet (same outcomes and account bytes; 73–84% fewer compute units). Tests use `typesol/testing` (accounts by name, plain values; [docs/test-your-program.md](docs/test-your-program.md)). To go to
|
|
179
180
|
mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
|
|
180
181
|
|
|
181
182
|
## How it works
|
|
@@ -323,6 +324,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
323
324
|
| [`relay.ts`](examples/relay.ts), [`caller.ts`](examples/caller.ts) | generic `invoke`, PDA-signed transfers, return data |
|
|
324
325
|
| [`profiles.ts`](examples/profiles.ts) | nested structs, FixedString, events |
|
|
325
326
|
| [`orderbook/`](examples/orderbook) | a three-file, 28 KiB zero-copy limit order book |
|
|
327
|
+
| [`multisig.ts`](examples/multisig.ts) | coral-xyz/multisig: stored instructions run with `invokeInstruction`, a PDA signing, Anchor's Borsh accounts |
|
|
326
328
|
|
|
327
329
|
## Status
|
|
328
330
|
|
|
@@ -2,6 +2,8 @@ import ts from "typescript";
|
|
|
2
2
|
import { Reporter } from "./diagnostics.js";
|
|
3
3
|
import { type DataEnumDef, type EnumDef, type GenericHelper, type Helper, type ProgramModel, type ScalarType, type StructDef, type ValueType, isScalar } from "./model.js";
|
|
4
4
|
export declare const LIB_PATH: string;
|
|
5
|
+
/** A struct used as account data: none of its fields may shadow an account member. */
|
|
6
|
+
export declare function checkAccountMembers(s: StructDef, at: ts.Node, reporter: Reporter): void;
|
|
5
7
|
/** Why a name cannot be used as a C member, or undefined when it can. */
|
|
6
8
|
export declare function cNameProblem(name: string): string | undefined;
|
|
7
9
|
/** Solana's account size limit, and what a program can allocate itself through a CPI (Init<T>). */
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -8,7 +8,15 @@ import { borshIdentical } from "./emit/borsh.js";
|
|
|
8
8
|
import { optionKey, INT_INFO, INT_NAMES, scalarKey, fixedVecName, isScalar, scalarSize, typeName, } from "./model.js";
|
|
9
9
|
export const LIB_PATH = fileURLToPath(new URL("../../lib/typesol.d.ts", import.meta.url));
|
|
10
10
|
/** Names a struct field may not use because account parameters already expose them. */
|
|
11
|
-
const RESERVED_FIELDS = new Set(["
|
|
11
|
+
const RESERVED_FIELDS = new Set(["create", "equals", "get", "set", "length"]);
|
|
12
|
+
/** Members every account parameter has (`acct.key`, `acct.isSigner`, …): a struct held in an account cannot use them. */
|
|
13
|
+
const ACCOUNT_MEMBERS = new Set(["key", "lamports", "dataLen", "isSigner", "isWritable", "pdaBump"]);
|
|
14
|
+
/** A struct used as account data: none of its fields may shadow an account member. */
|
|
15
|
+
export function checkAccountMembers(s, at, reporter) {
|
|
16
|
+
const clash = s.fields.find((f) => ACCOUNT_MEMBERS.has(f.name));
|
|
17
|
+
if (clash)
|
|
18
|
+
reporter.fail(at, `${s.name}.${clash.name}: "${clash.name}" is an account member (acct.${clash.name}); rename the field to hold ${s.name} in an account`);
|
|
19
|
+
}
|
|
12
20
|
/**
|
|
13
21
|
* Struct fields and enum variants become C member names as written. C keywords and the object-like
|
|
14
22
|
* macros of the Solana C SDK / TypeSOL runtime would break or silently change the generated C.
|
|
@@ -85,6 +93,9 @@ const TYPE_EXPORTS = new Set([
|
|
|
85
93
|
"Ata",
|
|
86
94
|
"Owner",
|
|
87
95
|
"Executable",
|
|
96
|
+
"Dup",
|
|
97
|
+
"Signed",
|
|
98
|
+
"Migrate",
|
|
88
99
|
"Optional",
|
|
89
100
|
"EventAuthority",
|
|
90
101
|
"MplCoreProgram",
|
|
@@ -114,6 +125,22 @@ export function snakeCase(name) {
|
|
|
114
125
|
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
115
126
|
.toLowerCase();
|
|
116
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* An account struct's discriminator: Anchor's sha256("account:<Name>")[..8], or the one a `/** @discriminator … *\/` tag
|
|
130
|
+
* gives (a sha256 preimage such as `account:Counter`, or 0x and 8 bytes), e.g. for an old version of a struct kept under
|
|
131
|
+
* a new name for Migrate<From, To>.
|
|
132
|
+
*/
|
|
133
|
+
function accountDiscriminator(decl, name, reporter) {
|
|
134
|
+
const tag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "discriminator");
|
|
135
|
+
if (!tag)
|
|
136
|
+
return discriminator(`account:${name}`);
|
|
137
|
+
const text = (typeof tag.comment === "string" ? tag.comment : (ts.getTextOfJSDocComment(tag.comment) ?? "")).trim();
|
|
138
|
+
if (/^0x([0-9a-fA-F]{2}){8}$/.test(text))
|
|
139
|
+
return [...Buffer.from(text.slice(2), "hex")];
|
|
140
|
+
if (text && !text.startsWith("0x"))
|
|
141
|
+
return discriminator(text);
|
|
142
|
+
reporter.fail(decl, "@discriminator on an account struct needs a sha256 preimage (account:<Name>) or 0x and 8 bytes");
|
|
143
|
+
}
|
|
117
144
|
export function discriminator(preimage) {
|
|
118
145
|
return [...createHash("sha256").update(preimage).digest().subarray(0, 8)];
|
|
119
146
|
}
|
|
@@ -601,12 +628,17 @@ export function analyze(entry) {
|
|
|
601
628
|
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
602
629
|
if (ix.returns?.kind === "struct")
|
|
603
630
|
collectStructs(ix.returns, structs, dataEnums, argStructs);
|
|
604
|
-
for (const a of ix.accounts)
|
|
631
|
+
for (const a of ix.accounts) {
|
|
632
|
+
if (a.kind.kind === "migrate") {
|
|
633
|
+
accountStructs.add(a.kind.from.name);
|
|
634
|
+
accountStructs.add(a.kind.to.name);
|
|
635
|
+
}
|
|
605
636
|
if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded") {
|
|
606
637
|
accountStructs.add(a.kind.struct.name);
|
|
607
638
|
if (a.kind.struct.viewsOnly)
|
|
608
639
|
reporter.error(a.node, `${a.kind.struct.name} holds a string (or an enum carrying one), so it is an argument, return value or event; account data uses FixedString<N>`);
|
|
609
640
|
}
|
|
641
|
+
}
|
|
610
642
|
}
|
|
611
643
|
// Arguments are Borsh, where a FixedVec is a Vec (only what it holds). A struct passed as a whole argument and used
|
|
612
644
|
// nowhere else (not account data, not nested, not returned) is read as Anchor sends it: @layout borsh, implied.
|
|
@@ -966,7 +998,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
|
|
|
966
998
|
name,
|
|
967
999
|
fields: stored,
|
|
968
1000
|
size: undefined,
|
|
969
|
-
accountDiscriminator:
|
|
1001
|
+
accountDiscriminator: accountDiscriminator(decl, name, reporter),
|
|
970
1002
|
eventDiscriminator: discriminator(`event:${name}`),
|
|
971
1003
|
node: decl,
|
|
972
1004
|
layout: layoutText === "borsh" ? "borsh" : "fixed",
|
|
@@ -1318,10 +1350,11 @@ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new S
|
|
|
1318
1350
|
wrapped === "Seeds" ||
|
|
1319
1351
|
wrapped === "CloseTo" ||
|
|
1320
1352
|
wrapped === "Address" ||
|
|
1321
|
-
wrapped === "Ata"
|
|
1353
|
+
wrapped === "Ata" ||
|
|
1354
|
+
wrapped === "Signed")
|
|
1322
1355
|
reporter.fail(p.type, "constraints belong on instruction parameters; helpers take the plain account type");
|
|
1323
1356
|
const account = resolveAccountKind(p.type, reporter, scope.imports, structs);
|
|
1324
|
-
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed")
|
|
1357
|
+
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed" || account.kind === "migrate")
|
|
1325
1358
|
reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
|
|
1326
1359
|
return { name: p.name.text, type: { kind: "account" }, account, node: p };
|
|
1327
1360
|
});
|
|
@@ -1445,7 +1478,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1445
1478
|
const inner = unwrapConstraints(typeNode, reporter, imports, constraints);
|
|
1446
1479
|
const kind = resolveAccountKind(inner, reporter, imports, structs);
|
|
1447
1480
|
if (optional &&
|
|
1448
|
-
(["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1481
|
+
(["init", "initIfNeeded", "zeroed", "migrate", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1449
1482
|
((kind.kind === "token" || kind.kind === "mint") && kind.init)))
|
|
1450
1483
|
reporter.fail(p, "Init, InitIfNeeded, Zeroed, PythPriceUpdate, EventAuthority, SelfProgram and ProgramData accounts cannot be optional");
|
|
1451
1484
|
accounts.push({ name: p.name.text, index: accounts.length, kind, node: p, constraints, ...(optional ? { optional } : {}) });
|
|
@@ -1506,9 +1539,20 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1506
1539
|
if (!accounts.some((o) => o.name === f))
|
|
1507
1540
|
reporter.fail(a.node, `HasOne<…, "${f}">: add an account parameter named "${f}"`);
|
|
1508
1541
|
}
|
|
1509
|
-
if (c.seeds?.bump) {
|
|
1542
|
+
if (c.seeds?.bump?.startsWith("@")) {
|
|
1543
|
+
// "@acct.field": the bump stored in another struct account (Anchor `bump = multisig.nonce`).
|
|
1544
|
+
const [owner, field, ...rest] = c.seeds.bump.slice(1).split(".");
|
|
1545
|
+
const o = accounts.find((x) => x.name === owner);
|
|
1546
|
+
const os = o?.kind.kind === "account" ? o.kind.struct : undefined;
|
|
1547
|
+
if (!os || !field || rest.length)
|
|
1548
|
+
reporter.fail(a.node, `Seeds bump: "${c.seeds.bump}" must name a u8 field of a struct account parameter ("@config.bump")`);
|
|
1549
|
+
const bf = os.fields.find((x) => x.name === field);
|
|
1550
|
+
if (!bf || bf.type.kind !== "int" || bf.type.name !== "u8")
|
|
1551
|
+
reporter.fail(a.node, `Seeds bump: ${os.name} needs a u8 field "${field}"`);
|
|
1552
|
+
}
|
|
1553
|
+
else if (c.seeds?.bump) {
|
|
1510
1554
|
if (!struct)
|
|
1511
|
-
reporter.fail(a.node,
|
|
1555
|
+
reporter.fail(a.node, `Seeds with a bump field needs a struct account; for a bump stored elsewhere name it "@acct.field"`);
|
|
1512
1556
|
const bf = struct.fields.find((x) => x.name === c.seeds.bump);
|
|
1513
1557
|
if (!bf || bf.type.kind !== "int" || bf.type.name !== "u8")
|
|
1514
1558
|
reporter.fail(a.node, `Seeds bump: ${struct.name} needs a u8 field "${c.seeds.bump}"`);
|
|
@@ -1539,6 +1583,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1539
1583
|
...c.hasOne.map((e) => e.split("=").at(-1)),
|
|
1540
1584
|
...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1).split(".")[0]),
|
|
1541
1585
|
...(c.seeds?.program?.startsWith("@") ? [c.seeds.program.slice(1)] : []),
|
|
1586
|
+
...(c.seeds?.bump?.startsWith("@") ? [c.seeds.bump.slice(1).split(".")[0]] : []),
|
|
1542
1587
|
...(c.closeTo ? [c.closeTo] : []),
|
|
1543
1588
|
...(c.ata
|
|
1544
1589
|
? [c.ata.owner, c.ata.mint, ...(a.kind.kind === "token" ? [] : accounts.filter((o) => o.kind.kind === "tokenProgram").map((o) => o.name))]
|
|
@@ -1566,6 +1611,18 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1566
1611
|
reporter.error(fn.name, "instructions that grow a Vec (push) need a SystemProgram parameter to top up rent");
|
|
1567
1612
|
if (needs("init") && !needs("system"))
|
|
1568
1613
|
reporter.error(fn.name, "instructions with Init<...> accounts need a SystemProgram parameter");
|
|
1614
|
+
if (accounts.some((a) => a.kind.kind === "migrate" && a.kind.to.size > a.kind.from.size) && !needs("system"))
|
|
1615
|
+
reporter.error(fn.name, "a Migrate<From, To> that grows the account needs a SystemProgram parameter (the payer tops up its rent)");
|
|
1616
|
+
for (const a of accounts)
|
|
1617
|
+
if (a.kind.kind === "migrate" &&
|
|
1618
|
+
(a.constraints.hasOne.length ||
|
|
1619
|
+
a.constraints.seeds ||
|
|
1620
|
+
a.constraints.closeTo ||
|
|
1621
|
+
a.constraints.ata ||
|
|
1622
|
+
a.constraints.address ||
|
|
1623
|
+
a.constraints.owner ||
|
|
1624
|
+
a.constraints.dup))
|
|
1625
|
+
reporter.error(a.node, "Migrate<From, To> takes no constraints yet: check the old fields with require(…, …) on acct.from");
|
|
1569
1626
|
const name = snakeCase(fn.name.text);
|
|
1570
1627
|
// `/** @discriminator spl-transfer-hook-interface:execute */` (sha256 preimage) or `@discriminator 0x...` (raw bytes)
|
|
1571
1628
|
// lets an instruction implement another program's interface.
|
|
@@ -1596,7 +1653,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1596
1653
|
/** Peel HasOne / Seeds / CloseTo wrappers off a parameter type, recording what they ask for. */
|
|
1597
1654
|
function unwrapConstraints(node, reporter, imports, out) {
|
|
1598
1655
|
const name = typesolName(node, imports);
|
|
1599
|
-
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
|
|
1656
|
+
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable", "Dup", "Signed"].includes(name ?? ""))
|
|
1600
1657
|
return node;
|
|
1601
1658
|
const args = node.typeArguments ?? [];
|
|
1602
1659
|
const lit = (t) => {
|
|
@@ -1638,6 +1695,10 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1638
1695
|
}
|
|
1639
1696
|
if (name === "Executable")
|
|
1640
1697
|
out.executable = true;
|
|
1698
|
+
if (name === "Dup")
|
|
1699
|
+
out.dup = true;
|
|
1700
|
+
if (name === "Signed")
|
|
1701
|
+
out.signed = true;
|
|
1641
1702
|
if (name === "Address") {
|
|
1642
1703
|
const text = lit(args[1])[0];
|
|
1643
1704
|
const bytes = text === undefined ? undefined : decodeAddress(text);
|
|
@@ -1668,6 +1729,8 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1668
1729
|
reporter.fail(node, `${name}<…> wraps an account type`);
|
|
1669
1730
|
return unwrapConstraints(args[0], reporter, imports, out);
|
|
1670
1731
|
}
|
|
1732
|
+
/** Solana: an account grows at most this many bytes per instruction. */
|
|
1733
|
+
const MAX_PERMITTED_DATA_INCREASE = 10_240;
|
|
1671
1734
|
function resolveAccountKind(node, reporter, imports, structs) {
|
|
1672
1735
|
const outer = typesolName(node, imports);
|
|
1673
1736
|
const ref = node;
|
|
@@ -1680,6 +1743,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1680
1743
|
const s = ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) ? structs.get(t.typeName.text) : undefined;
|
|
1681
1744
|
if (s && s.size === undefined)
|
|
1682
1745
|
reporter.fail(t, `${s.name} has a variable-length string field and cannot be account data; use FixedString<N>`);
|
|
1746
|
+
if (s)
|
|
1747
|
+
checkAccountMembers(s, t, reporter);
|
|
1683
1748
|
return s;
|
|
1684
1749
|
};
|
|
1685
1750
|
switch (outer) {
|
|
@@ -1769,14 +1834,30 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1769
1834
|
reporter.fail(node, `${s.name} is too large to create in a program; use Zeroed<${s.name}>`);
|
|
1770
1835
|
return { kind: "initIfNeeded", struct: s };
|
|
1771
1836
|
}
|
|
1837
|
+
case "Migrate": {
|
|
1838
|
+
const [fromNode, toNode] = ref.typeArguments ?? [];
|
|
1839
|
+
const from = fromNode && structOf(fromNode);
|
|
1840
|
+
const to = toNode && structOf(toNode);
|
|
1841
|
+
if (!from || !to || ref.typeArguments.length !== 2)
|
|
1842
|
+
reporter.fail(node, "Migrate<From, To> takes two account structs declared in this program");
|
|
1843
|
+
for (const s of [from, to]) {
|
|
1844
|
+
if (s.vec)
|
|
1845
|
+
reporter.fail(node, `${s.name} has a growable Vec; Migrate<From, To> takes fixed-size structs`);
|
|
1846
|
+
if (s.layout === "borsh")
|
|
1847
|
+
reporter.fail(node, `${s.name} is @layout borsh; Migrate<From, To> takes fixed layouts`);
|
|
1848
|
+
}
|
|
1849
|
+
if (from.accountDiscriminator.join() === to.accountDiscriminator.join())
|
|
1850
|
+
reporter.fail(node, `${from.name} and ${to.name} have the same discriminator: give the new struct another name (or the old one /** @discriminator account:<OldName> */)`);
|
|
1851
|
+
if (to.size - from.size > MAX_PERMITTED_DATA_INCREASE)
|
|
1852
|
+
reporter.fail(node, `${to.name} is more than 10 KiB larger than ${from.name}; an account grows at most 10 KiB per instruction`);
|
|
1853
|
+
return { kind: "migrate", from, to };
|
|
1854
|
+
}
|
|
1772
1855
|
case "Zeroed": {
|
|
1773
1856
|
const s = structOf(inner());
|
|
1774
1857
|
if (!s)
|
|
1775
1858
|
reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
|
|
1776
1859
|
if (s.vec)
|
|
1777
|
-
reporter.fail(node, `${s.name} has a growable Vec; Zeroed<T> is for fixed-size (
|
|
1778
|
-
if (s.layout === "borsh")
|
|
1779
|
-
reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
|
|
1860
|
+
reporter.fail(node, `${s.name} has a growable Vec; Zeroed<T> is for fixed-size accounts (or @layout borsh with FixedVec)`);
|
|
1780
1861
|
return { kind: "zeroed", struct: s };
|
|
1781
1862
|
}
|
|
1782
1863
|
case "Mut": {
|
|
@@ -112,6 +112,8 @@ export declare abstract class EmitterBase {
|
|
|
112
112
|
protected abstract call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
|
|
113
113
|
protected abstract isRemaining(node: ts.Expression): boolean;
|
|
114
114
|
protected abstract handle(node: ts.Expression): string | undefined;
|
|
115
|
+
protected abstract instructionsParam(node: ts.Expression): AccountParam | undefined;
|
|
116
|
+
protected abstract isTxIx(node: ts.Expression): boolean;
|
|
115
117
|
protected abstract seeds(node: ts.Expression, extra: number): {
|
|
116
118
|
name: string;
|
|
117
119
|
count: number;
|
|
@@ -129,6 +131,7 @@ export declare abstract class EmitterBase {
|
|
|
129
131
|
protected abstract coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
|
|
130
132
|
protected abstract bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
|
|
131
133
|
protected abstract invoke(node: ts.CallExpression): undefined;
|
|
134
|
+
protected abstract invokeInstruction(node: ts.CallExpression): undefined;
|
|
132
135
|
protected abstract emitEvent(node: ts.CallExpression, cpi?: {
|
|
133
136
|
authority: string;
|
|
134
137
|
program: string;
|
|
@@ -65,7 +65,10 @@ export class EmitterBase {
|
|
|
65
65
|
this.ix = target;
|
|
66
66
|
for (const a of target.accounts) {
|
|
67
67
|
this.accounts.set(a.name, a);
|
|
68
|
-
if (a.kind.kind === "init" ||
|
|
68
|
+
if (a.kind.kind === "init" ||
|
|
69
|
+
a.kind.kind === "initIfNeeded" ||
|
|
70
|
+
a.kind.kind === "migrate" ||
|
|
71
|
+
((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init))
|
|
69
72
|
this.pendingInit.add(a.name);
|
|
70
73
|
}
|
|
71
74
|
for (const a of target.args)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { type ErrorDef, type FieldType, type Helper } from "../model.js";
|
|
2
|
+
import { type AccountParam, type ErrorDef, type FieldType, type Helper } from "../model.js";
|
|
3
3
|
import { type Value, type Ref, type Closure } from "./shared.js";
|
|
4
4
|
import { ExpressionEmitter } from "./expressions.js";
|
|
5
5
|
export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
@@ -20,6 +20,15 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
|
20
20
|
/** `remainingAccounts()` or a const bound to it. */
|
|
21
21
|
protected isRemaining(node: ts.Expression): boolean;
|
|
22
22
|
/** C pointer (`TsAccount *`) for an expression naming a remaining account, or undefined. */
|
|
23
|
+
/** Whether `node` is an InstructionsSysvar parameter. */
|
|
24
|
+
protected instructionsParam(node: ts.Expression): AccountParam | undefined;
|
|
25
|
+
/** Whether `node` is a TransactionInstruction: a local holding one, or `ixs.get(i)`. */
|
|
26
|
+
protected isTxIx(node: ts.Expression): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* InstructionsSysvar.currentIndex() / .get(i) and the methods of a TransactionInstruction: bounds-checked reads of the
|
|
29
|
+
* sysvar (ts_ix_* in the runtime). Undefined when the call is something else.
|
|
30
|
+
*/
|
|
31
|
+
protected instructionsCall(node: ts.CallExpression, callee: ts.PropertyAccessExpression, method: string): Value | undefined;
|
|
23
32
|
protected handle(node: ts.Expression): string | undefined;
|
|
24
33
|
/** A declared account parameter or a remaining account, as a `TsAccount *` C expression. */
|
|
25
34
|
protected anyAccount(node: ts.Expression, what: string): string;
|
|
@@ -29,6 +38,12 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
|
29
38
|
protected loadForeign(node: ts.CallExpression): Value;
|
|
30
39
|
/** `loadAccount<T>(acct)` / `loadAccountMut<T>(acct)`: checked view of a program account. */
|
|
31
40
|
protected loadAccount(node: ts.CallExpression, mut: boolean): Value;
|
|
41
|
+
/**
|
|
42
|
+
* `acct.migrate(payer, value)` on a Migrate<From, To> account: the new value is built in full first (it may read
|
|
43
|
+
* acct.from, whose bytes the rewrite overwrites), then the account is resized (the payer tops up the rent when it
|
|
44
|
+
* grows; shrinking keeps the lamports) and rewritten with To's discriminator and the value.
|
|
45
|
+
*/
|
|
46
|
+
protected migrate(node: ts.CallExpression, a: AccountParam, topLevel: boolean): Value | undefined;
|
|
32
47
|
/** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
|
|
33
48
|
private helperGrows;
|
|
34
49
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
|
|
2
|
+
import { calleeName, discriminator, resolveScalarType, resolveValueType, snakeCase } 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";
|
|
@@ -21,6 +21,11 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
21
21
|
return this.cast(args[0], { kind: "int", name: builtin });
|
|
22
22
|
}
|
|
23
23
|
switch (builtin) {
|
|
24
|
+
case "stackHeight": {
|
|
25
|
+
if (args.length)
|
|
26
|
+
this.fail(node, "stackHeight() takes no arguments");
|
|
27
|
+
return { c: "sol_get_stack_height()", type: U64 };
|
|
28
|
+
}
|
|
24
29
|
case "require": {
|
|
25
30
|
if (args.length !== 2)
|
|
26
31
|
this.fail(node, "require(condition, error) takes two arguments");
|
|
@@ -283,6 +288,8 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
283
288
|
}
|
|
284
289
|
case "invoke":
|
|
285
290
|
return this.invoke(node);
|
|
291
|
+
case "invokeInstruction":
|
|
292
|
+
return this.invokeInstruction(node);
|
|
286
293
|
case "sha256":
|
|
287
294
|
case "keccak256":
|
|
288
295
|
return this.hashCall(node, builtin);
|
|
@@ -484,6 +491,9 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
484
491
|
return this.callHelper(node, helper);
|
|
485
492
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
486
493
|
const method = callee.name.text;
|
|
494
|
+
const introspected = this.instructionsCall(node, callee, method);
|
|
495
|
+
if (introspected)
|
|
496
|
+
return introspected;
|
|
487
497
|
// xs.some(…) and friends on a Vec, FixedArray, FixedVec or Vec argument: a loop with the callback expanded in it.
|
|
488
498
|
if (LIST_METHODS.has(method)) {
|
|
489
499
|
const l = ts.isIdentifier(callee.expression) ? this.lookup(callee.expression.text) : undefined;
|
|
@@ -570,6 +580,8 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
570
580
|
return target.kind.kind === "token" && target.constraints.ata ? this.createAta(node, target, topLevel) : this.createToken(node, target, topLevel);
|
|
571
581
|
if (target?.kind.kind === "init" && method === "create")
|
|
572
582
|
return this.create(node, target, topLevel);
|
|
583
|
+
if (target?.kind.kind === "migrate" && method === "migrate")
|
|
584
|
+
return this.migrate(node, target, topLevel);
|
|
573
585
|
if (target?.kind.kind === "initIfNeeded" && method === "createIfNeeded")
|
|
574
586
|
return this.create(node, target, topLevel, true);
|
|
575
587
|
if (target?.kind.kind === "system" && method === "transfer")
|
|
@@ -878,6 +890,92 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
878
890
|
return ts.isCallExpression(node) && this.exportName(node.expression) === "remainingAccounts";
|
|
879
891
|
}
|
|
880
892
|
/** C pointer (`TsAccount *`) for an expression naming a remaining account, or undefined. */
|
|
893
|
+
/** Whether `node` is an InstructionsSysvar parameter. */
|
|
894
|
+
instructionsParam(node) {
|
|
895
|
+
const a = ts.isIdentifier(node) ? this.accounts.get(node.text) : undefined;
|
|
896
|
+
return a?.kind.kind === "instructionsSysvar" ? a : undefined;
|
|
897
|
+
}
|
|
898
|
+
/** Whether `node` is a TransactionInstruction: a local holding one, or `ixs.get(i)`. */
|
|
899
|
+
isTxIx(node) {
|
|
900
|
+
if (ts.isParenthesizedExpression(node))
|
|
901
|
+
return this.isTxIx(node.expression);
|
|
902
|
+
if (ts.isIdentifier(node))
|
|
903
|
+
return this.lookup(node.text)?.type.kind === "txIx";
|
|
904
|
+
return (ts.isCallExpression(node) &&
|
|
905
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
906
|
+
node.expression.name.text === "get" &&
|
|
907
|
+
!!this.instructionsParam(node.expression.expression));
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* InstructionsSysvar.currentIndex() / .get(i) and the methods of a TransactionInstruction: bounds-checked reads of the
|
|
911
|
+
* sysvar (ts_ix_* in the runtime). Undefined when the call is something else.
|
|
912
|
+
*/
|
|
913
|
+
instructionsCall(node, callee, method) {
|
|
914
|
+
const args = node.arguments;
|
|
915
|
+
const sysvar = this.instructionsParam(callee.expression);
|
|
916
|
+
if (sysvar) {
|
|
917
|
+
this.assertUsable(sysvar, callee.expression);
|
|
918
|
+
if (method === "currentIndex") {
|
|
919
|
+
if (args.length)
|
|
920
|
+
this.fail(node, "currentIndex() takes no arguments");
|
|
921
|
+
const t = this.tmp("ixi");
|
|
922
|
+
this.line(`uint64_t ${t};`);
|
|
923
|
+
this.line(`TS_TRY(ts_ix_current(a_${sysvar.name}, &${t}));`);
|
|
924
|
+
return { c: t, type: U64 };
|
|
925
|
+
}
|
|
926
|
+
if (method === "get") {
|
|
927
|
+
if (args.length !== 1)
|
|
928
|
+
this.fail(node, "get(index) takes the instruction's index");
|
|
929
|
+
const i = this.expect(this.expr(args[0], U64), U64, args[0]);
|
|
930
|
+
const t = this.tmp("ix");
|
|
931
|
+
this.line(`TsIx ${t};`);
|
|
932
|
+
this.line(`TS_TRY(ts_ix_load(a_${sysvar.name}, ${i.c}, &${t}));`);
|
|
933
|
+
return { c: t, type: { kind: "txIx" } };
|
|
934
|
+
}
|
|
935
|
+
this.fail(node, "the Instructions sysvar has .length, currentIndex() and get(index)");
|
|
936
|
+
}
|
|
937
|
+
if (!this.isTxIx(callee.expression))
|
|
938
|
+
return undefined;
|
|
939
|
+
const ix = this.expr(callee.expression);
|
|
940
|
+
const one = (what) => {
|
|
941
|
+
if (args.length !== 1)
|
|
942
|
+
this.fail(node, `${method}(${what}) takes one argument`);
|
|
943
|
+
return this.expect(this.expr(args[0], U64), U64, args[0]).c;
|
|
944
|
+
};
|
|
945
|
+
if (method === "accountKey" || method === "accountIsSigner" || method === "accountIsWritable") {
|
|
946
|
+
const i = one("i");
|
|
947
|
+
const t = this.tmp("ixa");
|
|
948
|
+
this.line(`const uint8_t *${t};`);
|
|
949
|
+
this.line(`TS_TRY(ts_ix_account(&${ix.c}, ${i}, &${t}));`);
|
|
950
|
+
if (method === "accountKey")
|
|
951
|
+
return { c: `((const SolPubkey *)(${t} + 1))`, type: PUBKEY };
|
|
952
|
+
return { c: `((${t}[0] & ${method === "accountIsSigner" ? 1 : 2}) != 0)`, type: BOOL };
|
|
953
|
+
}
|
|
954
|
+
const reads = {
|
|
955
|
+
readU8: ["uint8_t", U8],
|
|
956
|
+
readU16: ["uint16_t", { kind: "int", name: "u16" }],
|
|
957
|
+
readU32: ["uint32_t", { kind: "int", name: "u32" }],
|
|
958
|
+
readU64: ["uint64_t", U64],
|
|
959
|
+
readPubkey: ["SolPubkey", PUBKEY],
|
|
960
|
+
};
|
|
961
|
+
if (reads[method]) {
|
|
962
|
+
const [c, type] = reads[method];
|
|
963
|
+
const off = one("offset");
|
|
964
|
+
const t = this.tmp("ixr");
|
|
965
|
+
this.line(`${c} ${t};`);
|
|
966
|
+
this.line(`TS_TRY(ts_ix_read(&${ix.c}, ${off}, sizeof(${c}), &${t}));`);
|
|
967
|
+
return type.kind === "pubkey" ? { c: `(&${t})`, type } : { c: t, type };
|
|
968
|
+
}
|
|
969
|
+
if (method === "is") {
|
|
970
|
+
if (args.length !== 2 || !ts.isStringLiteralLike(args[1]))
|
|
971
|
+
this.fail(node, 'is(program, "instruction_name") takes the program and the instruction name as a string literal');
|
|
972
|
+
const program = this.expect(this.expr(args[0], PUBKEY), PUBKEY, args[0]);
|
|
973
|
+
const disc = this.tmp("disc");
|
|
974
|
+
this.line(`static const uint8_t ${disc}[8] = ${cBytes(discriminator(`global:${snakeCase(args[1].text)}`))};`);
|
|
975
|
+
return { c: `ts_ix_is(&${ix.c}, ${program.c}, ${disc})`, type: BOOL };
|
|
976
|
+
}
|
|
977
|
+
this.fail(node, "a TransactionInstruction has programId, accountsLength, dataLength, accountKey/accountIsSigner/accountIsWritable(i), readU8/U16/U32/U64/readPubkey(offset) and is(program, name)");
|
|
978
|
+
}
|
|
881
979
|
handle(node) {
|
|
882
980
|
if (ts.isParenthesizedExpression(node))
|
|
883
981
|
return this.handle(node.expression);
|
|
@@ -984,6 +1082,38 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
984
1082
|
}
|
|
985
1083
|
return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
|
|
986
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* `acct.migrate(payer, value)` on a Migrate<From, To> account: the new value is built in full first (it may read
|
|
1087
|
+
* acct.from, whose bytes the rewrite overwrites), then the account is resized (the payer tops up the rent when it
|
|
1088
|
+
* grows; shrinking keeps the lamports) and rewritten with To's discriminator and the value.
|
|
1089
|
+
*/
|
|
1090
|
+
migrate(node, a, topLevel) {
|
|
1091
|
+
const k = a.kind;
|
|
1092
|
+
const call = `${a.name}.migrate`;
|
|
1093
|
+
if (!topLevel)
|
|
1094
|
+
this.fail(node, `${call}(…) must be a top-level statement of the instruction`);
|
|
1095
|
+
if (this.created.has(a.name))
|
|
1096
|
+
this.fail(node, `${a.name} is already migrated`);
|
|
1097
|
+
if (node.arguments.length !== 2)
|
|
1098
|
+
this.fail(node, `${call}(payer, value) takes the payer and the new ${k.to.name}`);
|
|
1099
|
+
const payer = this.accountParam(node.arguments[0], "the payer");
|
|
1100
|
+
if (payer.kind.kind !== "signer" || !payer.kind.mut)
|
|
1101
|
+
this.fail(node.arguments[0], "the payer must be a Mut<Signer>");
|
|
1102
|
+
this.assertNotAliased([a.name], node, "migrate");
|
|
1103
|
+
const To = k.to.name;
|
|
1104
|
+
const tmp = this.tmp("next");
|
|
1105
|
+
this.line(`TSS_${To} *${tmp} = NULL;`);
|
|
1106
|
+
this.line(`TS_TRY(ts_alloc(sizeof(TSS_${To}), (uint8_t **)&${tmp}));`);
|
|
1107
|
+
this.writeAggregate({ lv: `(*${tmp})`, type: { kind: "struct", name: To }, writable: true }, node.arguments[1]);
|
|
1108
|
+
this.line(`TS_TRY(ts_resize(params, a_${payer.name}, a_${a.name}, 8 + sizeof(TSS_${To}), entry_${a.name}));`);
|
|
1109
|
+
this.line(`ts_memcpy(a_${a.name}->data, TS_DISC_${To}, 8);`);
|
|
1110
|
+
this.line(`ts_memcpy(a_${a.name}->data + 8, ${tmp}, sizeof(TSS_${To}));`);
|
|
1111
|
+
this.line(`TSS_${To} *dt_${a.name} = (TSS_${To} *)(a_${a.name}->data + 8);`);
|
|
1112
|
+
this.line(`(void)dt_${a.name};`);
|
|
1113
|
+
this.created.add(a.name);
|
|
1114
|
+
this.pendingInit.delete(a.name);
|
|
1115
|
+
return undefined;
|
|
1116
|
+
}
|
|
987
1117
|
/** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
|
|
988
1118
|
helperGrows(body, seen = new Set()) {
|
|
989
1119
|
if (seen.has(body))
|
|
@@ -157,6 +157,15 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
157
157
|
to: string;
|
|
158
158
|
} | undefined;
|
|
159
159
|
protected invoke(node: ts.CallExpression): undefined;
|
|
160
|
+
/**
|
|
161
|
+
* `invokeInstruction(programId, metas, data, infos, signerSeeds?)`: a CPI decided at run time (Rust's
|
|
162
|
+
* `invoke_signed(&Instruction { program_id, accounts, data }, infos, seeds)`), e.g. one stored in account data. Each
|
|
163
|
+
* meta is a struct with `pubkey`, `isSigner` and `isWritable`; `data` a list of u8; every account in `infos` (the
|
|
164
|
+
* remaining accounts, or a slice of them) goes to the runtime, which finds each meta's account there by key (an
|
|
165
|
+
* account missing there fails the CPI, as under Rust) and checks the privileges: a meta may only be a signer or
|
|
166
|
+
* writable as the transaction passed it, or a PDA of this program that `signerSeeds` sign for.
|
|
167
|
+
*/
|
|
168
|
+
protected invokeInstruction(node: ts.CallExpression): undefined;
|
|
160
169
|
/**
|
|
161
170
|
* A byte buffer written front to back. Its C declaration is patched in afterwards so it is
|
|
162
171
|
* exactly as large as the fixed content, and only falls back to `max` when strings make it dynamic.
|
|
@@ -2746,6 +2746,69 @@ export class CpiEmitter extends CallEmitter {
|
|
|
2746
2746
|
this.line(`TS_TRY(ts_invoke(params, &${ixName}, NULL, 0));`);
|
|
2747
2747
|
return undefined;
|
|
2748
2748
|
}
|
|
2749
|
+
/**
|
|
2750
|
+
* `invokeInstruction(programId, metas, data, infos, signerSeeds?)`: a CPI decided at run time (Rust's
|
|
2751
|
+
* `invoke_signed(&Instruction { program_id, accounts, data }, infos, seeds)`), e.g. one stored in account data. Each
|
|
2752
|
+
* meta is a struct with `pubkey`, `isSigner` and `isWritable`; `data` a list of u8; every account in `infos` (the
|
|
2753
|
+
* remaining accounts, or a slice of them) goes to the runtime, which finds each meta's account there by key (an
|
|
2754
|
+
* account missing there fails the CPI, as under Rust) and checks the privileges: a meta may only be a signer or
|
|
2755
|
+
* writable as the transaction passed it, or a PDA of this program that `signerSeeds` sign for.
|
|
2756
|
+
*/
|
|
2757
|
+
invokeInstruction(node) {
|
|
2758
|
+
const a = node.arguments;
|
|
2759
|
+
if (!this.ix)
|
|
2760
|
+
this.fail(node, "invokeInstruction is only available in instructions (it passes remaining accounts)");
|
|
2761
|
+
if (a.length !== 4 && a.length !== 5)
|
|
2762
|
+
this.fail(node, "invokeInstruction(programId, accounts, data, infos, signerSeeds?) takes 4 or 5 arguments");
|
|
2763
|
+
const program = this.expect(this.expr(a[0], PUBKEY), PUBKEY, a[0]);
|
|
2764
|
+
const programKey = this.tmp("dprog");
|
|
2765
|
+
this.line(`SolPubkey ${programKey} = *(${program.c});`);
|
|
2766
|
+
const list = (e, what) => {
|
|
2767
|
+
const l = ts.isIdentifier(e) ? this.lookup(e.text) : undefined;
|
|
2768
|
+
if (l?.type.kind === "vecArg")
|
|
2769
|
+
return { items: `${l.c}.ptr`, count: `((uint64_t)${l.c}.len)`, elem: l.type.elem };
|
|
2770
|
+
const r = this.ref(e);
|
|
2771
|
+
if (r?.vec && r.type.kind === "vec")
|
|
2772
|
+
return { items: `${r.vec.items}`, count: vecCount(r.vec), elem: r.type.elem };
|
|
2773
|
+
this.fail(e, `${what}: a Vec or FixedVec (account data, an argument or a local list)`);
|
|
2774
|
+
};
|
|
2775
|
+
const metas = list(a[1], "the accounts");
|
|
2776
|
+
const elem = metas.elem;
|
|
2777
|
+
const st = elem.kind === "struct" ? this.model.structs.get(elem.name) : undefined;
|
|
2778
|
+
const field = (name, kind) => st?.fields.find((f) => f.name === name && f.type.kind === kind);
|
|
2779
|
+
if (!st || !field("pubkey", "pubkey") || !field("isSigner", "bool") || !field("isWritable", "bool"))
|
|
2780
|
+
this.fail(a[1], "each account is a struct with pubkey: Pubkey, isSigner: boolean and isWritable: boolean");
|
|
2781
|
+
const data = list(a[2], "the data");
|
|
2782
|
+
if (data.elem.kind !== "int" || data.elem.name !== "u8")
|
|
2783
|
+
this.fail(a[2], "the data is a list of u8");
|
|
2784
|
+
const range = this.remainingRange(a[3]) ?? this.fail(a[3], "pass the accounts the CPI may use: remainingAccounts() or rest.slice(a, b)");
|
|
2785
|
+
const seeds = a.length === 5 ? this.signingSeeds(a[4]) : undefined;
|
|
2786
|
+
const n = this.tmp("dn");
|
|
2787
|
+
const m = this.tmp("dmetas");
|
|
2788
|
+
const i = this.tmp("di");
|
|
2789
|
+
const k = this.tmp("dk");
|
|
2790
|
+
this.line(`uint64_t ${n} = ${metas.count};`);
|
|
2791
|
+
this.line(`SolAccountMeta *${m} = NULL;`);
|
|
2792
|
+
this.line(`TS_TRY(ts_alloc(${n} * sizeof(SolAccountMeta) + 1, (uint8_t **)&${m}));`);
|
|
2793
|
+
this.line(`for (uint64_t ${i} = 0; ${i} < ${n}; ${i}++) {`);
|
|
2794
|
+
this.line(` const TSS_${st.name} *e = &((const TSS_${st.name} *)${metas.items})[${i}];`);
|
|
2795
|
+
this.line(` ${m}[${i}] = (SolAccountMeta){(SolPubkey *)&e->pubkey, e->isWritable != 0, e->isSigner != 0};`);
|
|
2796
|
+
this.line(`}`);
|
|
2797
|
+
// Every account offered goes along as an AccountInfo (ts_invoke's scratch holds TS_MAX_ACCOUNTS of them).
|
|
2798
|
+
const base = this.ix.accounts.length;
|
|
2799
|
+
this.line(`uint64_t ${k} = 0;`);
|
|
2800
|
+
this.line(`for (uint64_t ${i}b = ${base} + ${range.from}; ${i}b < ${base} + ${range.to}; ${i}b++) ts_info(params->ka[${i}b], ¶ms->infos[${k}++]);`);
|
|
2801
|
+
const ixName = this.tmp("cpi");
|
|
2802
|
+
this.line(`SolInstruction ${ixName} = {&${programKey}, ${m}, ${n}, (uint8_t *)${data.items}, ${data.count}};`);
|
|
2803
|
+
if (seeds) {
|
|
2804
|
+
const signer = this.tmp("signer");
|
|
2805
|
+
this.line(`SolSignerSeeds ${signer} = {${seeds.name}, ${seeds.count}};`);
|
|
2806
|
+
this.line(`TS_TRY(sol_invoke_signed(&${ixName}, params->infos, ${k}, &${signer}, ${typeof seeds.count === "number" ? (seeds.count > 0 ? 1 : 0) : `${seeds.count} > 0 ? 1 : 0`}));`);
|
|
2807
|
+
}
|
|
2808
|
+
else
|
|
2809
|
+
this.line(`TS_TRY(sol_invoke_signed(&${ixName}, params->infos, ${k}, NULL, 0));`);
|
|
2810
|
+
return undefined;
|
|
2811
|
+
}
|
|
2749
2812
|
/**
|
|
2750
2813
|
* A byte buffer written front to back. Its C declaration is patched in afterwards so it is
|
|
2751
2814
|
* exactly as large as the fixed content, and only falls back to `max` when strings make it dynamic.
|