@typesol/cli 0.24.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +3 -2
- package/dist/clientgen.js +3 -0
- package/dist/compiler/analyze.js +11 -1
- package/dist/compiler/emit/calls.js +10 -10
- package/dist/compiler/emit/cpi.js +4 -0
- package/dist/compiler/emit/function.d.ts +9 -0
- package/dist/compiler/emit/function.js +169 -139
- package/dist/compiler/emit/program.js +3 -0
- package/dist/compiler/emit/shared.d.ts +1 -0
- package/dist/compiler/emit/shared.js +1 -0
- package/dist/compiler/emit/statements.d.ts +6 -0
- package/dist/compiler/emit/statements.js +35 -5
- package/docs/case-study-escrow.md +224 -0
- package/docs/errors.md +26 -1
- package/docs/language.md +12 -8
- package/docs/migrate-from-anchor.md +3 -1
- package/lib/typesol.d.ts +35 -13
- package/package.json +1 -1
- package/runtime/typesol_rt.h +33 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ codes, and says so here.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.25.0 — 2026-10-29
|
|
10
|
+
|
|
11
|
+
Security review (October 2026) fixes, and error codes aligned with Anchor. Some refused transactions now report a
|
|
12
|
+
different error code (below): a minor version.
|
|
13
|
+
|
|
14
|
+
- Fixed: `loadAccountMut` on the same remaining account twice (`rest.get(i)`, `rest.get(j)` with `i == j`) gave two
|
|
15
|
+
writable views of one account; a program reading both before writing both could be made to create value. The second
|
|
16
|
+
load is now refused with 2040.
|
|
17
|
+
- Fixed: a Vec element written with a right side that resizes the Vec (`book.bids.get(i).qty += settle(book)` where the
|
|
18
|
+
helper pops, a closure, or a `push` / `pop` / `clear` in the expression) wrote through the element's old position,
|
|
19
|
+
into the bytes of the next Vec. Such an assignment is now a compile error ("store its result in a local first"),
|
|
20
|
+
as it already was for a plain `=` with a helper.
|
|
21
|
+
- Fixed: `Ata<Init<…>>` / `Ata<InitIfNeeded<…>>` creation and `Ata<AccountInfo, …>` with two token program parameters
|
|
22
|
+
used the first one; they now need exactly one (use `TokenInterface`).
|
|
23
|
+
- New: `TokenOf<…, …, …, "tokenProgram">` / `MintOf<…, …, …, "tokenProgram">`, Anchor's `token::token_program` /
|
|
24
|
+
`mint::token_program` (2021 / 2022). The escrow port uses it.
|
|
25
|
+
- New: `pythPrice(update, feedId, maxAge, exponent)` refuses a feed whose exponent is not the one given;
|
|
26
|
+
`switchboardValue(…, minSamples)` refuses a result resting on fewer oracle samples.
|
|
27
|
+
- Changed: each account's constraints are checked in Anchor's order (seeds, associated token account, has_one, owner,
|
|
28
|
+
executable, address, token::, mint::), so a transaction breaking several fails with Anchor's code: e.g. a stranger's
|
|
29
|
+
call on a `Seeds<HasOne<…>>` account now fails with 2006 (ConstraintSeeds) instead of 2001, and an `Ata<…>` of
|
|
30
|
+
another wallet with 2015 instead of 2009. A `Mut<…>` account passed read-only fails with 2000 (ConstraintMut), as
|
|
31
|
+
Anchor's `#[account(mut)]`, instead of 3006. docs/errors.md lists the order and the differences left.
|
|
32
|
+
- Docs: unchecked raw Pyth fields, `requirePda` with a bump from the arguments, Merkle leaves, and what
|
|
33
|
+
`createIfNeeded` leaves unchecked on an existing account.
|
|
34
|
+
- Docs: [Case study: porting Anchor's escrow](docs/case-study-escrow.md), one Anchor program ported end to end, with the
|
|
35
|
+
differences, the checks run against the original and the cost.
|
|
36
|
+
- Releases: the release workflow checks that CHANGELOG.md has the version, and after publishing installs the package
|
|
37
|
+
from npm, scaffolds a project from each template and runs its typecheck, build and test.
|
|
38
|
+
|
|
9
39
|
## 0.24.1 — 2026-10-28
|
|
10
40
|
|
|
11
41
|
Smaller programs and fewer compute units, with the same results; no change to the language, IDLs or error codes. Also
|
package/README.md
CHANGED
|
@@ -162,8 +162,9 @@ existing project: `npm install -D typesol@npm:@typesol/cli`.
|
|
|
162
162
|
Deploying again upgrades the program in place. Keep `target/deploy/<name>-keypair.json`: that keypair is your
|
|
163
163
|
program's address.
|
|
164
164
|
|
|
165
|
-
Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct,
|
|
166
|
-
starts from your program's IDL
|
|
165
|
+
Coming from Anchor: [docs/migrate-from-anchor.md](docs/migrate-from-anchor.md) maps every construct, `typesol port`
|
|
166
|
+
starts from your program's IDL, and [docs/case-study-escrow.md](docs/case-study-escrow.md) follows one complete Anchor
|
|
167
|
+
program through the port. Tests use `typesol/testing` (accounts by name, plain values; [docs/test-your-program.md](docs/test-your-program.md)). To go to
|
|
167
168
|
mainnet (wallet, cost, deploy, IDL, verification), follow [docs/mainnet.md](docs/mainnet.md).
|
|
168
169
|
|
|
169
170
|
## How it works
|
package/dist/clientgen.js
CHANGED
|
@@ -32,6 +32,7 @@ export const TYPESOL_ERRORS = {
|
|
|
32
32
|
100: "InstructionMissing",
|
|
33
33
|
101: "InstructionFallbackNotFound",
|
|
34
34
|
102: "InstructionDidNotDeserialize",
|
|
35
|
+
2000: "ConstraintMut",
|
|
35
36
|
2001: "ConstraintHasOne",
|
|
36
37
|
2002: "ConstraintSigner",
|
|
37
38
|
2004: "ConstraintOwner",
|
|
@@ -45,6 +46,8 @@ export const TYPESOL_ERRORS = {
|
|
|
45
46
|
2016: "ConstraintMintMintAuthority",
|
|
46
47
|
2017: "ConstraintMintFreezeAuthority",
|
|
47
48
|
2018: "ConstraintMintDecimals",
|
|
49
|
+
2021: "ConstraintTokenTokenProgram",
|
|
50
|
+
2022: "ConstraintMintTokenProgram",
|
|
48
51
|
2040: "ConstraintDuplicateMutableAccount",
|
|
49
52
|
3000: "AccountDiscriminatorAlreadySet",
|
|
50
53
|
3002: "AccountDiscriminatorMismatch",
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -1473,6 +1473,13 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1473
1473
|
if (entry.includes("=")) {
|
|
1474
1474
|
if (!accounts.some((o) => o.name === param))
|
|
1475
1475
|
reporter.fail(a.node, `add an account parameter named "${param}"`);
|
|
1476
|
+
if (f === "tokenProgram") {
|
|
1477
|
+
if (a.kind.kind !== "token" && a.kind.kind !== "mint")
|
|
1478
|
+
reporter.fail(a.node, "TokenOf / MintOf apply to a TokenAccount / Mint");
|
|
1479
|
+
if (accounts.find((o) => o.name === param).kind.kind !== "tokenProgram")
|
|
1480
|
+
reporter.fail(a.node, `"${param}" must be a TokenProgram, Token2022Program or TokenInterface parameter`);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1476
1483
|
if (a.kind.kind === "token" ? f !== "mint" && f !== "owner" : a.kind.kind === "mint" ? false : true)
|
|
1477
1484
|
reporter.fail(a.node, a.kind.kind === "token" || a.kind.kind === "mint" ? "TokenOf<…> applies to a TokenAccount" : "TokenOf / MintOf apply to a TokenAccount / Mint");
|
|
1478
1485
|
if (a.kind.kind === "mint" && f !== "mintAuthority" && f !== "freezeAuthority")
|
|
@@ -1610,12 +1617,15 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1610
1617
|
const one = (t) => (t && t.kind !== ts.SyntaxKind.NeverKeyword ? lit(t)[0] : undefined);
|
|
1611
1618
|
const p1 = one(args[1]);
|
|
1612
1619
|
const p2 = one(args[2]);
|
|
1613
|
-
|
|
1620
|
+
const p3 = one(args[3]);
|
|
1621
|
+
if (!p1 && !p2 && !p3)
|
|
1614
1622
|
reporter.fail(node, `${name}<…> names at least one parameter`);
|
|
1615
1623
|
if (p1)
|
|
1616
1624
|
out.hasOne.push(`${first}=${p1}`);
|
|
1617
1625
|
if (p2)
|
|
1618
1626
|
out.hasOne.push(`${second}=${p2}`);
|
|
1627
|
+
if (p3)
|
|
1628
|
+
out.hasOne.push(`tokenProgram=${p3}`);
|
|
1619
1629
|
}
|
|
1620
1630
|
if (name === "CloseTo")
|
|
1621
1631
|
out.closeTo = lit(args[1])[0];
|
|
@@ -3,7 +3,7 @@ import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
|
|
|
3
3
|
import { ANCHOR_ERRORS } from "../anchor-errors.js";
|
|
4
4
|
import { decodeAddress } from "../../keys.js";
|
|
5
5
|
import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
|
|
6
|
-
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
|
|
6
|
+
import { U64, U8, I64, I32, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
|
|
7
7
|
import { ExpressionEmitter } from "./expressions.js";
|
|
8
8
|
/**
|
|
9
9
|
* Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
|
|
@@ -344,29 +344,29 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
344
344
|
return { c: `ts_secp256k1_verify(${h.c}, (uint64_t)${id.c}, ${r.c}, ${sv.c}, ${eth.c})`, type: BOOL };
|
|
345
345
|
}
|
|
346
346
|
case "pythPrice": {
|
|
347
|
-
if (args.length !== 3)
|
|
348
|
-
this.fail(node, "pythPrice(update, feedId, maxAgeSeconds) takes three arguments");
|
|
347
|
+
if (args.length !== 3 && args.length !== 4)
|
|
348
|
+
this.fail(node, "pythPrice(update, feedId, maxAgeSeconds, exponent?) takes three or four arguments");
|
|
349
349
|
const u = this.accountParam(args[0], "the price update");
|
|
350
350
|
if (u.kind.kind !== "pyth")
|
|
351
351
|
this.fail(args[0], "pass a PythPriceUpdate parameter");
|
|
352
|
-
const [feed, age] = this.lowerInOrder(args.slice(1), [HASH, U64]);
|
|
352
|
+
const [feed, age, expo] = this.lowerInOrder(args.slice(1), [HASH, U64, I32]);
|
|
353
353
|
const t = this.tmp("px");
|
|
354
354
|
this.line(`int64_t ${t};`);
|
|
355
|
-
this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, &${t}));`);
|
|
355
|
+
this.line(`TS_TRY(ts_pyth_price(a_${u.name}, ${feed.c}, ${age.c}, ${expo ? `true, ${expo.c}` : "false, 0"}, &${t}));`);
|
|
356
356
|
return { c: t, type: I64 };
|
|
357
357
|
}
|
|
358
358
|
case "switchboardValue": {
|
|
359
359
|
// Anyone can create a Switchboard feed: like pythPrice's feed id, the expected feed address is required.
|
|
360
|
-
if (args.length !== 4)
|
|
361
|
-
this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals) takes four arguments");
|
|
360
|
+
if (args.length !== 4 && args.length !== 5)
|
|
361
|
+
this.fail(node, "switchboardValue(feed, expectedFeed, maxStaleSlots, decimals, minSamples?) takes four or five arguments");
|
|
362
362
|
const f = this.accountParam(args[0], "the feed");
|
|
363
363
|
if (f.kind.kind !== "switchboard")
|
|
364
364
|
this.fail(args[0], "pass a SwitchboardFeed parameter");
|
|
365
|
-
const [expected, stale, dec] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8]);
|
|
365
|
+
const [expected, stale, dec, min] = this.lowerInOrder(args.slice(1), [PUBKEY, U64, U8, U8]);
|
|
366
366
|
const t = this.tmp("sb");
|
|
367
367
|
this.line(`int64_t ${t};`);
|
|
368
368
|
this.line(`if (!ts_key_eq(&a_${f.name}->key, ${expected.c})) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: not the expected Switchboard feed");`);
|
|
369
|
-
this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, &${t}));`);
|
|
369
|
+
this.line(`TS_TRY(ts_switchboard_value(a_${f.name}, ${stale.c}, ${dec.c}, ${min ? min.c : "1"}, &${t}));`);
|
|
370
370
|
return { c: t, type: I64 };
|
|
371
371
|
}
|
|
372
372
|
case "associatedTokenAddress": {
|
|
@@ -980,7 +980,7 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
980
980
|
this.line(l);
|
|
981
981
|
if (mut) {
|
|
982
982
|
this.line(`TS_TRY(ts_check_writable(${h}));`);
|
|
983
|
-
this.line(`TS_TRY(
|
|
983
|
+
this.line(`TS_TRY(ts_claim_mut(params, ${h}));`);
|
|
984
984
|
}
|
|
985
985
|
return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
|
|
986
986
|
}
|
|
@@ -774,6 +774,10 @@ export class CpiEmitter extends CallEmitter {
|
|
|
774
774
|
const ataProgram = need("ataProgram", "an AssociatedTokenProgram");
|
|
775
775
|
const system = need("system", "a SystemProgram");
|
|
776
776
|
const tokenProgram = need("tokenProgram", "a token program (TokenProgram, Token2022Program or TokenInterface)");
|
|
777
|
+
// The address is derived under the token program: with two, which one the mint uses is not known here.
|
|
778
|
+
const tokenPrograms = this.ix.accounts.filter((o) => o.kind.kind === "tokenProgram");
|
|
779
|
+
if (tokenPrograms.length > 1)
|
|
780
|
+
this.fail(node, `${call}(...) needs exactly one token program parameter (found ${tokenPrograms.map((o) => o.name).join(", ")}); use TokenInterface`);
|
|
777
781
|
const { owner, mint } = target.constraints.ata;
|
|
778
782
|
const a = `a_${target.name}`;
|
|
779
783
|
const create = `TS_TRY(ts_ata_create(params, a_${ataProgram.name}, a_${payer.name}, ${a}, a_${owner}, a_${mint}, a_${system.name}, a_${tokenProgram.name}, false));`;
|
|
@@ -9,7 +9,16 @@ export declare class FunctionEmitter extends StatementEmitter {
|
|
|
9
9
|
emitInstruction(): string;
|
|
10
10
|
/** CloseTo<…> accounts are closed when the instruction succeeds, at every return. */
|
|
11
11
|
protected epilogue(): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Each account's constraints, account by account in parameter order and, within one account, in Anchor's order
|
|
14
|
+
* (anchor-syn's `linearize`): seeds, associated token, has_one, owner, executable, address, token::, mint::.
|
|
15
|
+
* A transaction that breaks several checks then fails with the same error code as under Anchor.
|
|
16
|
+
*/
|
|
12
17
|
protected constraintChecks(ix: Instruction): string[];
|
|
18
|
+
/** Ata<…>: the account's owner (2015) and address (2009) as Anchor checks them, then its mint. */
|
|
19
|
+
private ataCheck;
|
|
20
|
+
/** Seeds<…>: the account is the PDA of its seeds (with the stored bump, or the canonical one found here). */
|
|
21
|
+
private seedsCheck;
|
|
13
22
|
/** Optional account parameters a piece of generated C refers to (a_name / d_name). */
|
|
14
23
|
protected optionalIn(code: string): string[];
|
|
15
24
|
}
|
|
@@ -38,8 +38,10 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
38
38
|
const k = a.kind;
|
|
39
39
|
if (k.kind === "signer")
|
|
40
40
|
head.push(` TS_TRY(ts_check_signer(${v}));`);
|
|
41
|
+
// Mut<…>: Anchor's `mut` (2000). Anchor checks it with the constraints, after seeds and associated token; here it
|
|
42
|
+
// sits with the type checks, which keeps binaries smaller (only a transaction breaking both sees the difference).
|
|
41
43
|
if (isWritable(k))
|
|
42
|
-
head.push(` TS_TRY(
|
|
44
|
+
head.push(` TS_TRY(ts_check_mut(${v}));`);
|
|
43
45
|
// A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
|
|
44
46
|
if (k.kind === "account")
|
|
45
47
|
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)}));`);
|
|
@@ -160,7 +162,9 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
160
162
|
pairs.push(`if (${present}a_${writable[i].name} == a_${writable[j].name}) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: ${writable[i].name} and ${writable[j].name} must be different accounts");`);
|
|
161
163
|
}
|
|
162
164
|
head.push(...pairs.map((p) => ` ${p}`));
|
|
163
|
-
// Arguments: Borsh, read sequentially (strings make later offsets dynamic).
|
|
165
|
+
// Arguments: Borsh, read sequentially (strings make later offsets dynamic). Anchor decodes them before the accounts;
|
|
166
|
+
// here they come after the account types (decoding first keeps them live across the checks: larger binaries), so
|
|
167
|
+
// only a transaction wrong in both ways sees a different error.
|
|
164
168
|
if (ix.args.length)
|
|
165
169
|
head.push(` uint64_t off = ${ix.discriminator.length};`);
|
|
166
170
|
const need = (bytes) => head.push(` if (params->data_len < off + ${bytes}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
@@ -365,6 +369,11 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
365
369
|
}),
|
|
366
370
|
];
|
|
367
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Each account's constraints, account by account in parameter order and, within one account, in Anchor's order
|
|
374
|
+
* (anchor-syn's `linearize`): seeds, associated token, has_one, owner, executable, address, token::, mint::.
|
|
375
|
+
* A transaction that breaks several checks then fails with the same error code as under Anchor.
|
|
376
|
+
*/
|
|
368
377
|
constraintChecks(ix) {
|
|
369
378
|
const out = [];
|
|
370
379
|
// Bumps found by Seeds<…> checks, declared ahead of the (possibly optional-guarded) checks for `acct.pdaBump`.
|
|
@@ -375,21 +384,40 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
375
384
|
const k = a.kind.kind;
|
|
376
385
|
if (c.hasOne.length && k !== "account" && k !== "token" && k !== "mint")
|
|
377
386
|
this.reporter.fail(a.node, "HasOne applies to existing accounts (Account<T> / Mut<T>), TokenAccount and Mint");
|
|
378
|
-
|
|
387
|
+
if (c.closeTo) {
|
|
388
|
+
const dest = ix.accounts.find((o) => o.name === c.closeTo);
|
|
389
|
+
if (!isWritable(dest.kind))
|
|
390
|
+
this.reporter.fail(a.node, `CloseTo<…, "${c.closeTo}">: ${c.closeTo} must be writable (Mut<…>)`);
|
|
391
|
+
}
|
|
392
|
+
if (c.seeds)
|
|
393
|
+
out.push(...this.seedsCheck(ix, a, bumpDecls));
|
|
394
|
+
if (c.ata)
|
|
395
|
+
out.push(...this.ataCheck(ix, a));
|
|
396
|
+
// has_one on a program account; on a token account or mint the same entries are Anchor's token:: / mint::
|
|
397
|
+
// constraints, which come last.
|
|
398
|
+
const hasOne = [];
|
|
399
|
+
// Anchor's order inside token:: (authority, mint, token program) and mint:: (authorities, token program).
|
|
400
|
+
const rank = (e) => ["owner=", "mint=", "mintAuthority=", "freezeAuthority=", "tokenProgram="].findIndex((x) => e.startsWith(x));
|
|
401
|
+
const entries = k === "token" || k === "mint" ? [...c.hasOne].sort((x, y) => rank(x) - rank(y)) : c.hasOne;
|
|
402
|
+
for (const entry of entries) {
|
|
379
403
|
// "field=param" (TokenOf / MintOf) or a field checked against the parameter of the same name (HasOne).
|
|
380
404
|
const [f, p = f] = entry.split("=");
|
|
381
|
-
if (
|
|
382
|
-
|
|
405
|
+
if (f === "tokenProgram")
|
|
406
|
+
hasOne.push(` if (!ts_key_eq(&a_${a.name}->owner, &a_${p}->key)) TS_FAIL(${k === "mint" ? "TS_ERR_MINT_TOKEN_PROGRAM" : "TS_ERR_TOKEN_TOKEN_PROGRAM"}, "TypeSOL: ${a.name} is not owned by ${p}");`);
|
|
407
|
+
else if (k === "token")
|
|
408
|
+
hasOne.push(f === "mint"
|
|
383
409
|
? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${p}");`
|
|
384
410
|
: ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${p}");`);
|
|
385
411
|
else if (k === "mint") {
|
|
386
412
|
const at = f === "mintAuthority" ? 0 : 46;
|
|
387
413
|
const code = f === "mintAuthority" ? "TS_ERR_MINT_AUTHORITY" : "TS_ERR_FREEZE_AUTHORITY";
|
|
388
|
-
|
|
414
|
+
hasOne.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${p}->key)) TS_FAIL(${code}, "TypeSOL: ${p} is not the ${f} of ${a.name}");`);
|
|
389
415
|
}
|
|
390
416
|
else
|
|
391
|
-
|
|
417
|
+
hasOne.push(` if (!ts_key_eq(&d_${a.name}->${f}, &a_${f}->key)) TS_FAIL(TS_ERR_CONSTRAINT, "TypeSOL: ${a.name}.${f} must be the ${f} account");`);
|
|
392
418
|
}
|
|
419
|
+
if (k === "account")
|
|
420
|
+
out.push(...hasOne);
|
|
393
421
|
if (c.owner) {
|
|
394
422
|
out.push(` static const SolPubkey co_${a.name} = {${cBytes(c.owner.bytes)}};`);
|
|
395
423
|
out.push(` if (!ts_key_eq(&a_${a.name}->owner, &co_${a.name})) TS_FAIL(TS_ERR_CONSTRAINT_OWNER, "TypeSOL: ${a.name} is not owned by the expected program");`);
|
|
@@ -400,138 +428,8 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
400
428
|
out.push(` static const SolPubkey ca_${a.name} = {${cBytes(c.address.bytes)}};`);
|
|
401
429
|
out.push(` if (!ts_key_eq(&a_${a.name}->key, &ca_${a.name})) TS_FAIL(TS_ERR_ADDRESS, "TypeSOL: ${a.name} is not the expected address");`);
|
|
402
430
|
}
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
// existing one there. Only the IDL entry is recorded here.
|
|
406
|
-
const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
|
|
407
|
-
if (tp && !ix.pdas.has(a.name)) {
|
|
408
|
-
ix.pdas.set(a.name, [
|
|
409
|
-
{ kind: "account", path: snakeCase(c.ata.owner) },
|
|
410
|
-
{ kind: "account", path: snakeCase(tp.name) },
|
|
411
|
-
{ kind: "account", path: snakeCase(c.ata.mint) },
|
|
412
|
-
]);
|
|
413
|
-
ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
else if (c.ata) {
|
|
417
|
-
// The token program is the account's owner; an AccountInfo not created yet takes the instruction's token program.
|
|
418
|
-
let program = `&a_${a.name}->owner`;
|
|
419
|
-
if (k !== "token") {
|
|
420
|
-
const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
|
|
421
|
-
if (!tp)
|
|
422
|
-
this.reporter.fail(a.node, "Ata<AccountInfo, …> needs a token program parameter to derive the address");
|
|
423
|
-
program = `&a_${tp.name}->key`;
|
|
424
|
-
}
|
|
425
|
-
out.push(` { SolPubkey ata_${a.name};`);
|
|
426
|
-
out.push(` TS_TRY(ts_ata_address(&a_${c.ata.owner}->key, &a_${c.ata.mint}->key, ${program}, &ata_${a.name}));`);
|
|
427
|
-
out.push(` if (!ts_key_eq(&ata_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_ASSOCIATED, "TypeSOL: ${a.name} is not the associated token account of ${c.ata.owner} and ${c.ata.mint}"); }`);
|
|
428
|
-
// The address alone is not enough: legacy SPL Token lets an ATA's owner be reassigned.
|
|
429
|
-
if (k === "token") {
|
|
430
|
-
out.push(` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${c.ata.mint}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${c.ata.mint}");`);
|
|
431
|
-
out.push(` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${c.ata.owner}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${c.ata.owner}");`);
|
|
432
|
-
}
|
|
433
|
-
const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
|
|
434
|
-
if (tp && !ix.pdas.has(a.name)) {
|
|
435
|
-
ix.pdas.set(a.name, [
|
|
436
|
-
{ kind: "account", path: snakeCase(c.ata.owner) },
|
|
437
|
-
{ kind: "account", path: snakeCase(tp.name) },
|
|
438
|
-
{ kind: "account", path: snakeCase(c.ata.mint) },
|
|
439
|
-
]);
|
|
440
|
-
ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
if (c.closeTo) {
|
|
444
|
-
const dest = ix.accounts.find((o) => o.name === c.closeTo);
|
|
445
|
-
if (!isWritable(dest.kind))
|
|
446
|
-
this.reporter.fail(a.node, `CloseTo<…, "${c.closeTo}">: ${c.closeTo} must be writable (Mut<…>)`);
|
|
447
|
-
}
|
|
448
|
-
if (c.seeds) {
|
|
449
|
-
const parts = [];
|
|
450
|
-
const idl = [];
|
|
451
|
-
for (const raw of c.seeds.names) {
|
|
452
|
-
// "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
|
|
453
|
-
const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
|
|
454
|
-
const n = ref ?? raw;
|
|
455
|
-
// "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
|
|
456
|
-
// `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
|
|
457
|
-
if (ref?.includes(".")) {
|
|
458
|
-
const [owner, field, ...rest] = ref.split(".");
|
|
459
|
-
const o = ix.accounts.find((x) => x.name === owner);
|
|
460
|
-
if (!o || o.kind.kind !== "account" || rest.length)
|
|
461
|
-
this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
|
|
462
|
-
const st = o.kind.struct;
|
|
463
|
-
const f = st.fields.find((x) => x.name === field);
|
|
464
|
-
if (!f)
|
|
465
|
-
this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
|
|
466
|
-
const t = f.type;
|
|
467
|
-
if (t.kind === "pubkey")
|
|
468
|
-
parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
|
|
469
|
-
else if (t.kind === "int" || t.kind === "enum")
|
|
470
|
-
parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
|
|
471
|
-
else
|
|
472
|
-
this.reporter.fail(a.node, `Seeds: field "${ref}" of type ${typeName(t)} cannot be a seed (use a Pubkey, an integer or a unit enum)`);
|
|
473
|
-
idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
|
|
477
|
-
const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
|
|
478
|
-
if (ref && !acct && !arg)
|
|
479
|
-
this.reporter.fail(a.node, `Seeds: "@${ref}" is neither an account parameter nor an argument`);
|
|
480
|
-
if (acct) {
|
|
481
|
-
parts.push(`{(const uint8_t *)&a_${n}->key, 32}`);
|
|
482
|
-
idl.push({ kind: "account", path: snakeCase(n) });
|
|
483
|
-
}
|
|
484
|
-
else if (arg) {
|
|
485
|
-
const t = arg.type;
|
|
486
|
-
if (t.kind === "string")
|
|
487
|
-
parts.push(`{arg_${n}.ptr, arg_${n}.len}`);
|
|
488
|
-
else if (t.kind === "pubkey")
|
|
489
|
-
parts.push(`{(const uint8_t *)&arg_${n}, 32}`);
|
|
490
|
-
else if (t.kind === "int" || t.kind === "enum")
|
|
491
|
-
parts.push(`{(const uint8_t *)&arg_${n}, ${scalarSize(t)}}`);
|
|
492
|
-
else
|
|
493
|
-
this.reporter.fail(a.node, `Seeds: argument "${n}" of type ${typeName(t)} cannot be a seed`);
|
|
494
|
-
idl.push({ kind: "arg", path: snakeCase(n) });
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
if (Buffer.byteLength(n, "utf8") > 32)
|
|
498
|
-
this.reporter.fail(a.node, "a seed can be at most 32 bytes");
|
|
499
|
-
parts.push(`{(const uint8_t *)${cString(n)}, ${Buffer.byteLength(n, "utf8")}}`);
|
|
500
|
-
idl.push({ kind: "const", value: [...Buffer.from(n, "utf8")] });
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
if (!ix.pdas.has(a.name))
|
|
504
|
-
ix.pdas.set(a.name, idl);
|
|
505
|
-
// The deriving program: this one, another account parameter ("@name"), or a fixed address.
|
|
506
|
-
let program = "params->program_id";
|
|
507
|
-
const p = c.seeds.program;
|
|
508
|
-
if (p?.startsWith("@")) {
|
|
509
|
-
const pa = ix.accounts.find((o) => o.name === p.slice(1));
|
|
510
|
-
if (!pa)
|
|
511
|
-
this.reporter.fail(a.node, `Seeds program: "${p}" is not an account parameter`);
|
|
512
|
-
program = `&a_${pa.name}->key`;
|
|
513
|
-
ix.pdaPrograms.set(a.name, { kind: "account", path: snakeCase(pa.name) });
|
|
514
|
-
}
|
|
515
|
-
else if (p) {
|
|
516
|
-
const bytes = [...decodeAddress(p)];
|
|
517
|
-
out.push(` static const SolPubkey sp_${a.name} = {${cBytes(bytes)}};`);
|
|
518
|
-
program = `&sp_${a.name}`;
|
|
519
|
-
ix.pdaPrograms.set(a.name, { kind: "const", value: bytes });
|
|
520
|
-
}
|
|
521
|
-
const seeds = `cs_${a.name}`;
|
|
522
|
-
out.push(` SolSignerSeed ${seeds}[${parts.length + 1}] = {${parts.join(", ")}};`);
|
|
523
|
-
if (c.seeds.bump) {
|
|
524
|
-
out.push(` ${seeds}[${parts.length}] = (SolSignerSeed){&d_${a.name}->${c.seeds.bump}, 1};`);
|
|
525
|
-
out.push(` TS_TRY(ts_require_pda(a_${a.name}, ${seeds}, ${parts.length + 1}, ${program}));`);
|
|
526
|
-
this.bumps.set(a.name, `d_${a.name}->${c.seeds.bump}`);
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
|
|
530
|
-
out.push(` { SolPubkey pda_${a.name};`);
|
|
531
|
-
out.push(` if (ts_find_pda(${seeds}, ${parts.length}, ${program}, &pda_${a.name}, &sb_${a.name}) != SUCCESS || !ts_key_eq(&pda_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: ${a.name} is not the expected PDA"); }`);
|
|
532
|
-
this.bumps.set(a.name, `sb_${a.name}`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
431
|
+
if (k !== "account")
|
|
432
|
+
out.push(...hasOne);
|
|
535
433
|
// Constraints run only when every optional account they mention was passed.
|
|
536
434
|
const lines = out.splice(start);
|
|
537
435
|
const optional = this.optionalIn(lines.join("\n"));
|
|
@@ -542,6 +440,138 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
542
440
|
}
|
|
543
441
|
return [...bumpDecls, ...out];
|
|
544
442
|
}
|
|
443
|
+
/** Ata<…>: the account's owner (2015) and address (2009) as Anchor checks them, then its mint. */
|
|
444
|
+
ataCheck(ix, a) {
|
|
445
|
+
const out = [];
|
|
446
|
+
const c = a.constraints;
|
|
447
|
+
const k = a.kind.kind;
|
|
448
|
+
const ata = c.ata;
|
|
449
|
+
// With two token programs the address depends on which the mint uses: no IDL derivation, and an Ata<AccountInfo>
|
|
450
|
+
// (derived here) is refused.
|
|
451
|
+
const tps = ix.accounts.filter((o) => o.kind.kind === "tokenProgram");
|
|
452
|
+
const tp = tps.length === 1 ? tps[0] : undefined;
|
|
453
|
+
if (tp && !ix.pdas.has(a.name)) {
|
|
454
|
+
ix.pdas.set(a.name, [
|
|
455
|
+
{ kind: "account", path: snakeCase(ata.owner) },
|
|
456
|
+
{ kind: "account", path: snakeCase(tp.name) },
|
|
457
|
+
{ kind: "account", path: snakeCase(ata.mint) },
|
|
458
|
+
]);
|
|
459
|
+
ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
|
|
460
|
+
}
|
|
461
|
+
// Created in the body: the ATA program checks the address when it creates it, and createIfNeeded checks an
|
|
462
|
+
// existing one there. Only the IDL entry is recorded.
|
|
463
|
+
if (k === "token" && a.kind.init)
|
|
464
|
+
return out;
|
|
465
|
+
// The address alone is not enough: legacy SPL Token lets an ATA's owner be reassigned.
|
|
466
|
+
if (k === "token")
|
|
467
|
+
out.push(` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${ata.owner}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${ata.owner}");`);
|
|
468
|
+
// The token program is the account's owner; an AccountInfo not created yet takes the instruction's token program.
|
|
469
|
+
let program = `&a_${a.name}->owner`;
|
|
470
|
+
if (k !== "token") {
|
|
471
|
+
if (!tp)
|
|
472
|
+
this.reporter.fail(a.node, tps.length
|
|
473
|
+
? "Ata<AccountInfo, …> needs exactly one token program parameter to derive the address; use TokenInterface"
|
|
474
|
+
: "Ata<AccountInfo, …> needs a token program parameter to derive the address");
|
|
475
|
+
program = `&a_${tp.name}->key`;
|
|
476
|
+
}
|
|
477
|
+
out.push(` { SolPubkey ata_${a.name};`);
|
|
478
|
+
out.push(` TS_TRY(ts_ata_address(&a_${ata.owner}->key, &a_${ata.mint}->key, ${program}, &ata_${a.name}));`);
|
|
479
|
+
out.push(` if (!ts_key_eq(&ata_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_ASSOCIATED, "TypeSOL: ${a.name} is not the associated token account of ${ata.owner} and ${ata.mint}"); }`);
|
|
480
|
+
if (k === "token")
|
|
481
|
+
out.push(` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${ata.mint}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${ata.mint}");`);
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
/** Seeds<…>: the account is the PDA of its seeds (with the stored bump, or the canonical one found here). */
|
|
485
|
+
seedsCheck(ix, a, bumpDecls) {
|
|
486
|
+
const out = [];
|
|
487
|
+
const c = a.constraints;
|
|
488
|
+
const parts = [];
|
|
489
|
+
const idl = [];
|
|
490
|
+
for (const raw of c.seeds.names) {
|
|
491
|
+
// "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
|
|
492
|
+
const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
|
|
493
|
+
const n = ref ?? raw;
|
|
494
|
+
// "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
|
|
495
|
+
// `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
|
|
496
|
+
if (ref?.includes(".")) {
|
|
497
|
+
const [owner, field, ...rest] = ref.split(".");
|
|
498
|
+
const o = ix.accounts.find((x) => x.name === owner);
|
|
499
|
+
if (!o || o.kind.kind !== "account" || rest.length)
|
|
500
|
+
this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
|
|
501
|
+
const st = o.kind.struct;
|
|
502
|
+
const f = st.fields.find((x) => x.name === field);
|
|
503
|
+
if (!f)
|
|
504
|
+
this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
|
|
505
|
+
const t = f.type;
|
|
506
|
+
if (t.kind === "pubkey")
|
|
507
|
+
parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
|
|
508
|
+
else if (t.kind === "int" || t.kind === "enum")
|
|
509
|
+
parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
|
|
510
|
+
else
|
|
511
|
+
this.reporter.fail(a.node, `Seeds: field "${ref}" of type ${typeName(t)} cannot be a seed (use a Pubkey, an integer or a unit enum)`);
|
|
512
|
+
idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
|
|
516
|
+
const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
|
|
517
|
+
if (ref && !acct && !arg)
|
|
518
|
+
this.reporter.fail(a.node, `Seeds: "@${ref}" is neither an account parameter nor an argument`);
|
|
519
|
+
if (acct) {
|
|
520
|
+
parts.push(`{(const uint8_t *)&a_${n}->key, 32}`);
|
|
521
|
+
idl.push({ kind: "account", path: snakeCase(n) });
|
|
522
|
+
}
|
|
523
|
+
else if (arg) {
|
|
524
|
+
const t = arg.type;
|
|
525
|
+
if (t.kind === "string")
|
|
526
|
+
parts.push(`{arg_${n}.ptr, arg_${n}.len}`);
|
|
527
|
+
else if (t.kind === "pubkey")
|
|
528
|
+
parts.push(`{(const uint8_t *)&arg_${n}, 32}`);
|
|
529
|
+
else if (t.kind === "int" || t.kind === "enum")
|
|
530
|
+
parts.push(`{(const uint8_t *)&arg_${n}, ${scalarSize(t)}}`);
|
|
531
|
+
else
|
|
532
|
+
this.reporter.fail(a.node, `Seeds: argument "${n}" of type ${typeName(t)} cannot be a seed`);
|
|
533
|
+
idl.push({ kind: "arg", path: snakeCase(n) });
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
if (Buffer.byteLength(n, "utf8") > 32)
|
|
537
|
+
this.reporter.fail(a.node, "a seed can be at most 32 bytes");
|
|
538
|
+
parts.push(`{(const uint8_t *)${cString(n)}, ${Buffer.byteLength(n, "utf8")}}`);
|
|
539
|
+
idl.push({ kind: "const", value: [...Buffer.from(n, "utf8")] });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (!ix.pdas.has(a.name))
|
|
543
|
+
ix.pdas.set(a.name, idl);
|
|
544
|
+
// The deriving program: this one, another account parameter ("@name"), or a fixed address.
|
|
545
|
+
let program = "params->program_id";
|
|
546
|
+
const p = c.seeds.program;
|
|
547
|
+
if (p?.startsWith("@")) {
|
|
548
|
+
const pa = ix.accounts.find((o) => o.name === p.slice(1));
|
|
549
|
+
if (!pa)
|
|
550
|
+
this.reporter.fail(a.node, `Seeds program: "${p}" is not an account parameter`);
|
|
551
|
+
program = `&a_${pa.name}->key`;
|
|
552
|
+
ix.pdaPrograms.set(a.name, { kind: "account", path: snakeCase(pa.name) });
|
|
553
|
+
}
|
|
554
|
+
else if (p) {
|
|
555
|
+
const bytes = [...decodeAddress(p)];
|
|
556
|
+
out.push(` static const SolPubkey sp_${a.name} = {${cBytes(bytes)}};`);
|
|
557
|
+
program = `&sp_${a.name}`;
|
|
558
|
+
ix.pdaPrograms.set(a.name, { kind: "const", value: bytes });
|
|
559
|
+
}
|
|
560
|
+
const seeds = `cs_${a.name}`;
|
|
561
|
+
out.push(` SolSignerSeed ${seeds}[${parts.length + 1}] = {${parts.join(", ")}};`);
|
|
562
|
+
if (c.seeds.bump) {
|
|
563
|
+
out.push(` ${seeds}[${parts.length}] = (SolSignerSeed){&d_${a.name}->${c.seeds.bump}, 1};`);
|
|
564
|
+
out.push(` TS_TRY(ts_require_pda(a_${a.name}, ${seeds}, ${parts.length + 1}, ${program}));`);
|
|
565
|
+
this.bumps.set(a.name, `d_${a.name}->${c.seeds.bump}`);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
|
|
569
|
+
out.push(` { SolPubkey pda_${a.name};`);
|
|
570
|
+
out.push(` if (ts_find_pda(${seeds}, ${parts.length}, ${program}, &pda_${a.name}, &sb_${a.name}) != SUCCESS || !ts_key_eq(&pda_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: ${a.name} is not the expected PDA"); }`);
|
|
571
|
+
this.bumps.set(a.name, `sb_${a.name}`);
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
545
575
|
/** Optional account parameters a piece of generated C refers to (a_name / d_name). */
|
|
546
576
|
optionalIn(code) {
|
|
547
577
|
return (this.ix?.accounts ?? []).filter((a) => a.optional && new RegExp(`\\b[ad]_${a.name}\\b`).test(code)).map((a) => a.name);
|
|
@@ -137,6 +137,9 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
137
137
|
// refuse more than the array holds instead (ts_deserialize relies on this bound).
|
|
138
138
|
out.push(` if (*(const uint64_t *)input > TS_MAX_ACCOUNTS) TS_FAIL(TS_ERR_TOO_MANY_ACCOUNTS, "TypeSOL: at most ${MAX_ACCOUNTS} accounts per instruction");`);
|
|
139
139
|
out.push(` ts_deserialize(input, ¶ms);`);
|
|
140
|
+
// loadAccountMut's record of the accounts it loaded: set up only in programs that use it (one store otherwise wasted).
|
|
141
|
+
if ([...helperBodies, ...ixBodies].some((b) => b.includes("ts_claim_mut(")))
|
|
142
|
+
out.push(` params.loaded_mut = 0;`);
|
|
140
143
|
const minDisc = Math.min(8, ...model.instructions.map((ix) => ix.discriminator.length));
|
|
141
144
|
out.push(` if (params.data_len < ${minDisc}) TS_FAIL(TS_ERR_INSTRUCTION_MISSING, "TypeSOL: instruction data too short");`);
|
|
142
145
|
// Self-CPI events (emitCpi): accept the event instruction when its authority PDA signed it, like Anchor.
|
|
@@ -11,6 +11,7 @@ export declare const ATA_PROGRAM_BYTES: number[];
|
|
|
11
11
|
export declare const U64: ScalarType;
|
|
12
12
|
export declare const U8: ScalarType;
|
|
13
13
|
export declare const I64: ScalarType;
|
|
14
|
+
export declare const I32: ScalarType;
|
|
14
15
|
export declare const BOOL: ScalarType;
|
|
15
16
|
export declare const PUBKEY: ScalarType;
|
|
16
17
|
export declare const STRING: ValueType;
|
|
@@ -13,6 +13,7 @@ export const ATA_PROGRAM_BYTES = [
|
|
|
13
13
|
export const U64 = { kind: "int", name: "u64" };
|
|
14
14
|
export const U8 = { kind: "int", name: "u8" };
|
|
15
15
|
export const I64 = { kind: "int", name: "i64" };
|
|
16
|
+
export const I32 = { kind: "int", name: "i32" };
|
|
16
17
|
export const BOOL = { kind: "bool" };
|
|
17
18
|
export const PUBKEY = { kind: "pubkey" };
|
|
18
19
|
export const STRING = { kind: "string" };
|
|
@@ -93,4 +93,10 @@ export declare abstract class StatementEmitter extends CpiEmitter {
|
|
|
93
93
|
/** Whether a place goes through a Vec element (`v.get(i).x`, `v.get(i)`). */
|
|
94
94
|
protected writesThroughVec(node: ts.Expression): boolean;
|
|
95
95
|
protected helperTakesMutAccounts(node: ts.CallExpression): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Whether evaluating `node` may resize a growable Vec (and so move the Vecs after it): a push / pop / clear, a
|
|
98
|
+
* realloc or close, a helper taking a writable account or a function argument, or a closure (its body is not
|
|
99
|
+
* looked into: it may capture the account). Conservative: it only decides whether a Vec element may be the target.
|
|
100
|
+
*/
|
|
101
|
+
protected mayResizeVecs(node: ts.Node): boolean;
|
|
96
102
|
}
|
|
@@ -1085,6 +1085,10 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1085
1085
|
this.fail(e, "this expression has no effect; only assignments and calls can be statements");
|
|
1086
1086
|
}
|
|
1087
1087
|
assign(target, valueNode, op, topLevel = false) {
|
|
1088
|
+
// The place is resolved before the right side runs (JS order). A right side that can resize a Vec of the account
|
|
1089
|
+
// would move the element out from under it: the write would land in the bytes that follow.
|
|
1090
|
+
if (this.writesThroughVec(target) && this.mayResizeVecs(valueNode))
|
|
1091
|
+
this.fail(target, "the right side may change this Vec (a helper, closure or push/pop/clear); store its result in a local first");
|
|
1088
1092
|
if (op !== undefined) {
|
|
1089
1093
|
const place = this.place(target);
|
|
1090
1094
|
let current = { c: place.read, type: place.type };
|
|
@@ -1143,8 +1147,6 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1143
1147
|
const structCall = isCall && ts.isIdentifier(valueNode.expression)
|
|
1144
1148
|
? this.helperOf(valueNode)?.returns?.kind === "struct"
|
|
1145
1149
|
: false;
|
|
1146
|
-
if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
|
|
1147
|
-
this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
|
|
1148
1150
|
// Whole arrays / structs: copy between identical layouts.
|
|
1149
1151
|
const targetRef = isCall && !structCall ? undefined : this.ref(target);
|
|
1150
1152
|
if (targetRef &&
|
|
@@ -1161,9 +1163,6 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1161
1163
|
}
|
|
1162
1164
|
const place = this.place(target);
|
|
1163
1165
|
if (isCall && !createsFirst) {
|
|
1164
|
-
// A helper that may replace a Vec element would leave `place` pointing at the new one (JS writes the old object).
|
|
1165
|
-
if (this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
|
|
1166
|
-
this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
|
|
1167
1166
|
raw = this.call(valueNode, topLevel);
|
|
1168
1167
|
if (!raw)
|
|
1169
1168
|
this.fail(valueNode, "this call does not produce a value");
|
|
@@ -1194,4 +1193,35 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1194
1193
|
const h = this.helperOf(node);
|
|
1195
1194
|
return !!h?.params.some((p) => p.account && isWritable(p.account));
|
|
1196
1195
|
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Whether evaluating `node` may resize a growable Vec (and so move the Vecs after it): a push / pop / clear, a
|
|
1198
|
+
* realloc or close, a helper taking a writable account or a function argument, or a closure (its body is not
|
|
1199
|
+
* looked into: it may capture the account). Conservative: it only decides whether a Vec element may be the target.
|
|
1200
|
+
*/
|
|
1201
|
+
mayResizeVecs(node) {
|
|
1202
|
+
let found = false;
|
|
1203
|
+
const isClosure = (e) => ts.isArrowFunction(e) || ts.isFunctionExpression(e) || (ts.isIdentifier(e) && !!this.lookup(e.text)?.closure);
|
|
1204
|
+
const visit = (n) => {
|
|
1205
|
+
if (found)
|
|
1206
|
+
return;
|
|
1207
|
+
if (ts.isCallExpression(n)) {
|
|
1208
|
+
const callee = n.expression;
|
|
1209
|
+
if (ts.isPropertyAccessExpression(callee) && ["push", "pop", "clear"].includes(callee.name.text))
|
|
1210
|
+
found = true;
|
|
1211
|
+
else if (ts.isCallExpression(callee))
|
|
1212
|
+
found = true; // a function a function returns
|
|
1213
|
+
else if (ts.isIdentifier(callee)) {
|
|
1214
|
+
if (this.lookup(callee.text)?.closure)
|
|
1215
|
+
found = true;
|
|
1216
|
+
else if (["realloc", "close"].includes(this.exportName(callee) ?? ""))
|
|
1217
|
+
found = true;
|
|
1218
|
+
else if (this.helperOf(n) && (this.helperTakesMutAccounts(n) || n.arguments.some(isClosure)))
|
|
1219
|
+
found = true;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
ts.forEachChild(n, visit);
|
|
1223
|
+
};
|
|
1224
|
+
visit(node);
|
|
1225
|
+
return found;
|
|
1226
|
+
}
|
|
1197
1227
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Case study: porting Anchor's escrow
|
|
2
|
+
|
|
3
|
+
This page follows one complete Anchor program into TypeSOL: the token escrow from
|
|
4
|
+
[solana-developers/program-examples](https://github.com/solana-developers/program-examples/tree/de090195e4de698e150572c4f886c333323a64a5/tokens/escrow/anchor)
|
|
5
|
+
(`tokens/escrow`, commit `de09019`). It shows the original and the port side by side, what changed, how the two were
|
|
6
|
+
checked against each other, where they still differ, and what the port costs to run. The port is
|
|
7
|
+
[`test/ports/tokens/escrow.ts`](../test/ports/tokens/escrow.ts); [Migrate from Anchor](migrate-from-anchor.md) is the
|
|
8
|
+
general reference.
|
|
9
|
+
|
|
10
|
+
## The program
|
|
11
|
+
|
|
12
|
+
A maker offers an amount of token A for an amount of token B.
|
|
13
|
+
|
|
14
|
+
- `make_offer(id, token_a_offered_amount, token_b_wanted_amount)` creates the offer account, a PDA at
|
|
15
|
+
`["offer", maker, id]`, creates the vault (the offer's associated token account for mint A) and moves the offered
|
|
16
|
+
tokens into it.
|
|
17
|
+
- `take_offer()` sends token B from the taker to the maker, releases the vault to the taker, closes the vault and the
|
|
18
|
+
offer, and returns their rent to the maker. It creates the taker's mint A account and the maker's mint B account if
|
|
19
|
+
they do not exist.
|
|
20
|
+
- `refund_offer()` returns the vault to the maker and closes both accounts.
|
|
21
|
+
|
|
22
|
+
Upstream, this is 398 lines of Rust in ten files. The port is one 95-line TypeScript file.
|
|
23
|
+
|
|
24
|
+
## Side by side
|
|
25
|
+
|
|
26
|
+
### State
|
|
27
|
+
|
|
28
|
+
```rust
|
|
29
|
+
#[account]
|
|
30
|
+
#[derive(InitSpace)]
|
|
31
|
+
pub struct Offer {
|
|
32
|
+
pub id: u64,
|
|
33
|
+
pub maker: Pubkey,
|
|
34
|
+
pub token_mint_a: Pubkey,
|
|
35
|
+
pub token_mint_b: Pubkey,
|
|
36
|
+
pub token_b_wanted_amount: u64,
|
|
37
|
+
pub bump: u8,
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export interface Offer {
|
|
43
|
+
id: u64;
|
|
44
|
+
maker: Pubkey;
|
|
45
|
+
tokenMintA: Pubkey;
|
|
46
|
+
tokenMintB: Pubkey;
|
|
47
|
+
tokenBWantedAmount: u64;
|
|
48
|
+
bump: u8;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The account has the same discriminator and the same Borsh bytes: an Anchor client decodes a TypeSOL offer unchanged.
|
|
53
|
+
Field names are camelCase in TypeScript and snake_case in the IDL, as Anchor writes them.
|
|
54
|
+
|
|
55
|
+
### make_offer
|
|
56
|
+
|
|
57
|
+
Anchor declares the accounts in a struct with constraint attributes, then splits the work into helper functions:
|
|
58
|
+
|
|
59
|
+
```rust
|
|
60
|
+
#[derive(Accounts)]
|
|
61
|
+
#[instruction(id: u64)]
|
|
62
|
+
pub struct MakeOffer<'info> {
|
|
63
|
+
#[account(mut)]
|
|
64
|
+
pub maker: Signer<'info>,
|
|
65
|
+
#[account(mint::token_program = token_program)]
|
|
66
|
+
pub token_mint_a: InterfaceAccount<'info, Mint>,
|
|
67
|
+
#[account(mint::token_program = token_program)]
|
|
68
|
+
pub token_mint_b: InterfaceAccount<'info, Mint>,
|
|
69
|
+
#[account(mut, associated_token::mint = token_mint_a, associated_token::authority = maker,
|
|
70
|
+
associated_token::token_program = token_program)]
|
|
71
|
+
pub maker_token_account_a: InterfaceAccount<'info, TokenAccount>,
|
|
72
|
+
#[account(init, payer = maker, space = ANCHOR_DISCRIMINATOR + Offer::INIT_SPACE,
|
|
73
|
+
seeds = [b"offer", maker.key().as_ref(), id.to_le_bytes().as_ref()], bump)]
|
|
74
|
+
pub offer: Account<'info, Offer>,
|
|
75
|
+
#[account(init, payer = maker, associated_token::mint = token_mint_a, associated_token::authority = offer,
|
|
76
|
+
associated_token::token_program = token_program)]
|
|
77
|
+
pub vault: InterfaceAccount<'info, TokenAccount>,
|
|
78
|
+
pub associated_token_program: Program<'info, AssociatedToken>,
|
|
79
|
+
pub token_program: Interface<'info, TokenInterface>,
|
|
80
|
+
pub system_program: Program<'info, System>,
|
|
81
|
+
}
|
|
82
|
+
// + send_offered_tokens_to_vault (a TransferChecked CPI through a shared transfer_tokens helper)
|
|
83
|
+
// + save_offer (offer.set_inner(Offer { … bump: context.bumps.offer }))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In TypeSOL the accounts are the function's parameters, in the same order, and each constraint is part of the
|
|
87
|
+
parameter's type:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export function makeOffer(
|
|
91
|
+
maker: Mut<Signer>,
|
|
92
|
+
tokenMintA: MintOf<Mint, never, never, "tokenProgram">,
|
|
93
|
+
tokenMintB: MintOf<Mint, never, never, "tokenProgram">,
|
|
94
|
+
makerTokenAccountA: Ata<Mut<TokenAccount>, "maker", "tokenMintA">,
|
|
95
|
+
offer: Init<Offer>,
|
|
96
|
+
vault: Ata<Init<TokenAccount>, "offer", "tokenMintA">,
|
|
97
|
+
associatedTokenProgram: AssociatedTokenProgram,
|
|
98
|
+
tokenProgram: TokenInterface,
|
|
99
|
+
systemProgram: SystemProgram,
|
|
100
|
+
id: u64,
|
|
101
|
+
tokenAOfferedAmount: u64,
|
|
102
|
+
tokenBWantedAmount: u64,
|
|
103
|
+
): void {
|
|
104
|
+
offer.bump = offer.create(maker, ["offer", maker.key, id]);
|
|
105
|
+
vault.create(maker);
|
|
106
|
+
tokenProgram.transferChecked(makerTokenAccountA, tokenMintA, vault, maker, tokenAOfferedAmount);
|
|
107
|
+
offer.id = id;
|
|
108
|
+
offer.maker = maker.key;
|
|
109
|
+
offer.tokenMintA = tokenMintA.key;
|
|
110
|
+
offer.tokenMintB = tokenMintB.key;
|
|
111
|
+
offer.tokenBWantedAmount = tokenBWantedAmount;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`MintOf<Mint, never, never, "tokenProgram">` is `mint::token_program = token_program`: the mint must be owned by
|
|
116
|
+
the token program passed (2022 otherwise). `offer.create` finds the canonical bump, creates the PDA with the computed space and returns the bump, which Anchor
|
|
117
|
+
exposes as `context.bumps.offer`. `transferChecked` reads the mint's decimals itself.
|
|
118
|
+
|
|
119
|
+
### take_offer and refund_offer
|
|
120
|
+
|
|
121
|
+
The offer is checked declaratively, as in Anchor: the seeds come from the maker account and the offer's own `id`
|
|
122
|
+
field, and the stored bump is used.
|
|
123
|
+
|
|
124
|
+
```rust
|
|
125
|
+
#[account(mut, close = maker, has_one = maker, has_one = token_mint_a, has_one = token_mint_b,
|
|
126
|
+
seeds = [b"offer", maker.key().as_ref(), offer.id.to_le_bytes().as_ref()], bump = offer.bump)]
|
|
127
|
+
offer: Account<'info, Offer>,
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
offer: Seeds<CloseTo<HasOne<Mut<Offer>, "maker" | "tokenMintA" | "tokenMintB">, "maker">, ["offer", "@maker", "@offer.id"], "bump">,
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The body signs for the vault with the offer's seeds, where Anchor builds a `signer_seeds` array and a
|
|
135
|
+
`CpiContext::new_with_signer` for each call:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
takerTokenAccountA.createIfNeeded(taker);
|
|
139
|
+
makerTokenAccountB.createIfNeeded(taker);
|
|
140
|
+
tokenProgram.transferChecked(takerTokenAccountB, tokenMintB, makerTokenAccountB, taker, offer.tokenBWantedAmount);
|
|
141
|
+
tokenProgram.transferChecked(vault, tokenMintA, takerTokenAccountA, offer, vault.amount, ["offer", maker.key, offer.id, offer.bump]);
|
|
142
|
+
tokenProgram.closeAccount(vault, maker, offer, ["offer", maker.key, offer.id, offer.bump]);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`CloseTo<…, "maker">` closes the offer after the body returns, as Anchor's `close = maker` does.
|
|
146
|
+
|
|
147
|
+
## What changed
|
|
148
|
+
|
|
149
|
+
| Anchor | Port |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `Signer`, `SystemAccount` (`#[account(mut)]`) | `Mut<Signer>`, `Mut<SystemAccount>` |
|
|
152
|
+
| `InterfaceAccount<Mint>`, `InterfaceAccount<TokenAccount>` | `Mint`, `TokenAccount` |
|
|
153
|
+
| `mint::token_program = token_program` | `MintOf<Mint, never, never, "tokenProgram">` |
|
|
154
|
+
| `associated_token::mint = m, associated_token::authority = a` | `Ata<TokenAccount, "a", "m">` |
|
|
155
|
+
| `init` / `init_if_needed` on an associated token account | `Ata<Init<…>>` + `vault.create(payer)` / `Ata<InitIfNeeded<…>>` + `createIfNeeded(payer)` |
|
|
156
|
+
| `init, seeds, bump` + `context.bumps.offer` | `Init<Offer>` + `offer.bump = offer.create(maker, seeds)` |
|
|
157
|
+
| `seeds = […], bump = offer.bump` | `Seeds<…, ["offer", "@maker", "@offer.id"], "bump">` |
|
|
158
|
+
| `has_one`, `close` | `HasOne<…>`, `CloseTo<…>` |
|
|
159
|
+
| `transfer_checked(CpiContext::new_with_signer(…), amount, decimals)` | `tokenProgram.transferChecked(from, mint, to, authority, amount, seeds)` |
|
|
160
|
+
| `ErrorCode::CustomError` (unused upstream) | `enum ErrorCode`, the same code 6000 |
|
|
161
|
+
|
|
162
|
+
Nothing in the program needed a construct TypeSOL lacks, and every account constraint has its TypeSOL form; the port
|
|
163
|
+
is marked PORTED in [the ports table](../test/ports/README.md).
|
|
164
|
+
|
|
165
|
+
## Same inputs, same results
|
|
166
|
+
|
|
167
|
+
Two independent checks run the original and the port against each other.
|
|
168
|
+
|
|
169
|
+
- **The port's own test** ([`test/ports-tokens.test.ts`](../test/ports-tokens.test.ts)) runs the upstream flow with SPL
|
|
170
|
+
Token mints and again with Token-2022 mints: make, take (creating the taker's and the maker's missing token accounts),
|
|
171
|
+
make again, a refund attempt by someone other than the maker, and the refund. It checks balances, the decoded offer
|
|
172
|
+
and that offer and vault are closed.
|
|
173
|
+
- **The four-way comparison** in [typesol/benchmarks](https://github.com/typesol/benchmarks) builds the upstream Anchor,
|
|
174
|
+
native and Pinocchio versions unmodified and sends all four the same 21 offers (ten taken, ten refunded, one left
|
|
175
|
+
open). The decoded open offer, every token balance, the vault's authority and which accounts were closed must be
|
|
176
|
+
identical before any number is reported. They are.
|
|
177
|
+
|
|
178
|
+
Both implementations refuse the hostile calls the comparison makes:
|
|
179
|
+
|
|
180
|
+
| Hostile call | Anchor | TypeSOL |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| refund_offer signed by someone other than the maker | 2006 ConstraintSeeds | 2006 ConstraintSeeds |
|
|
183
|
+
| take_offer naming the taker as the maker | 2015 ConstraintTokenOwner | 2040 ConstraintDuplicateMutableAccount |
|
|
184
|
+
|
|
185
|
+
TypeSOL checks each account's constraints in Anchor's order, so a refused transaction usually gets the same error
|
|
186
|
+
code (see [which error a transaction gets](errors.md#which-error-a-transaction-gets)). The second call differs on
|
|
187
|
+
purpose: taker and maker are then the same writable account, which TypeSOL refuses for any two writable parameters,
|
|
188
|
+
where Anchor only compares program and token accounts and fails later, on the maker's token account. A client that
|
|
189
|
+
matches on this code needs to accept either.
|
|
190
|
+
|
|
191
|
+
## Cost
|
|
192
|
+
|
|
193
|
+
Mean compute units per instruction in LiteSVM (21 make_offer, 10 take_offer and 10 refund_offer calls), and the
|
|
194
|
+
program size, from typesol/benchmarks (TypeSOL at this version; Anchor 1.0.2). Lower is better.
|
|
195
|
+
|
|
196
|
+
| | TypeSOL | Anchor | Native Rust | Pinocchio |
|
|
197
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
198
|
+
| make_offer | **19,697** | 35,726 | 33,206 | 20,581 |
|
|
199
|
+
| take_offer | **7,053** | 29,235 | 24,995 | 21,606 |
|
|
200
|
+
| refund_offer | **4,271** | 15,326 | 12,477 | 8,137 |
|
|
201
|
+
| Binary | 32.0 KB | 244.5 KB | 120.4 KB | **25.1 KB** |
|
|
202
|
+
|
|
203
|
+
Most of `make_offer` in every version is the Associated Token Account program creating the vault. The native and
|
|
204
|
+
Pinocchio versions are teaching code with fewer checks (no owner or type check on the offer, `Transfer` instead of
|
|
205
|
+
`TransferChecked`; Pinocchio takes the offer's bump from the client).
|
|
206
|
+
|
|
207
|
+
The same program, built with an earlier TypeSOL, runs on mainnet next to the other three; the transactions are listed
|
|
208
|
+
with their Solscan links in the [performance report](https://typesol.com/report/) and the
|
|
209
|
+
[README](../README.md#on-mainnet).
|
|
210
|
+
|
|
211
|
+
## Porting your own program
|
|
212
|
+
|
|
213
|
+
The steps this port followed, in order:
|
|
214
|
+
|
|
215
|
+
1. Generate the skeleton from the Anchor IDL: `npx typesol port target/idl/escrow.json --out programs/escrow.ts`. It
|
|
216
|
+
keeps instruction names, discriminators, account order and error codes.
|
|
217
|
+
2. Turn each `#[account(...)]` into the parameter's type, using the table above and
|
|
218
|
+
[Accounts and constraints](migrate-from-anchor.md#accounts-and-constraints). Keep the account order: it is the
|
|
219
|
+
wire format.
|
|
220
|
+
3. Port the bodies. Helpers like `transfer_tokens` usually disappear into one call; `signer_seeds` arrays become the
|
|
221
|
+
last argument of the CPI.
|
|
222
|
+
4. Run the upstream test flow against the port with `typesol/testing`, including the failure paths, and compare the
|
|
223
|
+
decoded accounts.
|
|
224
|
+
5. Compare the IDLs, and check the error codes your clients match on (the table above shows how they can differ).
|
package/docs/errors.md
CHANGED
|
@@ -5,6 +5,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
5
5
|
| Code | Meaning |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| 100 / 101 / 102 | instruction data missing / unknown instruction / malformed arguments (InstructionMissing / FallbackNotFound / DidNotDeserialize) |
|
|
8
|
+
| 2000 | a `Mut<…>` / `Init<…>` account passed read-only (ConstraintMut) |
|
|
8
9
|
| 2001 | HasOne constraint failed (ConstraintHasOne) |
|
|
9
10
|
| 2002 | event authority did not sign (ConstraintSigner) |
|
|
10
11
|
| 2004 | Owner constraint failed (ConstraintOwner) |
|
|
@@ -15,6 +16,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
15
16
|
| 2012 | not the expected address (ConstraintAddress) |
|
|
16
17
|
| 2014 / 2015 | token account mint / owner mismatch (ConstraintTokenMint / ConstraintTokenOwner) |
|
|
17
18
|
| 2016 / 2017 / 2018 | mint authority / freeze authority / decimals mismatch (2018: an existing mint `createIfNeeded` found) |
|
|
19
|
+
| 2021 / 2022 | token account / mint not owned by the token program `TokenOf` / `MintOf` names (ConstraintTokenTokenProgram / ConstraintMintTokenProgram) |
|
|
18
20
|
| 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…) |
|
|
19
21
|
| 2040 | the same account passed in two writable slots (ConstraintDuplicateMutableAccount) |
|
|
20
22
|
| 3000 | account already exists / initialized (AccountDiscriminatorAlreadySet) |
|
|
@@ -22,7 +24,7 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
22
24
|
| 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) |
|
|
23
25
|
| 3004 | a `@layout borsh` account too small for the value it would now hold (AccountDidNotSerialize) |
|
|
24
26
|
| 3005 | not enough accounts (AccountNotEnoughKeys) |
|
|
25
|
-
| 3006 | account
|
|
27
|
+
| 3006 | a remaining account or CPI account the program needs writable was passed read-only (AccountNotMutable) |
|
|
26
28
|
| 3007 | wrong owner program (AccountOwnedByWrongProgram) |
|
|
27
29
|
| 3008 / 3009 | wrong program address / not executable (InvalidProgramId / InvalidProgramExecutable) |
|
|
28
30
|
| 3010 | account must sign (AccountNotSigner; also a new keypair account a `create(payer, null, …)` makes) |
|
|
@@ -47,3 +49,26 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
|
|
|
47
49
|
|
|
48
50
|
Any of Anchor's own codes can be raised directly: `require(cond, AnchorError.RequireKeysEqViolated)` fails with 2502,
|
|
49
51
|
as `require_keys_eq!` does.
|
|
52
|
+
|
|
53
|
+
## Which error a transaction gets
|
|
54
|
+
|
|
55
|
+
A transaction can break several checks; it fails with the first. Within each account's constraints the checks run in
|
|
56
|
+
the order Anchor's generated code runs them, so a transaction gets the same code under both:
|
|
57
|
+
|
|
58
|
+
1. The account count (3005), then each account in parameter order: its type, signer (3010), `Mut` (2000), owner,
|
|
59
|
+
discriminator and size of a program account (3007 / 3012 / 3002 / 3003), token account or mint, program address.
|
|
60
|
+
2. Two writable parameters naming one account (2040).
|
|
61
|
+
3. The instruction's arguments (102).
|
|
62
|
+
4. Each account in parameter order, its constraints in Anchor's order: seeds (2006), associated token account (owner
|
|
63
|
+
2015, then address 2009), `has_one` (2001), owner (2004), executable (2007), address (2012), then `TokenOf` /
|
|
64
|
+
`MintOf` (2015, 2014, 2021 / 2016, 2017, 2022).
|
|
65
|
+
|
|
66
|
+
Where TypeSOL's order differs, only a transaction that breaks two of these checks at once sees it:
|
|
67
|
+
|
|
68
|
+
- Anchor decodes the arguments first, and checks `mut` with the constraints (after seeds and the associated token
|
|
69
|
+
account). TypeSOL keeps both earlier and later as above, which keeps binaries smaller.
|
|
70
|
+
- Too few accounts fails with 3005 before any account is checked; Anchor fails on the first account it cannot find.
|
|
71
|
+
- Accounts the instruction creates (`Init<…>`, `create(…)`) are created in the body, after every other check, where
|
|
72
|
+
Anchor creates them before checking the other accounts.
|
|
73
|
+
- TypeSOL refuses any two writable parameters naming one account (2040), where Anchor only compares program and token
|
|
74
|
+
accounts: a signer passed as two writable parameters fails with 2040 here and later, or not at all, under Anchor.
|
package/docs/language.md
CHANGED
|
@@ -38,8 +38,8 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
38
38
|
| Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
|
|
39
39
|
| Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full): a template is joined in one allocation, text known at compile time (literals, string and Pubkey constants) is folded in, and `s += …` grows the newest heap string in place |
|
|
40
40
|
| `switch` | on integers (128-bit ones too), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
|
|
41
|
-
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
42
|
-
| Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds)` (fully verified, feed-matched, fresh); raw `update.price / conf / exponent / publishTime
|
|
41
|
+
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention; inner nodes and leaves are hashed alike, so hash leaves as OpenZeppelin does, `keccak256(keccak256(data))`, and never take the leaf itself from the arguments: an inner node would pass as a leaf); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
42
|
+
| Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds, exponent?)` (fully verified, feed-matched, fresh; with `exponent`, a feed at another scale is refused); raw `update.price / conf / exponent / publishTime` are unchecked (any feed, partially verified or stale: check `feedId`, `fullyVerified` and the age yourself, or use `pythPrice`); `switchboardValue(feed: SwitchboardFeed, expectedFeed, maxStaleSlots, decimals, minSamples?)` (the feed must be the expected one: anyone can create feeds; `minSamples`, default 1, the fewest oracle samples accepted) (mainnet program unless `SwitchboardFeed<"devnet">`) |
|
|
43
43
|
| Multiple files | `import { Side, min } from "./math"` — structs, enums, constants and helpers can live in other files; only the entry file's exported functions are instructions; names are program-wide, so imports from TypeSOL files cannot be renamed; error codes number imported files first |
|
|
44
44
|
| Loops | `for (let i = 0n; i < n; i++)`, `while`, `do … while`, `for (const x of list)` over a `Vec` or `FixedArray` (each `x` is what `list.get(i)` gives: a value, or a reference into account data for structs; the length is read again every step, as in JS), `break`, `continue` — bounded by the compute budget. Remaining accounts are iterated by index |
|
|
45
45
|
| Helpers | non-exported functions (and every function of an imported file): take values, structs, Vec arguments, `SignerSeeds` and accounts the instruction already checked (`Signer`, `AccountInfo`/`AccountMeta`, `Mut<T>`, token accounts, programs); return `void`, a scalar, a `string`, a function (see below) or a fixed-size struct (`function at(x: u64, y: u64): Point { return { x, y }; }`: the caller gets its own copy, to bind, store with `acct.home = at(…)` / `arr.set(i, at(…))` or read as `at(…).x`); no recursion. A helper that takes a struct account as `Mut<T>` can grow its Vec (`push(value, payer)`: the account's size at instruction entry is passed along) |
|
|
@@ -65,9 +65,9 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
65
65
|
| `Signer` / `Mut<Signer>` | signed (and writable) |
|
|
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
|
-
| `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 |
|
|
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
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 |
|
|
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">` / `MintOf<Mint, "authorityParam", "freezeParam">` name the parameters, as Anchor's `token::mint = …` / `mint::authority = …` do (`never` leaves one out) |
|
|
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 |
|
|
73
73
|
| `Address<A, "4Nd1…">` | exact address (2012; `address` in the IDL) |
|
|
@@ -77,8 +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
|
| `Zeroed<T>` | large accounts (up to 10 MiB) the client allocated with System `CreateAccount`: owned by this program, big enough, rent-exempt, discriminator still zero — then initialized; like Anchor's zero-copy `zero` |
|
|
80
|
-
| `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). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
|
|
81
|
-
| `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) |
|
|
80
|
+
| `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) |
|
|
81
|
+
| `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) |
|
|
82
82
|
| `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` |
|
|
83
83
|
| `Mint` / `Mut<Mint>` | owned by SPL Token or Token-2022, initialized; `decimals`, `supply`, `mintAuthority`, `freezeAuthority`, `tokenProgram` |
|
|
84
84
|
| `SystemProgram`, `TokenProgram`, `Token2022Program` | exact program address |
|
|
@@ -94,7 +94,9 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
94
94
|
Every account exposes `key`, `lamports`, `dataLen`, `isSigner`, `isWritable`. At most 32 accounts per instruction (more is refused, 5113).
|
|
95
95
|
|
|
96
96
|
Two writable parameters can never be the same account: aliasing is refused with error 2040 (also when a remaining
|
|
97
|
-
account written through `.lamports` or `loadAccountMut` is another account of the instruction).
|
|
97
|
+
account written through `.lamports` or `loadAccountMut` is another account of the instruction). A remaining account
|
|
98
|
+
can be loaded with `loadAccountMut` once per instruction: loading the same one again (`rest.get(i)` and `rest.get(j)`
|
|
99
|
+
with `i == j`) is refused with 2040 too, since the two views would alias.
|
|
98
100
|
|
|
99
101
|
## Built-ins
|
|
100
102
|
|
|
@@ -106,7 +108,9 @@ account written through `.lamports` or `loadAccountMut` is another account of th
|
|
|
106
108
|
`freezeAccount` / `thawAccount(account, mint, freezeAuthority)`, `syncNative(account)`, `initializeMint2(mint, decimals, mintAuthority, freezeAuthority | null)`,
|
|
107
109
|
`initializeAccount3(account, mint, owner)`, and on Token-2022 `initializeMetadata(mint, updateAuthority, mintAuthority, name, symbol, uri)` (metadata extension) and the extension initializers to call between `createAccount` and `initializeMint2` / `initializeAccount3`: `initializeTransferFeeConfig(mint, configAuthority | null, withdrawAuthority | null, basisPoints, maximumFee)`, `initializeMintCloseAuthority(mint, authority | null)`, `initializeNonTransferableMint(mint)`, `initializePermanentDelegate(mint, delegate)`, `initializeDefaultAccountState(mint, frozen)`, `initializeMetadataPointer` / `initializeGroupPointer` / `initializeGroupMemberPointer` / `initializeTransferHook(mint, authority | null, address | null)`, `initializeInterestBearingMint(mint, rateAuthority | null, rate)`, `initializeImmutableOwner(account)`; the extension instructions `updateDefaultAccountState(mint, freezeAuthority, frozen)`, `updateTransferHook(mint, authority, programId | null)`, `enableRequiredMemoTransfers` / `disableRequiredMemoTransfers(account, owner)`, `updateInterestRate(mint, rateAuthority, rate)`, `transferCheckedWithFee(from, mint, to, authority, amount, fee)`, `harvestWithheldTokensToMint(mint, remainingAccounts())`, `withdrawWithheldTokensFromMint(mint, destination, authority)`, `setTransferFee(mint, authority, basisPoints, maximumFee)`; and the Token Metadata interface `updateMetadataField(mint, updateAuthority, field, value)` ("name" / "symbol" / "uri" or any other key), `removeMetadataKey(mint, updateAuthority, key, idempotent)`, `updateMetadataAuthority(mint, updateAuthority, next | null)`, `emitMetadata(mint)`. Each takes optional signerSeeds
|
|
108
110
|
- `account.lamports` read/write on `Mut` accounts, `close(account, destination)` for program accounts
|
|
109
|
-
- `requirePda(account, seeds, bump)`
|
|
111
|
+
- `requirePda(account, seeds, bump)`: any bump that gives a valid PDA passes, as with Anchor's `bump = expr`. Use a bump
|
|
112
|
+
stored when the account was created (or `Seeds<…>`); one taken from the instruction's arguments lets the caller pick
|
|
113
|
+
among up to 256 addresses for the same seeds
|
|
110
114
|
|
|
111
115
|
## Implementing another program's interface (transfer hooks)
|
|
112
116
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
TypeSOL programs speak Anchor's wire format: the same instruction and account discriminators, Borsh arguments, an
|
|
4
4
|
Anchor-format IDL, the same error numbering and event logs. A ported program can keep its clients. This page maps
|
|
5
|
-
Anchor's constructs to TypeSOL's and shows the porting workflow.
|
|
5
|
+
Anchor's constructs to TypeSOL's and shows the porting workflow. For one complete program ported this way, with the
|
|
6
|
+
original side by side, the checks run against it and its cost, see [Case study: porting Anchor's escrow](case-study-escrow.md).
|
|
6
7
|
|
|
7
8
|
## Start from the IDL
|
|
8
9
|
|
|
@@ -50,6 +51,7 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
50
51
|
| `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
|
|
51
52
|
| `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
|
|
52
53
|
| `token::mint = m, token::authority = a` | `TokenOf<TokenAccount, "m", "a">` (2014 / 2015), or `HasOne<TokenAccount, "mint" \| "owner">` when the parameters have those names; `mint::authority = a, mint::freeze_authority = f` on an existing mint: `MintOf<Mint, "a", "f">` (2016 / 2017) |
|
|
54
|
+
| `mint::token_program = tp` / `token::token_program = tp` | `MintOf<Mint, never, never, "tp">` / `TokenOf<TokenAccount, never, never, "tp">` (2022 / 2021) |
|
|
53
55
|
| `extensions::metadata_pointer::authority = a, …` (with `mint::…`) | `mint.create(p, …, { metadataPointer: { authority: a.key, metadataAddress: mint.key }, … })`: transferHook, closeAuthority, permanentDelegate, nonTransferable, groupPointer, groupMemberPointer and more |
|
|
54
56
|
| `#[account(init, payer = p, seeds = […], bump, mint::decimals = 6, mint::authority = a, mint::freeze_authority = f)]` | `Init<Mint>`, then `const bump = mint.create(p, […], 6n, a.key, f.key)` (`null` seeds: a keypair mint; `InitIfNeeded<Mint>` + `createIfNeeded`) |
|
|
55
57
|
| `#[account(init, payer = p, seeds = […], bump, token::mint = m, token::authority = a)]` | `Init<TokenAccount>`, then `vault.create(p, […], m, a.key)` (`InitIfNeeded<TokenAccount>` + `createIfNeeded`) |
|
package/lib/typesol.d.ts
CHANGED
|
@@ -162,7 +162,8 @@ export declare function remainingAccounts(): RemainingAccounts;
|
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* Read a struct of this program from a remaining account: owner, discriminator and size are checked.
|
|
165
|
-
* `loadAccountMut` also requires it to be writable
|
|
165
|
+
* `loadAccountMut` also requires it to be writable, not aliased by another writable account, and not already loaded
|
|
166
|
+
* with `loadAccountMut` in this instruction (2040).
|
|
166
167
|
*/
|
|
167
168
|
export declare function loadAccount<T>(account: RemainingAccount): Readonly<T>;
|
|
168
169
|
export declare function loadAccountMut<T>(account: RemainingAccount): T;
|
|
@@ -180,7 +181,8 @@ export declare function keccak256(...parts: (string | Pubkey | Hash | bigint | b
|
|
|
180
181
|
|
|
181
182
|
/**
|
|
182
183
|
* Verify a Merkle proof (keccak256, sorted pairs: the OpenZeppelin convention most airdrop tools use).
|
|
183
|
-
* `proof` must be a `Vec<Hash>` instruction argument.
|
|
184
|
+
* `proof` must be a `Vec<Hash>` instruction argument. Leaves and inner nodes hash alike: build the leaf in the program
|
|
185
|
+
* as OpenZeppelin does, `keccak256(keccak256(data))`, never take it from the arguments (an inner node would pass).
|
|
184
186
|
*/
|
|
185
187
|
export declare function merkleVerify(leaf: Hash, proof: Vec<Hash>, root: Hash): boolean;
|
|
186
188
|
|
|
@@ -211,7 +213,8 @@ export declare function ethMessageHash(message: string): Hash;
|
|
|
211
213
|
|
|
212
214
|
/**
|
|
213
215
|
* A Pyth `PriceUpdateV2` account (Pyth Solana Receiver, rec5EKMG…). Owner and discriminator are checked.
|
|
214
|
-
* The fields are the raw update
|
|
216
|
+
* The fields are the raw update and are NOT checked: it may be only partially verified, for another feed, or stale.
|
|
217
|
+
* Use pythPrice() for a verified, fresh, feed-matched price (with `exponent` to pin the scale).
|
|
215
218
|
*/
|
|
216
219
|
export interface PythPriceUpdate extends AccountMeta {
|
|
217
220
|
readonly __pyth: void;
|
|
@@ -223,8 +226,12 @@ export interface PythPriceUpdate extends AccountMeta {
|
|
|
223
226
|
readonly publishTime: i64;
|
|
224
227
|
readonly fullyVerified: boolean;
|
|
225
228
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Like Pyth's get_price_no_older_than: fully verified, `feedId` matches, published within `maxAgeSeconds` (5111
|
|
231
|
+
* otherwise). The price is in units of 10^exponent: pass the `exponent` your arithmetic assumes and a feed whose
|
|
232
|
+
* exponent differs is refused, instead of read at the wrong scale.
|
|
233
|
+
*/
|
|
234
|
+
export declare function pythPrice(update: PythPriceUpdate, feedId: Hash, maxAgeSeconds: u64, exponent?: i32): i64;
|
|
228
235
|
|
|
229
236
|
/**
|
|
230
237
|
* A Switchboard On-Demand pull feed (PullFeedAccountData). Mainnet program unless `SwitchboardFeed<"devnet">`
|
|
@@ -237,7 +244,14 @@ export interface SwitchboardFeed<Cluster extends "mainnet" | "devnet" = "mainnet
|
|
|
237
244
|
* The feed's current result scaled to `decimals` (from 18), if `feed` is `expectedFeed` (anyone can create feeds),
|
|
238
245
|
* it has samples and it is at most `maxStaleSlots` old.
|
|
239
246
|
*/
|
|
240
|
-
export declare function switchboardValue(
|
|
247
|
+
export declare function switchboardValue(
|
|
248
|
+
feed: SwitchboardFeed<"mainnet" | "devnet">,
|
|
249
|
+
expectedFeed: Pubkey,
|
|
250
|
+
maxStaleSlots: u64,
|
|
251
|
+
decimals: u8,
|
|
252
|
+
/** The fewest oracle samples the result may rest on (default 1). */
|
|
253
|
+
minSamples?: u8,
|
|
254
|
+
): i64;
|
|
241
255
|
|
|
242
256
|
/** The Associated Token Account program (ATokenGP…). Its address is checked. */
|
|
243
257
|
export interface AssociatedTokenProgram extends AccountMeta {
|
|
@@ -570,13 +584,18 @@ export type InitIfNeeded<T> = T extends TokenAccount
|
|
|
570
584
|
*/
|
|
571
585
|
export type HasOne<A, Field extends string> = A & { readonly __hasOne?: Field };
|
|
572
586
|
/**
|
|
573
|
-
* Anchor's `token::mint = mintParam, token::authority = authorityParam` on an
|
|
574
|
-
* parameters (2014 / 2015); `never` leaves one out.
|
|
587
|
+
* Anchor's `token::mint = mintParam, token::authority = authorityParam, token::token_program = tokenProgramParam` on an
|
|
588
|
+
* existing token account, naming the parameters (2014 / 2015 / 2021: owned by that token program); `never` leaves one out.
|
|
575
589
|
*/
|
|
576
|
-
export type TokenOf<A, MintParam extends string, AuthorityParam extends string = never> = A & {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
590
|
+
export type TokenOf<A, MintParam extends string, AuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
|
|
591
|
+
readonly __tokenOf?: [MintParam, AuthorityParam, TokenProgramParam];
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Anchor's `mint::authority = a, mint::freeze_authority = f, mint::token_program = tp` on an existing mint, naming the
|
|
595
|
+
* parameters (2016 / 2017 / 2022: owned by that token program); `never` leaves one out.
|
|
596
|
+
*/
|
|
597
|
+
export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
|
|
598
|
+
readonly __mintOf?: [AuthorityParam, FreezeAuthorityParam, TokenProgramParam];
|
|
580
599
|
};
|
|
581
600
|
export type Seeds<A, Names extends readonly string[], Bump extends string = never, ProgramId extends string = never> = A & {
|
|
582
601
|
readonly __seeds?: [Names, Bump, ProgramId];
|
|
@@ -1642,7 +1661,10 @@ export declare function emit<T>(event: T): void;
|
|
|
1642
1661
|
*/
|
|
1643
1662
|
export declare function returnData<T extends bigint | boolean | Pubkey | Hash>(program: AccountMeta): T;
|
|
1644
1663
|
|
|
1645
|
-
/**
|
|
1664
|
+
/**
|
|
1665
|
+
* Verify `account.key` is the PDA for `seeds` + `bump` under this program. Any bump that gives a PDA passes (Anchor's
|
|
1666
|
+
* `bump = expr`): use a stored bump, not one from the arguments, or the caller picks among up to 256 addresses.
|
|
1667
|
+
*/
|
|
1646
1668
|
export declare function requirePda(account: AccountMeta, seeds: Seed[], bump: u8): void;
|
|
1647
1669
|
|
|
1648
1670
|
/** Close a program-owned account: move all lamports to `destination` and wipe it. */
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
#define TS_ERR_INSTRUCTION_NOT_FOUND 101 /* InstructionFallbackNotFound */
|
|
19
19
|
#define TS_ERR_INSTRUCTION_DATA 102 /* InstructionDidNotDeserialize */
|
|
20
20
|
#define TS_ERR_NOT_WRITABLE 3006 /* AccountNotMutable */
|
|
21
|
+
#define TS_ERR_CONSTRAINT_MUT 2000 /* ConstraintMut */
|
|
21
22
|
#define TS_ERR_CONSTRAINT 2001 /* ConstraintHasOne */
|
|
22
23
|
#define TS_ERR_NOT_RENT_EXEMPT 2005 /* ConstraintRentExempt */
|
|
23
24
|
#define TS_ERR_CONSTRAINT_SIGNER 2002 /* ConstraintSigner */
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
#define TS_ERR_MINT_AUTHORITY 2016 /* ConstraintMintMintAuthority */
|
|
32
33
|
#define TS_ERR_FREEZE_AUTHORITY 2017 /* ConstraintMintFreezeAuthority */
|
|
33
34
|
#define TS_ERR_MINT_DECIMALS 2018 /* ConstraintMintDecimals */
|
|
35
|
+
#define TS_ERR_TOKEN_TOKEN_PROGRAM 2021 /* ConstraintTokenTokenProgram */
|
|
36
|
+
#define TS_ERR_MINT_TOKEN_PROGRAM 2022 /* ConstraintMintTokenProgram */
|
|
34
37
|
#define TS_ERR_DUPLICATE_MUT 2040 /* ConstraintDuplicateMutableAccount */
|
|
35
38
|
#define TS_ERR_ALREADY_INITIALIZED 3000 /* AccountDiscriminatorAlreadySet */
|
|
36
39
|
#define TS_ERR_DISCRIMINATOR 3002 /* AccountDiscriminatorMismatch */
|
|
@@ -275,6 +278,7 @@ typedef struct {
|
|
|
275
278
|
const SolPubkey *program_id;
|
|
276
279
|
TsAccount *ka[TS_MAX_ACCOUNTS]; /* ka_num entries */
|
|
277
280
|
SolAccountInfo infos[TS_MAX_ACCOUNTS]; /* scratch for CPI account lists (ts_invoke) */
|
|
281
|
+
uint64_t loaded_mut; /* bit i: ka[i] loaded with loadAccountMut (zeroed by the entrypoint of a program that uses it) */
|
|
278
282
|
} TsParams;
|
|
279
283
|
|
|
280
284
|
/*
|
|
@@ -410,6 +414,12 @@ static inline uint64_t ts_check_writable(const TsAccount *a) {
|
|
|
410
414
|
if (!a->is_writable) TS_FAIL(TS_ERR_NOT_WRITABLE, "TypeSOL: account must be writable");
|
|
411
415
|
return SUCCESS;
|
|
412
416
|
}
|
|
417
|
+
/* A Mut<…> parameter passed read-only: Anchor's `#[account(mut)]` (ConstraintMut). ts_check_writable is for accounts
|
|
418
|
+
the program makes writable at runtime (remaining accounts, CPI metas). */
|
|
419
|
+
static inline uint64_t ts_check_mut(const TsAccount *a) {
|
|
420
|
+
if (!a->is_writable) TS_FAIL(TS_ERR_CONSTRAINT_MUT, "TypeSOL: account must be writable (mut)");
|
|
421
|
+
return SUCCESS;
|
|
422
|
+
}
|
|
413
423
|
|
|
414
424
|
static inline uint64_t ts_check_program_account(const TsAccount *a, const SolPubkey *program_id,
|
|
415
425
|
const uint8_t disc[8], uint64_t size) {
|
|
@@ -428,7 +438,7 @@ static inline uint64_t ts_check_vec_len(const TsAccount *a, uint64_t fixed, uint
|
|
|
428
438
|
return SUCCESS;
|
|
429
439
|
}
|
|
430
440
|
|
|
431
|
-
/* No other account in the instruction may be the same account (
|
|
441
|
+
/* No other account in the instruction may be the same account (remaining accounts written in place). */
|
|
432
442
|
static inline uint64_t ts_check_unaliased(const TsParams *params, const TsAccount *a) {
|
|
433
443
|
/* The loader passes each account once and marks repeats as duplicates, which share its record. */
|
|
434
444
|
uint64_t seen = 0;
|
|
@@ -437,6 +447,18 @@ static inline uint64_t ts_check_unaliased(const TsParams *params, const TsAccoun
|
|
|
437
447
|
return SUCCESS;
|
|
438
448
|
}
|
|
439
449
|
|
|
450
|
+
/* loadAccountMut: unaliased, and not already loaded mutably in this instruction. Two loads of the same slot would be
|
|
451
|
+
two writable views of one account, as two slots naming it would (read both, write both: the classic drain). */
|
|
452
|
+
static inline uint64_t ts_claim_mut(TsParams *params, const TsAccount *a) {
|
|
453
|
+
uint64_t seen = 0, at = 0;
|
|
454
|
+
for (uint64_t i = 0; i < params->ka_num; i++)
|
|
455
|
+
if (params->ka[i] == a) seen++, at = i;
|
|
456
|
+
if (seen > 1) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: account passed more than once");
|
|
457
|
+
if (params->loaded_mut >> at & 1) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: account already loaded with loadAccountMut");
|
|
458
|
+
params->loaded_mut |= (uint64_t)1 << at;
|
|
459
|
+
return SUCCESS;
|
|
460
|
+
}
|
|
461
|
+
|
|
440
462
|
static inline uint64_t ts_check_address(const TsAccount *a, const SolPubkey *expected) {
|
|
441
463
|
if (!ts_key_eq(&a->key, expected)) TS_FAIL(TS_ERR_WRONG_PROGRAM, "TypeSOL: unexpected program address");
|
|
442
464
|
return SUCCESS;
|
|
@@ -1466,7 +1488,8 @@ static inline int64_t ts_le_i64(const uint8_t *p) {
|
|
|
1466
1488
|
}
|
|
1467
1489
|
|
|
1468
1490
|
/* Like get_price_no_older_than: fully verified, matching feed id, publish_time + max_age >= now. */
|
|
1469
|
-
|
|
1491
|
+
/* expo: the exponent the caller scales by; checked when check_expo (a feed's exponent can change). */
|
|
1492
|
+
static inline uint64_t ts_pyth_price(const TsAccount *a, const SolPubkey *feed, uint64_t max_age, bool check_expo, int32_t expo, int64_t *out) {
|
|
1470
1493
|
uint64_t m;
|
|
1471
1494
|
bool full;
|
|
1472
1495
|
TS_TRY(ts_pyth_message(a, &m, &full));
|
|
@@ -1478,6 +1501,11 @@ static inline uint64_t ts_pyth_price(const TsAccount *a, const SolPubkey *feed,
|
|
|
1478
1501
|
int64_t deadline;
|
|
1479
1502
|
if (__builtin_add_overflow(published, (int64_t)(max_age > INT64_MAX ? INT64_MAX : max_age), &deadline)) deadline = INT64_MAX;
|
|
1480
1503
|
if (deadline < now) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: Pyth price is too old");
|
|
1504
|
+
if (check_expo) {
|
|
1505
|
+
int32_t e;
|
|
1506
|
+
ts_memcpy(&e, a->data + m + 48, 4);
|
|
1507
|
+
if (e != expo) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: Pyth exponent is not the expected one");
|
|
1508
|
+
}
|
|
1481
1509
|
*out = ts_le_i64(a->data + m + 32);
|
|
1482
1510
|
return SUCCESS;
|
|
1483
1511
|
}
|
|
@@ -1503,9 +1531,10 @@ static inline uint64_t ts_check_switchboard(const TsAccount *a, bool devnet) {
|
|
|
1503
1531
|
* The feed's current result (an 18-decimal fixed-point i128) scaled to `decimals`, if it has samples
|
|
1504
1532
|
* and was produced within `max_stale_slots` slots of now.
|
|
1505
1533
|
*/
|
|
1506
|
-
static inline uint64_t ts_switchboard_value(const TsAccount *a, uint64_t max_stale_slots, uint8_t decimals, int64_t *out) {
|
|
1534
|
+
static inline uint64_t ts_switchboard_value(const TsAccount *a, uint64_t max_stale_slots, uint8_t decimals, uint8_t min_samples, int64_t *out) {
|
|
1507
1535
|
if (decimals > 18) TS_FAIL(TS_ERR_ORACLE, "TypeSOL: at most 18 decimals");
|
|
1508
|
-
if (a->data[TS_SB_RESULT_SAMPLES] == 0
|
|
1536
|
+
if (a->data[TS_SB_RESULT_SAMPLES] == 0 || a->data[TS_SB_RESULT_SAMPLES] < min_samples)
|
|
1537
|
+
TS_FAIL(TS_ERR_ORACLE, "TypeSOL: Switchboard feed has too few samples");
|
|
1509
1538
|
uint64_t clock[5] = {0};
|
|
1510
1539
|
if (sol_get_clock_sysvar(clock) != SUCCESS) TS_FAIL(TS_ERR_SYSVAR, "TypeSOL: clock sysvar unavailable");
|
|
1511
1540
|
uint64_t slot = (uint64_t)ts_le_i64(a->data + TS_SB_RESULT_SLOT);
|