@typesol/cli 0.12.0 → 0.13.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 +55 -0
- package/README.md +3 -3
- package/dist/compiler/analyze.js +3 -5
- package/dist/compiler/emit/calls.d.ts +10 -0
- package/dist/compiler/emit/calls.js +112 -4
- package/dist/compiler/emit/cpi.d.ts +8 -0
- package/dist/compiler/emit/cpi.js +79 -4
- package/dist/compiler/emit/function.js +21 -0
- package/dist/compiler/emit/program.js +2 -0
- package/docs/language.md +12 -6
- package/lib/typesol.d.ts +59 -7
- package/package.json +1 -1
- package/runtime/typesol_rt.h +41 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,61 @@ until 1.0, a minor version may change the language, the IDL layout or error code
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.13.0 — 2026-10-27
|
|
9
|
+
|
|
10
|
+
Every program in solana-developers/program-examples now ports to TypeSOL: PDA seeds from account fields, remaining
|
|
11
|
+
accounts passed on in CPIs, transfer hooks with extra accounts, `findProgramAddress` and marker accounts. Not externally
|
|
12
|
+
audited (see SECURITY.md).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Transfer hooks with extra accounts: `initExtraAccountMetas(payer, account, mint, systemProgram, extraAccounts)` writes
|
|
16
|
+
an ExtraAccountMetaList (fixed addresses, PDAs of this program or of another program's account index, with literal,
|
|
17
|
+
account-key, account-data and instruction-data seeds), byte for byte what spl-tlv-account-resolution writes; Token-2022
|
|
18
|
+
checks the extra accounts against it on every transfer. `updateExtraAccountMetas(...)` rewrites and resizes a list.
|
|
19
|
+
- Remaining accounts in a CPI: spread them into `invoke`'s account list with the flags they were passed with
|
|
20
|
+
(`...remainingAccounts()`, Rust's `extend_from_slice`) or all with one role (`...readonly(rest)`,
|
|
21
|
+
`...writable(rest)`), and take part of them with `rest.slice(a, b)`. A CPI lists at most 32 accounts (5113); a slice
|
|
22
|
+
out of range fails with 5104.
|
|
23
|
+
- `findProgramAddress(seeds, program)`: the canonical PDA of another program at runtime.
|
|
24
|
+
- PDA seeds from account fields: `Seeds<…, ["@pool.amm", "@mintA", "authority"]>` takes a Pubkey, integer or unit
|
|
25
|
+
enum field of a struct account parameter, including the account's own (`Seeds<Mut<Offer>, ["offer", "@maker",
|
|
26
|
+
"@offer.id"], "bump">`), like Anchor's `seeds = [pool.amm.as_ref(), …]`. Recorded in the IDL as account paths
|
|
27
|
+
(`pool.amm`).
|
|
28
|
+
- Structs with no fields, for marker accounts that are only a discriminator (Anchor `#[account] pub struct
|
|
29
|
+
NftAuthority {}`).
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- An associated token account created in the instruction (`Ata<Init|InitIfNeeded<TokenAccount>>`) refuses optional
|
|
33
|
+
TokenProgram / AssociatedTokenProgram / SystemProgram parameters at compile time (they used to fail at runtime with an
|
|
34
|
+
access violation when left out), and refuses `Owner<…>` / `Executable<…>` (which could never hold).
|
|
35
|
+
- Unit enum values work as seeds in `create`, `signer([...])`, `findProgramAddress` and the other seed lists, one byte
|
|
36
|
+
each, as they already did in `Seeds<…>`.
|
|
37
|
+
- `rest.slice(a, b)` is typed as something to spread into a CPI only (`RemainingSlice`), with a clear message elsewhere.
|
|
38
|
+
|
|
39
|
+
### Tooling
|
|
40
|
+
- Ports: the five transfer-hook examples with extra accounts (account-data-as-seed, allow-block-list-token, counter,
|
|
41
|
+
transfer-cost, transfer-switch) are ported; their tests transfer through Token-2022, which checks the extra accounts.
|
|
42
|
+
All 46 program-examples are ported now: 9 PORTED, 37 PARTIAL, 0 BLOCKED. The blocked-construct probes
|
|
43
|
+
(test/ports-blocked.test.ts) are gone with the last blocker.
|
|
44
|
+
- Ports: compression/cnft-burn, cnft-vault, cutils and token-2022/transfer-fee, blocked until now, are ported (PARTIAL;
|
|
45
|
+
test/ports/README.md). The compression ports run against Bubblegum, SPL Account Compression and Token Metadata, with
|
|
46
|
+
the leaf hashes, roots and proofs computed in the tests.
|
|
47
|
+
- Ports: the eight remaining program-examples that had not been attempted are ported: basics/repository-layout and
|
|
48
|
+
token-2022 cpi-guard, group, immutable-owner, interest-bearing, memo-transfer, metadata and nft-meta-data-pointer
|
|
49
|
+
(all PARTIAL; test/ports/README.md lists the differences). Every one of the 46 programs is now ported or blocked
|
|
50
|
+
by a named construct.
|
|
51
|
+
- Ports: token-swap is now PORTED (its `amm`, `pool`, authority and liquidity-mint PDAs are all checked
|
|
52
|
+
declaratively, with no bump arguments); escrow and token-fundraiser check their PDAs declaratively instead of with
|
|
53
|
+
`requirePda` in the body.
|
|
54
|
+
|
|
55
|
+
## 0.12.1 — 2026-10-27
|
|
56
|
+
|
|
57
|
+
No change to the compiler, the runtime or the generated code.
|
|
58
|
+
|
|
59
|
+
### Tooling
|
|
60
|
+
- Releases are published to npm through trusted publishing: the release workflow authenticates with GitHub's OIDC
|
|
61
|
+
token, and no npm token is stored in the repository.
|
|
62
|
+
|
|
8
63
|
## 0.12.0 — 2026-10-27
|
|
9
64
|
|
|
10
65
|
Associated token accounts created in the instruction, PDA bumps without arguments, and the first npm release, as
|
package/README.md
CHANGED
|
@@ -183,13 +183,13 @@ Runtime failures point back to your source: `Error 6000 Unauthorized (counter.ts
|
|
|
183
183
|
| | |
|
|
184
184
|
| --- | --- |
|
|
185
185
|
| **Accounts** | `Signer`, `Account<T>`, `Mut<T>`, `Init<T>`, `InitIfNeeded<T>` (PDAs and associated token accounts), `Zeroed<T>` (up to 10 MiB), optional accounts (`A \| null`), token accounts and mints, programs, sysvars |
|
|
186
|
-
| **Constraints** | `HasOne`, `Seeds` (also under another program; `pdaBump` like `ctx.bumps`), `CloseTo`, `Ata`, `Address`, `Owner`, `Executable`. They are checked before the body runs and recorded in the IDL |
|
|
186
|
+
| **Constraints** | `HasOne`, `Seeds` (also from account fields and under another program; `pdaBump` like `ctx.bumps`), `CloseTo`, `Ata`, `Address`, `Owner`, `Executable`. They are checked before the body runs and recorded in the IDL |
|
|
187
187
|
| **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account |
|
|
188
188
|
| **Control flow** | `if`, `for`, `while`, `switch` (with narrowing on data enums), ternaries, helper functions across multiple files |
|
|
189
189
|
| **CPIs** | System, SPL Token, Token-2022 (including extension reads and metadata), ATA, Metaplex Token Metadata, Core, Bubblegum. Generic `invoke` with PDA signing, return data, and typed bindings generated from any Anchor IDL (`typesol cpi`) |
|
|
190
190
|
| **Events** | `emit` (log-based) and `emitCpi` (Anchor `emit_cpi!`, through a self-CPI) |
|
|
191
191
|
| **Crypto & oracles** | `sha256`, `keccak256`, Merkle proofs, Ed25519 and secp256k1 signature checks, Pyth pull oracle, Switchboard |
|
|
192
|
-
| **Interfaces** | custom discriminators (`@discriminator`), so a TypeSOL program can be a Token-2022 transfer hook |
|
|
192
|
+
| **Interfaces** | custom discriminators (`@discriminator`), so a TypeSOL program can be a Token-2022 transfer hook, with extra accounts (ExtraAccountMetaList) |
|
|
193
193
|
|
|
194
194
|
The full reference, with every type, check and built-in, is in [docs/language.md](docs/language.md).
|
|
195
195
|
|
|
@@ -275,7 +275,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
275
275
|
|
|
276
276
|
## Status
|
|
277
277
|
|
|
278
|
-
**TypeSOL is pre-1.0 (0.
|
|
278
|
+
**TypeSOL is pre-1.0 (0.13).** Releases follow the organization's
|
|
279
279
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
280
280
|
change.
|
|
281
281
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -657,8 +657,6 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
657
657
|
reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size)");
|
|
658
658
|
fields.push({ name, type });
|
|
659
659
|
}
|
|
660
|
-
if (fields.length === 0)
|
|
661
|
-
reporter.fail(decl, "structs need at least one field");
|
|
662
660
|
checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
|
|
663
661
|
const name = decl.name.text;
|
|
664
662
|
structs.set(name, {
|
|
@@ -923,8 +921,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
923
921
|
const c = a.constraints;
|
|
924
922
|
if (a.kind.kind === "token" && a.kind.init && !c.ata)
|
|
925
923
|
reporter.fail(a.node, 'Init<TokenAccount> / InitIfNeeded<TokenAccount> create associated token accounts: wrap them in Ata<…, "owner", "mint">');
|
|
926
|
-
if (a.kind.kind === "token" && a.kind.init && (c.hasOne.length || c.seeds || c.address))
|
|
927
|
-
reporter.fail(a.node, "an associated token account created here is checked against Ata<…>'s owner and mint; drop HasOne/Seeds/Address");
|
|
924
|
+
if (a.kind.kind === "token" && a.kind.init && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
|
|
925
|
+
reporter.fail(a.node, "an associated token account created here is checked against Ata<…>'s owner and mint; drop HasOne/Seeds/Address/Owner/Executable");
|
|
928
926
|
if (!c.hasOne.length && !c.seeds && !c.closeTo && !c.ata)
|
|
929
927
|
continue;
|
|
930
928
|
const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
|
|
@@ -981,7 +979,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
981
979
|
const c = a.constraints;
|
|
982
980
|
const refs = [
|
|
983
981
|
...c.hasOne,
|
|
984
|
-
...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1)),
|
|
982
|
+
...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1).split(".")[0]),
|
|
985
983
|
...(c.seeds?.program?.startsWith("@") ? [c.seeds.program.slice(1)] : []),
|
|
986
984
|
...(c.closeTo ? [c.closeTo] : []),
|
|
987
985
|
...(c.ata
|
|
@@ -10,6 +10,16 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
|
10
10
|
protected vecMethod(node: ts.CallExpression, method: string, v: Ref): Value | undefined;
|
|
11
11
|
/** sha256(...) / keccak256(...) over strings, Pubkeys, Hashes, integers (LE) and booleans. */
|
|
12
12
|
protected hashCall(node: ts.CallExpression, fn: "sha256" | "keccak256"): Value;
|
|
13
|
+
/**
|
|
14
|
+
* The extra accounts of a transfer hook, as ExtraAccountMeta records (35 bytes each): `{ address, isSigner?, isWritable? }`
|
|
15
|
+
* for a fixed address, `{ seeds: [...], program?, isSigner?, isWritable? }` for a PDA of this program (or, with
|
|
16
|
+
* `program: index`, of the program at that account index). Seeds: string literals, seedAccountKey(i),
|
|
17
|
+
* seedAccountData(i, offset, length), seedInstructionData(offset, length); packed into 32 bytes.
|
|
18
|
+
*/
|
|
19
|
+
protected extraAccountMetas(node: ts.Expression): {
|
|
20
|
+
name: string;
|
|
21
|
+
count: number;
|
|
22
|
+
};
|
|
13
23
|
/** `remainingAccounts()` or a const bound to it. */
|
|
14
24
|
protected isRemaining(node: ts.Expression): boolean;
|
|
15
25
|
/** C pointer (`TsAccount *`) for an expression naming a remaining account, or undefined. */
|
|
@@ -352,6 +352,18 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
352
352
|
this.line(`TS_TRY(ts_ata_address(${o.c}, ${m.c}, ${tp.c}, &${t}));`);
|
|
353
353
|
return { c: `(&${t})`, type: PUBKEY };
|
|
354
354
|
}
|
|
355
|
+
case "findProgramAddress": {
|
|
356
|
+
// Pubkey::find_program_address(seeds, program).0: the canonical PDA of any program.
|
|
357
|
+
if (args.length !== 2)
|
|
358
|
+
this.fail(node, "findProgramAddress(seeds, program) takes the seeds and the program's address");
|
|
359
|
+
const seeds = this.seeds(args[0], 0);
|
|
360
|
+
const program = this.expect(this.expr(args[1], PUBKEY), PUBKEY, args[1]);
|
|
361
|
+
const t = this.tmp("pda");
|
|
362
|
+
const b = this.tmp("bump");
|
|
363
|
+
this.line(`SolPubkey ${t}; uint8_t ${b};`);
|
|
364
|
+
this.line(`if (sol_try_find_program_address(${seeds.name}, ${seeds.count}, ${program.c}, &${t}, &${b}) != SUCCESS) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: no program address for these seeds");`);
|
|
365
|
+
return { c: `(&${t})`, type: PUBKEY };
|
|
366
|
+
}
|
|
355
367
|
case "metadataAddress": {
|
|
356
368
|
if (args.length !== 1)
|
|
357
369
|
this.fail(node, "metadataAddress(mint) takes the mint");
|
|
@@ -361,9 +373,10 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
361
373
|
this.line(`TS_TRY(ts_metadata_address(${m.c}, &${t}));`);
|
|
362
374
|
return { c: `(&${t})`, type: PUBKEY };
|
|
363
375
|
}
|
|
364
|
-
case "initExtraAccountMetas":
|
|
365
|
-
|
|
366
|
-
|
|
376
|
+
case "initExtraAccountMetas":
|
|
377
|
+
case "updateExtraAccountMetas": {
|
|
378
|
+
if (args.length !== 4 && args.length !== 5)
|
|
379
|
+
this.fail(node, `${builtin}(payer, account, mint, systemProgram, extraAccounts?)`);
|
|
367
380
|
const payer = this.accountParam(args[0], "the payer");
|
|
368
381
|
if (payer.kind.kind !== "signer" || !payer.kind.mut)
|
|
369
382
|
this.fail(args[0], "the payer must be a Mut<Signer>");
|
|
@@ -374,7 +387,9 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
374
387
|
const sys = this.accountParam(args[3], "the system program");
|
|
375
388
|
if (sys.kind.kind !== "system")
|
|
376
389
|
this.fail(args[3], "pass the SystemProgram parameter");
|
|
377
|
-
this.
|
|
390
|
+
const { name: entries, count } = args[4] ? this.extraAccountMetas(args[4]) : { name: "NULL", count: 0 };
|
|
391
|
+
const fn = builtin === "initExtraAccountMetas" ? "ts_init_extra_metas" : "ts_update_extra_metas";
|
|
392
|
+
this.line(`TS_TRY(${fn}(params, a_${payer.name}, a_${acct.name}, &a_${mint.name}->key, ${entries}, ${count}));`);
|
|
378
393
|
return undefined;
|
|
379
394
|
}
|
|
380
395
|
case "remainingAccounts":
|
|
@@ -513,6 +528,8 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
513
528
|
}
|
|
514
529
|
if (ts.isPropertyAccessExpression(callee) && ts.isCallExpression(callee.expression) && this.exportName(callee.expression.expression) === "fixedArray")
|
|
515
530
|
this.fail(callee.expression, "fixedArray<T, N>(fill) is only allowed as a local declaration: const xs = fixedArray<u64, 4>(0n)");
|
|
531
|
+
if (ts.isPropertyAccessExpression(callee) && callee.name.text === "slice" && this.isRemaining(callee.expression))
|
|
532
|
+
this.fail(node, "rest.slice(a, b) is only for spreading into a CPI's accounts: invoke(p, [..., ...readonly(rest.slice(a, b))], data)");
|
|
516
533
|
this.fail(node, "unsupported call");
|
|
517
534
|
}
|
|
518
535
|
vecMethod(node, method, v) {
|
|
@@ -658,6 +675,97 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
658
675
|
this.line(`sol_${fn}(${arr}, ${parts.length}, (uint8_t *)&${out});`);
|
|
659
676
|
return { c: `(&${out})`, type: HASH };
|
|
660
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* The extra accounts of a transfer hook, as ExtraAccountMeta records (35 bytes each): `{ address, isSigner?, isWritable? }`
|
|
680
|
+
* for a fixed address, `{ seeds: [...], program?, isSigner?, isWritable? }` for a PDA of this program (or, with
|
|
681
|
+
* `program: index`, of the program at that account index). Seeds: string literals, seedAccountKey(i),
|
|
682
|
+
* seedAccountData(i, offset, length), seedInstructionData(offset, length); packed into 32 bytes.
|
|
683
|
+
*/
|
|
684
|
+
extraAccountMetas(node) {
|
|
685
|
+
if (!ts.isArrayLiteralExpression(node))
|
|
686
|
+
this.fail(node, "the extra accounts must be an array literal of { address } / { seeds } objects");
|
|
687
|
+
const bytes = [];
|
|
688
|
+
const fixups = [];
|
|
689
|
+
const small = (e, what, max = 255) => {
|
|
690
|
+
if (!ts.isBigIntLiteral(e))
|
|
691
|
+
this.fail(e, `${what} must be a bigint literal`);
|
|
692
|
+
const v = Number(BigInt(e.text.replace(/n$/, "")));
|
|
693
|
+
if (v < 0 || v > max)
|
|
694
|
+
this.fail(e, `${what} must be 0..${max}`);
|
|
695
|
+
return v;
|
|
696
|
+
};
|
|
697
|
+
for (const el of node.elements) {
|
|
698
|
+
if (!ts.isObjectLiteralExpression(el))
|
|
699
|
+
this.fail(el, "each extra account is { address, … } or { seeds: [...], … }");
|
|
700
|
+
const props = new Map();
|
|
701
|
+
for (const p of el.properties) {
|
|
702
|
+
if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name))
|
|
703
|
+
this.fail(p, "use plain properties: address, seeds, program, isSigner, isWritable");
|
|
704
|
+
if (!["address", "seeds", "program", "isSigner", "isWritable"].includes(p.name.text))
|
|
705
|
+
this.fail(p.name, `unknown property "${p.name.text}"`);
|
|
706
|
+
props.set(p.name.text, p.initializer);
|
|
707
|
+
}
|
|
708
|
+
const flag = (k) => {
|
|
709
|
+
const v = props.get(k);
|
|
710
|
+
if (!v)
|
|
711
|
+
return 0;
|
|
712
|
+
if (v.kind === ts.SyntaxKind.TrueKeyword)
|
|
713
|
+
return 1;
|
|
714
|
+
if (v.kind === ts.SyntaxKind.FalseKeyword)
|
|
715
|
+
return 0;
|
|
716
|
+
this.fail(v, `${k} must be true or false`);
|
|
717
|
+
};
|
|
718
|
+
const start = bytes.length;
|
|
719
|
+
const address = props.get("address");
|
|
720
|
+
const seeds = props.get("seeds");
|
|
721
|
+
if (!!address === !!seeds)
|
|
722
|
+
this.fail(el, "give either address or seeds");
|
|
723
|
+
if (address) {
|
|
724
|
+
if (props.has("program"))
|
|
725
|
+
this.fail(props.get("program"), "program goes with seeds, not with address");
|
|
726
|
+
const v = this.expect(this.expr(address, PUBKEY), PUBKEY, address);
|
|
727
|
+
bytes.push(0, ...new Array(32).fill(0));
|
|
728
|
+
fixups.push({ offset: start + 1, c: v.c });
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
if (!ts.isArrayLiteralExpression(seeds))
|
|
732
|
+
this.fail(seeds, "seeds must be an array literal");
|
|
733
|
+
const packed = [];
|
|
734
|
+
for (const s of seeds.elements) {
|
|
735
|
+
if (ts.isStringLiteral(s)) {
|
|
736
|
+
const b = [...Buffer.from(s.text, "utf8")];
|
|
737
|
+
if (b.length > 32)
|
|
738
|
+
this.fail(s, "a literal seed can be at most 32 bytes");
|
|
739
|
+
packed.push(1, b.length, ...b);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const fn = ts.isCallExpression(s) ? this.exportName(s.expression) : undefined;
|
|
743
|
+
const a = ts.isCallExpression(s) ? s.arguments : [];
|
|
744
|
+
if (fn === "seedInstructionData" && a.length === 2)
|
|
745
|
+
packed.push(2, small(a[0], "the data index"), small(a[1], "the length"));
|
|
746
|
+
else if (fn === "seedAccountKey" && a.length === 1)
|
|
747
|
+
packed.push(3, small(a[0], "the account index"));
|
|
748
|
+
else if (fn === "seedAccountData" && a.length === 3)
|
|
749
|
+
packed.push(4, small(a[0], "the account index"), small(a[1], "the data index"), small(a[2], "the length"));
|
|
750
|
+
else
|
|
751
|
+
this.fail(s, "a seed is a string literal, seedAccountKey(i), seedAccountData(i, offset, length) or seedInstructionData(offset, length)");
|
|
752
|
+
}
|
|
753
|
+
if (packed.length > 32)
|
|
754
|
+
this.fail(seeds, `the packed seeds take ${packed.length} bytes; at most 32 fit`);
|
|
755
|
+
const program = props.get("program");
|
|
756
|
+
bytes.push(program ? 128 + small(program, "program (an account index)", 127) : 1, ...packed, ...new Array(32 - packed.length).fill(0));
|
|
757
|
+
}
|
|
758
|
+
bytes.push(flag("isSigner"), flag("isWritable"));
|
|
759
|
+
}
|
|
760
|
+
const count = node.elements.length;
|
|
761
|
+
if (!count)
|
|
762
|
+
return { name: "NULL", count: 0 };
|
|
763
|
+
const name = this.tmp("extra_metas");
|
|
764
|
+
this.line(`${fixups.length ? "" : "static const "}uint8_t ${name}[${bytes.length}] = ${cBytes(bytes)};`);
|
|
765
|
+
for (const f of fixups)
|
|
766
|
+
this.line(`ts_memcpy(${name} + ${f.offset}, ${f.c}, 32);`);
|
|
767
|
+
return { name, count };
|
|
768
|
+
}
|
|
661
769
|
/** `remainingAccounts()` or a const bound to it. */
|
|
662
770
|
isRemaining(node) {
|
|
663
771
|
if (ts.isIdentifier(node))
|
|
@@ -66,6 +66,14 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
66
66
|
protected coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
|
|
67
67
|
/** Bubblegum (compressed NFTs on SPL account compression): createTree, mint (MintV1, no collection, no creators). */
|
|
68
68
|
protected bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
|
|
69
|
+
/**
|
|
70
|
+
* `remainingAccounts()` (or a const bound to it): every remaining account; `rest.slice(a, b?)`: indexes a..b of them
|
|
71
|
+
* (both checked against the count, 5104). C expressions for the range, relative to the first remaining account.
|
|
72
|
+
*/
|
|
73
|
+
protected remainingRange(node: ts.Expression): {
|
|
74
|
+
from: string;
|
|
75
|
+
to: string;
|
|
76
|
+
} | undefined;
|
|
69
77
|
protected invoke(node: ts.CallExpression): undefined;
|
|
70
78
|
/**
|
|
71
79
|
* A byte buffer written front to back. Its C declaration is patched in afterwards so it is
|
|
@@ -63,8 +63,14 @@ export class CpiEmitter extends CallEmitter {
|
|
|
63
63
|
this.line(`${info.c} ${t} = ${typed.c};`);
|
|
64
64
|
parts.push(`{(const uint8_t *)&${t}, ${info.bytes}}`);
|
|
65
65
|
}
|
|
66
|
+
else if (v.type.kind === "enum") {
|
|
67
|
+
// A unit enum value is its variant index, one byte (as in Seeds<…> and Borsh).
|
|
68
|
+
const t = this.tmp("seed");
|
|
69
|
+
this.line(`uint8_t ${t} = (uint8_t)(${v.c});`);
|
|
70
|
+
parts.push(`{(const uint8_t *)&${t}, 1}`);
|
|
71
|
+
}
|
|
66
72
|
else
|
|
67
|
-
this.fail(el, "seeds can be strings, Pubkeys or
|
|
73
|
+
this.fail(el, "seeds can be strings, Pubkeys, integers or unit enum values");
|
|
68
74
|
}
|
|
69
75
|
const name = this.tmp("seeds");
|
|
70
76
|
const slots = parts.length + extra;
|
|
@@ -168,6 +174,8 @@ export class CpiEmitter extends CallEmitter {
|
|
|
168
174
|
const p = this.ix.accounts.find((o) => o.kind.kind === kind);
|
|
169
175
|
if (!p)
|
|
170
176
|
this.fail(node, `${call}(...) needs ${what} parameter`);
|
|
177
|
+
if (p.optional)
|
|
178
|
+
this.fail(p.node, `${call}(...) needs "${p.name}": it cannot be optional`);
|
|
171
179
|
return p;
|
|
172
180
|
};
|
|
173
181
|
const ataProgram = need("ataProgram", "an AssociatedTokenProgram");
|
|
@@ -647,6 +655,27 @@ export class CpiEmitter extends CallEmitter {
|
|
|
647
655
|
}
|
|
648
656
|
this.fail(node.expression, `Bubblegum supports createTree and mint, not ${method}`);
|
|
649
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* `remainingAccounts()` (or a const bound to it): every remaining account; `rest.slice(a, b?)`: indexes a..b of them
|
|
660
|
+
* (both checked against the count, 5104). C expressions for the range, relative to the first remaining account.
|
|
661
|
+
*/
|
|
662
|
+
remainingRange(node) {
|
|
663
|
+
const n = this.ix.accounts.length;
|
|
664
|
+
const count = `(params->ka_num - ${n})`;
|
|
665
|
+
if (this.isRemaining(node))
|
|
666
|
+
return { from: "0", to: count };
|
|
667
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "slice")
|
|
668
|
+
return undefined;
|
|
669
|
+
if (!this.isRemaining(node.expression.expression))
|
|
670
|
+
return undefined;
|
|
671
|
+
if (node.arguments.length < 1 || node.arguments.length > 2)
|
|
672
|
+
this.fail(node, "slice(start, end?) takes one or two indexes");
|
|
673
|
+
// Both ends may equal the count (an empty slice at the end).
|
|
674
|
+
const from = this.index(node.arguments[0], `${count} + 1`);
|
|
675
|
+
const to = node.arguments[1] ? this.index(node.arguments[1], `${count} + 1`) : count;
|
|
676
|
+
this.line(`if (${from} > ${to}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: remaining accounts slice out of range");`);
|
|
677
|
+
return { from, to };
|
|
678
|
+
}
|
|
650
679
|
invoke(node) {
|
|
651
680
|
const a = node.arguments;
|
|
652
681
|
if (a.length !== 3 && a.length !== 4)
|
|
@@ -665,11 +694,26 @@ export class CpiEmitter extends CallEmitter {
|
|
|
665
694
|
programKey = `&a_${program.name}->key`;
|
|
666
695
|
}
|
|
667
696
|
const hasSeeds = a.length === 4;
|
|
668
|
-
// Accounts: readonly(x) / writable(x) / readonlySigner(x) / writableSigner(x)
|
|
697
|
+
// Accounts: readonly(x) / writable(x) / readonlySigner(x) / writableSigner(x), and the remaining accounts spread in:
|
|
698
|
+
// `...remainingAccounts()` (each with the flags it was passed with, Rust's `extend_from_slice`) or
|
|
699
|
+
// `...readonly(rest)` / `...writable(rest)` (all with that role).
|
|
669
700
|
if (!ts.isArrayLiteralExpression(a[1]))
|
|
670
701
|
this.fail(a[1], "accounts must be an array literal");
|
|
671
702
|
const metas = [];
|
|
703
|
+
const parts = [];
|
|
672
704
|
for (const el of a[1].elements) {
|
|
705
|
+
if (ts.isSpreadElement(el)) {
|
|
706
|
+
if (!this.ix)
|
|
707
|
+
this.fail(el, "remainingAccounts() is only available in instructions");
|
|
708
|
+
const inner = el.expression;
|
|
709
|
+
const fnName = ts.isCallExpression(inner) ? this.exportName(inner.expression) : undefined;
|
|
710
|
+
const wrapped = ts.isCallExpression(inner) && (fnName === "readonly" || fnName === "writable") && inner.arguments.length === 1;
|
|
711
|
+
const range = this.remainingRange(wrapped ? inner.arguments[0] : inner);
|
|
712
|
+
if (!range)
|
|
713
|
+
this.fail(el, "spread the remaining accounts: ...remainingAccounts(), ...readonly(rest), ...writable(rest) or rest.slice(a, b)");
|
|
714
|
+
parts.push({ kind: "spread", mode: wrapped ? fnName : "asPassed", ...range });
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
673
717
|
const fn = ts.isCallExpression(el) ? this.exportName(el.expression) : undefined;
|
|
674
718
|
if (!ts.isCallExpression(el) || !fn || !["readonly", "writable", "readonlySigner", "writableSigner"].includes(fn) || el.arguments.length !== 1)
|
|
675
719
|
this.fail(el, "each account must be readonly(x), writable(x), readonlySigner(x) or writableSigner(x)");
|
|
@@ -683,6 +727,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
683
727
|
if (isS && !hasSeeds)
|
|
684
728
|
this.line(`TS_TRY(ts_check_signer(${h}));`);
|
|
685
729
|
metas.push(`{&${h}->key, ${isW}, ${isS}}`);
|
|
730
|
+
parts.push({ kind: "meta", c: metas[metas.length - 1] });
|
|
686
731
|
continue;
|
|
687
732
|
}
|
|
688
733
|
const acct = this.accountParam(el.arguments[0], `${fn}()'s argument`);
|
|
@@ -691,6 +736,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
691
736
|
if (isS && acct.kind.kind !== "signer" && !hasSeeds)
|
|
692
737
|
this.fail(el, `"${acct.name}" did not sign; pass signerSeeds to sign for a PDA, or declare it Signer`);
|
|
693
738
|
metas.push(`{&a_${acct.name}->key, ${isW}, ${isS}}`);
|
|
739
|
+
parts.push({ kind: "meta", c: metas[metas.length - 1] });
|
|
694
740
|
}
|
|
695
741
|
// The CPI's account list is built in a buffer with room for one entry per instruction account.
|
|
696
742
|
if (metas.length > MAX_ACCOUNTS)
|
|
@@ -746,9 +792,38 @@ export class CpiEmitter extends CallEmitter {
|
|
|
746
792
|
finish(a[2]);
|
|
747
793
|
const seeds = hasSeeds ? this.signingSeeds(a[3]) : { name: "NULL", count: 0 };
|
|
748
794
|
const metasName = this.tmp("metas");
|
|
749
|
-
|
|
795
|
+
let count = String(metas.length);
|
|
796
|
+
if (parts.some((p) => p.kind === "spread")) {
|
|
797
|
+
// Room for every account of the instruction (ts_invoke's scratch holds TS_MAX_ACCOUNTS records).
|
|
798
|
+
const n = this.ix.accounts.length;
|
|
799
|
+
const cnt = this.tmp("nmetas");
|
|
800
|
+
this.line(`SolAccountMeta ${metasName}[TS_MAX_ACCOUNTS];`);
|
|
801
|
+
this.line(`uint64_t ${cnt} = 0;`);
|
|
802
|
+
const spreads = parts.filter((p) => p.kind === "spread");
|
|
803
|
+
const total = [String(metas.length), ...spreads.map((p) => `(${p.to} - ${p.from})`)].join(" + ");
|
|
804
|
+
this.line(`if (${total} > TS_MAX_ACCOUNTS) TS_FAIL(TS_ERR_TOO_MANY_ACCOUNTS, "TypeSOL: the CPI lists more than ${MAX_ACCOUNTS} accounts");`);
|
|
805
|
+
for (const p of parts) {
|
|
806
|
+
if (p.kind === "meta") {
|
|
807
|
+
this.line(`${metasName}[${cnt}++] = (SolAccountMeta)${p.c};`);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const i = this.tmp("ri");
|
|
811
|
+
const r = this.tmp("racct");
|
|
812
|
+
this.line(`for (uint64_t ${i} = ${n} + ${p.from}; ${i} < ${n} + ${p.to}; ${i}++) {`);
|
|
813
|
+
this.line(` TsAccount *${r} = params->ka[${i}];`);
|
|
814
|
+
if (p.mode === "writable")
|
|
815
|
+
this.line(` TS_TRY(ts_check_writable(${r}));`);
|
|
816
|
+
const w = p.mode === "asPassed" ? `${r}->is_writable` : p.mode === "writable" ? "1" : "0";
|
|
817
|
+
const sg = p.mode === "asPassed" ? `${r}->is_signer` : "0";
|
|
818
|
+
this.line(` ${metasName}[${cnt}++] = (SolAccountMeta){&${r}->key, ${w}, ${sg}};`);
|
|
819
|
+
this.line(`}`);
|
|
820
|
+
}
|
|
821
|
+
count = cnt;
|
|
822
|
+
}
|
|
823
|
+
else
|
|
824
|
+
this.line(`SolAccountMeta ${metasName}[${Math.max(metas.length, 1)}] = {${metas.join(", ")}};`);
|
|
750
825
|
const ixName = this.tmp("cpi");
|
|
751
|
-
this.line(`SolInstruction ${ixName} = {${programKey}, ${metasName}, ${
|
|
826
|
+
this.line(`SolInstruction ${ixName} = {${programKey}, ${metasName}, ${count}, ${buf}, ${len}};`);
|
|
752
827
|
if (hasSeeds) {
|
|
753
828
|
const signer = this.tmp("signer");
|
|
754
829
|
this.line(`SolSignerSeeds ${signer} = {${seeds.name}, ${seeds.count}};`);
|
|
@@ -368,6 +368,27 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
368
368
|
// "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
|
|
369
369
|
const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
|
|
370
370
|
const n = ref ?? raw;
|
|
371
|
+
// "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
|
|
372
|
+
// `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
|
|
373
|
+
if (ref?.includes(".")) {
|
|
374
|
+
const [owner, field, ...rest] = ref.split(".");
|
|
375
|
+
const o = ix.accounts.find((x) => x.name === owner);
|
|
376
|
+
if (!o || o.kind.kind !== "account" || rest.length)
|
|
377
|
+
this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
|
|
378
|
+
const st = o.kind.struct;
|
|
379
|
+
const f = st.fields.find((x) => x.name === field);
|
|
380
|
+
if (!f)
|
|
381
|
+
this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
|
|
382
|
+
const t = f.type;
|
|
383
|
+
if (t.kind === "pubkey")
|
|
384
|
+
parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
|
|
385
|
+
else if (t.kind === "int" || t.kind === "enum")
|
|
386
|
+
parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
|
|
387
|
+
else
|
|
388
|
+
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)`);
|
|
389
|
+
idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
371
392
|
const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
|
|
372
393
|
const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
|
|
373
394
|
if (ref && !acct && !arg)
|
|
@@ -82,6 +82,8 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
82
82
|
out.push(`typedef struct __attribute__((packed)) {`);
|
|
83
83
|
for (const f of s.fields)
|
|
84
84
|
out.push(` ${storageDecl(f.type, f.name)};`);
|
|
85
|
+
if (!s.fields.length)
|
|
86
|
+
out.push(` uint8_t __empty[0];`); // a marker account (Anchor `pub struct NftAuthority {}`): discriminator only
|
|
85
87
|
out.push(`} TSS_${s.name};`);
|
|
86
88
|
out.push(`_Static_assert(sizeof(TSS_${s.name}) == ${s.size}, "TSS_${s.name} layout");`);
|
|
87
89
|
if (s.vec)
|
package/docs/language.md
CHANGED
|
@@ -25,7 +25,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
25
25
|
| Options | `best: Option<u64>` (or `u64 \| null`) — `null`, `=== null`, TypeScript narrowing; `===` between options or with a value follows JS (null equals null, empty never equals a value); empty use aborts (5108); `${opt}` prints the value or `null`. Borsh `option` in arguments/events, fixed `{ is_some, value }` in account data |
|
|
26
26
|
| Vecs | `entries: Vec<Entry>` as the last account field — `get`, `set`, `push(v, payer)`, `pop()`, `clear()`, `.length`; the account is resized (realloc) and rent topped up by `payer`; `Vec<u64>` arguments are read-only views; `pop()` of a Vec of structs works as a statement |
|
|
27
27
|
| Struct arguments | `order: Order`, `ids: FixedArray<u8, 4>`, `orders: Vec<Order>` — fixed layouts, Borsh-identical, validated on entry (bools, enums, option tags, string lengths, enum tags; 102) and read-only |
|
|
28
|
-
| Remaining accounts | `remainingAccounts()` — `.length`, `.get(i)` → `key`, `owner`, `lamports` (writes check writability), `isSigner`, `isWritable`; `loadAccount<T>` / `loadAccountMut<T>` give checked struct views; pass them to `invoke` |
|
|
28
|
+
| Remaining accounts | `remainingAccounts()` — `.length`, `.get(i)` → `key`, `owner`, `lamports` (writes check writability), `isSigner`, `isWritable`; `loadAccount<T>` / `loadAccountMut<T>` give checked struct views; pass them to `invoke`, one at a time or spread: `...remainingAccounts()` (each with the flags it was passed with), `...readonly(rest)` / `...writable(rest)` (all with that role), `rest.slice(a, b)` for part of them; a CPI lists at most 32 accounts (5113) |
|
|
29
29
|
| Struct literals | `book.entries.push({ who: who.key, amount, status: Status.Pending, note }, who)` — every field required |
|
|
30
30
|
| Struct references | `const o = side === Side.Bid ? book.bids.get(i) : book.asks.get(i)` — aliases account data like a JS object reference (no copy); replacing the referenced place (`book.bids.set(i, …)`, `pop()`, `clear()`, `acct.p = …`) while the reference is still used afterwards is a compile error, because JS would keep the old object |
|
|
31
31
|
| Other programs' accounts | `const pool = loadForeign<Pool>(account, AMM_PROGRAM)` — owner, `account:Pool` discriminator and size checked (3007 / 3002 / 3003); `readData<u64>(account, offset)` for raw little-endian reads (bounds-checked, 3003) |
|
|
@@ -35,7 +35,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
35
35
|
| Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
|
|
36
36
|
| Strings at runtime | `"hello, " + name`, `` `#${n}` `` — built on the 32 KiB heap (5110 when full) |
|
|
37
37
|
| `switch` | on integers (up to 64 bits), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
|
|
38
|
-
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `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) |
|
|
38
|
+
| 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) |
|
|
39
39
|
| Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds)` (fully verified, feed-matched, fresh); raw `update.price / conf / exponent / publishTime`; `switchboardValue(feed: SwitchboardFeed, expectedFeed, maxStaleSlots, decimals)` (the feed must be the expected one: anyone can create feeds) (mainnet program unless `SwitchboardFeed<"devnet">`) |
|
|
40
40
|
| 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 |
|
|
41
41
|
| Loops | `for (let i = 0n; i < n; i++)`, `while`, `break`, `continue` — bounded by the compute budget |
|
|
@@ -59,7 +59,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
59
59
|
| `Account<T>` / `Mut<T>` | owned by this program, 8-byte discriminator matches, data large enough (and writable) |
|
|
60
60
|
| `Init<T>` | writable; `create(payer, seeds)` derives the PDA, verifies it, funds rent, writes the discriminator (up to 10 KiB) |
|
|
61
61
|
| `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 |
|
|
62
|
-
| `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, 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 |
|
|
62
|
+
| `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 |
|
|
63
63
|
| `HasOne<TokenAccount, "mint" \| "owner">`, `HasOne<Mint, "mintAuthority" \| "freezeAuthority">` | token constraints (2014 / 2015 / 2016 / 2017) against the parameter with that name |
|
|
64
64
|
| `Ata<TokenAccount, "owner", "mint">` | the account is the associated token account of those parameters (2009; ATA `pda` in the IDL) |
|
|
65
65
|
| `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 |
|
|
@@ -102,8 +102,14 @@ account written through `.lamports` or `loadAccountMut` is another account of th
|
|
|
102
102
|
|
|
103
103
|
`/** @discriminator spl-transfer-hook-interface:execute */` above an instruction replaces its Anchor discriminator with
|
|
104
104
|
sha256(preimage)[..8] (or `@discriminator 0x…` for raw bytes), so a TypeSOL program can be a Token-2022 transfer hook:
|
|
105
|
-
`execute(source, mint, destination, owner, extraMetas, amount)` plus
|
|
106
|
-
for the validation account
|
|
105
|
+
`execute(source, mint, destination, owner, extraMetas, ...extraAccounts, amount)` plus
|
|
106
|
+
`initExtraAccountMetas(payer, account, mint, systemProgram, extraAccounts?)` for the validation account (an
|
|
107
|
+
ExtraAccountMetaList). Extra accounts are listed as `{ address, isSigner?, isWritable? }` (a fixed address) or
|
|
108
|
+
`{ seeds: [...], program?, isSigner?, isWritable? }` (a PDA of this program, or with `program: i` of the program at account
|
|
109
|
+
index i), with seeds from string literals, `seedAccountKey(i)`, `seedAccountData(i, offset, length)` and
|
|
110
|
+
`seedInstructionData(offset, length)`; Token-2022 resolves and checks them on every transfer. The first extra account has
|
|
111
|
+
index 5. `updateExtraAccountMetas(...)` rewrites and resizes an existing list. See `test/fixtures/hook.ts` and the
|
|
112
|
+
transfer-hook ports.
|
|
107
113
|
|
|
108
114
|
## Calling other programs
|
|
109
115
|
|
|
@@ -140,7 +146,7 @@ an integer literal, an argument, `acct.key` or a scalar field of an account, the
|
|
|
140
146
|
|
|
141
147
|
Vecs anywhere but the last account field, growing an account by more than 10 KiB in one instruction, growing a Vec
|
|
142
148
|
inside a helper, data-enum arguments carrying strings/structs, Pubkeys in templates, `?.` / `!` / `**`, `switch` on
|
|
143
|
-
128-bit integers, `mulDiv` beyond u64,
|
|
149
|
+
128-bit integers, `mulDiv` beyond u64, Token Metadata creators/collections, Core plugins, Bubblegum v2 / collections /
|
|
144
150
|
transfers, floating point (by design), `do … while`.
|
|
145
151
|
They are rejected at compile time rather than half-supported.
|
|
146
152
|
|
package/lib/typesol.d.ts
CHANGED
|
@@ -84,10 +84,20 @@ export interface RemainingAccount {
|
|
|
84
84
|
readonly isWritable: boolean;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The accounts a client passed beyond the instruction's declared parameters. Spread them into a CPI's account list:
|
|
89
|
+
* `...remainingAccounts()` passes each with the flags it was passed with (Rust's `extend_from_slice`),
|
|
90
|
+
* `...readonly(rest)` / `...writable(rest)` pass them all with that role (a cNFT proof, a harvest's sources).
|
|
91
|
+
*/
|
|
92
|
+
export interface RemainingAccounts extends Iterable<CpiAccount> {
|
|
89
93
|
readonly length: u64;
|
|
90
94
|
get(index: bigint): RemainingAccount;
|
|
95
|
+
/** Indexes start..end (end: all) of them, only to spread into a CPI: `...readonly(rest.slice(0n, proofLength))`. */
|
|
96
|
+
slice(start: bigint, end?: bigint): RemainingSlice;
|
|
97
|
+
}
|
|
98
|
+
/** Part of the remaining accounts (`rest.slice(a, b)`): spread it into a CPI's account list, directly or via readonly/writable. */
|
|
99
|
+
export interface RemainingSlice extends Iterable<CpiAccount> {
|
|
100
|
+
readonly __remainingSlice: void;
|
|
91
101
|
}
|
|
92
102
|
export declare function remainingAccounts(): RemainingAccounts;
|
|
93
103
|
|
|
@@ -213,15 +223,53 @@ export interface TokenMetadataProgram extends AccountMeta {
|
|
|
213
223
|
signerSeeds?: Seed[] | SignerSeeds,
|
|
214
224
|
): void;
|
|
215
225
|
}
|
|
226
|
+
/** The canonical program address of `seeds` under `program` (Rust `Pubkey::find_program_address(seeds, program).0`). */
|
|
227
|
+
export declare function findProgramAddress(seeds: Seed[], program: Pubkey): Pubkey;
|
|
216
228
|
/** The Metaplex metadata account address of a mint. */
|
|
217
229
|
export declare function metadataAddress(mint: Pubkey): Pubkey;
|
|
218
230
|
|
|
219
231
|
/**
|
|
220
|
-
* Create the transfer-hook validation account (an
|
|
221
|
-
*
|
|
232
|
+
* Create the transfer-hook validation account (an ExtraAccountMetaList) at PDA ["extra-account-metas", mint], listing
|
|
233
|
+
* the extra accounts Token-2022 passes to the hook after the standard four and this account (so the first extra account
|
|
234
|
+
* has index 5). Pair it with instructions tagged `@discriminator spl-transfer-hook-interface:execute` and
|
|
222
235
|
* `@discriminator spl-transfer-hook-interface:initialize-extra-account-metas`.
|
|
236
|
+
*
|
|
237
|
+
* Each extra account (a literal list; Rust `ExtraAccountMeta::new_with_pubkey` / `new_with_seeds` /
|
|
238
|
+
* `new_external_pda_with_seeds`):
|
|
239
|
+
* - `{ address: WSOL_MINT, isSigner: false, isWritable: false }`: a fixed address;
|
|
240
|
+
* - `{ seeds: ["counter", seedAccountKey(3n)], isWritable: true }`: a PDA of this program;
|
|
241
|
+
* - `{ seeds: [...], program: 6n }`: a PDA of the program at account index 6.
|
|
242
|
+
*/
|
|
243
|
+
export declare function initExtraAccountMetas(
|
|
244
|
+
payer: Mut<Signer>,
|
|
245
|
+
account: AccountMeta,
|
|
246
|
+
mint: AccountMeta,
|
|
247
|
+
systemProgram: SystemProgram,
|
|
248
|
+
extraAccounts?: ExtraAccountMeta[],
|
|
249
|
+
): void;
|
|
250
|
+
/**
|
|
251
|
+
* Rewrite this program's existing ExtraAccountMetaList for `mint` (Rust `ExtraAccountMetaList::update`), resizing it to
|
|
252
|
+
* the new list; `payer` tops it up to the rent of the new size when needed.
|
|
223
253
|
*/
|
|
224
|
-
export declare function
|
|
254
|
+
export declare function updateExtraAccountMetas(
|
|
255
|
+
payer: Mut<Signer>,
|
|
256
|
+
account: AccountMeta,
|
|
257
|
+
mint: AccountMeta,
|
|
258
|
+
systemProgram: SystemProgram,
|
|
259
|
+
extraAccounts?: ExtraAccountMeta[],
|
|
260
|
+
): void;
|
|
261
|
+
export type ExtraAccountMeta =
|
|
262
|
+
| { address: Pubkey; isSigner?: boolean; isWritable?: boolean }
|
|
263
|
+
| { seeds: (string | ExtraAccountSeed)[]; program?: bigint; isSigner?: boolean; isWritable?: boolean };
|
|
264
|
+
export interface ExtraAccountSeed {
|
|
265
|
+
readonly __extraSeed: void;
|
|
266
|
+
}
|
|
267
|
+
/** Seed::AccountKey: the key of the account at this index of the execute instruction. */
|
|
268
|
+
export declare function seedAccountKey(index: bigint): ExtraAccountSeed;
|
|
269
|
+
/** Seed::AccountData: `length` bytes at `offset` of the data of the account at `index`. */
|
|
270
|
+
export declare function seedAccountData(index: bigint, offset: bigint, length: bigint): ExtraAccountSeed;
|
|
271
|
+
/** Seed::InstructionData: `length` bytes at `offset` of the execute instruction's data. */
|
|
272
|
+
export declare function seedInstructionData(offset: bigint, length: bigint): ExtraAccountSeed;
|
|
225
273
|
|
|
226
274
|
/** Fields every account parameter exposes. */
|
|
227
275
|
export interface AccountMeta {
|
|
@@ -307,7 +355,8 @@ export type Account<T> = Readonly<T> & AccountMeta;
|
|
|
307
355
|
/** Like `Account<T>` but writable. Also usable as `Mut<Signer>`, `Mut<AccountInfo>`, `Mut<TokenAccount>`. */
|
|
308
356
|
export type Mut<T> = T & AccountMeta;
|
|
309
357
|
|
|
310
|
-
|
|
358
|
+
/** A PDA seed: a string, a Pubkey, an integer (at its width) or a unit enum value (one byte). */
|
|
359
|
+
export type Seed = string | Pubkey | bigint | number;
|
|
311
360
|
|
|
312
361
|
/**
|
|
313
362
|
* The seeds (with the bump) of one PDA signature, for helpers that sign CPIs: declare a `seeds: SignerSeeds`
|
|
@@ -343,7 +392,8 @@ export type InitIfNeeded<T> = T extends TokenAccount
|
|
|
343
392
|
* Declarative constraints, checked before your code runs (like Anchor's account attributes):
|
|
344
393
|
* - `HasOne<Mut<Vault>, "owner">`: vault.owner must be the key of the parameter named `owner` (union for several).
|
|
345
394
|
* - `Seeds<Mut<Vault>, ["vault", "@owner"], "bump">`: the account must be the PDA of these seeds; "@name" is an
|
|
346
|
-
* account parameter (its key) or an argument (its bytes),
|
|
395
|
+
* account parameter (its key) or an argument (its bytes), "@acct.field" a Pubkey, integer or unit enum field of a struct
|
|
396
|
+
* account parameter (this account's own fields too), anything else a literal string; the bump is an optional u8 field.
|
|
347
397
|
* A fourth argument derives the PDA under another program: `Seeds<AccountInfo, ["metadata", "@mint"], never, "@tokenMetadata">`
|
|
348
398
|
* ("@param" names a program parameter, or give a base58 address).
|
|
349
399
|
* - `CloseTo<Mut<Vault>, "owner">`: close the account into `owner` when the instruction succeeds.
|
|
@@ -610,7 +660,9 @@ export interface CpiAccount {
|
|
|
610
660
|
readonly __cpi_account: void;
|
|
611
661
|
}
|
|
612
662
|
export declare function readonly(account: AccountMeta | RemainingAccount): CpiAccount;
|
|
663
|
+
export declare function readonly(accounts: RemainingAccounts | RemainingSlice): Iterable<CpiAccount>;
|
|
613
664
|
export declare function writable(account: AccountMeta | RemainingAccount): CpiAccount;
|
|
665
|
+
export declare function writable(accounts: RemainingAccounts | RemainingSlice): Iterable<CpiAccount>;
|
|
614
666
|
export declare function readonlySigner(account: AccountMeta | RemainingAccount): CpiAccount;
|
|
615
667
|
export declare function writableSigner(account: AccountMeta | RemainingAccount): CpiAccount;
|
|
616
668
|
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -1197,13 +1197,50 @@ static inline uint64_t ts_mpl_create_v3(TsParams *params, const TsAccount *progr
|
|
|
1197
1197
|
* ["extra-account-metas", mint]: TLV = execute discriminator | u32 length 4 | u32 count 0.
|
|
1198
1198
|
*/
|
|
1199
1199
|
static const uint8_t TS_TRANSFER_HOOK_EXECUTE_DISC[8] = {105, 37, 101, 197, 75, 251, 102, 26};
|
|
1200
|
-
|
|
1200
|
+
/*
|
|
1201
|
+
* The ExtraAccountMetaList (spl-tlv-account-resolution) at PDA ["extra-account-metas", mint]: the execute
|
|
1202
|
+
* discriminator, a u32 length, then a PodSlice of `n` 35-byte ExtraAccountMeta records (discriminator, 32-byte address
|
|
1203
|
+
* or packed seeds, is_signer, is_writable), which the compiler encodes into `entries`.
|
|
1204
|
+
*/
|
|
1205
|
+
static inline uint64_t ts_init_extra_metas(TsParams *params, TsAccount *payer, TsAccount *acct, const SolPubkey *mint, const uint8_t *entries,
|
|
1206
|
+
uint32_t n) {
|
|
1201
1207
|
SolSignerSeed seeds[3] = {{(const uint8_t *)"extra-account-metas", 19}, {(const uint8_t *)mint, 32}};
|
|
1202
1208
|
uint8_t bump;
|
|
1203
|
-
|
|
1204
|
-
TS_TRY(ts_create_pda(params, payer, acct, seeds, 2, TS_TRANSFER_HOOK_EXECUTE_DISC,
|
|
1205
|
-
uint32_t len = 4;
|
|
1209
|
+
uint64_t space = 16 + 35 * (uint64_t)n;
|
|
1210
|
+
TS_TRY(ts_create_pda(params, payer, acct, seeds, 2, TS_TRANSFER_HOOK_EXECUTE_DISC, space, &bump));
|
|
1211
|
+
uint32_t len = 4 + 35 * n;
|
|
1212
|
+
ts_memcpy(acct->data + 8, &len, 4);
|
|
1213
|
+
ts_memcpy(acct->data + 12, &n, 4);
|
|
1214
|
+
if (n) ts_memcpy(acct->data + 16, entries, 35 * (uint64_t)n);
|
|
1215
|
+
return SUCCESS;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/*
|
|
1219
|
+
* ExtraAccountMetaList::update: rewrite an existing list (this program's PDA ["extra-account-metas", mint], holding the
|
|
1220
|
+
* execute discriminator), resizing it to `n` records. The payer tops it up to the rent of the new size when its
|
|
1221
|
+
* lamports fall short, as the program-examples do before `resize`.
|
|
1222
|
+
*/
|
|
1223
|
+
static inline uint64_t ts_update_extra_metas(TsParams *params, TsAccount *payer, TsAccount *acct, const SolPubkey *mint, const uint8_t *entries,
|
|
1224
|
+
uint32_t n) {
|
|
1225
|
+
SolSignerSeed seeds[2] = {{(const uint8_t *)"extra-account-metas", 19}, {(const uint8_t *)mint, 32}};
|
|
1226
|
+
SolPubkey pda;
|
|
1227
|
+
uint8_t bump;
|
|
1228
|
+
if (sol_try_find_program_address(seeds, 2, params->program_id, &pda, &bump) != SUCCESS || !ts_key_eq(&pda, &acct->key))
|
|
1229
|
+
TS_FAIL(TS_ERR_SEEDS, "TypeSOL: not the mint's extra-account-metas PDA");
|
|
1230
|
+
if (!ts_key_eq(&acct->owner, params->program_id)) TS_FAIL(TS_ERR_WRONG_OWNER, "TypeSOL: the extra-account-metas account is not this program's");
|
|
1231
|
+
if (acct->data_len < 16 || ts_ld64(acct->data) != ts_ld64(TS_TRANSFER_HOOK_EXECUTE_DISC))
|
|
1232
|
+
TS_FAIL(TS_ERR_DISCRIMINATOR, "TypeSOL: not an ExtraAccountMetaList");
|
|
1233
|
+
uint64_t new_len = 16 + 35 * (uint64_t)n;
|
|
1234
|
+
uint64_t rent;
|
|
1235
|
+
TS_TRY(ts_minimum_balance(new_len, &rent));
|
|
1236
|
+
if (rent > acct->lamports) TS_TRY(ts_system_transfer(params, payer, acct, rent - acct->lamports));
|
|
1237
|
+
if (new_len > acct->data_len + MAX_PERMITTED_DATA_INCREASE) TS_FAIL(TS_ERR_REALLOC, "TypeSOL: account would grow more than 10 KiB in one instruction");
|
|
1238
|
+
if (new_len > acct->data_len) ts_memzero(acct->data + acct->data_len, new_len - acct->data_len);
|
|
1239
|
+
acct->data_len = new_len;
|
|
1240
|
+
uint32_t len = 4 + 35 * n;
|
|
1206
1241
|
ts_memcpy(acct->data + 8, &len, 4);
|
|
1242
|
+
ts_memcpy(acct->data + 12, &n, 4);
|
|
1243
|
+
if (n) ts_memcpy(acct->data + 16, entries, 35 * (uint64_t)n);
|
|
1207
1244
|
return SUCCESS;
|
|
1208
1245
|
}
|
|
1209
1246
|
|