@typesol/cli 0.27.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 +21 -0
- package/README.md +3 -1
- package/dist/compiler/analyze.d.ts +2 -0
- package/dist/compiler/analyze.js +32 -8
- package/dist/compiler/emit/base.d.ts +1 -0
- package/dist/compiler/emit/calls.js +2 -0
- package/dist/compiler/emit/cpi.d.ts +9 -0
- package/dist/compiler/emit/cpi.js +63 -0
- package/dist/compiler/emit/function.js +13 -5
- package/dist/compiler/emit/storage.js +22 -1
- package/dist/compiler/idl.js +1 -1
- package/dist/compiler/model.d.ts +2 -0
- package/docs/architecture.md +2 -1
- package/docs/audit.md +1 -0
- package/docs/case-study-multisig.md +167 -0
- package/docs/errors.md +3 -2
- package/docs/language.md +19 -4
- package/docs/migrate-from-anchor.md +5 -0
- package/docs/security-model.md +4 -0
- package/lib/typesol.d.ts +37 -4
- package/package.json +1 -1
- package/runtime/typesol_rt.h +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ 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
|
+
|
|
9
30
|
## 0.27.0 — 2026-10-29
|
|
10
31
|
|
|
11
32
|
Anchor 1.0 parity: instruction introspection, `Dup<…>` and account migration. Existing programs compile to the same
|
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.
|
|
@@ -86,6 +94,7 @@ const TYPE_EXPORTS = new Set([
|
|
|
86
94
|
"Owner",
|
|
87
95
|
"Executable",
|
|
88
96
|
"Dup",
|
|
97
|
+
"Signed",
|
|
89
98
|
"Migrate",
|
|
90
99
|
"Optional",
|
|
91
100
|
"EventAuthority",
|
|
@@ -1341,7 +1350,8 @@ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new S
|
|
|
1341
1350
|
wrapped === "Seeds" ||
|
|
1342
1351
|
wrapped === "CloseTo" ||
|
|
1343
1352
|
wrapped === "Address" ||
|
|
1344
|
-
wrapped === "Ata"
|
|
1353
|
+
wrapped === "Ata" ||
|
|
1354
|
+
wrapped === "Signed")
|
|
1345
1355
|
reporter.fail(p.type, "constraints belong on instruction parameters; helpers take the plain account type");
|
|
1346
1356
|
const account = resolveAccountKind(p.type, reporter, scope.imports, structs);
|
|
1347
1357
|
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed" || account.kind === "migrate")
|
|
@@ -1529,9 +1539,20 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1529
1539
|
if (!accounts.some((o) => o.name === f))
|
|
1530
1540
|
reporter.fail(a.node, `HasOne<…, "${f}">: add an account parameter named "${f}"`);
|
|
1531
1541
|
}
|
|
1532
|
-
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) {
|
|
1533
1554
|
if (!struct)
|
|
1534
|
-
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"`);
|
|
1535
1556
|
const bf = struct.fields.find((x) => x.name === c.seeds.bump);
|
|
1536
1557
|
if (!bf || bf.type.kind !== "int" || bf.type.name !== "u8")
|
|
1537
1558
|
reporter.fail(a.node, `Seeds bump: ${struct.name} needs a u8 field "${c.seeds.bump}"`);
|
|
@@ -1562,6 +1583,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1562
1583
|
...c.hasOne.map((e) => e.split("=").at(-1)),
|
|
1563
1584
|
...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1).split(".")[0]),
|
|
1564
1585
|
...(c.seeds?.program?.startsWith("@") ? [c.seeds.program.slice(1)] : []),
|
|
1586
|
+
...(c.seeds?.bump?.startsWith("@") ? [c.seeds.bump.slice(1).split(".")[0]] : []),
|
|
1565
1587
|
...(c.closeTo ? [c.closeTo] : []),
|
|
1566
1588
|
...(c.ata
|
|
1567
1589
|
? [c.ata.owner, c.ata.mint, ...(a.kind.kind === "token" ? [] : accounts.filter((o) => o.kind.kind === "tokenProgram").map((o) => o.name))]
|
|
@@ -1631,7 +1653,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1631
1653
|
/** Peel HasOne / Seeds / CloseTo wrappers off a parameter type, recording what they ask for. */
|
|
1632
1654
|
function unwrapConstraints(node, reporter, imports, out) {
|
|
1633
1655
|
const name = typesolName(node, imports);
|
|
1634
|
-
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable", "Dup"].includes(name ?? ""))
|
|
1656
|
+
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable", "Dup", "Signed"].includes(name ?? ""))
|
|
1635
1657
|
return node;
|
|
1636
1658
|
const args = node.typeArguments ?? [];
|
|
1637
1659
|
const lit = (t) => {
|
|
@@ -1675,6 +1697,8 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1675
1697
|
out.executable = true;
|
|
1676
1698
|
if (name === "Dup")
|
|
1677
1699
|
out.dup = true;
|
|
1700
|
+
if (name === "Signed")
|
|
1701
|
+
out.signed = true;
|
|
1678
1702
|
if (name === "Address") {
|
|
1679
1703
|
const text = lit(args[1])[0];
|
|
1680
1704
|
const bytes = text === undefined ? undefined : decodeAddress(text);
|
|
@@ -1719,6 +1743,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1719
1743
|
const s = ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) ? structs.get(t.typeName.text) : undefined;
|
|
1720
1744
|
if (s && s.size === undefined)
|
|
1721
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);
|
|
1722
1748
|
return s;
|
|
1723
1749
|
};
|
|
1724
1750
|
switch (outer) {
|
|
@@ -1831,9 +1857,7 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1831
1857
|
if (!s)
|
|
1832
1858
|
reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
|
|
1833
1859
|
if (s.vec)
|
|
1834
|
-
reporter.fail(node, `${s.name} has a growable Vec; Zeroed<T> is for fixed-size (
|
|
1835
|
-
if (s.layout === "borsh")
|
|
1836
|
-
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)`);
|
|
1837
1861
|
return { kind: "zeroed", struct: s };
|
|
1838
1862
|
}
|
|
1839
1863
|
case "Mut": {
|
|
@@ -131,6 +131,7 @@ export declare abstract class EmitterBase {
|
|
|
131
131
|
protected abstract coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
|
|
132
132
|
protected abstract bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
|
|
133
133
|
protected abstract invoke(node: ts.CallExpression): undefined;
|
|
134
|
+
protected abstract invokeInstruction(node: ts.CallExpression): undefined;
|
|
134
135
|
protected abstract emitEvent(node: ts.CallExpression, cpi?: {
|
|
135
136
|
authority: string;
|
|
136
137
|
program: string;
|
|
@@ -288,6 +288,8 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
288
288
|
}
|
|
289
289
|
case "invoke":
|
|
290
290
|
return this.invoke(node);
|
|
291
|
+
case "invokeInstruction":
|
|
292
|
+
return this.invokeInstruction(node);
|
|
291
293
|
case "sha256":
|
|
292
294
|
case "keccak256":
|
|
293
295
|
return this.hashCall(node, builtin);
|
|
@@ -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.
|
|
@@ -40,8 +40,14 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
40
40
|
head.push(` TS_TRY(ts_check_signer(${v}));`);
|
|
41
41
|
// Mut<…>: Anchor's `mut` (2000). Anchor checks it with the constraints, after seeds and associated token; here it
|
|
42
42
|
// sits with the type checks, which keeps binaries smaller (only a transaction breaking both sees the difference).
|
|
43
|
+
// Zeroed<T> (Anchor's `zero`): its discriminator, owner and size come before `mut`, as under Anchor.
|
|
44
|
+
if (k.kind === "zeroed")
|
|
45
|
+
head.push(` TS_TRY(ts_check_zeroed(${v}, params->program_id, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : `sizeof(TSS_${k.struct.name})`}));`);
|
|
43
46
|
if (isWritable(k))
|
|
44
47
|
head.push(` TS_TRY(ts_check_mut(${v}));`);
|
|
48
|
+
// Signed<…>: Anchor's `signer` constraint (2002), right after `mut`.
|
|
49
|
+
if (a.constraints.signed)
|
|
50
|
+
head.push(` if (!${v}->is_signer) TS_FAIL(TS_ERR_CONSTRAINT_SIGNER, "TypeSOL: ${a.name} must sign");`);
|
|
45
51
|
// A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
|
|
46
52
|
if (k.kind === "account")
|
|
47
53
|
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : emptySizeC(k.struct)}));`);
|
|
@@ -94,10 +100,10 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
94
100
|
if (k.kind === "migrate")
|
|
95
101
|
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.from.name}, sizeof(TSS_${k.from.name})));`, ` const TSS_${k.from.name} *df_${a.name} = (const TSS_${k.from.name} *)(${v}->data + 8);`, ` uint64_t entry_${a.name} = ${v}->data_len;`);
|
|
96
102
|
if (k.kind === "zeroed")
|
|
97
|
-
head.push(` TS_TRY(ts_init_zeroed(${v},
|
|
103
|
+
head.push(` TS_TRY(ts_init_zeroed(${v}, TS_DISC_${k.struct.name}));`);
|
|
98
104
|
if (k.kind === "initIfNeeded" && k.struct.vec)
|
|
99
105
|
this.reporter.fail(a.node, "InitIfNeeded accounts cannot end in a Vec yet");
|
|
100
|
-
if ((k.kind === "account" || k.kind === "init" || k.kind === "initIfNeeded") && k.struct.layout === "borsh") {
|
|
106
|
+
if ((k.kind === "account" || k.kind === "init" || k.kind === "initIfNeeded" || k.kind === "zeroed") && k.struct.layout === "borsh") {
|
|
101
107
|
// Worked on as its fixed-layout struct on the heap, packed back when the instruction returns. The heap is zero at
|
|
102
108
|
// entry and these are its first allocations (before the body uses it), so a string's padding and a list's
|
|
103
109
|
// unused slots start as zero: ts_unpack_* only writes what the Borsh bytes hold.
|
|
@@ -379,7 +385,7 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
379
385
|
return [];
|
|
380
386
|
// @layout borsh accounts go back to their Borsh bytes (unless closed, or never created).
|
|
381
387
|
const packs = this.ix.accounts
|
|
382
|
-
.filter((a) => (a.kind.kind === "account" ? a.kind.mut : a.kind.kind === "init" || a.kind.kind === "initIfNeeded"))
|
|
388
|
+
.filter((a) => (a.kind.kind === "account" ? a.kind.mut : a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed"))
|
|
383
389
|
.filter((a) => a.kind.struct.layout === "borsh")
|
|
384
390
|
.map((a) => {
|
|
385
391
|
const v = `a_${a.name}`;
|
|
@@ -588,9 +594,11 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
588
594
|
const seeds = `cs_${a.name}`;
|
|
589
595
|
out.push(` SolSignerSeed ${seeds}[${parts.length + 1}] = {${parts.join(", ")}};`);
|
|
590
596
|
if (c.seeds.bump) {
|
|
591
|
-
|
|
597
|
+
// The account's own bump field, or "@acct.field": one stored in another struct account.
|
|
598
|
+
const bump = c.seeds.bump.startsWith("@") ? `d_${c.seeds.bump.slice(1).replace(".", "->")}` : `d_${a.name}->${c.seeds.bump}`;
|
|
599
|
+
out.push(` ${seeds}[${parts.length}] = (SolSignerSeed){&${bump}, 1};`);
|
|
592
600
|
out.push(` TS_TRY(ts_require_pda(a_${a.name}, ${seeds}, ${parts.length + 1}, ${program}));`);
|
|
593
|
-
this.bumps.set(a.name,
|
|
601
|
+
this.bumps.set(a.name, bump);
|
|
594
602
|
}
|
|
595
603
|
else {
|
|
596
604
|
bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
|
|
@@ -685,9 +685,30 @@ export class StorageEmitter extends EmitterBase {
|
|
|
685
685
|
this.line(`}`);
|
|
686
686
|
return;
|
|
687
687
|
}
|
|
688
|
+
// `list = owners`: a Vec argument, or a Vec / FixedVec of the same elements, copied whole into a FixedVec (Anchor's
|
|
689
|
+
// `multisig.owners = owners`); more elements than it holds fail with 5104, as push does.
|
|
690
|
+
if (target.type.kind === "vec" && target.type.cap !== undefined && target.type.elem.kind !== "string" && target.type.elem.kind !== "fstring") {
|
|
691
|
+
const t = target.type;
|
|
692
|
+
const l = ts.isIdentifier(valueNode) ? this.lookup(valueNode.text) : undefined;
|
|
693
|
+
const r = l?.type.kind === "vecArg" ? undefined : this.reporter.guard(() => this.ref(valueNode));
|
|
694
|
+
const from = l?.type.kind === "vecArg" && sameType(l.type.elem, t.elem)
|
|
695
|
+
? { items: `${l.c}.ptr`, count: `((uint64_t)${l.c}.len)` }
|
|
696
|
+
: r?.vec && r.type.kind === "vec" && sameType(r.type.elem, t.elem) && !sameType(r.type, t)
|
|
697
|
+
? { items: r.vec.items, count: vecCount(r.vec) }
|
|
698
|
+
: undefined;
|
|
699
|
+
if (from) {
|
|
700
|
+
const n = this.tmp("vn");
|
|
701
|
+
this.line(`uint64_t ${n} = ${from.count};`);
|
|
702
|
+
this.line(`if (${n} > ${t.cap}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: more elements than the ${typeName(t)} holds");`);
|
|
703
|
+
// Slots past the new length keep what they held, as after pop() / clear().
|
|
704
|
+
this.line(`ts_memcpy(${target.lv}.items, (const uint8_t *)${from.items}, ${n} * ${this.fieldBytes(t.elem)});`);
|
|
705
|
+
this.line(`${target.lv}.len = (uint32_t)${n};`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
688
709
|
const src = this.ref(valueNode) ?? this.wholeAccount(valueNode);
|
|
689
710
|
if (!src || !sameType(src.type, target.type))
|
|
690
|
-
this.fail(valueNode, `expected a ${typeName(target.type)}: an object literal
|
|
711
|
+
this.fail(valueNode, `expected a ${typeName(target.type)}: an object literal, another ${typeName(target.type)}${target.type.kind === "vec" && target.type.cap !== undefined ? " or a Vec of the same elements" : ""}`);
|
|
691
712
|
// Same-typed places are either distinct or identical (`a.p = a.p`); ts_copy skips the latter.
|
|
692
713
|
this.line(`ts_copy(&${target.lv}, &${src.lv}, sizeof(${target.lv}));`);
|
|
693
714
|
}
|
package/dist/compiler/idl.js
CHANGED
|
@@ -93,7 +93,7 @@ function accountEntry(ix, name, k, programAddress) {
|
|
|
93
93
|
entry.writable = true;
|
|
94
94
|
if (param?.optional)
|
|
95
95
|
entry.optional = true;
|
|
96
|
-
if (k.kind === "signer" || param?.signsInit)
|
|
96
|
+
if (k.kind === "signer" || param?.signsInit || param?.constraints.signed)
|
|
97
97
|
entry.signer = true;
|
|
98
98
|
if (k.kind === "system")
|
|
99
99
|
entry.address = SYSTEM_PROGRAM;
|
package/dist/compiler/model.d.ts
CHANGED
|
@@ -275,6 +275,8 @@ export interface Constraints {
|
|
|
275
275
|
executable?: boolean;
|
|
276
276
|
/** Dup<A> (Anchor's `dup`): may be the same account as another writable parameter (see function.ts). */
|
|
277
277
|
dup?: boolean;
|
|
278
|
+
/** Signed<A> (Anchor's `signer` constraint on an account of another type): it must sign (2002). */
|
|
279
|
+
signed?: boolean;
|
|
278
280
|
/** Close this account into that parameter when the instruction succeeds. */
|
|
279
281
|
closeTo?: string;
|
|
280
282
|
/** Address<A, "base58">: the account must be exactly this address. */
|
package/docs/architecture.md
CHANGED
|
@@ -102,7 +102,8 @@ These invariants are easy to break. Each one has tests.
|
|
|
102
102
|
pointer per account, and lengths, lamports and owners a CPI changes are seen without copying anything back. CPIs
|
|
103
103
|
go through `ts_invoke`, which builds `SolAccountInfo` records only for the accounts the call lists. It finds each
|
|
104
104
|
record from its meta (the key sits 8 bytes into it), so every `SolAccountMeta` must point at `&acct->key` of an
|
|
105
|
-
instruction account, never at a derived or local `SolPubkey`.
|
|
105
|
+
instruction account, never at a derived or local `SolPubkey`. `invokeInstruction` is the one exception: its metas
|
|
106
|
+
point into data, so it calls `sol_invoke_signed` directly with records for every account it was handed.
|
|
106
107
|
- **Runtime messages carry the source location.** `line()` appends `(file.ts:line)` to every `"TypeSOL: …"`
|
|
107
108
|
message.
|
|
108
109
|
- **No SDK byte loops.** Generated code and the runtime compare, copy and zero memory with `ts_key_eq`, `ts_mem_eq`,
|
package/docs/audit.md
CHANGED
|
@@ -40,6 +40,7 @@ accepts. Areas where a bug would be most damaging, and where earlier internal re
|
|
|
40
40
|
the Vecs that follow, `@layout borsh` accounts.
|
|
41
41
|
- **Arithmetic**: every operator and cast at every width, compound assignment, `mulDiv`, rent.
|
|
42
42
|
- **CPIs and signing**: signer-seed arrays (including their reuse across statements), account flags in `invoke`,
|
|
43
|
+
`invokeInstruction` (metas from data, the infos it hands over, `ts_alloc` of the meta array),
|
|
43
44
|
account creation on pre-funded addresses, closing.
|
|
44
45
|
- **The runtime's parsing**: instruction data, the serialized accounts, Token-2022 extension data, oracle accounts and
|
|
45
46
|
the instructions sysvar.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Case study: porting coral-xyz/multisig
|
|
2
|
+
|
|
3
|
+
This page ports a program that holds real authority on mainnet:
|
|
4
|
+
[coral-xyz/multisig](https://github.com/coral-xyz/multisig) (`programs/multisig/src/lib.rs`, Anchor 0.25, Apache-2.0),
|
|
5
|
+
deployed at `msigUdDBsR4zSUYqYEDrc1LcgtmuSDDM7KxpRUXNC6U`. The port is
|
|
6
|
+
[`examples/multisig.ts`](../examples/multisig.ts). Instead of a twin built for the occasion, it is checked against the
|
|
7
|
+
binary that runs on mainnet: [`test/examples/multisig.test.ts`](../test/examples/multisig.test.ts) runs both under the
|
|
8
|
+
same program id and compares every outcome and every account byte. See
|
|
9
|
+
[case-study-escrow.md](case-study-escrow.md) for a token program and [Migrate from Anchor](migrate-from-anchor.md) for
|
|
10
|
+
the general reference.
|
|
11
|
+
|
|
12
|
+
## The program
|
|
13
|
+
|
|
14
|
+
A multisig executes arbitrary instructions once `threshold` of its owners approve them. Its PDA (`[multisig]`, bump
|
|
15
|
+
`multisig.nonce`) signs the instruction, so it can hold anything a key can: a program's upgrade authority, a mint
|
|
16
|
+
authority, a treasury.
|
|
17
|
+
|
|
18
|
+
- `create_multisig(owners, threshold, nonce)` initializes an account the client allocated in the same transaction.
|
|
19
|
+
- `create_transaction(pid, accs, data)` stores a proposed instruction; the proposer, an owner, approves it.
|
|
20
|
+
- `approve()` adds an owner's approval.
|
|
21
|
+
- `execute_transaction()` runs the stored instruction through a CPI signed by the PDA once enough owners approved.
|
|
22
|
+
- `set_owners(owners)`, `change_threshold(threshold)` and `set_owners_and_change_threshold(...)` change the multisig;
|
|
23
|
+
only its PDA can sign them, so they run as stored transactions the multisig executes on itself.
|
|
24
|
+
|
|
25
|
+
Upstream it is 337 lines of Rust; the port is 184 lines of TypeScript, comments included.
|
|
26
|
+
|
|
27
|
+
## What it needed
|
|
28
|
+
|
|
29
|
+
The port needed five features TypeSOL did not have. They are general features now, each with tests of its own
|
|
30
|
+
(`test/language/dynamic-cpi.test.ts`):
|
|
31
|
+
|
|
32
|
+
| Anchor | TypeSOL | |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `invoke_signed(&ix, ctx.remaining_accounts, …)` with `ix` built from account data | `invokeInstruction(programId, metas, data, remainingAccounts(), seeds)` | A CPI decided at run time. Each meta is a struct with `pubkey`, `isSigner`, `isWritable`. The runtime finds each account among the infos by key and checks privileges, as it does for Rust. |
|
|
35
|
+
| `#[account(zero, signer)] multisig: Box<Account<'info, Multisig>>` | `Signed<Zeroed<Multisig>>` of a `/** @layout borsh */` struct | `Zeroed` now takes Borsh structs (they start empty and are written as Borsh on return). `Signed<…>` is Anchor's `signer` constraint (2002). `Zeroed` now reports an already-set discriminator first, with Anchor's 2013. |
|
|
36
|
+
| `seeds = [multisig.key().as_ref()], bump = multisig.nonce` | `Seeds<Signer, ["@multisig"], "@multisig.nonce">` | A bump stored in another account. |
|
|
37
|
+
| `multisig.owners = owners` | the same, into a `FixedVec<Pubkey, 34>` | A `Vec` argument copied whole into a list in account data. |
|
|
38
|
+
| `TransactionAccount { pubkey, is_signer, is_writable }` | `interface TransactionAccount { pubkey; isSigner; isWritable }` | `isSigner` / `isWritable` were reserved field names everywhere; now only in structs held in an account, where they would shadow `acct.isSigner`. |
|
|
39
|
+
|
|
40
|
+
## Side by side
|
|
41
|
+
|
|
42
|
+
The heart of the program, executing an approved transaction:
|
|
43
|
+
|
|
44
|
+
```rust
|
|
45
|
+
pub fn execute_transaction(ctx: Context<ExecuteTransaction>) -> Result<()> {
|
|
46
|
+
if ctx.accounts.transaction.did_execute {
|
|
47
|
+
return Err(ErrorCode::AlreadyExecuted.into());
|
|
48
|
+
}
|
|
49
|
+
let sig_count = ctx.accounts.transaction.signers.iter().filter(|&did_sign| *did_sign).count() as u64;
|
|
50
|
+
if sig_count < ctx.accounts.multisig.threshold {
|
|
51
|
+
return Err(ErrorCode::NotEnoughSigners.into());
|
|
52
|
+
}
|
|
53
|
+
let mut ix: Instruction = (*ctx.accounts.transaction).deref().into();
|
|
54
|
+
ix.accounts = ix.accounts.iter().map(|acc| {
|
|
55
|
+
let mut acc = acc.clone();
|
|
56
|
+
if &acc.pubkey == ctx.accounts.multisig_signer.key {
|
|
57
|
+
acc.is_signer = true;
|
|
58
|
+
}
|
|
59
|
+
acc
|
|
60
|
+
}).collect();
|
|
61
|
+
let multisig_key = ctx.accounts.multisig.key();
|
|
62
|
+
let seeds = &[multisig_key.as_ref(), &[ctx.accounts.multisig.nonce]];
|
|
63
|
+
let signer = &[&seeds[..]];
|
|
64
|
+
let accounts = ctx.remaining_accounts;
|
|
65
|
+
solana_program::program::invoke_signed(&ix, accounts, signer)?;
|
|
66
|
+
ctx.accounts.transaction.did_execute = true;
|
|
67
|
+
Ok(())
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export function executeTransaction(
|
|
73
|
+
multisig: Account<Multisig>,
|
|
74
|
+
multisigSigner: Seeds<AccountInfo, ["@multisig"], "@multisig.nonce">,
|
|
75
|
+
transaction: HasOne<Mut<Transaction>, "multisig">,
|
|
76
|
+
): void {
|
|
77
|
+
require(multisig.ownerSetSeqno === transaction.ownerSetSeqno, AnchorError.ConstraintRaw);
|
|
78
|
+
require(!transaction.didExecute, ErrorCode.AlreadyExecuted);
|
|
79
|
+
let sigCount = 0n;
|
|
80
|
+
for (const didSign of transaction.signers) if (didSign) sigCount++;
|
|
81
|
+
require(sigCount >= multisig.threshold, ErrorCode.NotEnoughSigners);
|
|
82
|
+
const metas: FixedVec<TransactionAccount, 32> = transaction.accounts.map((acc) => ({
|
|
83
|
+
pubkey: acc.pubkey,
|
|
84
|
+
isSigner: acc.isSigner || acc.pubkey === multisigSigner.key,
|
|
85
|
+
isWritable: acc.isWritable,
|
|
86
|
+
}));
|
|
87
|
+
invokeInstruction(transaction.programId, metas, transaction.data, remainingAccounts(), [multisig.key, multisig.nonce]);
|
|
88
|
+
transaction.didExecute = true;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The `#[derive(Accounts)]` structs become parameter types. Anchor's `constraint = multisig.owner_set_seqno ==
|
|
93
|
+
transaction.owner_set_seqno` becomes the first `require`, raising the same 2003. The error enum keeps its order and
|
|
94
|
+
messages, so the codes stay 6000–6008. Account and instruction names and the Borsh layout are the original's, so
|
|
95
|
+
existing clients and existing accounts work with the port unchanged.
|
|
96
|
+
|
|
97
|
+
## How it was checked
|
|
98
|
+
|
|
99
|
+
`test/programs/coral_multisig.so` is the program dumped from mainnet on 2026-10-29 (sha256 in
|
|
100
|
+
[`test/programs/SOURCES.md`](../test/programs/SOURCES.md)). The test loads it into one LiteSVM and the port into
|
|
101
|
+
another, both at `msigUdDB…`, then sends each transaction to both. After each one it requires the same outcome (success,
|
|
102
|
+
or the same error down to the code) and the same lamports, owner and data bytes for every account involved.
|
|
103
|
+
|
|
104
|
+
There are 43 transactions:
|
|
105
|
+
|
|
106
|
+
- **The upstream test suite** (`tests/multisig.js`): create a 2-of-3 multisig, propose `set_owners`, approve, and
|
|
107
|
+
execute through the PDA. Then `Assert Unique Owners` (6008).
|
|
108
|
+
- **Every `create_multisig` refusal**: threshold 0 or above the owner count (6007), no owners, the account not signing
|
|
109
|
+
(2002), not rent-exempt (2005), owned by another program (3007), too small for the empty value (3003) or for the
|
|
110
|
+
owners (3004), already initialized (2013).
|
|
111
|
+
- **Proposals, approvals and executions**:
|
|
112
|
+
- a proposer or approver outside the owner set (6000);
|
|
113
|
+
- another multisig's transaction (2001);
|
|
114
|
+
- too few approvals (6002);
|
|
115
|
+
- a wrong PDA (2006);
|
|
116
|
+
- executing twice (6006);
|
|
117
|
+
- a changed owner set invalidating a proposal (2003);
|
|
118
|
+
- `set_owners` sent directly, which the PDA cannot sign (3010);
|
|
119
|
+
- a Transaction passed as a Multisig (3002);
|
|
120
|
+
- the target program missing from the remaining accounts (the runtime's error).
|
|
121
|
+
- **Self-calls through `execute_transaction`**:
|
|
122
|
+
- `change_threshold`;
|
|
123
|
+
- `set_owners` shrinking the owner set below the threshold, which drops the threshold;
|
|
124
|
+
- `set_owners_and_change_threshold`;
|
|
125
|
+
- a threshold too high, which fails inside the CPI and fails the whole execution with 6007.
|
|
126
|
+
- **A real payout**: the PDA sends 1.5 SOL through a System transfer. A stored meta claiming the recipient signed is
|
|
127
|
+
refused by the runtime under both programs.
|
|
128
|
+
|
|
129
|
+
## Cost
|
|
130
|
+
|
|
131
|
+
These are compute units per transaction, original versus port, from that test. Each `execute` figure includes the
|
|
132
|
+
instruction it runs.
|
|
133
|
+
|
|
134
|
+
| Instruction | Anchor 0.25 (mainnet) | TypeSOL | |
|
|
135
|
+
| --- | ---: | ---: | ---: |
|
|
136
|
+
| `create_multisig` | 3,591 | 564 | −84% |
|
|
137
|
+
| `create_transaction` | 6,591 | 1,077 | −84% |
|
|
138
|
+
| `approve` | 5,515 | 1,003 | −82% |
|
|
139
|
+
| `execute_transaction` → `set_owners` | 15,227 | 4,133 | −73% |
|
|
140
|
+
| `execute_transaction` → `change_threshold` | 14,723 | 3,781 | −74% |
|
|
141
|
+
| `execute_transaction` → `set_owners_and_change_threshold` | 15,129 | 4,095 | −73% |
|
|
142
|
+
| `execute_transaction` → System transfer | 9,961 | 3,182 | −68% |
|
|
143
|
+
|
|
144
|
+
Refusals cost less too. For example, an already-initialized account costs 3,378 against 160, and a missing signature
|
|
145
|
+
4,229 against 177. The binary is 526,688 bytes on mainnet and 29,984 bytes for the port, 18 times smaller. Deploying it
|
|
146
|
+
needs about 3.5 SOL less rent.
|
|
147
|
+
|
|
148
|
+
The original was built with Anchor 0.25; a rebuild with a current Anchor would cost less than the mainnet figures. The
|
|
149
|
+
escrow case study and the benchmarks compare against Anchor 1.x.
|
|
150
|
+
|
|
151
|
+
## Where they differ
|
|
152
|
+
|
|
153
|
+
- **List capacities.** Anchor's `Vec` fields are bounded only by the account's size. The port's lists are `FixedVec`s:
|
|
154
|
+
- 34 owners;
|
|
155
|
+
- 32 accounts and 1,100 data bytes per stored instruction;
|
|
156
|
+
- 34 approvals.
|
|
157
|
+
|
|
158
|
+
A transaction (1,232 bytes) cannot carry more than these, so a client never meets them. A program calling the
|
|
159
|
+
multisig through a CPI can send more (CPI data may reach 10 KiB). The original would accept that if the account were
|
|
160
|
+
large enough; the port refuses with 5104. Reading an account the original wrote with more elements fails with 3003.
|
|
161
|
+
- **Check order on one double fault.** In `approve` and `execute_transaction` the original checks the owner-set
|
|
162
|
+
sequence number (2003) with the multisig account, before the transaction's `has_one` (2001). The port checks
|
|
163
|
+
`has_one` in the prologue and the sequence number first in the body. So a transaction that belongs to another
|
|
164
|
+
multisig *and* has a stale sequence number gets 2001 here and 2003 there. A transaction breaking only one of the two
|
|
165
|
+
gets the same code under both.
|
|
166
|
+
- **Logs.** Error messages are logged in TypeSOL's words and the original's error names are not logged; codes are the
|
|
167
|
+
same.
|
package/docs/errors.md
CHANGED
|
@@ -7,19 +7,20 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
7
7
|
| 100 / 101 / 102 | instruction data missing / unknown instruction / malformed arguments (InstructionMissing / FallbackNotFound / DidNotDeserialize) |
|
|
8
8
|
| 2000 | a `Mut<…>` / `Init<…>` account passed read-only (ConstraintMut) |
|
|
9
9
|
| 2001 | HasOne constraint failed (ConstraintHasOne) |
|
|
10
|
-
| 2002 | event authority did not sign (ConstraintSigner) |
|
|
10
|
+
| 2002 | a `Signed<…>` account (or the event authority) did not sign (ConstraintSigner) |
|
|
11
11
|
| 2004 | Owner constraint failed (ConstraintOwner) |
|
|
12
12
|
| 2005 | Zeroed account is not rent-exempt (ConstraintRentExempt) |
|
|
13
13
|
| 2006 | PDA seeds mismatch (ConstraintSeeds) |
|
|
14
14
|
| 2007 | Executable constraint failed (ConstraintExecutable) |
|
|
15
15
|
| 2009 | not the associated token account (ConstraintAssociated) |
|
|
16
16
|
| 2012 | not the expected address (ConstraintAddress) |
|
|
17
|
+
| 2013 | a `Zeroed<…>` account whose discriminator is already set (ConstraintZero; checked before its owner, as under Anchor) |
|
|
17
18
|
| 2014 / 2015 | token account mint / owner mismatch (ConstraintTokenMint / ConstraintTokenOwner) |
|
|
18
19
|
| 2016 / 2017 / 2018 | mint authority / freeze authority / decimals mismatch (2018: an existing mint `createIfNeeded` found) |
|
|
19
20
|
| 2021 / 2022 | token account / mint not owned by the token program `TokenOf` / `MintOf` names (ConstraintTokenTokenProgram / ConstraintMintTokenProgram) |
|
|
20
21
|
| 2024–2039 | an existing mint `createIfNeeded` found lacks a Token-2022 extension it was given, or holds other keys in it: group pointer 2024–2026, group member pointer 2027–2029, metadata pointer 2030–2032, close authority 2033–2034, permanent delegate 2035–2036, transfer hook 2037–2039 (the first code of each: the extension is missing; the next: the authority / address differs) (ConstraintMint…Extension…) |
|
|
21
22
|
| 2040 | the same account passed in two writable slots (ConstraintDuplicateMutableAccount) |
|
|
22
|
-
| 3000 | account already exists
|
|
23
|
+
| 3000 | account already exists (AccountDiscriminatorAlreadySet; an `Init` account) |
|
|
23
24
|
| 3002 | discriminator mismatch (AccountDiscriminatorMismatch) |
|
|
24
25
|
| 3003 | data too small, not an initialized token account / mint, a read past the end, or a `@layout borsh` account whose bytes are not a valid Borsh value (AccountDidNotDeserialize) |
|
|
25
26
|
| 3004 | a `@layout borsh` account too small for the value it would now hold (AccountDidNotSerialize) |
|
package/docs/language.md
CHANGED
|
@@ -25,8 +25,8 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
25
25
|
| Enums | `export enum Status { Pending, Approved }` — unit variants stored as the Borsh u8 index; `Status.Approved`, `===` |
|
|
26
26
|
| Options | `best: Option<u64>` (or `u64 \| null`) — `null`, `=== null`, TypeScript narrowing; `===` between options or with a value follows JS (null equals null, empty never equals a value); empty use aborts (5108); `acct?.field` on an optional account and `x.home?.field` on an `Option<struct>` give an Option (empty when the account was left out / the option is empty), `a ?? b` takes b only when a is empty (b runs only then; the result is an Option when b can be null), `x!` is the value or aborts with 5108; `${opt}` prints the value or `null`. Borsh `option` in arguments/events, fixed `{ is_some, value }` in account data |
|
|
27
27
|
| Vecs | `entries: Vec<Entry>` in account data, any number of them, declared anywhere: they are stored after the fixed-size fields in the order declared (so every other field keeps its offset; the IDL lists that order), and a push, pop or clear moves the Vecs after it (a reference into one of those cannot be used across it). In a `@layout borsh` struct a Vec is the last field (Anchor's order) — `get`, `set`, `push(v, payer)`, `pop()`, `clear()`, `.length`; the account is resized (realloc) and rent topped up by `payer`; `Vec<u64>` arguments are read-only views; `Vec<string>` arguments (Anchor's `Vec<String>`) are views of the instruction data, each checked as UTF-8 on entry (102), and go back out as Borsh through `invoke`; `pop()` of a Vec of structs works as a statement |
|
|
28
|
-
| Anchor's account bytes | `/** @layout borsh */` on an account struct stores it as Anchor does (Borsh: a `FixedString<N>` is a String, a `FixedVec<T, N>` a Vec, an `Option` one byte when empty, and the fields after them move), so a TypeSOL program can take over accounts an Anchor program wrote and the IDL lists Anchor's own types. The instruction unpacks it into the usual fixed layout when it starts (bad bytes fail with 3003) and packs it back when it returns (3004 when the account is too small for the new value), so everything else works on it unchanged. Programs invoked from the body (CPIs) see the account's bytes as they were when the instruction started. Costs more compute than the fixed layout (the whole account is read and written each time; see the benchmarks); nested structs and list elements must be fixed-size (FixedString elements allowed). The same struct is Borsh as a whole instruction argument (checked like Borsh deserialization, UTF-8 included), as return data and as an event; as those (not as account data) it may also hold `string` fields and data enums carrying strings, Anchor's `String` and enums in argument structs (account data takes data enums without strings); not inside list or enum arguments, `
|
|
29
|
-
| Fixed-capacity lists | `owners: FixedVec<Pubkey, 8>` — any field, any number of them, in nested structs too: a u32 length and room for N elements (always 4 + N × size bytes), the same `get`, `set`, `push(v)`, `pop()`, `clear()`, `.length` and `for … of`; `push` on a full one fails (5104); `acct.owners = fixedVec([a, b])` writes a whole one (unused slots zeroed). In account data, returned structs and events; as an argument (`ids: FixedVec<u64, 8>`) it is a Borsh Vec of at most N elements, read like a `Vec` argument (more fail with 102) |
|
|
28
|
+
| Anchor's account bytes | `/** @layout borsh */` on an account struct stores it as Anchor does (Borsh: a `FixedString<N>` is a String, a `FixedVec<T, N>` a Vec, an `Option` one byte when empty, and the fields after them move), so a TypeSOL program can take over accounts an Anchor program wrote and the IDL lists Anchor's own types. The instruction unpacks it into the usual fixed layout when it starts (bad bytes fail with 3003) and packs it back when it returns (3004 when the account is too small for the new value), so everything else works on it unchanged. Programs invoked from the body (CPIs) see the account's bytes as they were when the instruction started. Costs more compute than the fixed layout (the whole account is read and written each time; see the benchmarks); nested structs and list elements must be fixed-size (FixedString elements allowed). The same struct is Borsh as a whole instruction argument (checked like Borsh deserialization, UTF-8 included), as return data and as an event; as those (not as account data) it may also hold `string` fields and data enums carrying strings, Anchor's `String` and enums in argument structs (account data takes data enums without strings); not inside list or enum arguments, `loadAccount` or a growable `Vec<T>` |
|
|
29
|
+
| Fixed-capacity lists | `owners: FixedVec<Pubkey, 8>` — any field, any number of them, in nested structs too: a u32 length and room for N elements (always 4 + N × size bytes), the same `get`, `set`, `push(v)`, `pop()`, `clear()`, `.length` and `for … of`; `push` on a full one fails (5104); `acct.owners = fixedVec([a, b])` writes a whole one (unused slots zeroed), and `acct.owners = owners` copies a `Vec` argument (or another list of the same elements) into it, Anchor's `multisig.owners = owners` (more than N fail with 5104). In account data, returned structs and events; as an argument (`ids: FixedVec<u64, 8>`) it is a Borsh Vec of at most N elements, read like a `Vec` argument (more fail with 102) |
|
|
30
30
|
| Struct arguments | `order: Order`, `ids: FixedArray<u8, 4>`, `orders: Vec<Order>` — fixed layouts, Borsh-identical, validated on entry (bools, enums, option tags, string lengths, enum tags; 102) and read-only. A struct holding a FixedVec that is only ever an argument is read as Anchor sends it (its FixedVec a Borsh Vec, as `@layout borsh`) |
|
|
31
31
|
| Remaining accounts | `remainingAccounts()` — `.length`, `.get(i)` → `key`, `owner`, `lamports` (writes check writability), `isSigner`, `isWritable`; `loadAccount<T>` / `loadAccountMut<T>` give checked struct views; pass them to `invoke`, one at a time or spread: `...remainingAccounts()` (each with the flags it was passed with), `...readonly(rest)` / `...writable(rest)` (all with that role), `rest.slice(a, b)` for part of them; a CPI lists at most 32 accounts (5113) |
|
|
32
32
|
| Struct literals | `book.entries.push({ who: who.key, amount, status: Status.Pending, note }, who)` — every field required |
|
|
@@ -66,7 +66,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
66
66
|
| `Account<T>` / `Mut<T>` | owned by this program, 8-byte discriminator matches, data large enough (and writable) |
|
|
67
67
|
| `Init<T>` | writable; `create(payer, seeds)` derives the PDA, verifies it, funds rent, writes the discriminator (up to 10 KiB); `create(payer, null)` creates a new keypair account that signs the transaction instead (3010 if it did not; a signer in the IDL); a third argument is Anchor's `space = …` (discriminator included, at least 8 + the struct's size) |
|
|
68
68
|
| `InitIfNeeded<T>` | `const created = acct.createIfNeeded(payer, seeds)` — creates once; afterwards re-derives the PDA and checks owner, discriminator and size (Anchor `init_if_needed`); `acct.pdaBump` after create. An existing account is used as it is: check the fields you rely on |
|
|
69
|
-
| `HasOne<A, "owner">`, `Seeds<A, ["vault", "@owner"], "bump">`, `CloseTo<A, "owner">` | declarative constraints like Anchor's `has_one`, `seeds`/`bump` and `close`: checked before the body (2001 / 2006), close runs when the instruction succeeds; `"@name"` = account key or argument bytes, `"@acct.field"` = a Pubkey, integer or unit enum field of a struct account parameter (also the account's own: `["offer", "@maker", "@offer.id"]`), other strings are literals; recorded as `relations` / `pda` in the IDL. `acct.pdaBump` is the bump (Anchor `ctx.bumps.acct`): the stored field when one is named, else the canonical bump the check found — use it to sign as the PDA |
|
|
69
|
+
| `HasOne<A, "owner">`, `Seeds<A, ["vault", "@owner"], "bump">`, `CloseTo<A, "owner">` | declarative constraints like Anchor's `has_one`, `seeds`/`bump` and `close`: checked before the body (2001 / 2006), close runs when the instruction succeeds; `"@name"` = account key or argument bytes, `"@acct.field"` = a Pubkey, integer or unit enum field of a struct account parameter (also the account's own: `["offer", "@maker", "@offer.id"]`), other strings are literals; recorded as `relations` / `pda` in the IDL. a bump stored in another struct account is named `"@acct.field"` (Anchor `bump = multisig.nonce`); `acct.pdaBump` is the bump (Anchor `ctx.bumps.acct`): the stored field when one is named, else the canonical bump the check found — use it to sign as the PDA |
|
|
70
70
|
| `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name; `TokenOf<TokenAccount, "mintParam", "authorityParam", "tokenProgramParam">` / `MintOf<Mint, "authorityParam", "freezeParam", "tokenProgramParam">` name the parameters, as Anchor's `token::mint = …` / `mint::authority = …` / `…::token_program = …` do (`never` leaves one out; the token program one: owned by that token program, 2021 / 2022) |
|
|
71
71
|
| `Ata<TokenAccount, "owner", "mint">` | the account is the associated token account of those parameters (2009; ATA `pda` in the IDL) |
|
|
72
72
|
| `Ata<Init<TokenAccount>, "owner", "mint">`, `Ata<InitIfNeeded<TokenAccount>, "owner", "mint">` | Anchor `init` / `init_if_needed` with `associated_token::…`: `acct.create(payer)` creates the ATA (fails if it exists); `const created = acct.createIfNeeded(payer)` creates it only when missing, otherwise checks it is that ATA (2009). Then it is a `TokenAccount` of that mint and owner (2014 / 2015). Needs `AssociatedTokenProgram`, `SystemProgram` and a token program parameter; call it at the top level before using the account |
|
|
@@ -77,7 +77,8 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
77
77
|
| `ProgramData` | this program's ProgramData (address derived from the program id, upgradeable-loader owner and state checked); `upgradeAuthority`, `deploySlot` — gate admin setup on `programData.upgradeAuthority === admin.key` |
|
|
78
78
|
| `EventAuthority`, `SelfProgram` | Anchor's `["__event_authority"]` PDA, and this program's own address, for `emitCpi` |
|
|
79
79
|
| `Migrate<From, To>` | Anchor 1.0's `Migration`: an existing `From` account (owner, discriminator, size) rewritten as `To` by `acct.migrate(payer, value)`, a top-level statement the instruction must reach. `acct.from` (read-only) before it, `acct.to` after; the value is built in full before the rewrite, so it may read `acct.from`. Resized to `To` (the payer tops up rent when it grows, at most 10 KiB; shrinking keeps the lamports; growing needs `SystemProgram`). Fixed-layout structs without growable Vecs; no constraints yet; the account cannot be passed to a helper (use `acct.to` fields in the instruction). An account already migrated fails with 3002. Keep the old struct under a new name with its discriminator: `/** @discriminator account:Counter */ interface CounterV1 { … }` |
|
|
80
|
-
| `Zeroed<T>` |
|
|
80
|
+
| `Zeroed<T>` | accounts (up to 10 MiB) the client allocated with System `CreateAccount`, Anchor's `zero`: discriminator still zero (2013, checked first as under Anchor), owned by this program, big enough, rent-exempt — then initialized. A `@layout borsh` struct starts empty and is written as Borsh when the instruction returns |
|
|
81
|
+
| `Signed<A>` | the account must sign the transaction (Anchor's `signer` constraint, 2002), e.g. `Signed<Zeroed<Multisig>>` for an account the client creates with a fresh keypair; the IDL marks it a signer |
|
|
81
82
|
| `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (its only token program parameter, or one passed as the last argument of `create` / `createIfNeeded`; plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. With `createIfNeeded` an existing mint must carry groupPointer / groupMemberPointer / metadataPointer / closeAuthority / permanentDelegate / transferHook with those keys (Anchor's 2024–2039); extensions not listed are not looked at, so an existing mint may carry others (a permanent delegate, say). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
|
|
82
83
|
| `Init<TokenAccount>`, `InitIfNeeded<TokenAccount>` (no `Ata<…>`) | Anchor `init` / `init_if_needed` with `token::mint` / `token::authority`: `vault.create(payer, ["vault", mint.key], mint, authority)` (the authority may be `vault.key` itself) creates a token account at that PDA (or a keypair account with `null`), sized for the mint's extensions under Token-2022. `createIfNeeded` checks an existing one has that mint and owner (2014 / 2015), not its delegate or close authority (as Anchor) |
|
|
83
84
|
| `TokenAccount` / `Mut<TokenAccount>` | owned by SPL Token or Token-2022 (extensions allowed, multisig accounts refused), initialized; `mint`, `owner`, `amount`, `delegate`, `delegatedAmount`, `isFrozen`, `isNative`, `closeAuthority`, `tokenProgram` |
|
|
@@ -151,6 +152,20 @@ export function payOut(admin: Signer, treasury: Mut<SystemAccount>, recipient: M
|
|
|
151
152
|
- `signerSeeds` (with the bump) signs for this program's PDA.
|
|
152
153
|
- After the call, `returnData<u64>(program)` reads what the callee returned, checking the size and that it came from `program`.
|
|
153
154
|
|
|
155
|
+
A call decided at run time, such as one a multisig stored in account data, is `invokeInstruction` (Rust's
|
|
156
|
+
`invoke_signed(&Instruction { program_id, accounts, data }, infos, seeds)`):
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
invokeInstruction(transaction.programId, transaction.accounts, transaction.data, remainingAccounts(), [multisig.key, multisig.nonce]);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- the accounts are a list of structs with `pubkey`, `isSigner` and `isWritable` (a Vec or FixedVec in account data, a
|
|
163
|
+
Vec argument, or a stored `map` result); the data a list of u8.
|
|
164
|
+
- every account in the fourth argument (`remainingAccounts()` or a slice) is handed to the runtime, which finds each
|
|
165
|
+
meta's account there by key: one that is missing fails the call, as under Rust.
|
|
166
|
+
- the runtime checks the privileges: a meta may be a signer or writable only as the transaction passed that account,
|
|
167
|
+
or as a PDA of this program the seeds (fifth argument) sign for. See `examples/multisig.ts`.
|
|
168
|
+
|
|
154
169
|
Typed calls from an IDL: `typesol cpi other.json --out other_cpi.ts` writes one helper per instruction
|
|
155
170
|
(`vaultDeposit(program, owner, vault, systemProgram, amount, seeds)`), with the account flags, discriminator and
|
|
156
171
|
argument encoding taken from the IDL (Anchor 0.30+ / 1.x, or TypeSOL's own). Call it with `signer([...seeds, bump])`
|
|
@@ -49,6 +49,9 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
49
49
|
| `Migration<'info, From, To>` | `Migrate<From, To>`, then `acct.migrate(payer, value)`; the old struct keeps its on-chain discriminator with `/** @discriminator account:<OldName> */` |
|
|
50
50
|
| `#[account(discriminator = …)]` | `/** @discriminator <preimage or 0x8 bytes> */` on the struct |
|
|
51
51
|
| `AccountLoader<T>` with `#[account(zero)]` | `Zeroed<T>` (TypeSOL reads every account in place: no copy to avoid) |
|
|
52
|
+
| `Account<T>` with `#[account(zero)]` (Borsh) | `Zeroed<T>` of a `/** @layout borsh */` struct |
|
|
53
|
+
| `#[account(signer)]` on a non-`Signer` account | `Signed<A>`, e.g. `Signed<Zeroed<Multisig>>` |
|
|
54
|
+
| `seeds = [multisig.key().as_ref()], bump = multisig.nonce` | `Seeds<A, ["@multisig"], "@multisig.nonce">` |
|
|
52
55
|
| `Program<'info, System>` | `SystemProgram` |
|
|
53
56
|
| `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
|
|
54
57
|
| `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
|
|
@@ -108,6 +111,8 @@ Anchor instruction that takes or returns a struct with `String` / `Vec` / `Optio
|
|
|
108
111
|
| `system_program::transfer(CpiContext::new(…), lamports)` | `systemProgram.transfer(from, to, lamports)` |
|
|
109
112
|
| `token::transfer_checked(CpiContext::new_with_signer(…, seeds), amount, decimals)` | `tokenProgram.transferChecked(from, mint, to, authority, amount, ["seed", key, bump])` |
|
|
110
113
|
| `invoke_signed(&ix, &accounts, &[seeds])` | `invoke(program, [writable(a), readonly(b)], [data…], seeds)` |
|
|
114
|
+
| `invoke_signed(&Instruction { program_id, accounts, data }, ctx.remaining_accounts, …)` built at run time | `invokeInstruction(programId, metas, data, remainingAccounts(), seeds)` |
|
|
115
|
+
| `multisig.owners = owners` (a `Vec` argument into account data) | the same, into a `FixedVec<Pubkey, N>` |
|
|
111
116
|
| `declare_program!(other)` / CPI to another Anchor program | `typesol cpi other.json` generates typed helpers |
|
|
112
117
|
| `for x in v.iter()` | `for (const x of v)` |
|
|
113
118
|
| `v.iter().any(\|x\| …)`, `.all`, `.position`, `.find`, `.fold(init, …)`, `.contains(&x)` | `v.some((x) => …)`, `every`, `findIndex`, `find`, `reduce((acc, x) => …, init)`, `includes(x)` |
|
package/docs/security-model.md
CHANGED
|
@@ -65,6 +65,10 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
65
65
|
12. **CPIs.** A program signs only for its own PDAs (the runtime enforces it; the seeds are those written in the
|
|
66
66
|
source). `invoke` marks an account writable or signer only when the parameter is `Mut` / `Signer` or the program
|
|
67
67
|
signs for it; remaining accounts keep the flags they were passed with, or are checked for the role requested.
|
|
68
|
+
`invokeInstruction` is the exception by design: its program, account flags and data come from values (stored in
|
|
69
|
+
account data or passed in), so the compiler cannot check them; the runtime does, refusing any account the
|
|
70
|
+
transaction did not pass that way and any signer the program's seeds do not cover. Only programs that store
|
|
71
|
+
instructions to run (a multisig, a DAO) need it; review who can write those values.
|
|
68
72
|
Return data is read only from the program named, at the expected size (5106).
|
|
69
73
|
13. **Events.** `emitCpi` is accepted only from this program's event-authority PDA, signed (2002).
|
|
70
74
|
|
package/lib/typesol.d.ts
CHANGED
|
@@ -123,7 +123,8 @@ export interface FixedVec<T, N extends number> extends ListMethods<T> {
|
|
|
123
123
|
readonly length: u64;
|
|
124
124
|
get(index: bigint): T;
|
|
125
125
|
set(index: bigint, value: T): void;
|
|
126
|
-
|
|
126
|
+
// `...rest: never[]` takes nothing, but lets a Vec (whose push takes a payer) be assigned to a FixedVec: `acct.owners = owners`.
|
|
127
|
+
push(value: T, ...rest: never[]): void;
|
|
127
128
|
pop(): T;
|
|
128
129
|
clear(): void;
|
|
129
130
|
[Symbol.iterator](): Iterator<T>;
|
|
@@ -636,6 +637,11 @@ export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extend
|
|
|
636
637
|
* what you need before writing. Not for accounts that are created, closed, a close destination or `@layout borsh`.
|
|
637
638
|
*/
|
|
638
639
|
export type Dup<A> = A & { readonly __dup?: true };
|
|
640
|
+
/**
|
|
641
|
+
* The account must sign the transaction (Anchor's `signer` constraint on an account that is not a `Signer`; 2002), e.g.
|
|
642
|
+
* `Signed<Zeroed<Multisig>>` for an account the client creates with a fresh keypair in the same transaction.
|
|
643
|
+
*/
|
|
644
|
+
export type Signed<A> = A & { readonly __signed?: true };
|
|
639
645
|
/**
|
|
640
646
|
* Anchor 1.0's `Migration<From, To>`: an existing `From` account (owner, discriminator and size checked) that this
|
|
641
647
|
* instruction rewrites as `To` with `acct.migrate(payer, value)`, a top-level statement every path must reach. Read
|
|
@@ -1379,9 +1385,10 @@ export type Init<T> = T extends TokenAccount
|
|
|
1379
1385
|
};
|
|
1380
1386
|
|
|
1381
1387
|
/**
|
|
1382
|
-
*
|
|
1383
|
-
* owned by this program and still zeroed
|
|
1384
|
-
* the discriminator is written: from here on it is a `Mut<T>`.
|
|
1388
|
+
* An account the client already allocated with System CreateAccount (any size up to 10 MiB; over 10 KiB a program
|
|
1389
|
+
* cannot create it itself), owned by this program and still zeroed: Anchor's `zero`. Emptiness (2013), owner, size and
|
|
1390
|
+
* rent exemption are checked, then the discriminator is written: from here on it is a `Mut<T>`. A `@layout borsh`
|
|
1391
|
+
* struct starts empty (lists and strings empty, Options None) and is written as Borsh when the instruction returns.
|
|
1385
1392
|
*/
|
|
1386
1393
|
export type Zeroed<T> = T & AccountMeta;
|
|
1387
1394
|
|
|
@@ -1610,6 +1617,32 @@ export declare function invoke(
|
|
|
1610
1617
|
signerSeeds?: Seed[] | SignerSeeds,
|
|
1611
1618
|
): void;
|
|
1612
1619
|
|
|
1620
|
+
/** One account of an instruction built at run time (Rust's `AccountMeta`): any struct with these three fields. */
|
|
1621
|
+
export interface InstructionAccountMeta {
|
|
1622
|
+
readonly pubkey: Pubkey;
|
|
1623
|
+
readonly isSigner: boolean;
|
|
1624
|
+
readonly isWritable: boolean;
|
|
1625
|
+
}
|
|
1626
|
+
/** A list read by index: a Vec or FixedVec in account data, a Vec argument, or a stored local list. */
|
|
1627
|
+
export interface IndexedList<T> {
|
|
1628
|
+
readonly length: u64;
|
|
1629
|
+
get(index: bigint): T;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* A CPI decided at run time, e.g. one stored in account data (Rust's `invoke_signed(&Instruction { program_id,
|
|
1633
|
+
* accounts, data }, infos, seeds)`). Every account in `infos` (`remainingAccounts()` or a slice of it) is handed to the
|
|
1634
|
+
* runtime, which finds each meta's account there by key: one that is missing fails the CPI, as under Rust. A meta may
|
|
1635
|
+
* be a signer or writable only as the transaction passed that account, or as a PDA of this program `signerSeeds` sign
|
|
1636
|
+
* for (`signer([multisig.key, multisig.nonce])`).
|
|
1637
|
+
*/
|
|
1638
|
+
export declare function invokeInstruction(
|
|
1639
|
+
programId: Pubkey,
|
|
1640
|
+
accounts: IndexedList<InstructionAccountMeta>,
|
|
1641
|
+
data: IndexedList<u8>,
|
|
1642
|
+
infos: RemainingAccounts | RemainingSlice,
|
|
1643
|
+
signerSeeds?: Seed[] | SignerSeeds,
|
|
1644
|
+
): void;
|
|
1645
|
+
|
|
1613
1646
|
/**
|
|
1614
1647
|
* Anchor's own error codes (anchor-lang `ErrorCode`): `require(condition, AnchorError.AccountDidNotDeserialize)` fails
|
|
1615
1648
|
* with that code, as `require!(condition, ErrorCode::AccountDidNotDeserialize)` does. Not listed in the IDL's errors.
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
#define TS_ERR_CONSTRAINT_EXECUTABLE 2007 /* ConstraintExecutable */
|
|
28
28
|
#define TS_ERR_ASSOCIATED 2009 /* ConstraintAssociated */
|
|
29
29
|
#define TS_ERR_ADDRESS 2012 /* ConstraintAddress */
|
|
30
|
+
#define TS_ERR_CONSTRAINT_ZERO 2013 /* ConstraintZero */
|
|
30
31
|
#define TS_ERR_TOKEN_MINT 2014 /* ConstraintTokenMint */
|
|
31
32
|
#define TS_ERR_TOKEN_OWNER 2015 /* ConstraintTokenOwner */
|
|
32
33
|
#define TS_ERR_MINT_AUTHORITY 2016 /* ConstraintMintMintAuthority */
|
|
@@ -654,10 +655,15 @@ static inline uint64_t ts_now(int64_t *out) {
|
|
|
654
655
|
* to this program. It must be writable, rent-exempt, big enough and still all-zero in its discriminator;
|
|
655
656
|
* the discriminator is written here so the account can never be initialized twice.
|
|
656
657
|
*/
|
|
657
|
-
|
|
658
|
+
/* Zeroed<T> (Anchor's `zero`), in Anchor's order: a discriminator already set (2013), then owner and size as the
|
|
659
|
+
* account type checks them; the rent check and the discriminator write come after the mut / signer checks. */
|
|
660
|
+
static inline uint64_t ts_check_zeroed(const TsAccount *a, const SolPubkey *program_id, uint64_t size) {
|
|
661
|
+
if (a->data_len >= 8 && ts_ld64(a->data) != 0) TS_FAIL(TS_ERR_CONSTRAINT_ZERO, "TypeSOL: account already initialized");
|
|
658
662
|
if (!ts_key_eq(&a->owner, program_id)) TS_FAIL(TS_ERR_WRONG_OWNER, "TypeSOL: account not owned by this program");
|
|
659
663
|
if (a->data_len < 8 + size) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account data too small");
|
|
660
|
-
|
|
664
|
+
return SUCCESS;
|
|
665
|
+
}
|
|
666
|
+
static inline uint64_t ts_init_zeroed(TsAccount *a, const uint8_t disc[8]) {
|
|
661
667
|
uint64_t need;
|
|
662
668
|
TS_TRY(ts_minimum_balance(a->data_len, &need));
|
|
663
669
|
if (a->lamports < need) TS_FAIL(TS_ERR_NOT_RENT_EXEMPT, "TypeSOL: account is not rent-exempt");
|