@typesol/cli 0.19.0 → 0.21.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 +28 -0
- package/README.md +1 -1
- package/dist/cli.js +29 -6
- package/dist/clientgen.d.ts +4 -1
- package/dist/clientgen.js +355 -23
- package/dist/idlpublish.d.ts +12 -0
- package/dist/idlpublish.js +38 -1
- package/docs/cli.md +1 -1
- package/docs/client.md +43 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.21.0 — 2026-10-28
|
|
9
|
+
|
|
10
|
+
Clients from a program's address alone, and the last pieces Anchor's client resolves for you: `has_one` accounts,
|
|
11
|
+
PDAs seeded from another account's fields, and `emitCpi` events as they happen. Programs and their bytes are
|
|
12
|
+
unchanged. Not externally audited (see SECURITY.md).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `typesol client --address <program id> --url <rpc>`: a client from the IDL on chain, the canonical Program Metadata
|
|
16
|
+
IDL (`typesol idl publish`, Codama) or else Anchor's IDL account (`anchor idl init`).
|
|
17
|
+
- Generated clients: `resolve.<instruction>(rpc, accounts)` fills in `has_one` accounts that do not sign from the
|
|
18
|
+
account that names them, as Anchor's client resolves `relations`, and PDAs seeded from another account's fields
|
|
19
|
+
(`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived).
|
|
20
|
+
- `watchEvents(rpcSubscriptions, { abortSignal, rpc })`: with an RPC, `emitCpi` events arrive live too (the transaction
|
|
21
|
+
is fetched when the program invoked itself).
|
|
22
|
+
|
|
23
|
+
## 0.20.0 — 2026-10-28
|
|
24
|
+
|
|
25
|
+
The generated TypeScript clients catch up with Anchor's: remaining accounts, listing and watching accounts, sending
|
|
26
|
+
and simulating, views, `emitCpi` events and every Anchor error by name. Programs and their bytes are unchanged. Not
|
|
27
|
+
externally audited (see SECURITY.md).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Generated clients: remaining accounts on every instruction builder; `accounts.<Name>.fetchMultiple`, `fetchAll`
|
|
31
|
+
(getProgramAccounts by discriminator, with `{ offset, bytes }` filters) and `watch` (account subscription);
|
|
32
|
+
`sendInstructions` (send and confirm), `simulate`, and `views.<instruction>` (a return value by simulation, Anchor's
|
|
33
|
+
`.view()`); `parseCpiEvents` / `eventsOf` for `emitCpi` events, and `watchEvents` (log subscription). Every Anchor
|
|
34
|
+
framework error has its name in `errors`, so `isError` knows the codes `AnchorError` and the constraints raise.
|
|
35
|
+
|
|
8
36
|
## 0.19.0 — 2026-10-28
|
|
9
37
|
|
|
10
38
|
Anchor's error codes where TypeSOL raised its own: `TokenOf` / `MintOf` constraints that name the parameters as
|
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.21).** 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/cli.js
CHANGED
|
@@ -277,14 +277,37 @@ const COMMANDS = {
|
|
|
277
277
|
},
|
|
278
278
|
},
|
|
279
279
|
client: {
|
|
280
|
-
usage: "typesol client <idl.json> [--out <file.ts>] [--address <base58>]",
|
|
281
|
-
help:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
usage: "typesol client <idl.json> [--out <file.ts>] [--address <base58>]\n typesol client --address <program id> [--url <rpc>] [--config <solana cli config>] [--out <file.ts>]",
|
|
281
|
+
help: [
|
|
282
|
+
"Generate a typed TypeScript client (on @solana/kit) from an Anchor IDL (0.30+/1.x or TypeSOL). The IDL is validated first;",
|
|
283
|
+
"what the client leaves out is printed as notes. `typesol build` already writes <name>.client.ts for TypeSOL programs.",
|
|
284
|
+
"Without an IDL file, the IDL is read from the chain for the program at --address: the canonical Program Metadata IDL",
|
|
285
|
+
"(`typesol idl publish`, Codama), else Anchor's IDL account (`anchor idl init`).",
|
|
286
|
+
].join("\n "),
|
|
287
|
+
options: { out: { type: "string" }, address: { type: "string" }, url: { type: "string" }, config: { type: "string" } },
|
|
288
|
+
async run(v, args) {
|
|
285
289
|
if (v.address && !decodeAddress(v.address))
|
|
286
290
|
throw new UsageError(`--address "${v.address}" is not a base58 Solana address`);
|
|
287
|
-
|
|
291
|
+
let idl;
|
|
292
|
+
if (args.length === 0) {
|
|
293
|
+
if (!v.address)
|
|
294
|
+
throw new UsageError("pass an IDL file, or --address to read the program's IDL from the chain");
|
|
295
|
+
const rpcUrl = v.url ?? solanaConfig(v.config).url;
|
|
296
|
+
if (!rpcUrl)
|
|
297
|
+
throw new Error("no RPC: pass --url, or set json_rpc_url in the Solana CLI config");
|
|
298
|
+
// Loaded only here: the other commands do not need an RPC client.
|
|
299
|
+
const kit = await import("@solana/kit");
|
|
300
|
+
const { fetchProgramIdl } = await import("./idlpublish.js");
|
|
301
|
+
const found = await fetchProgramIdl(kit.createSolanaRpc(clusterUrl(rpcUrl)), kit.address(v.address));
|
|
302
|
+
console.error(` IDL from ${found.source === "anchor" ? "Anchor's IDL account" : "the Program Metadata program"}`);
|
|
303
|
+
idl = found.idl;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
if (v.url)
|
|
307
|
+
throw new UsageError("--url reads the IDL from the chain: leave out the IDL file");
|
|
308
|
+
idl = readJson(one(args, "<idl.json>"));
|
|
309
|
+
}
|
|
310
|
+
const res = generateClient(idl, { address: v.address });
|
|
288
311
|
if (v.out) {
|
|
289
312
|
writeFileSync(v.out, res.source);
|
|
290
313
|
log(` wrote ${v.out}`);
|
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,25 @@ 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
|
+
"GetTransactionApi",
|
|
184
|
+
"LogsNotificationsApi",
|
|
185
|
+
"RpcSubscriptions",
|
|
186
|
+
"SendTransactionApi",
|
|
187
|
+
"Signature",
|
|
188
|
+
"SignatureNotificationsApi",
|
|
189
|
+
"SimulateTransactionApi",
|
|
190
|
+
"SlotNotificationsApi",
|
|
168
191
|
"Codec",
|
|
169
192
|
"FetchAccountConfig",
|
|
170
193
|
"GetAccountInfoApi",
|
|
@@ -176,6 +199,21 @@ const KIT_TYPES = [
|
|
|
176
199
|
];
|
|
177
200
|
const KIT_VALUES = [
|
|
178
201
|
"AccountRole",
|
|
202
|
+
"appendTransactionMessageInstructions",
|
|
203
|
+
"assertIsTransactionWithBlockhashLifetime",
|
|
204
|
+
"compileTransaction",
|
|
205
|
+
"createTransactionMessage",
|
|
206
|
+
"fetchEncodedAccounts",
|
|
207
|
+
"getBase58Decoder",
|
|
208
|
+
"getBase58Encoder",
|
|
209
|
+
"getBase64EncodedWireTransaction",
|
|
210
|
+
"getSignatureFromTransaction",
|
|
211
|
+
"pipe",
|
|
212
|
+
"sendAndConfirmTransactionFactory",
|
|
213
|
+
"setTransactionMessageFeePayer",
|
|
214
|
+
"setTransactionMessageFeePayerSigner",
|
|
215
|
+
"setTransactionMessageLifetimeUsingBlockhash",
|
|
216
|
+
"signTransactionMessageWithSigners",
|
|
179
217
|
"SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM",
|
|
180
218
|
"addCodecSizePrefix",
|
|
181
219
|
"address",
|
|
@@ -214,7 +252,23 @@ const KIT_VALUES = [
|
|
|
214
252
|
"transformCodec",
|
|
215
253
|
];
|
|
216
254
|
// Keys of the program object that instructions must not take.
|
|
217
|
-
const PROGRAM_KEYS = new Set([
|
|
255
|
+
const PROGRAM_KEYS = new Set([
|
|
256
|
+
"address",
|
|
257
|
+
"accounts",
|
|
258
|
+
"pdas",
|
|
259
|
+
"returns",
|
|
260
|
+
"views",
|
|
261
|
+
"resolve",
|
|
262
|
+
"errors",
|
|
263
|
+
"errorCode",
|
|
264
|
+
"isError",
|
|
265
|
+
"parseEvents",
|
|
266
|
+
"parseCpiEvents",
|
|
267
|
+
"eventsOf",
|
|
268
|
+
"watchEvents",
|
|
269
|
+
"sendInstructions",
|
|
270
|
+
"simulate",
|
|
271
|
+
]);
|
|
218
272
|
// snake_case → camelCase by splitting (linear time, whatever the input): "taker_token_a" → "takerTokenA".
|
|
219
273
|
const camel = (s) => s
|
|
220
274
|
.split("_")
|
|
@@ -671,6 +725,8 @@ export function generateClient(idl, opts = {}) {
|
|
|
671
725
|
// ---- Accounts ---------------------------------------------------------------------------------------------
|
|
672
726
|
const accountOut = [];
|
|
673
727
|
const accountKeys = [];
|
|
728
|
+
/** Each account's decoder object and its Pubkey fields (snake_case), for has_one relations. */
|
|
729
|
+
const accountConsts = [];
|
|
674
730
|
for (const a of idl.accounts ?? []) {
|
|
675
731
|
if (!defs.has(a.name) || typeIssue(a.name) || special(a.name)) {
|
|
676
732
|
notes.push(`account ${a.name}: ${typeIssue(a.name) ?? "no type definition"}; it is left out`);
|
|
@@ -699,8 +755,28 @@ export function generateClient(idl, opts = {}) {
|
|
|
699
755
|
` const found = await fetchEncodedAccount(rpc, at, config);\n` +
|
|
700
756
|
` return found.exists ? ${A}.decode(found.data) : null;\n` +
|
|
701
757
|
` },\n` +
|
|
758
|
+
` /** Several accounts in one request, in order; null where one does not exist. */\n` +
|
|
759
|
+
` async fetchMultiple(rpc: Rpc<GetMultipleAccountsApi>, at: readonly Address[], config?: FetchAccountsConfig): Promise<(${T} | null)[]> {\n` +
|
|
760
|
+
` const found = await fetchEncodedAccounts(rpc, [...at], config);\n` +
|
|
761
|
+
` return found.map((a) => (a.exists ? ${A}.decode(a.data) : null));\n` +
|
|
762
|
+
` },\n` +
|
|
763
|
+
` /** Every ${commentText(a.name)} account of the program (getProgramAccounts by discriminator), optionally where \`bytes\` are at \`offset\` of the data. */\n` +
|
|
764
|
+
` async fetchAll(rpc: Rpc<GetProgramAccountsApi>, filters: readonly { offset: number; bytes: ReadonlyUint8Array }[] = []): Promise<{ address: Address; data: ${T} }[]> {\n` +
|
|
765
|
+
` const found = await rpc.getProgramAccounts(PROGRAM_ADDRESS, { encoding: "base64", filters: memcmp([{ offset: 0, bytes: ${A}_DISCRIMINATOR }, ...filters]) }).send();\n` +
|
|
766
|
+
` return found.map((a) => ({ address: a.pubkey, data: ${A}.decode(getBase64Encoder().encode(a.account.data[0])) }));\n` +
|
|
767
|
+
` },\n` +
|
|
768
|
+
` /** Each change of the account, decoded (null once it is closed), until \`abortSignal\` aborts. */\n` +
|
|
769
|
+
` async *watch(rpcSubscriptions: RpcSubscriptions<AccountNotificationsApi>, at: Address, options: { abortSignal: AbortSignal; commitment?: Commitment }): AsyncGenerator<${T} | null> {\n` +
|
|
770
|
+
` const updates = await rpcSubscriptions.accountNotifications(at, { encoding: "base64", ...(options.commitment ? { commitment: options.commitment } : {}) }).subscribe({ abortSignal: options.abortSignal });\n` +
|
|
771
|
+
` for await (const u of updates) {\n` +
|
|
772
|
+
` const data = getBase64Encoder().encode(u.value.data[0]);\n` +
|
|
773
|
+
` yield data.length === 0 ? null : ${A}.decode(data);\n` +
|
|
774
|
+
` }\n` +
|
|
775
|
+
` },\n` +
|
|
702
776
|
`};\n`);
|
|
703
777
|
accountKeys.push(`${prop(a.name)}: ${A}`);
|
|
778
|
+
const fields = (defs.get(a.name).type.fields ?? []).filter((f) => typeof f === "object" && f !== null && "name" in f);
|
|
779
|
+
accountConsts.push({ name: a.name, A, pubkeys: new Set(fields.filter((f) => f.type === "pubkey" || f.type === "publicKey").map((f) => f.name)) });
|
|
704
780
|
}
|
|
705
781
|
// ---- Events -----------------------------------------------------------------------------------------------
|
|
706
782
|
const events = (idl.events ?? []).filter((e) => {
|
|
@@ -724,6 +800,8 @@ export function generateClient(idl, opts = {}) {
|
|
|
724
800
|
const ixKeys = [];
|
|
725
801
|
const pdaFns = new Map();
|
|
726
802
|
const returnKeys = [];
|
|
803
|
+
const viewKeys = [];
|
|
804
|
+
const resolveKeys = [];
|
|
727
805
|
const usedKeys = new Set();
|
|
728
806
|
for (const ix of idl.instructions) {
|
|
729
807
|
const argIssue = ix.args.map((a) => issue(a.type)).find(Boolean);
|
|
@@ -825,7 +903,7 @@ export function generateClient(idl, opts = {}) {
|
|
|
825
903
|
resolvable.delete(a.key);
|
|
826
904
|
}
|
|
827
905
|
const auto = (a) => Boolean(a.address) || resolvable.has(a.key);
|
|
828
|
-
use("type Address", "type TransactionSigner", "type Instruction", "AccountRole");
|
|
906
|
+
use("type Address", "type TransactionSigner", "type Instruction", "type AccountMeta", "AccountRole");
|
|
829
907
|
const accType = (a) => `${a.signer ? "TransactionSigner | Address" : "Address"}${a.optional ? " | null" : ""}`;
|
|
830
908
|
const accountsIface = `export interface ${P}Accounts {\n` +
|
|
831
909
|
flat.map((a) => `${doc(a.docs, " ")} ${prop(a.key)}${auto(a) || a.optional ? "?" : ""}: ${accType(a)};`).join("\n") +
|
|
@@ -896,16 +974,132 @@ export function generateClient(idl, opts = {}) {
|
|
|
896
974
|
: bytes(ix.discriminator);
|
|
897
975
|
ixOut.push(accountsIface +
|
|
898
976
|
argsIface +
|
|
899
|
-
`${doc(ix.docs)}async function build${P}(input: ${P}Accounts${argsField.length ? `, args: ${P}Args` : ""}): Promise<Instruction> {\n` +
|
|
977
|
+
`${doc(ix.docs)}async function build${P}(input: ${P}Accounts${argsField.length ? `, args: ${P}Args` : ""}, remaining: readonly AccountMeta[] = []): Promise<Instruction> {\n` +
|
|
900
978
|
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`);
|
|
979
|
+
`\n return {\n programAddress: PROGRAM_ADDRESS,\n accounts: [\n${metas.join("\n")}\n ...remaining,\n ],\n data: ${data},\n } as Instruction;\n}\n`);
|
|
902
980
|
ixKeys.push(`${prop(fn)}: build${P}`);
|
|
981
|
+
// has_one relations (Anchor's `relations`): a non-signing account another account of the instruction names in a
|
|
982
|
+
// Pubkey field can be read from that account on chain.
|
|
983
|
+
const rels = [];
|
|
984
|
+
for (const a of flat) {
|
|
985
|
+
if (auto(a) || a.optional || a.signer || !a.relations?.length)
|
|
986
|
+
continue;
|
|
987
|
+
for (const r of a.relations) {
|
|
988
|
+
const source = byPath(r, a.group);
|
|
989
|
+
if (!source || source.optional)
|
|
990
|
+
continue;
|
|
991
|
+
const readers = accountConsts
|
|
992
|
+
.filter((c) => c.pubkeys.has(a.name))
|
|
993
|
+
.map((c) => `(d) => (hasPrefix(d, ${c.A}.discriminator) ? ${c.A}.decode(d)[${JSON.stringify(camel(a.name))}] : undefined)`);
|
|
994
|
+
if (!readers.length)
|
|
995
|
+
continue;
|
|
996
|
+
rels.push({ a, source, readers });
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// PDAs seeded from fields of another account of the instruction (Anchor `seeds = [b"vault", book.owner]`): the
|
|
1001
|
+
// account is read on chain, then the PDA derived. Only scalar fields (integers, bool, Pubkey, unit enums).
|
|
1002
|
+
const inputOf = (f) => `input${NAME.test(f.key) && !JS_RESERVED.has(f.key) ? `.${f.key}` : `[${JSON.stringify(f.key)}]`}`;
|
|
1003
|
+
const scalarSeed = (t) => {
|
|
1004
|
+
if (t === undefined)
|
|
1005
|
+
return false;
|
|
1006
|
+
if (typeof t === "string")
|
|
1007
|
+
return t in INTS || t === "bool" || t === "pubkey" || t === "publicKey";
|
|
1008
|
+
if (!t.defined)
|
|
1009
|
+
return false;
|
|
1010
|
+
const d = defs.get(definedName(t) ?? "");
|
|
1011
|
+
return d?.type.kind === "enum" && (d.type.variants ?? []).every((v) => !v.fields?.length);
|
|
1012
|
+
};
|
|
1013
|
+
const fieldPdas = [];
|
|
1014
|
+
for (const a of flat) {
|
|
1015
|
+
if (!a.pda || a.address || resolvable.has(a.key) || a.optional)
|
|
1016
|
+
continue;
|
|
1017
|
+
const p = a.pda.program;
|
|
1018
|
+
if (p && p.kind !== "const")
|
|
1019
|
+
continue;
|
|
1020
|
+
const bases = new Map();
|
|
1021
|
+
const seeds = [];
|
|
1022
|
+
let ok = true;
|
|
1023
|
+
let usesArgs = false;
|
|
1024
|
+
for (const sd of a.pda.seeds) {
|
|
1025
|
+
if (sd.kind === "const")
|
|
1026
|
+
seeds.push(bytes(sd.value));
|
|
1027
|
+
else if (sd.kind === "arg") {
|
|
1028
|
+
const e = seedExpr(sd, a.group);
|
|
1029
|
+
if (typeof e !== "string")
|
|
1030
|
+
ok = false;
|
|
1031
|
+
else
|
|
1032
|
+
(seeds.push(e), (usesArgs = true));
|
|
1033
|
+
}
|
|
1034
|
+
else if (!sd.path.includes(".")) {
|
|
1035
|
+
const t = byPath(sd.path, a.group);
|
|
1036
|
+
if (!t || t === a || auto(t) || t.optional)
|
|
1037
|
+
ok = false;
|
|
1038
|
+
else
|
|
1039
|
+
seeds.push(`getAddressEncoder().encode(key(${inputOf(t)}))`);
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
const parts = sd.path.split(".");
|
|
1043
|
+
const base = parts.length === 2 ? byPath(parts[0], a.group) : undefined;
|
|
1044
|
+
const acc = accountConsts.find((c) => c.name === sd.account);
|
|
1045
|
+
const field = (defs.get(sd.account ?? "")?.type.fields ?? []).find((f) => typeof f === "object" && f !== null && f.name === parts[1]);
|
|
1046
|
+
if (!base || base === a || base.optional || auto(base) || !acc || !field || !scalarSeed(field.type))
|
|
1047
|
+
ok = false;
|
|
1048
|
+
else {
|
|
1049
|
+
bases.set(base.key, { base, A: acc.A });
|
|
1050
|
+
seeds.push(`${codec(field.type)}.encode(${"$$" + base.key}[${JSON.stringify(camel(parts[1]))}])`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (!ok)
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
if (!ok || bases.size === 0)
|
|
1057
|
+
continue;
|
|
1058
|
+
const program = p ? `programFromBytes(${bytes(p.value)})` : "PROGRAM_ADDRESS";
|
|
1059
|
+
fieldPdas.push({
|
|
1060
|
+
a,
|
|
1061
|
+
usesArgs,
|
|
1062
|
+
lines: [
|
|
1063
|
+
` const ${"$" + a.key} =`,
|
|
1064
|
+
` ${inputOf(a)} ??`,
|
|
1065
|
+
` (await (async () => {`,
|
|
1066
|
+
...[...bases.values()].map(({ base, A }) => ` const ${"$$" + base.key} = await ${A}.fetch(rpc, key(${inputOf(base)}));`),
|
|
1067
|
+
` return (await getProgramDerivedAddress({ programAddress: ${program}, seeds: [${seeds.join(", ")}] }))[0];`,
|
|
1068
|
+
` })());`,
|
|
1069
|
+
],
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (rels.length || fieldPdas.length) {
|
|
1073
|
+
const filled = [...rels.map((r) => r.a), ...fieldPdas.map((f) => f.a)];
|
|
1074
|
+
const keysOf = filled.map((a) => JSON.stringify(a.key)).join(" | ");
|
|
1075
|
+
const withArgs = fieldPdas.some((f) => f.usesArgs);
|
|
1076
|
+
const what = [
|
|
1077
|
+
...rels.map((r) => `${r.a.key} (from ${r.source.key}.${camel(r.a.name)})`),
|
|
1078
|
+
...fieldPdas.map((f) => `${f.a.key} (a PDA seeded from account fields)`),
|
|
1079
|
+
].join(", ");
|
|
1080
|
+
resolveKeys.push(` /** ${P}Accounts with ${what} read from the chain when left out. */\n` +
|
|
1081
|
+
` async ${prop(fn)}(rpc: Rpc<GetAccountInfoApi>, input: Omit<${P}Accounts, ${keysOf}> & Partial<Pick<${P}Accounts, ${keysOf}>>${withArgs ? `, args: ${P}Args` : ""}): Promise<${P}Accounts> {\n` +
|
|
1082
|
+
[
|
|
1083
|
+
...rels.map((r) => ` const ${"$" + r.a.key} = ${inputOf(r.a)} ?? (await readRelation(rpc, key(${inputOf(r.source)}), ${JSON.stringify(r.a.key)}, [${r.readers.join(", ")}]));`),
|
|
1084
|
+
...fieldPdas.flatMap((f) => f.lines),
|
|
1085
|
+
].join("\n") +
|
|
1086
|
+
`\n return { ...input, ${filled.map((a) => `${prop(a.key)}: ${"$" + a.key}`).join(", ")} } as ${P}Accounts;\n },`);
|
|
1087
|
+
}
|
|
903
1088
|
if (ix.returns !== undefined) {
|
|
904
1089
|
const why = issue(ix.returns);
|
|
905
1090
|
if (why)
|
|
906
1091
|
notes.push(`instruction ${ix.name}: the return value is not decoded (${why})`);
|
|
907
|
-
else
|
|
1092
|
+
else {
|
|
908
1093
|
returnKeys.push(` ${prop(fn)}: (data: ReadonlyUint8Array): ${tsType(ix.returns, "out")} => ${codec(ix.returns)}.decode(data),`);
|
|
1094
|
+
// Read-only call: simulate the instruction and decode what it returns (Anchor's `.view()`).
|
|
1095
|
+
viewKeys.push(` /** ${ix.name}'s return value, by simulation (nothing is sent). */\n` +
|
|
1096
|
+
` async ${prop(fn)}(rpc: Rpc<SimulateTransactionApi & GetLatestBlockhashApi>, feePayer: Address | TransactionSigner, input: ${P}Accounts${argsField.length ? `, args: ${P}Args` : ""}): Promise<${tsType(ix.returns, "out")}> {\n` +
|
|
1097
|
+
` const res = await simulate(rpc, feePayer, [await build${P}(input${argsField.length ? ", args" : ""})]);\n` +
|
|
1098
|
+
` 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` +
|
|
1099
|
+
` if (res.returnData === null) throw new Error(${JSON.stringify(`${ix.name} returned nothing`)});\n` +
|
|
1100
|
+
` return ${codec(ix.returns)}.decode(res.returnData);\n` +
|
|
1101
|
+
` },`);
|
|
1102
|
+
}
|
|
909
1103
|
}
|
|
910
1104
|
}
|
|
911
1105
|
// ---- Assemble ---------------------------------------------------------------------------------------------
|
|
@@ -961,6 +1155,28 @@ export function generateClient(idl, opts = {}) {
|
|
|
961
1155
|
],
|
|
962
1156
|
},
|
|
963
1157
|
{ name: "programFromBytes", lines: [`const programFromBytes = (b: Uint8Array): Address => getAddressDecoder().decode(b);`] },
|
|
1158
|
+
{
|
|
1159
|
+
name: "readRelation",
|
|
1160
|
+
lines: [
|
|
1161
|
+
`/** A Pubkey field of an account on chain, read by the first reader that knows its type. */`,
|
|
1162
|
+
`const readRelation = async (rpc: Rpc<GetAccountInfoApi>, at: Address, name: string, readers: ((data: ReadonlyUint8Array) => Address | undefined)[]): Promise<Address> => {`,
|
|
1163
|
+
` const found = await fetchEncodedAccount(rpc, at);`,
|
|
1164
|
+
` if (!found.exists) throw new Error("cannot fill in " + name + ": account " + at + " does not exist");`,
|
|
1165
|
+
` for (const read of readers) {`,
|
|
1166
|
+
` const v = read(found.data);`,
|
|
1167
|
+
` if (v !== undefined) return v;`,
|
|
1168
|
+
` }`,
|
|
1169
|
+
` throw new Error("cannot fill in " + name + ": " + at + " is not an account that names it");`,
|
|
1170
|
+
`};`,
|
|
1171
|
+
],
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
name: "memcmp",
|
|
1175
|
+
lines: [
|
|
1176
|
+
`const memcmp = (filters: readonly { offset: number; bytes: ReadonlyUint8Array }[]) =>`,
|
|
1177
|
+
` filters.map((f) => ({ memcmp: { offset: BigInt(f.offset), bytes: getBase58Decoder().decode(f.bytes) as Base58EncodedBytes, encoding: "base58" as const } }));`,
|
|
1178
|
+
],
|
|
1179
|
+
},
|
|
964
1180
|
];
|
|
965
1181
|
if (needsFixedString)
|
|
966
1182
|
helperDefs.push({
|
|
@@ -1078,30 +1294,135 @@ export function generateClient(idl, opts = {}) {
|
|
|
1078
1294
|
"",
|
|
1079
1295
|
`// ---- Events ----`,
|
|
1080
1296
|
`export type Event = ${events.length ? events.map((e) => `{ name: ${JSON.stringify(e.name)}; data: ${tsName(e.name)} }`).join(" | ") : "never"};`,
|
|
1297
|
+
`/** One event's bytes (discriminator, then the fields), or null when they are not one of this program's events. */`,
|
|
1298
|
+
`function decodeEvent(data: ReadonlyUint8Array): Event | null {`,
|
|
1299
|
+
events.length
|
|
1300
|
+
? [
|
|
1301
|
+
` try {`,
|
|
1302
|
+
...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}) };`),
|
|
1303
|
+
` } catch {`,
|
|
1304
|
+
` // not one of this program's events`,
|
|
1305
|
+
` }`,
|
|
1306
|
+
` return null;`,
|
|
1307
|
+
].join("\n")
|
|
1308
|
+
: ` void data;\n return null;`,
|
|
1309
|
+
`}`,
|
|
1081
1310
|
`/**`,
|
|
1082
1311
|
` * Events in a transaction's log messages (\`Program data:\` lines, as \`emit\` writes them). Lines are matched by`,
|
|
1083
1312
|
` * discriminator, whichever program wrote them; lines that do not decode are skipped. \`emitCpi\` events travel in inner`,
|
|
1084
|
-
` * instructions
|
|
1313
|
+
` * instructions: see parseCpiEvents and eventsOf.`,
|
|
1085
1314
|
` */`,
|
|
1086
1315
|
`export function parseEvents(logs: readonly string[]): Event[] {`,
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1316
|
+
` const found: Event[] = [];`,
|
|
1317
|
+
` for (const line of logs) {`,
|
|
1318
|
+
` if (!line.startsWith("Program data: ")) continue;`,
|
|
1319
|
+
` let data: ReadonlyUint8Array;`,
|
|
1320
|
+
` try {`,
|
|
1321
|
+
` data = getBase64Encoder().encode(line.slice("Program data: ".length));`,
|
|
1322
|
+
` } catch {`,
|
|
1323
|
+
` continue;`,
|
|
1324
|
+
` }`,
|
|
1325
|
+
` const e = decodeEvent(data);`,
|
|
1326
|
+
` if (e) found.push(e);`,
|
|
1327
|
+
` }`,
|
|
1328
|
+
` return found;`,
|
|
1329
|
+
`}`,
|
|
1330
|
+
`/** \`emitCpi\` events: the data of inner instructions (bytes, or base58 as RPC returns them) that carry one. */`,
|
|
1331
|
+
`export function parseCpiEvents(instructionData: Iterable<ReadonlyUint8Array | string>): Event[] {`,
|
|
1332
|
+
` const tag = new Uint8Array([228, 69, 165, 46, 81, 203, 154, 29]);`,
|
|
1333
|
+
` const found: Event[] = [];`,
|
|
1334
|
+
` for (const item of instructionData) {`,
|
|
1335
|
+
` let data: ReadonlyUint8Array;`,
|
|
1336
|
+
` try {`,
|
|
1337
|
+
` data = typeof item === "string" ? getBase58Encoder().encode(item) : item;`,
|
|
1338
|
+
` } catch {`,
|
|
1339
|
+
` continue;`,
|
|
1340
|
+
` }`,
|
|
1341
|
+
` if (!hasPrefix(data, tag)) continue;`,
|
|
1342
|
+
` const e = decodeEvent(data.slice(tag.length));`,
|
|
1343
|
+
` if (e) found.push(e);`,
|
|
1344
|
+
` }`,
|
|
1345
|
+
` return found;`,
|
|
1346
|
+
`}`,
|
|
1347
|
+
`/** Every event of a fetched transaction (getTransaction, json encoding): its logs, then its inner instructions. */`,
|
|
1348
|
+
`export function eventsOf(tx: { meta: { logMessages?: readonly string[] | null; innerInstructions?: readonly { instructions: readonly { data: string }[] }[] | null } | null } | null): Event[] {`,
|
|
1349
|
+
` const meta = tx?.meta;`,
|
|
1350
|
+
` if (!meta) return [];`,
|
|
1351
|
+
` return [...parseEvents(meta.logMessages ?? []), ...parseCpiEvents((meta.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))];`,
|
|
1352
|
+
`}`,
|
|
1353
|
+
`/**`,
|
|
1354
|
+
` * Events of every transaction that mentions the program, as their logs arrive, until \`abortSignal\` aborts. With`,
|
|
1355
|
+
` * \`rpc\`, \`emitCpi\` events too: a transaction in which the program invoked itself is fetched (getTransaction) and its`,
|
|
1356
|
+
` * inner instructions read, after its log events.`,
|
|
1357
|
+
` */`,
|
|
1358
|
+
`export async function* watchEvents(`,
|
|
1359
|
+
` rpcSubscriptions: RpcSubscriptions<LogsNotificationsApi>,`,
|
|
1360
|
+
` options: { abortSignal: AbortSignal; commitment?: Commitment; rpc?: Rpc<GetTransactionApi> },`,
|
|
1361
|
+
`): AsyncGenerator<{ signature: Signature; event: Event }> {`,
|
|
1362
|
+
` const logs = await rpcSubscriptions.logsNotifications({ mentions: [PROGRAM_ADDRESS] }, options.commitment ? { commitment: options.commitment } : {}).subscribe({ abortSignal: options.abortSignal });`,
|
|
1363
|
+
` const selfInvoke = new RegExp("^Program " + PROGRAM_ADDRESS + " invoke \\\\[([2-9]|\\\\d{2,})\\\\]$");`,
|
|
1364
|
+
` for await (const n of logs) {`,
|
|
1365
|
+
` for (const event of parseEvents(n.value.logs)) yield { signature: n.value.signature, event };`,
|
|
1366
|
+
` if (!options.rpc || !n.value.logs.some((l) => selfInvoke.test(l))) continue;`,
|
|
1367
|
+
` // The transaction can trail its logs by a moment: a few tries.`,
|
|
1368
|
+
` for (let attempt = 0; attempt < 5 && !options.abortSignal.aborted; attempt++) {`,
|
|
1369
|
+
` const tx = await options.rpc`,
|
|
1370
|
+
` .getTransaction(n.value.signature, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: options.commitment === "finalized" ? "finalized" : "confirmed" })`,
|
|
1371
|
+
` .send();`,
|
|
1372
|
+
` if (tx) {`,
|
|
1373
|
+
` for (const event of parseCpiEvents((tx.meta?.innerInstructions ?? []).flatMap((g) => g.instructions.map((i) => i.data)))) yield { signature: n.value.signature, event };`,
|
|
1374
|
+
` break;`,
|
|
1375
|
+
` }`,
|
|
1376
|
+
` await new Promise((resolve) => setTimeout(resolve, 400));`,
|
|
1377
|
+
` }`,
|
|
1378
|
+
` }`,
|
|
1379
|
+
`}`,
|
|
1380
|
+
"",
|
|
1381
|
+
`// ---- Transactions ----`,
|
|
1382
|
+
`/** Send instructions in one transaction paid by \`feePayer\` (signers in the accounts sign too) and wait for confirmation. */`,
|
|
1383
|
+
`export async function sendInstructions(`,
|
|
1384
|
+
` connection: { rpc: Rpc<GetEpochInfoApi & GetLatestBlockhashApi & GetSignatureStatusesApi & SendTransactionApi>; rpcSubscriptions: RpcSubscriptions<SignatureNotificationsApi & SlotNotificationsApi> },`,
|
|
1385
|
+
` feePayer: TransactionSigner,`,
|
|
1386
|
+
` instructions: readonly Instruction[],`,
|
|
1387
|
+
` options: { commitment?: Commitment; abortSignal?: AbortSignal } = {},`,
|
|
1388
|
+
`): Promise<Signature> {`,
|
|
1389
|
+
` const { value: latestBlockhash } = await connection.rpc.getLatestBlockhash().send();`,
|
|
1390
|
+
` const message = pipe(`,
|
|
1391
|
+
` createTransactionMessage({ version: 0 }),`,
|
|
1392
|
+
` (m) => setTransactionMessageFeePayerSigner(feePayer, m),`,
|
|
1393
|
+
` (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),`,
|
|
1394
|
+
` (m) => appendTransactionMessageInstructions(instructions, m),`,
|
|
1395
|
+
` );`,
|
|
1396
|
+
` const signed = await signTransactionMessageWithSigners(message);`,
|
|
1397
|
+
` assertIsTransactionWithBlockhashLifetime(signed);`,
|
|
1398
|
+
` await sendAndConfirmTransactionFactory(connection)(signed, { commitment: options.commitment ?? "confirmed", ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}) });`,
|
|
1399
|
+
` return getSignatureFromTransaction(signed);`,
|
|
1400
|
+
`}`,
|
|
1401
|
+
`/** Simulate instructions (nothing is sent, signatures are not checked): the error if any, logs, units and the program's return data. */`,
|
|
1402
|
+
`export async function simulate(`,
|
|
1403
|
+
` rpc: Rpc<SimulateTransactionApi & GetLatestBlockhashApi>,`,
|
|
1404
|
+
` feePayer: Address | TransactionSigner,`,
|
|
1405
|
+
` instructions: readonly Instruction[],`,
|
|
1406
|
+
`): Promise<{ err: unknown; logs: readonly string[]; unitsConsumed: bigint | null; returnData: ReadonlyUint8Array | null }> {`,
|
|
1407
|
+
` const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();`,
|
|
1408
|
+
` const message = pipe(`,
|
|
1409
|
+
` createTransactionMessage({ version: 0 }),`,
|
|
1410
|
+
` (m) => setTransactionMessageFeePayer(key(feePayer), m),`,
|
|
1411
|
+
` (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),`,
|
|
1412
|
+
` (m) => appendTransactionMessageInstructions(instructions, m),`,
|
|
1413
|
+
` );`,
|
|
1414
|
+
` const wire = getBase64EncodedWireTransaction(compileTransaction(message));`,
|
|
1415
|
+
` const { value } = await rpc.simulateTransaction(wire, { encoding: "base64", sigVerify: false, replaceRecentBlockhash: true }).send();`,
|
|
1416
|
+
` const ret = value.returnData;`,
|
|
1417
|
+
` return {`,
|
|
1418
|
+
` err: value.err,`,
|
|
1419
|
+
` logs: value.logs ?? [],`,
|
|
1420
|
+
` unitsConsumed: value.unitsConsumed ?? null,`,
|
|
1421
|
+
` returnData: ret && ret.programId === PROGRAM_ADDRESS ? getBase64Encoder().encode(ret.data[0]) : null,`,
|
|
1422
|
+
` };`,
|
|
1102
1423
|
`}`,
|
|
1103
1424
|
"",
|
|
1104
|
-
`/** The ${displayName} program: instructions, accounts, PDAs, errors, events
|
|
1425
|
+
`/** The ${displayName} program: instructions, accounts, PDAs, errors, events, return values and transactions. */`,
|
|
1105
1426
|
`export const ${programName} = {`,
|
|
1106
1427
|
` address: PROGRAM_ADDRESS,`,
|
|
1107
1428
|
...ixKeys.map((k) => ` ${k},`),
|
|
@@ -1112,10 +1433,21 @@ export function generateClient(idl, opts = {}) {
|
|
|
1112
1433
|
` returns: {`,
|
|
1113
1434
|
...returnKeys,
|
|
1114
1435
|
` },`,
|
|
1436
|
+
` views: {`,
|
|
1437
|
+
...viewKeys,
|
|
1438
|
+
` },`,
|
|
1439
|
+
` resolve: {`,
|
|
1440
|
+
...resolveKeys,
|
|
1441
|
+
` },`,
|
|
1115
1442
|
` errors: ERRORS,`,
|
|
1116
1443
|
` errorCode,`,
|
|
1117
1444
|
` isError,`,
|
|
1118
1445
|
` parseEvents,`,
|
|
1446
|
+
` parseCpiEvents,`,
|
|
1447
|
+
` eventsOf,`,
|
|
1448
|
+
` watchEvents,`,
|
|
1449
|
+
` sendInstructions,`,
|
|
1450
|
+
` simulate,`,
|
|
1119
1451
|
`};`,
|
|
1120
1452
|
"",
|
|
1121
1453
|
];
|
package/dist/idlpublish.d.ts
CHANGED
|
@@ -27,3 +27,15 @@ export declare function publishIdl(chain: Chain, opts: PublishOptions): Promise<
|
|
|
27
27
|
export declare function metadataAddress(program: Address, authority: Address | null): Promise<Address>;
|
|
28
28
|
/** Read an IDL back (canonical, or the one `authority` wrote). */
|
|
29
29
|
export declare function fetchIdl(rpc: Chain["rpc"], program: Address, authority?: Address | null): Promise<object>;
|
|
30
|
+
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
31
|
+
export declare function anchorIdlAddress(program: Address): Promise<Address>;
|
|
32
|
+
/** The IDL in Anchor's IDL account (`anchor idl init`), or null when the program has none. */
|
|
33
|
+
export declare function fetchAnchorIdl(rpc: Chain["rpc"], program: Address): Promise<object | null>;
|
|
34
|
+
/**
|
|
35
|
+
* A program's IDL from the chain: the canonical Program Metadata IDL (`typesol idl publish`, Codama) first, else
|
|
36
|
+
* Anchor's IDL account (`anchor idl init`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function fetchProgramIdl(rpc: Chain["rpc"], program: Address): Promise<{
|
|
39
|
+
idl: object;
|
|
40
|
+
source: "program-metadata" | "anchor";
|
|
41
|
+
}>;
|
package/dist/idlpublish.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Works against an RPC (`rpcChain`) or anything else that can read accounts and send transactions (tests use LiteSVM).
|
|
7
7
|
*/
|
|
8
|
+
import { inflateSync } from "node:zlib";
|
|
8
9
|
import { Compression, Encoding, Format, fetchMetadataContent, findMetadataPda, packDirectData, writeMetadata } from "@solana-program/program-metadata";
|
|
9
|
-
import { createClient, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, extendClient, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit";
|
|
10
|
+
import { createAddressWithSeed, createClient, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, extendClient, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit";
|
|
10
11
|
export const PROGRAM_METADATA_ID = "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S";
|
|
11
12
|
/** A Chain over an RPC endpoint (polls signature status: no websocket needed). */
|
|
12
13
|
export function rpcChain(rpc) {
|
|
@@ -114,3 +115,39 @@ export async function metadataAddress(program, authority) {
|
|
|
114
115
|
export async function fetchIdl(rpc, program, authority = null) {
|
|
115
116
|
return JSON.parse(await fetchMetadataContent(rpc, program, "idl", authority));
|
|
116
117
|
}
|
|
118
|
+
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
119
|
+
export async function anchorIdlAddress(program) {
|
|
120
|
+
const [base] = await getProgramDerivedAddress({ programAddress: program, seeds: [] });
|
|
121
|
+
return createAddressWithSeed({ baseAddress: base, programAddress: program, seed: "anchor:idl" });
|
|
122
|
+
}
|
|
123
|
+
/** The IDL in Anchor's IDL account (`anchor idl init`), or null when the program has none. */
|
|
124
|
+
export async function fetchAnchorIdl(rpc, program) {
|
|
125
|
+
const { value } = await rpc.getAccountInfo(await anchorIdlAddress(program), { encoding: "base64" }).send();
|
|
126
|
+
if (!value)
|
|
127
|
+
return null;
|
|
128
|
+
const data = Buffer.from(value.data[0], "base64");
|
|
129
|
+
// IdlAccount: 8-byte discriminator, authority (32 bytes), a u32 length, then the zlib-compressed JSON.
|
|
130
|
+
if (data.length < 44)
|
|
131
|
+
throw new Error("Anchor IDL account is too short");
|
|
132
|
+
const len = data.readUInt32LE(40);
|
|
133
|
+
if (44 + len > data.length)
|
|
134
|
+
throw new Error("Anchor IDL account is truncated");
|
|
135
|
+
return JSON.parse(inflateSync(data.subarray(44, 44 + len)).toString("utf8"));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A program's IDL from the chain: the canonical Program Metadata IDL (`typesol idl publish`, Codama) first, else
|
|
139
|
+
* Anchor's IDL account (`anchor idl init`).
|
|
140
|
+
*/
|
|
141
|
+
export async function fetchProgramIdl(rpc, program) {
|
|
142
|
+
let metadataError;
|
|
143
|
+
try {
|
|
144
|
+
return { idl: await fetchIdl(rpc, program), source: "program-metadata" };
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
metadataError = e;
|
|
148
|
+
}
|
|
149
|
+
const anchor = await fetchAnchorIdl(rpc, program);
|
|
150
|
+
if (anchor)
|
|
151
|
+
return { idl: anchor, source: "anchor" };
|
|
152
|
+
throw new Error(`no IDL on chain for ${program}: none in the Program Metadata program (${metadataError instanceof Error ? metadataError.message : String(metadataError)}) and no Anchor IDL account at ${await anchorIdlAddress(program)}`);
|
|
153
|
+
}
|
package/docs/cli.md
CHANGED
|
@@ -15,7 +15,7 @@ error. Exit codes: **0** success, **1** failure (compile errors, failed deploy,
|
|
|
15
15
|
| `typesol idl publish [<program>…] [--url <rpc>] [--keypair <authority.json>] [--config <cli config>] [--non-canonical]` | Compile (the binary is not rebuilt), then write each IDL to the Program Metadata program (seed `idl`). Canonical IDLs must be signed by the upgrade authority. Defaults come from the Solana CLI config (`~/.config/solana/cli/config.yml`). |
|
|
16
16
|
| `typesol cpi <idl.json> [--out <file.ts>] [--prefix <name>] [--address <base58>]` | Generate TypeSOL helpers that call another program from its IDL. Unsupported instructions are listed on stderr. |
|
|
17
17
|
| `typesol port <idl.json> [--out <file.ts>]` | A TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program: account structs, enums, errors and one function per instruction with its accounts and arguments, each body `require(false, …NotPorted)` until written. Instruction discriminators (an explicit `@discriminator` when the name derives another), account names and error codes are kept, so existing clients keep working where the layouts match; notes on stderr list where they do not (Borsh `String`/`Option` in account data, a `Vec` that is not the last field, custom discriminators or error codes). |
|
|
18
|
-
| `typesol client <idl.json> [--out <file.ts>] [--address <base58>]` | Generate a typed TypeScript client on `@solana/kit` from an Anchor IDL (0.30+/1.x or TypeSOL); the IDL is validated first, and what the client leaves out is printed as notes. See [client.md](client.md). |
|
|
18
|
+
| `typesol client <idl.json> [--out <file.ts>] [--address <base58>]`, `typesol client --address <program id> [--url <rpc>] [--config <cli config>] [--out <file.ts>]` | Generate a typed TypeScript client on `@solana/kit` from an Anchor IDL (0.30+/1.x or TypeSOL); the IDL is validated first, and what the client leaves out is printed as notes. Without a file, the IDL is read from the chain: the canonical Program Metadata IDL, else Anchor's IDL account. See [client.md](client.md). |
|
|
19
19
|
| `typesol verify <program\|file.ts> [<deployed.so>] [--address <program id>] [--url <rpc>] [--config <cli config>] [--sbpf …]` | Rebuild and compare with what is deployed. Without a `.so`, reads the program from the cluster (`--url`, cluster monikers such as `mainnet-beta` included, else the Solana CLI config) at `--address` or the address `typesol.json` records, under any loader (v2, upgradeable v3, v4); prints the upgrade authority (or that the program is immutable) and the last deploy slot, and compares the IDL published with `typesol idl publish` (a published IDL that differs is a mismatch). With a `.so` (e.g. `solana program dump`), compares with that file. Trailing zero padding is ignored; the SBPF version is read from the deployed ELF unless `--sbpf` is given. |
|
|
20
20
|
| `typesol profile <logs.txt\|-> [--program <id>]` | Compute units per TypeScript line from the logs of a `--profile` build's transaction (exit 1 when the logs have no markers). |
|
|
21
21
|
| `typesol help <command>` | Same as `typesol <command> --help`. |
|
package/docs/client.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
Every `typesol build` writes a typed client next to the program: `<out>/<name>/<name>.client.ts`. It is one file, it
|
|
4
4
|
depends only on [`@solana/kit`](https://github.com/anza-xyz/kit) (8.5 or later), and it is rewritten on every build, so
|
|
5
5
|
it always matches the program. `typesol client <idl.json>` writes the same kind of client from an Anchor IDL (Anchor 0.30+
|
|
6
|
-
and 1.x), so one client style covers TypeSOL programs and the Anchor programs they talk to.
|
|
6
|
+
and 1.x), so one client style covers TypeSOL programs and the Anchor programs they talk to. `typesol client --address
|
|
7
|
+
<program id> --url <rpc>` reads the IDL from the chain instead: the canonical Program Metadata IDL (`typesol idl
|
|
8
|
+
publish`, Codama), else Anchor's IDL account (`anchor idl init`), so any deployed Anchor or TypeSOL program with a
|
|
9
|
+
published IDL gets a client from its address alone.
|
|
7
10
|
|
|
8
11
|
```ts
|
|
9
12
|
import { ledgerbook } from "./target/ledgerbook/ledgerbook.client.js";
|
|
@@ -25,23 +28,49 @@ ledgerbook.errorCode(e); // 6002
|
|
|
25
28
|
// Events written by `emit`, from a transaction's log messages
|
|
26
29
|
for (const ev of ledgerbook.parseEvents(logs)) if (ev.name === "Filed") ev.data.amount;
|
|
27
30
|
|
|
28
|
-
// Return data of an instruction that returns a value
|
|
31
|
+
// Return data of an instruction that returns a value, or the value itself by simulation (Anchor's `.view()`)
|
|
29
32
|
ledgerbook.returns.total(returnDataBytes); // bigint
|
|
33
|
+
await ledgerbook.views.total(rpc, signer, { book }, { status: Status.Approved }); // bigint, nothing sent
|
|
34
|
+
|
|
35
|
+
// Send and confirm (one transaction, signers in the accounts sign too), or simulate
|
|
36
|
+
const signature = await ledgerbook.sendInstructions({ rpc, rpcSubscriptions }, signer, [create, file]);
|
|
37
|
+
const { err, logs, unitsConsumed, returnData } = await ledgerbook.simulate(rpc, signer, [file]);
|
|
38
|
+
|
|
39
|
+
// Every Book of the program, or those with given bytes at an offset (after the 8-byte discriminator)
|
|
40
|
+
const books = await ledgerbook.accounts.Book.fetchAll(rpc); // [{ address, data }]
|
|
41
|
+
const several = await ledgerbook.accounts.Book.fetchMultiple(rpc, [a, b]); // (Book | null)[]
|
|
42
|
+
|
|
43
|
+
// Live updates, until the AbortSignal aborts
|
|
44
|
+
for await (const book of ledgerbook.accounts.Book.watch(rpcSubscriptions, address, { abortSignal })) render(book);
|
|
45
|
+
for await (const { signature, event } of ledgerbook.watchEvents(rpcSubscriptions, { abortSignal, rpc })) show(event); // with rpc: emitCpi events too
|
|
46
|
+
|
|
47
|
+
// Remaining accounts go after the instruction's own (Anchor's `.remainingAccounts([...])`)
|
|
48
|
+
await ledgerbook.approveMany({ approver: signer, book }, { indices: [0n, 1n] }, [{ address: extra, role: AccountRole.WRITABLE }]);
|
|
49
|
+
|
|
50
|
+
// has_one accounts that do not sign, read from the account that names them (Anchor resolves `relations` the same way)
|
|
51
|
+
const accounts = await ledgerbook.resolve.someInstruction(rpc, { book }); // fills in what book's Pubkey fields name
|
|
52
|
+
|
|
53
|
+
// Events of a fetched transaction (getTransaction, json): log events and emitCpi events from inner instructions
|
|
54
|
+
ledgerbook.eventsOf(await rpc.getTransaction(signature, { maxSupportedTransactionVersion: 0 }).send());
|
|
30
55
|
```
|
|
31
56
|
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
Signer accounts take a `TransactionSigner`, which kit uses to sign; an `Address` works too when you sign separately.
|
|
58
|
+
Kit's own transaction functions work with the instructions as well; `sendInstructions` is that pipeline in one call.
|
|
34
59
|
|
|
35
60
|
## What the client contains
|
|
36
61
|
|
|
37
62
|
| | |
|
|
38
63
|
| --- | --- |
|
|
39
64
|
| `<program>.<instruction>(accounts, args)` | Builds the `Instruction`. Accounts with a fixed address and PDAs whose seeds are constants, arguments or other accounts' keys are optional and filled in; pass one to override. An optional account passed as `null` is left out (sent as the program id, the convention TypeSOL and Anchor share); left undefined, it is filled in like any other. |
|
|
40
|
-
| `<program
|
|
65
|
+
| `<program>.<instruction>(accounts, args, remaining?)` | The same, with remaining accounts appended in order (kit `AccountMeta`s). |
|
|
66
|
+
| `<program>.accounts.<Name>` | `decode(bytes)`, `encode(value)`, `fetch(rpc, address)`, `fetchMaybe(rpc, address)`, `fetchMultiple(rpc, addresses)`, `fetchAll(rpc, filters?)` (getProgramAccounts by discriminator, plus `{ offset, bytes }` filters), `watch(rpcSubscriptions, address, { abortSignal })`, `discriminator`. `decode` checks the discriminator. |
|
|
41
67
|
| `<program>.pdas.<account>(seeds)` | The PDA (address and bump) for each account an instruction derives. |
|
|
42
68
|
| `<program>.returns.<instruction>(bytes)` | Decodes return data. |
|
|
43
|
-
| `<program>.
|
|
44
|
-
| `<program>.
|
|
69
|
+
| `<program>.resolve.<instruction>(rpc, accounts, args?)` | Fills in what needs the chain: `has_one` accounts that do not sign (read from the account that names them) and PDAs seeded from another account's fields (`seeds = [b"vault", book.owner]`: the account is read, then the PDA derived). Given accounts are kept. |
|
|
70
|
+
| `<program>.views.<instruction>(rpc, feePayer, accounts, args)` | Simulates an instruction that returns a value and decodes it (throws with the logs if it fails). |
|
|
71
|
+
| `<program>.sendInstructions(connection, feePayer, instructions)`, `simulate(rpc, feePayer, instructions)` | One transaction sent and confirmed (kit's sendAndConfirm), or simulated without signatures (error, logs, units, this program's return data). |
|
|
72
|
+
| `<program>.errors`, `errorCode(e)`, `isError(e, name)` | The program's errors (6000+), and for TypeSOL programs also every Anchor framework error (`AnchorError` and the constraints raise them) and TypeSOL's runtime errors. |
|
|
73
|
+
| `<program>.parseEvents(logs)`, `parseCpiEvents(data)`, `eventsOf(tx)`, `watchEvents(rpcSubscriptions, { abortSignal })` | Events from `Program data:` log lines, from `emitCpi` inner instructions (bytes or base58), from a fetched transaction (both), or live from log subscriptions. |
|
|
45
74
|
| Types and codecs | Every defined type as a TypeScript type (`Book`, the decoded value) and an input type (`BookArgs`, which also accepts `number` for 64- and 128-bit integers), with a kit codec (`getBookCodec()`). |
|
|
46
75
|
|
|
47
76
|
Types follow the program's TypeScript: `u64`/`i64`/`u128`/`i128` are `bigint` (inputs accept `number` too), smaller
|
|
@@ -60,10 +89,12 @@ Anchor IDLs are refused with a hint to convert them (`anchor idl convert`).
|
|
|
60
89
|
|
|
61
90
|
- **Errors and events are matched by code and discriminator, not by program.** A program this one calls can raise the
|
|
62
91
|
same error code, and `parseEvents` reads every `Program data:` line that decodes as one of this program's events.
|
|
63
|
-
- **Not filled in automatically** (pass them): accounts
|
|
64
|
-
|
|
65
|
-
for each.
|
|
92
|
+
- **Not filled in automatically** (pass them): `has_one` accounts that sign (they have to sign), PDAs seeded from an
|
|
93
|
+
account's own fields or from fields inside an argument, PDAs seeded from non-scalar fields (strings, structs), and PDAs
|
|
94
|
+
whose program comes from an argument. `typesol client` prints a note for each. `has_one` accounts that do not sign and
|
|
95
|
+
PDAs seeded from another account's scalar fields are filled in by `resolve` (it reads the chain).
|
|
66
96
|
- **Left out of a client generated from an Anchor IDL**, with a note: generic types and anything using them, and
|
|
67
97
|
zero-copy (`bytemuck`) accounts.
|
|
68
|
-
- **Not done yet**:
|
|
69
|
-
|
|
98
|
+
- **Not done yet**: a LiteSVM loader for tests (use `svm.addProgramFromFile(ledgerbook.address, soPath)`, or
|
|
99
|
+
`typesol/testing`). `watchEvents` reads log events; with `rpc` it also fetches each transaction in which the program
|
|
100
|
+
invoked itself and reads its `emitCpi` events (log subscriptions do not carry inner instructions).
|