@typesol/cli 0.18.0 → 0.20.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 +37 -0
- package/README.md +1 -1
- package/dist/clientgen.d.ts +4 -1
- package/dist/clientgen.js +201 -23
- package/dist/compiler/analyze.js +36 -4
- package/dist/compiler/anchor-errors.d.ts +1 -0
- package/dist/compiler/anchor-errors.js +80 -0
- package/dist/compiler/emit/calls.js +8 -0
- package/dist/compiler/emit/cpi.d.ts +2 -1
- package/dist/compiler/emit/cpi.js +112 -6
- package/dist/compiler/emit/expressions.d.ts +19 -0
- package/dist/compiler/emit/expressions.js +71 -16
- package/dist/compiler/emit/function.js +6 -4
- package/dist/compiler/emit/statements.js +17 -0
- package/docs/client.md +31 -8
- package/docs/language.md +5 -6
- package/docs/migrate-from-anchor.md +3 -1
- package/lib/typesol.d.ts +132 -1
- package/package.json +1 -1
- package/runtime/typesol_rt.h +98 -22
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ 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.20.0 — 2026-10-28
|
|
9
|
+
|
|
10
|
+
The generated TypeScript clients catch up with Anchor's: remaining accounts, listing and watching accounts, sending
|
|
11
|
+
and simulating, views, `emitCpi` events and every Anchor error by name. Programs and their bytes are unchanged. Not
|
|
12
|
+
externally audited (see SECURITY.md).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Generated clients: remaining accounts on every instruction builder; `accounts.<Name>.fetchMultiple`, `fetchAll`
|
|
16
|
+
(getProgramAccounts by discriminator, with `{ offset, bytes }` filters) and `watch` (account subscription);
|
|
17
|
+
`sendInstructions` (send and confirm), `simulate`, and `views.<instruction>` (a return value by simulation, Anchor's
|
|
18
|
+
`.view()`); `parseCpiEvents` / `eventsOf` for `emitCpi` events, and `watchEvents` (log subscription). Every Anchor
|
|
19
|
+
framework error has its name in `errors`, so `isError` knows the codes `AnchorError` and the constraints raise.
|
|
20
|
+
|
|
21
|
+
## 0.19.0 — 2026-10-28
|
|
22
|
+
|
|
23
|
+
Anchor's error codes where TypeSOL raised its own: `TokenOf` / `MintOf` constraints that name the parameters as
|
|
24
|
+
`token::mint = …` does, every Anchor `ErrorCode` for `require`, and `init_if_needed` mints with extensions checked as
|
|
25
|
+
Anchor checks them. Strings and logs cost up to a third fewer compute units. 30 of the 46 official examples are now
|
|
26
|
+
full ports. Existing programs keep their layouts and error codes. Not externally audited (see SECURITY.md).
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `TokenOf<TokenAccount, "mintParam", "authorityParam">` and `MintOf<Mint, "authorityParam", "freezeParam">`: Anchor's
|
|
30
|
+
`token::mint = …` / `mint::authority = …` naming the parameters (2014 / 2015 / 2016 / 2017).
|
|
31
|
+
- `AnchorError`: Anchor's own error codes for `require(condition, AnchorError.X)` (`RequireKeysEqViolated`,
|
|
32
|
+
`AccountDidNotDeserialize`, the extension constraints …), as `require!` / `err!` with `ErrorCode` raise them.
|
|
33
|
+
- Extensions on `InitIfNeeded<Mint>`: an existing mint is checked as Anchor's `extensions::…` constraints check it
|
|
34
|
+
(2024–2039).
|
|
35
|
+
- Bubblegum `mintToCollection` (MintToCollectionV1 on V1 trees, Token Metadata collections).
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Strings are built with less work and heap: a template literal is joined in one allocation (it was copied piece by
|
|
39
|
+
piece), text known at compile time (literals, string and Pubkey constants) is folded in, `s += …` and `a + b` grow
|
|
40
|
+
the newest heap string in place, and base58 of a Pubkey is about 20% cheaper. Results are the same; official
|
|
41
|
+
examples that log addresses use 11–36% fewer compute units (set_favorites 10,244 → 6,601, transfer_tokens
|
|
42
|
+
9,760 → 8,467, create_system_account 4,664 → 4,075, go_to_park 435 → 372); binaries of programs that build
|
|
43
|
+
strings grow by up to 2.4 KB.
|
|
44
|
+
|
|
8
45
|
## 0.18.0 — 2026-10-28
|
|
9
46
|
|
|
10
47
|
The remaining Anchor differences, closed: Anchor's argument structs (Strings, Vecs, Options and enums in Borsh),
|
package/README.md
CHANGED
|
@@ -306,7 +306,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
306
306
|
|
|
307
307
|
## Status
|
|
308
308
|
|
|
309
|
-
**TypeSOL is pre-1.0 (0.
|
|
309
|
+
**TypeSOL is pre-1.0 (0.20).** Releases follow the organization's
|
|
310
310
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
311
311
|
change.
|
|
312
312
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
package/dist/clientgen.d.ts
CHANGED
|
@@ -90,7 +90,10 @@ export interface ClientGenResult {
|
|
|
90
90
|
/** An IDL the generator refuses: malformed, unsafe to embed, or older than the 0.30 IDL format. */
|
|
91
91
|
export declare class ClientIdlError extends Error {
|
|
92
92
|
}
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Errors a TypeSOL program can return, by code: every Anchor framework error (`AnchorError` raises any of them, and
|
|
95
|
+
* the constraints raise their Anchor codes), then TypeSOL's own runtime errors (runtime/typesol_rt.h).
|
|
96
|
+
*/
|
|
94
97
|
export declare const TYPESOL_ERRORS: Record<number, string>;
|
|
95
98
|
export declare function generateClient(idl: ClientIdl, opts?: {
|
|
96
99
|
address?: string;
|
package/dist/clientgen.js
CHANGED
|
@@ -18,12 +18,17 @@
|
|
|
18
18
|
* sanitized comments.
|
|
19
19
|
*/
|
|
20
20
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { ANCHOR_ERRORS } from "./compiler/anchor-errors.js";
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
/** An IDL the generator refuses: malformed, unsafe to embed, or older than the 0.30 IDL format. */
|
|
23
24
|
export class ClientIdlError extends Error {
|
|
24
25
|
}
|
|
25
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Errors a TypeSOL program can return, by code: every Anchor framework error (`AnchorError` raises any of them, and
|
|
28
|
+
* the constraints raise their Anchor codes), then TypeSOL's own runtime errors (runtime/typesol_rt.h).
|
|
29
|
+
*/
|
|
26
30
|
export const TYPESOL_ERRORS = {
|
|
31
|
+
...Object.fromEntries(Object.entries(ANCHOR_ERRORS).map(([name, code]) => [code, name])),
|
|
27
32
|
100: "InstructionMissing",
|
|
28
33
|
101: "InstructionFallbackNotFound",
|
|
29
34
|
102: "InstructionDidNotDeserialize",
|
|
@@ -164,7 +169,24 @@ const TAKEN = [
|
|
|
164
169
|
"globalThis",
|
|
165
170
|
];
|
|
166
171
|
const KIT_TYPES = [
|
|
172
|
+
"AccountMeta",
|
|
173
|
+
"AccountNotificationsApi",
|
|
167
174
|
"Address",
|
|
175
|
+
"Base58EncodedBytes",
|
|
176
|
+
"Commitment",
|
|
177
|
+
"FetchAccountsConfig",
|
|
178
|
+
"GetEpochInfoApi",
|
|
179
|
+
"GetLatestBlockhashApi",
|
|
180
|
+
"GetMultipleAccountsApi",
|
|
181
|
+
"GetProgramAccountsApi",
|
|
182
|
+
"GetSignatureStatusesApi",
|
|
183
|
+
"LogsNotificationsApi",
|
|
184
|
+
"RpcSubscriptions",
|
|
185
|
+
"SendTransactionApi",
|
|
186
|
+
"Signature",
|
|
187
|
+
"SignatureNotificationsApi",
|
|
188
|
+
"SimulateTransactionApi",
|
|
189
|
+
"SlotNotificationsApi",
|
|
168
190
|
"Codec",
|
|
169
191
|
"FetchAccountConfig",
|
|
170
192
|
"GetAccountInfoApi",
|
|
@@ -176,6 +198,21 @@ const KIT_TYPES = [
|
|
|
176
198
|
];
|
|
177
199
|
const KIT_VALUES = [
|
|
178
200
|
"AccountRole",
|
|
201
|
+
"appendTransactionMessageInstructions",
|
|
202
|
+
"assertIsTransactionWithBlockhashLifetime",
|
|
203
|
+
"compileTransaction",
|
|
204
|
+
"createTransactionMessage",
|
|
205
|
+
"fetchEncodedAccounts",
|
|
206
|
+
"getBase58Decoder",
|
|
207
|
+
"getBase58Encoder",
|
|
208
|
+
"getBase64EncodedWireTransaction",
|
|
209
|
+
"getSignatureFromTransaction",
|
|
210
|
+
"pipe",
|
|
211
|
+
"sendAndConfirmTransactionFactory",
|
|
212
|
+
"setTransactionMessageFeePayer",
|
|
213
|
+
"setTransactionMessageFeePayerSigner",
|
|
214
|
+
"setTransactionMessageLifetimeUsingBlockhash",
|
|
215
|
+
"signTransactionMessageWithSigners",
|
|
179
216
|
"SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM",
|
|
180
217
|
"addCodecSizePrefix",
|
|
181
218
|
"address",
|
|
@@ -214,7 +251,22 @@ const KIT_VALUES = [
|
|
|
214
251
|
"transformCodec",
|
|
215
252
|
];
|
|
216
253
|
// Keys of the program object that instructions must not take.
|
|
217
|
-
const PROGRAM_KEYS = new Set([
|
|
254
|
+
const PROGRAM_KEYS = new Set([
|
|
255
|
+
"address",
|
|
256
|
+
"accounts",
|
|
257
|
+
"pdas",
|
|
258
|
+
"returns",
|
|
259
|
+
"views",
|
|
260
|
+
"errors",
|
|
261
|
+
"errorCode",
|
|
262
|
+
"isError",
|
|
263
|
+
"parseEvents",
|
|
264
|
+
"parseCpiEvents",
|
|
265
|
+
"eventsOf",
|
|
266
|
+
"watchEvents",
|
|
267
|
+
"sendInstructions",
|
|
268
|
+
"simulate",
|
|
269
|
+
]);
|
|
218
270
|
// snake_case → camelCase by splitting (linear time, whatever the input): "taker_token_a" → "takerTokenA".
|
|
219
271
|
const camel = (s) => s
|
|
220
272
|
.split("_")
|
|
@@ -699,6 +751,24 @@ export function generateClient(idl, opts = {}) {
|
|
|
699
751
|
` const found = await fetchEncodedAccount(rpc, at, config);\n` +
|
|
700
752
|
` return found.exists ? ${A}.decode(found.data) : null;\n` +
|
|
701
753
|
` },\n` +
|
|
754
|
+
` /** Several accounts in one request, in order; null where one does not exist. */\n` +
|
|
755
|
+
` async fetchMultiple(rpc: Rpc<GetMultipleAccountsApi>, at: readonly Address[], config?: FetchAccountsConfig): Promise<(${T} | null)[]> {\n` +
|
|
756
|
+
` const found = await fetchEncodedAccounts(rpc, [...at], config);\n` +
|
|
757
|
+
` return found.map((a) => (a.exists ? ${A}.decode(a.data) : null));\n` +
|
|
758
|
+
` },\n` +
|
|
759
|
+
` /** Every ${commentText(a.name)} account of the program (getProgramAccounts by discriminator), optionally where \`bytes\` are at \`offset\` of the data. */\n` +
|
|
760
|
+
` async fetchAll(rpc: Rpc<GetProgramAccountsApi>, filters: readonly { offset: number; bytes: ReadonlyUint8Array }[] = []): Promise<{ address: Address; data: ${T} }[]> {\n` +
|
|
761
|
+
` const found = await rpc.getProgramAccounts(PROGRAM_ADDRESS, { encoding: "base64", filters: memcmp([{ offset: 0, bytes: ${A}_DISCRIMINATOR }, ...filters]) }).send();\n` +
|
|
762
|
+
` return found.map((a) => ({ address: a.pubkey, data: ${A}.decode(getBase64Encoder().encode(a.account.data[0])) }));\n` +
|
|
763
|
+
` },\n` +
|
|
764
|
+
` /** Each change of the account, decoded (null once it is closed), until \`abortSignal\` aborts. */\n` +
|
|
765
|
+
` async *watch(rpcSubscriptions: RpcSubscriptions<AccountNotificationsApi>, at: Address, options: { abortSignal: AbortSignal; commitment?: Commitment }): AsyncGenerator<${T} | null> {\n` +
|
|
766
|
+
` const updates = await rpcSubscriptions.accountNotifications(at, { encoding: "base64", ...(options.commitment ? { commitment: options.commitment } : {}) }).subscribe({ abortSignal: options.abortSignal });\n` +
|
|
767
|
+
` for await (const u of updates) {\n` +
|
|
768
|
+
` const data = getBase64Encoder().encode(u.value.data[0]);\n` +
|
|
769
|
+
` yield data.length === 0 ? null : ${A}.decode(data);\n` +
|
|
770
|
+
` }\n` +
|
|
771
|
+
` },\n` +
|
|
702
772
|
`};\n`);
|
|
703
773
|
accountKeys.push(`${prop(a.name)}: ${A}`);
|
|
704
774
|
}
|
|
@@ -724,6 +794,7 @@ export function generateClient(idl, opts = {}) {
|
|
|
724
794
|
const ixKeys = [];
|
|
725
795
|
const pdaFns = new Map();
|
|
726
796
|
const returnKeys = [];
|
|
797
|
+
const viewKeys = [];
|
|
727
798
|
const usedKeys = new Set();
|
|
728
799
|
for (const ix of idl.instructions) {
|
|
729
800
|
const argIssue = ix.args.map((a) => issue(a.type)).find(Boolean);
|
|
@@ -825,7 +896,7 @@ export function generateClient(idl, opts = {}) {
|
|
|
825
896
|
resolvable.delete(a.key);
|
|
826
897
|
}
|
|
827
898
|
const auto = (a) => Boolean(a.address) || resolvable.has(a.key);
|
|
828
|
-
use("type Address", "type TransactionSigner", "type Instruction", "AccountRole");
|
|
899
|
+
use("type Address", "type TransactionSigner", "type Instruction", "type AccountMeta", "AccountRole");
|
|
829
900
|
const accType = (a) => `${a.signer ? "TransactionSigner | Address" : "Address"}${a.optional ? " | null" : ""}`;
|
|
830
901
|
const accountsIface = `export interface ${P}Accounts {\n` +
|
|
831
902
|
flat.map((a) => `${doc(a.docs, " ")} ${prop(a.key)}${auto(a) || a.optional ? "?" : ""}: ${accType(a)};`).join("\n") +
|
|
@@ -896,16 +967,25 @@ export function generateClient(idl, opts = {}) {
|
|
|
896
967
|
: bytes(ix.discriminator);
|
|
897
968
|
ixOut.push(accountsIface +
|
|
898
969
|
argsIface +
|
|
899
|
-
`${doc(ix.docs)}async function build${P}(input: ${P}Accounts${argsField.length ? `, args: ${P}Args` : ""}): Promise<Instruction> {\n` +
|
|
970
|
+
`${doc(ix.docs)}async function build${P}(input: ${P}Accounts${argsField.length ? `, args: ${P}Args` : ""}, remaining: readonly AccountMeta[] = []): Promise<Instruction> {\n` +
|
|
900
971
|
lines.join("\n") +
|
|
901
|
-
`\n return {\n programAddress: PROGRAM_ADDRESS,\n accounts: [\n${metas.join("\n")}\n ],\n data: ${data},\n } as Instruction;\n}\n`);
|
|
972
|
+
`\n return {\n programAddress: PROGRAM_ADDRESS,\n accounts: [\n${metas.join("\n")}\n ...remaining,\n ],\n data: ${data},\n } as Instruction;\n}\n`);
|
|
902
973
|
ixKeys.push(`${prop(fn)}: build${P}`);
|
|
903
974
|
if (ix.returns !== undefined) {
|
|
904
975
|
const why = issue(ix.returns);
|
|
905
976
|
if (why)
|
|
906
977
|
notes.push(`instruction ${ix.name}: the return value is not decoded (${why})`);
|
|
907
|
-
else
|
|
978
|
+
else {
|
|
908
979
|
returnKeys.push(` ${prop(fn)}: (data: ReadonlyUint8Array): ${tsType(ix.returns, "out")} => ${codec(ix.returns)}.decode(data),`);
|
|
980
|
+
// Read-only call: simulate the instruction and decode what it returns (Anchor's `.view()`).
|
|
981
|
+
viewKeys.push(` /** ${ix.name}'s return value, by simulation (nothing is sent). */\n` +
|
|
982
|
+
` async ${prop(fn)}(rpc: Rpc<SimulateTransactionApi & GetLatestBlockhashApi>, feePayer: Address | TransactionSigner, input: ${P}Accounts${argsField.length ? `, args: ${P}Args` : ""}): Promise<${tsType(ix.returns, "out")}> {\n` +
|
|
983
|
+
` const res = await simulate(rpc, feePayer, [await build${P}(input${argsField.length ? ", args" : ""})]);\n` +
|
|
984
|
+
` if (res.err !== null) throw new Error(${JSON.stringify(`${ix.name} failed in simulation: `)} + JSON.stringify(res.err, (_, v) => (typeof v === "bigint" ? v.toString() : v)) + "\\n" + res.logs.join("\\n"));\n` +
|
|
985
|
+
` if (res.returnData === null) throw new Error(${JSON.stringify(`${ix.name} returned nothing`)});\n` +
|
|
986
|
+
` return ${codec(ix.returns)}.decode(res.returnData);\n` +
|
|
987
|
+
` },`);
|
|
988
|
+
}
|
|
909
989
|
}
|
|
910
990
|
}
|
|
911
991
|
// ---- Assemble ---------------------------------------------------------------------------------------------
|
|
@@ -961,6 +1041,13 @@ export function generateClient(idl, opts = {}) {
|
|
|
961
1041
|
],
|
|
962
1042
|
},
|
|
963
1043
|
{ name: "programFromBytes", lines: [`const programFromBytes = (b: Uint8Array): Address => getAddressDecoder().decode(b);`] },
|
|
1044
|
+
{
|
|
1045
|
+
name: "memcmp",
|
|
1046
|
+
lines: [
|
|
1047
|
+
`const memcmp = (filters: readonly { offset: number; bytes: ReadonlyUint8Array }[]) =>`,
|
|
1048
|
+
` filters.map((f) => ({ memcmp: { offset: BigInt(f.offset), bytes: getBase58Decoder().decode(f.bytes) as Base58EncodedBytes, encoding: "base58" as const } }));`,
|
|
1049
|
+
],
|
|
1050
|
+
},
|
|
964
1051
|
];
|
|
965
1052
|
if (needsFixedString)
|
|
966
1053
|
helperDefs.push({
|
|
@@ -1078,30 +1165,113 @@ export function generateClient(idl, opts = {}) {
|
|
|
1078
1165
|
"",
|
|
1079
1166
|
`// ---- Events ----`,
|
|
1080
1167
|
`export type Event = ${events.length ? events.map((e) => `{ name: ${JSON.stringify(e.name)}; data: ${tsName(e.name)} }`).join(" | ") : "never"};`,
|
|
1168
|
+
`/** One event's bytes (discriminator, then the fields), or null when they are not one of this program's events. */`,
|
|
1169
|
+
`function decodeEvent(data: ReadonlyUint8Array): Event | null {`,
|
|
1170
|
+
events.length
|
|
1171
|
+
? [
|
|
1172
|
+
` try {`,
|
|
1173
|
+
...events.map((e) => ` if (hasPrefix(data, ${bytes(e.discriminator)})) return { name: ${JSON.stringify(e.name)}, data: get${tsName(e.name)}Codec().decode(data, ${e.discriminator.length}) };`),
|
|
1174
|
+
` } catch {`,
|
|
1175
|
+
` // not one of this program's events`,
|
|
1176
|
+
` }`,
|
|
1177
|
+
` return null;`,
|
|
1178
|
+
].join("\n")
|
|
1179
|
+
: ` void data;\n return null;`,
|
|
1180
|
+
`}`,
|
|
1081
1181
|
`/**`,
|
|
1082
1182
|
` * Events in a transaction's log messages (\`Program data:\` lines, as \`emit\` writes them). Lines are matched by`,
|
|
1083
1183
|
` * discriminator, whichever program wrote them; lines that do not decode are skipped. \`emitCpi\` events travel in inner`,
|
|
1084
|
-
` * instructions
|
|
1184
|
+
` * instructions: see parseCpiEvents and eventsOf.`,
|
|
1085
1185
|
` */`,
|
|
1086
1186
|
`export function parseEvents(logs: readonly string[]): Event[] {`,
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1187
|
+
` const found: Event[] = [];`,
|
|
1188
|
+
` for (const line of logs) {`,
|
|
1189
|
+
` if (!line.startsWith("Program data: ")) continue;`,
|
|
1190
|
+
` let data: ReadonlyUint8Array;`,
|
|
1191
|
+
` try {`,
|
|
1192
|
+
` data = getBase64Encoder().encode(line.slice("Program data: ".length));`,
|
|
1193
|
+
` } catch {`,
|
|
1194
|
+
` continue;`,
|
|
1195
|
+
` }`,
|
|
1196
|
+
` const e = decodeEvent(data);`,
|
|
1197
|
+
` if (e) found.push(e);`,
|
|
1198
|
+
` }`,
|
|
1199
|
+
` return found;`,
|
|
1200
|
+
`}`,
|
|
1201
|
+
`/** \`emitCpi\` events: the data of inner instructions (bytes, or base58 as RPC returns them) that carry one. */`,
|
|
1202
|
+
`export function parseCpiEvents(instructionData: Iterable<ReadonlyUint8Array | string>): Event[] {`,
|
|
1203
|
+
` const tag = new Uint8Array([228, 69, 165, 46, 81, 203, 154, 29]);`,
|
|
1204
|
+
` const found: Event[] = [];`,
|
|
1205
|
+
` for (const item of instructionData) {`,
|
|
1206
|
+
` let data: ReadonlyUint8Array;`,
|
|
1207
|
+
` try {`,
|
|
1208
|
+
` data = typeof item === "string" ? getBase58Encoder().encode(item) : item;`,
|
|
1209
|
+
` } catch {`,
|
|
1210
|
+
` continue;`,
|
|
1211
|
+
` }`,
|
|
1212
|
+
` if (!hasPrefix(data, tag)) continue;`,
|
|
1213
|
+
` const e = decodeEvent(data.slice(tag.length));`,
|
|
1214
|
+
` if (e) found.push(e);`,
|
|
1215
|
+
` }`,
|
|
1216
|
+
` return found;`,
|
|
1217
|
+
`}`,
|
|
1218
|
+
`/** Every event of a fetched transaction (getTransaction, json encoding): its logs, then its inner instructions. */`,
|
|
1219
|
+
`export function eventsOf(tx: { meta: { logMessages?: readonly string[] | null; innerInstructions?: readonly { instructions: readonly { data: string }[] }[] | null } | null } | null): Event[] {`,
|
|
1220
|
+
` const meta = tx?.meta;`,
|
|
1221
|
+
` if (!meta) return [];`,
|
|
1222
|
+
` return [...parseEvents(meta.logMessages ?? []), ...parseCpiEvents((meta.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))];`,
|
|
1223
|
+
`}`,
|
|
1224
|
+
`/** Events of every transaction that mentions the program, as their logs arrive, until \`abortSignal\` aborts. */`,
|
|
1225
|
+
`export async function* watchEvents(rpcSubscriptions: RpcSubscriptions<LogsNotificationsApi>, options: { abortSignal: AbortSignal; commitment?: Commitment }): AsyncGenerator<{ signature: Signature; event: Event }> {`,
|
|
1226
|
+
` const logs = await rpcSubscriptions.logsNotifications({ mentions: [PROGRAM_ADDRESS] }, options.commitment ? { commitment: options.commitment } : {}).subscribe({ abortSignal: options.abortSignal });`,
|
|
1227
|
+
` for await (const n of logs) for (const event of parseEvents(n.value.logs)) yield { signature: n.value.signature, event };`,
|
|
1102
1228
|
`}`,
|
|
1103
1229
|
"",
|
|
1104
|
-
|
|
1230
|
+
`// ---- Transactions ----`,
|
|
1231
|
+
`/** Send instructions in one transaction paid by \`feePayer\` (signers in the accounts sign too) and wait for confirmation. */`,
|
|
1232
|
+
`export async function sendInstructions(`,
|
|
1233
|
+
` connection: { rpc: Rpc<GetEpochInfoApi & GetLatestBlockhashApi & GetSignatureStatusesApi & SendTransactionApi>; rpcSubscriptions: RpcSubscriptions<SignatureNotificationsApi & SlotNotificationsApi> },`,
|
|
1234
|
+
` feePayer: TransactionSigner,`,
|
|
1235
|
+
` instructions: readonly Instruction[],`,
|
|
1236
|
+
` options: { commitment?: Commitment; abortSignal?: AbortSignal } = {},`,
|
|
1237
|
+
`): Promise<Signature> {`,
|
|
1238
|
+
` const { value: latestBlockhash } = await connection.rpc.getLatestBlockhash().send();`,
|
|
1239
|
+
` const message = pipe(`,
|
|
1240
|
+
` createTransactionMessage({ version: 0 }),`,
|
|
1241
|
+
` (m) => setTransactionMessageFeePayerSigner(feePayer, m),`,
|
|
1242
|
+
` (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),`,
|
|
1243
|
+
` (m) => appendTransactionMessageInstructions(instructions, m),`,
|
|
1244
|
+
` );`,
|
|
1245
|
+
` const signed = await signTransactionMessageWithSigners(message);`,
|
|
1246
|
+
` assertIsTransactionWithBlockhashLifetime(signed);`,
|
|
1247
|
+
` await sendAndConfirmTransactionFactory(connection)(signed, { commitment: options.commitment ?? "confirmed", ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}) });`,
|
|
1248
|
+
` return getSignatureFromTransaction(signed);`,
|
|
1249
|
+
`}`,
|
|
1250
|
+
`/** Simulate instructions (nothing is sent, signatures are not checked): the error if any, logs, units and the program's return data. */`,
|
|
1251
|
+
`export async function simulate(`,
|
|
1252
|
+
` rpc: Rpc<SimulateTransactionApi & GetLatestBlockhashApi>,`,
|
|
1253
|
+
` feePayer: Address | TransactionSigner,`,
|
|
1254
|
+
` instructions: readonly Instruction[],`,
|
|
1255
|
+
`): Promise<{ err: unknown; logs: readonly string[]; unitsConsumed: bigint | null; returnData: ReadonlyUint8Array | null }> {`,
|
|
1256
|
+
` const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();`,
|
|
1257
|
+
` const message = pipe(`,
|
|
1258
|
+
` createTransactionMessage({ version: 0 }),`,
|
|
1259
|
+
` (m) => setTransactionMessageFeePayer(key(feePayer), m),`,
|
|
1260
|
+
` (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),`,
|
|
1261
|
+
` (m) => appendTransactionMessageInstructions(instructions, m),`,
|
|
1262
|
+
` );`,
|
|
1263
|
+
` const wire = getBase64EncodedWireTransaction(compileTransaction(message));`,
|
|
1264
|
+
` const { value } = await rpc.simulateTransaction(wire, { encoding: "base64", sigVerify: false, replaceRecentBlockhash: true }).send();`,
|
|
1265
|
+
` const ret = value.returnData;`,
|
|
1266
|
+
` return {`,
|
|
1267
|
+
` err: value.err,`,
|
|
1268
|
+
` logs: value.logs ?? [],`,
|
|
1269
|
+
` unitsConsumed: value.unitsConsumed ?? null,`,
|
|
1270
|
+
` returnData: ret && ret.programId === PROGRAM_ADDRESS ? getBase64Encoder().encode(ret.data[0]) : null,`,
|
|
1271
|
+
` };`,
|
|
1272
|
+
`}`,
|
|
1273
|
+
"",
|
|
1274
|
+
`/** The ${displayName} program: instructions, accounts, PDAs, errors, events, return values and transactions. */`,
|
|
1105
1275
|
`export const ${programName} = {`,
|
|
1106
1276
|
` address: PROGRAM_ADDRESS,`,
|
|
1107
1277
|
...ixKeys.map((k) => ` ${k},`),
|
|
@@ -1112,10 +1282,18 @@ export function generateClient(idl, opts = {}) {
|
|
|
1112
1282
|
` returns: {`,
|
|
1113
1283
|
...returnKeys,
|
|
1114
1284
|
` },`,
|
|
1285
|
+
` views: {`,
|
|
1286
|
+
...viewKeys,
|
|
1287
|
+
` },`,
|
|
1115
1288
|
` errors: ERRORS,`,
|
|
1116
1289
|
` errorCode,`,
|
|
1117
1290
|
` isError,`,
|
|
1118
1291
|
` parseEvents,`,
|
|
1292
|
+
` parseCpiEvents,`,
|
|
1293
|
+
` eventsOf,`,
|
|
1294
|
+
` watchEvents,`,
|
|
1295
|
+
` sendInstructions,`,
|
|
1296
|
+
` simulate,`,
|
|
1119
1297
|
`};`,
|
|
1120
1298
|
"",
|
|
1121
1299
|
];
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -77,6 +77,8 @@ const TYPE_EXPORTS = new Set([
|
|
|
77
77
|
"TokenMetadataProgram",
|
|
78
78
|
"SwitchboardFeed",
|
|
79
79
|
"HasOne",
|
|
80
|
+
"TokenOf",
|
|
81
|
+
"MintOf",
|
|
80
82
|
"Seeds",
|
|
81
83
|
"CloseTo",
|
|
82
84
|
"Address",
|
|
@@ -1120,7 +1122,13 @@ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new S
|
|
|
1120
1122
|
return { name: p.name.text, type: { kind: "signerSeeds" }, node: p };
|
|
1121
1123
|
// Accounts the caller already checked: Signer, AccountInfo, Account<T>/Mut<T>, token accounts, programs.
|
|
1122
1124
|
const wrapped = typesolName(p.type, scope.imports);
|
|
1123
|
-
if (wrapped === "HasOne" ||
|
|
1125
|
+
if (wrapped === "HasOne" ||
|
|
1126
|
+
wrapped === "TokenOf" ||
|
|
1127
|
+
wrapped === "MintOf" ||
|
|
1128
|
+
wrapped === "Seeds" ||
|
|
1129
|
+
wrapped === "CloseTo" ||
|
|
1130
|
+
wrapped === "Address" ||
|
|
1131
|
+
wrapped === "Ata")
|
|
1124
1132
|
reporter.fail(p.type, "constraints belong on instruction parameters; helpers take the plain account type");
|
|
1125
1133
|
const account = resolveAccountKind(p.type, reporter, scope.imports, structs);
|
|
1126
1134
|
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed")
|
|
@@ -1270,7 +1278,17 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1270
1278
|
continue;
|
|
1271
1279
|
const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
|
|
1272
1280
|
reporter.guard(() => {
|
|
1273
|
-
for (const
|
|
1281
|
+
for (const entry of c.hasOne) {
|
|
1282
|
+
const [f, param = f] = entry.split("=");
|
|
1283
|
+
if (entry.includes("=")) {
|
|
1284
|
+
if (!accounts.some((o) => o.name === param))
|
|
1285
|
+
reporter.fail(a.node, `add an account parameter named "${param}"`);
|
|
1286
|
+
if (a.kind.kind === "token" ? f !== "mint" && f !== "owner" : a.kind.kind === "mint" ? false : true)
|
|
1287
|
+
reporter.fail(a.node, a.kind.kind === "token" || a.kind.kind === "mint" ? "TokenOf<…> applies to a TokenAccount" : "TokenOf / MintOf apply to a TokenAccount / Mint");
|
|
1288
|
+
if (a.kind.kind === "mint" && f !== "mintAuthority" && f !== "freezeAuthority")
|
|
1289
|
+
reporter.fail(a.node, "MintOf<…> applies to a Mint");
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1274
1292
|
if (!accounts.some((o) => o.name === f))
|
|
1275
1293
|
reporter.fail(a.node, `HasOne<…, "${f}">: add an account parameter named "${f}"`);
|
|
1276
1294
|
if (a.kind.kind === "token") {
|
|
@@ -1321,7 +1339,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1321
1339
|
for (const a of accounts) {
|
|
1322
1340
|
const c = a.constraints;
|
|
1323
1341
|
const refs = [
|
|
1324
|
-
...c.hasOne,
|
|
1342
|
+
...c.hasOne.map((e) => e.split("=").at(-1)),
|
|
1325
1343
|
...(c.seeds?.names ?? []).filter((n) => n.startsWith("@")).map((n) => n.slice(1).split(".")[0]),
|
|
1326
1344
|
...(c.seeds?.program?.startsWith("@") ? [c.seeds.program.slice(1)] : []),
|
|
1327
1345
|
...(c.closeTo ? [c.closeTo] : []),
|
|
@@ -1381,7 +1399,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1381
1399
|
/** Peel HasOne / Seeds / CloseTo wrappers off a parameter type, recording what they ask for. */
|
|
1382
1400
|
function unwrapConstraints(node, reporter, imports, out) {
|
|
1383
1401
|
const name = typesolName(node, imports);
|
|
1384
|
-
if (!["HasOne", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
|
|
1402
|
+
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
|
|
1385
1403
|
return node;
|
|
1386
1404
|
const args = node.typeArguments ?? [];
|
|
1387
1405
|
const lit = (t) => {
|
|
@@ -1395,6 +1413,20 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1395
1413
|
};
|
|
1396
1414
|
if (name === "HasOne")
|
|
1397
1415
|
out.hasOne.push(...lit(args[1]));
|
|
1416
|
+
// Anchor's `token::mint = m, token::authority = a` / `mint::authority = a, mint::freeze_authority = f` naming the
|
|
1417
|
+
// parameters: recorded as "field=param" (`never` leaves one out).
|
|
1418
|
+
if (name === "TokenOf" || name === "MintOf") {
|
|
1419
|
+
const [first, second] = name === "TokenOf" ? ["mint", "owner"] : ["mintAuthority", "freezeAuthority"];
|
|
1420
|
+
const one = (t) => (t && t.kind !== ts.SyntaxKind.NeverKeyword ? lit(t)[0] : undefined);
|
|
1421
|
+
const p1 = one(args[1]);
|
|
1422
|
+
const p2 = one(args[2]);
|
|
1423
|
+
if (!p1 && !p2)
|
|
1424
|
+
reporter.fail(node, `${name}<…> names at least one parameter`);
|
|
1425
|
+
if (p1)
|
|
1426
|
+
out.hasOne.push(`${first}=${p1}`);
|
|
1427
|
+
if (p2)
|
|
1428
|
+
out.hasOne.push(`${second}=${p2}`);
|
|
1429
|
+
}
|
|
1398
1430
|
if (name === "CloseTo")
|
|
1399
1431
|
out.closeTo = lit(args[1])[0];
|
|
1400
1432
|
if (name === "Owner") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ANCHOR_ERRORS: Record<string, number>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Anchor's own error codes (anchor-lang ErrorCode), for `require(condition, AnchorError.X)`.
|
|
2
|
+
export const ANCHOR_ERRORS = {
|
|
3
|
+
InstructionMissing: 100,
|
|
4
|
+
InstructionFallbackNotFound: 101,
|
|
5
|
+
InstructionDidNotDeserialize: 102,
|
|
6
|
+
InstructionDidNotSerialize: 103,
|
|
7
|
+
IdlInstructionStub: 1000,
|
|
8
|
+
IdlInstructionInvalidProgram: 1001,
|
|
9
|
+
IdlAccountNotEmpty: 1002,
|
|
10
|
+
EventInstructionStub: 1500,
|
|
11
|
+
ConstraintMut: 2000,
|
|
12
|
+
ConstraintHasOne: 2001,
|
|
13
|
+
ConstraintSigner: 2002,
|
|
14
|
+
ConstraintRaw: 2003,
|
|
15
|
+
ConstraintOwner: 2004,
|
|
16
|
+
ConstraintRentExempt: 2005,
|
|
17
|
+
ConstraintSeeds: 2006,
|
|
18
|
+
ConstraintExecutable: 2007,
|
|
19
|
+
ConstraintState: 2008,
|
|
20
|
+
ConstraintAssociated: 2009,
|
|
21
|
+
ConstraintAssociatedInit: 2010,
|
|
22
|
+
ConstraintClose: 2011,
|
|
23
|
+
ConstraintAddress: 2012,
|
|
24
|
+
ConstraintZero: 2013,
|
|
25
|
+
ConstraintTokenMint: 2014,
|
|
26
|
+
ConstraintTokenOwner: 2015,
|
|
27
|
+
ConstraintMintMintAuthority: 2016,
|
|
28
|
+
ConstraintMintFreezeAuthority: 2017,
|
|
29
|
+
ConstraintMintDecimals: 2018,
|
|
30
|
+
ConstraintSpace: 2019,
|
|
31
|
+
ConstraintAccountIsNone: 2020,
|
|
32
|
+
ConstraintTokenTokenProgram: 2021,
|
|
33
|
+
ConstraintMintTokenProgram: 2022,
|
|
34
|
+
ConstraintAssociatedTokenTokenProgram: 2023,
|
|
35
|
+
ConstraintMintGroupPointerExtension: 2024,
|
|
36
|
+
ConstraintMintGroupPointerExtensionAuthority: 2025,
|
|
37
|
+
ConstraintMintGroupPointerExtensionGroupAddress: 2026,
|
|
38
|
+
ConstraintMintGroupMemberPointerExtension: 2027,
|
|
39
|
+
ConstraintMintGroupMemberPointerExtensionAuthority: 2028,
|
|
40
|
+
ConstraintMintGroupMemberPointerExtensionMemberAddress: 2029,
|
|
41
|
+
ConstraintMintMetadataPointerExtension: 2030,
|
|
42
|
+
ConstraintMintMetadataPointerExtensionAuthority: 2031,
|
|
43
|
+
ConstraintMintMetadataPointerExtensionMetadataAddress: 2032,
|
|
44
|
+
ConstraintMintCloseAuthorityExtension: 2033,
|
|
45
|
+
ConstraintMintCloseAuthorityExtensionAuthority: 2034,
|
|
46
|
+
ConstraintMintPermanentDelegateExtension: 2035,
|
|
47
|
+
ConstraintMintPermanentDelegateExtensionDelegate: 2036,
|
|
48
|
+
ConstraintMintTransferHookExtension: 2037,
|
|
49
|
+
ConstraintMintTransferHookExtensionAuthority: 2038,
|
|
50
|
+
ConstraintMintTransferHookExtensionProgramId: 2039,
|
|
51
|
+
RequireViolated: 2500,
|
|
52
|
+
RequireEqViolated: 2501,
|
|
53
|
+
RequireKeysEqViolated: 2502,
|
|
54
|
+
RequireNeqViolated: 2503,
|
|
55
|
+
RequireKeysNeqViolated: 2504,
|
|
56
|
+
RequireGtViolated: 2505,
|
|
57
|
+
RequireGteViolated: 2506,
|
|
58
|
+
AccountDiscriminatorAlreadySet: 3000,
|
|
59
|
+
AccountDiscriminatorNotFound: 3001,
|
|
60
|
+
AccountDiscriminatorMismatch: 3002,
|
|
61
|
+
AccountDidNotDeserialize: 3003,
|
|
62
|
+
AccountDidNotSerialize: 3004,
|
|
63
|
+
AccountNotEnoughKeys: 3005,
|
|
64
|
+
AccountNotMutable: 3006,
|
|
65
|
+
AccountOwnedByWrongProgram: 3007,
|
|
66
|
+
InvalidProgramId: 3008,
|
|
67
|
+
InvalidProgramExecutable: 3009,
|
|
68
|
+
AccountNotSigner: 3010,
|
|
69
|
+
AccountNotSystemOwned: 3011,
|
|
70
|
+
AccountNotInitialized: 3012,
|
|
71
|
+
AccountNotProgramData: 3013,
|
|
72
|
+
AccountNotAssociatedTokenAccount: 3014,
|
|
73
|
+
AccountSysvarMismatch: 3015,
|
|
74
|
+
AccountReallocExceedsLimit: 3016,
|
|
75
|
+
AccountDuplicateReallocs: 3017,
|
|
76
|
+
DeclaredProgramIdMismatch: 4100,
|
|
77
|
+
TryingToInitPayerAsProgramAccount: 4101,
|
|
78
|
+
InvalidNumericConversion: 4102,
|
|
79
|
+
Deprecated: 5000,
|
|
80
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { calleeName, resolveScalarType } from "../analyze.js";
|
|
3
|
+
import { ANCHOR_ERRORS } from "../anchor-errors.js";
|
|
3
4
|
import { decodeAddress } from "../../keys.js";
|
|
4
5
|
import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
|
|
5
6
|
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
|
|
@@ -1091,6 +1092,13 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
1091
1092
|
this.fail(n, "pass signer([...seeds with the bump]) or a SignerSeeds parameter");
|
|
1092
1093
|
}
|
|
1093
1094
|
errorRef(node) {
|
|
1095
|
+
// `AnchorError.X`: one of Anchor's own codes (not one of this program's errors, so not in the IDL).
|
|
1096
|
+
if (ts.isPropertyAccessExpression(node) && this.exportName(node.expression) === "AnchorError") {
|
|
1097
|
+
const code = ANCHOR_ERRORS[node.name.text];
|
|
1098
|
+
if (code === undefined)
|
|
1099
|
+
this.fail(node.name, `AnchorError has no ${node.name.text}`);
|
|
1100
|
+
return { code, name: node.name.text, msg: node.name.text };
|
|
1101
|
+
}
|
|
1094
1102
|
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
|
|
1095
1103
|
const def = this.model.errorEnums.get(node.expression.text)?.get(node.name.text);
|
|
1096
1104
|
if (def)
|
|
@@ -41,9 +41,10 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
41
41
|
* in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
|
|
42
42
|
* InitializeMint2) run after the account is created, sized for exactly these extensions.
|
|
43
43
|
*/
|
|
44
|
-
protected mintExtensions(lit: ts.ObjectLiteralExpression, tp: string, acct: string): {
|
|
44
|
+
protected mintExtensions(lit: ts.ObjectLiteralExpression, tp: string, acct: string, ifNeeded?: boolean): {
|
|
45
45
|
space: number;
|
|
46
46
|
lines: string[];
|
|
47
|
+
checks: string[];
|
|
47
48
|
};
|
|
48
49
|
/**
|
|
49
50
|
* `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
|