@typesol/cli 0.22.0 → 0.24.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 +82 -0
- package/README.md +13 -12
- package/SECURITY.md +1 -1
- package/dist/clientgen.js +190 -41
- package/dist/compiler/analyze.js +195 -25
- package/dist/compiler/emit/base.d.ts +28 -5
- package/dist/compiler/emit/base.js +81 -15
- package/dist/compiler/emit/calls.d.ts +18 -1
- package/dist/compiler/emit/calls.js +159 -7
- package/dist/compiler/emit/cpi.d.ts +23 -0
- package/dist/compiler/emit/cpi.js +274 -5
- package/dist/compiler/emit/expressions.d.ts +9 -1
- package/dist/compiler/emit/expressions.js +25 -12
- package/dist/compiler/emit/function.js +8 -4
- package/dist/compiler/emit/program.js +9 -2
- package/dist/compiler/emit/shared.d.ts +43 -3
- package/dist/compiler/emit/shared.js +43 -0
- package/dist/compiler/emit/statements.d.ts +39 -2
- package/dist/compiler/emit/statements.js +346 -87
- package/dist/compiler/emit/storage.d.ts +6 -2
- package/dist/compiler/emit/storage.js +75 -45
- package/dist/compiler/model.d.ts +26 -1
- package/dist/idlpublish.d.ts +6 -1
- package/dist/idlpublish.js +47 -5
- package/dist/portgen.js +1 -1
- package/dist/svmrpc.d.ts +7 -1
- package/dist/svmrpc.js +133 -19
- package/dist/testing.js +23 -3
- package/docs/cli.md +2 -2
- package/docs/client.md +13 -10
- package/docs/errors.md +9 -3
- package/docs/language.md +21 -20
- package/docs/mainnet.md +1 -1
- package/docs/migrate-from-anchor.md +8 -9
- package/docs/test-your-program.md +6 -2
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +3 -2
- package/lib/typesol.d.ts +127 -20
- package/package.json +1 -1
- package/runtime/typesol_rt.h +73 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { resolveScalarType } from "../analyze.js";
|
|
3
3
|
import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName } from "../model.js";
|
|
4
|
-
import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount } from "./shared.js";
|
|
4
|
+
import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount, vecPlace, emptySizeC } from "./shared.js";
|
|
5
5
|
import { EmitterBase } from "./base.js";
|
|
6
6
|
/**
|
|
7
7
|
* Places in account data and locals: references, reads and writes, Vec/array indexing, aliasing rules and
|
|
@@ -73,16 +73,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
73
73
|
return r?.type.kind === "dataEnum" ? r : undefined;
|
|
74
74
|
}
|
|
75
75
|
looksLikeDataEnum(node) {
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
return this.reporter.guard(() => this.ref(node))?.type.kind === "dataEnum";
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
this.lines.length = saved.lines;
|
|
82
|
-
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
83
|
-
this.temp = saved.temp;
|
|
84
|
-
this.reporter.diagnostics.length = saved.diagnostics;
|
|
85
|
-
}
|
|
76
|
+
return this.probe(() => this.ref(node))?.type.kind === "dataEnum";
|
|
86
77
|
}
|
|
87
78
|
variantIndex(enumName, lit) {
|
|
88
79
|
const e = this.model.dataEnums.get(enumName);
|
|
@@ -137,11 +128,37 @@ export class StorageEmitter extends EmitterBase {
|
|
|
137
128
|
: `"${name}" still refers to ${hit}; replacing it here would change what "${name}" reads (in JS it keeps the old value). Copy the fields you need into locals first`);
|
|
138
129
|
}
|
|
139
130
|
}
|
|
140
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`).
|
|
133
|
+
* A parameter (of a closure or helper expanded here) is read after `at` when its function reads it after `at`; when
|
|
134
|
+
* `at` is in another function (a closure or helper it calls), any read of it in its own function counts.
|
|
135
|
+
*/
|
|
141
136
|
usedAfter(decl, at) {
|
|
142
137
|
const symbol = this.checker.getSymbolAtLocation(decl.name);
|
|
143
138
|
if (!symbol)
|
|
144
139
|
return true;
|
|
140
|
+
if (ts.isParameter(decl)) {
|
|
141
|
+
const fn = decl.parent;
|
|
142
|
+
const inside = at.getSourceFile() === fn.getSourceFile() && at.pos >= fn.pos && at.end <= fn.end;
|
|
143
|
+
let read = false;
|
|
144
|
+
const visitParam = (n) => {
|
|
145
|
+
if (read)
|
|
146
|
+
return;
|
|
147
|
+
if (ts.isIdentifier(n) && n !== decl.name && this.checker.getSymbolAtLocation(n) === symbol) {
|
|
148
|
+
if (!inside || n.pos >= at.end)
|
|
149
|
+
read = true;
|
|
150
|
+
// A loop inside the function, around `at`, reads it again on its next turn.
|
|
151
|
+
for (let p = at.parent; inside && p && p !== fn; p = p.parent)
|
|
152
|
+
if ((ts.isForStatement(p) || ts.isForOfStatement(p) || ts.isWhileStatement(p) || ts.isDoStatement(p)) && n.pos >= p.pos && n.end <= p.end)
|
|
153
|
+
read = true;
|
|
154
|
+
}
|
|
155
|
+
ts.forEachChild(n, visitParam);
|
|
156
|
+
};
|
|
157
|
+
const body = fn.body;
|
|
158
|
+
if (body)
|
|
159
|
+
visitParam(body);
|
|
160
|
+
return read;
|
|
161
|
+
}
|
|
145
162
|
// Loops around `at` that do not also contain the declaration run their earlier statements again later.
|
|
146
163
|
let loop;
|
|
147
164
|
for (let n = at.parent; n && n !== decl.parent; n = n.parent)
|
|
@@ -208,17 +225,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
208
225
|
return this.looksLikeStructPlace(node.expression);
|
|
209
226
|
if (ts.isConditionalExpression(node))
|
|
210
227
|
return this.looksLikeStructPlace(node.whenTrue) && this.looksLikeStructPlace(node.whenFalse);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
const r = this.reporter.guard(() => this.ref(node));
|
|
214
|
-
return r?.type.kind === "struct";
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
this.lines.length = saved.lines;
|
|
218
|
-
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
219
|
-
this.temp = saved.temp;
|
|
220
|
-
this.reporter.diagnostics.length = saved.diagnostics;
|
|
221
|
-
}
|
|
228
|
+
const r = this.probe(() => this.ref(node));
|
|
229
|
+
return r?.type.kind === "struct";
|
|
222
230
|
}
|
|
223
231
|
/** `fixedArray<T, N>(fill)` as a local declaration. */
|
|
224
232
|
localArray(init, c) {
|
|
@@ -258,6 +266,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
258
266
|
const l = this.lookup(node.text);
|
|
259
267
|
if (l && l.type.kind === "array")
|
|
260
268
|
return { lv: l.c, type: l.type, writable: !l.readonly };
|
|
269
|
+
if (l && l.type.kind === "vec")
|
|
270
|
+
return this.withFixedVec({ lv: l.c, type: l.type, writable: true });
|
|
261
271
|
// A struct loaded from a remaining account.
|
|
262
272
|
if (l && l.type.kind === "structRef")
|
|
263
273
|
return { lv: `(*${l.c})`, type: { kind: "struct", name: l.type.struct }, writable: l.type.writable };
|
|
@@ -278,15 +288,35 @@ export class StorageEmitter extends EmitterBase {
|
|
|
278
288
|
const r = { lv: `d_${a.name}->${f.name}`, type: f.type, writable: isWritable(a.kind), account: a, path: [f.name] };
|
|
279
289
|
if (f.type.kind === "vec" && f.type.cap !== undefined)
|
|
280
290
|
r.vec = this.fixedVec(r.lv, f.type.elem, f.type.cap);
|
|
281
|
-
else if (f.type.kind === "vec")
|
|
291
|
+
else if (f.type.kind === "vec") {
|
|
282
292
|
// Growing needs the account's length at instruction entry: instructions have it, helpers get it passed.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
293
|
+
const s = a.kind.struct;
|
|
294
|
+
const k = s.vecs.findIndex((v) => v.name === f.name);
|
|
295
|
+
if (s.vecs.length === 1)
|
|
296
|
+
r.vec = {
|
|
297
|
+
len: `d_${a.name}->${f.name}__len`,
|
|
298
|
+
items: r.lv,
|
|
299
|
+
fixed: `sizeof(TSS_${s.name})`,
|
|
300
|
+
elemSize: s.vec.elemSize,
|
|
301
|
+
resizable: isWritable(a.kind),
|
|
302
|
+
};
|
|
303
|
+
else {
|
|
304
|
+
const base = `(*d_${a.name})`;
|
|
305
|
+
const at = vecPlace(s, base, k);
|
|
306
|
+
const others = s.vecs.map((v, j) => (j === k ? "" : ` + (uint64_t)${vecPlace(s, base, j).len} * ${v.elemSize}`)).join("");
|
|
307
|
+
r.lv = at.items;
|
|
308
|
+
r.vec = {
|
|
309
|
+
len: at.len,
|
|
310
|
+
items: at.items,
|
|
311
|
+
fixed: `(${emptySizeC(s)}${others})`,
|
|
312
|
+
elemSize: s.vecs[k].elemSize,
|
|
313
|
+
resizable: isWritable(a.kind),
|
|
314
|
+
tail: k < s.vecs.length - 1
|
|
315
|
+
? { end: at.end, dataEnd: `(a_${a.name}->data + a_${a.name}->data_len)`, paths: s.vecs.slice(k + 1).map((v) => `${a.name}.${v.name}[]`) }
|
|
316
|
+
: undefined,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
290
320
|
return r;
|
|
291
321
|
}
|
|
292
322
|
}
|
|
@@ -330,8 +360,12 @@ export class StorageEmitter extends EmitterBase {
|
|
|
330
360
|
this.fail(node.name, `${s.name} has no field "${field}"`);
|
|
331
361
|
const r = { ...base, lv: `${base.lv}.${f.name}`, type: f.type, path: base.path && [...base.path, f.name], vec: undefined };
|
|
332
362
|
// Vecs of loaded accounts can be read and updated in place but not resized (no entry length known).
|
|
333
|
-
if (f.type.kind === "vec" && f.type.cap === undefined)
|
|
334
|
-
|
|
363
|
+
if (f.type.kind === "vec" && f.type.cap === undefined) {
|
|
364
|
+
const k = s.vecs.findIndex((v) => v.name === f.name);
|
|
365
|
+
const at = s.vecs.length === 1 ? { len: `${base.lv}.${f.name}__len`, items: r.lv } : vecPlace(s, base.lv, k);
|
|
366
|
+
r.lv = at.items;
|
|
367
|
+
r.vec = { len: at.len, items: at.items, fixed: emptySizeC(s), elemSize: s.vecs[k].elemSize, resizable: false };
|
|
368
|
+
}
|
|
335
369
|
return this.withFixedVec(r);
|
|
336
370
|
}
|
|
337
371
|
return undefined;
|
|
@@ -565,6 +599,11 @@ export class StorageEmitter extends EmitterBase {
|
|
|
565
599
|
const els = valueNode.elements;
|
|
566
600
|
if (els.length > t.cap)
|
|
567
601
|
this.fail(valueNode, `${els.length} elements do not fit in ${typeName(t)}`);
|
|
602
|
+
// Empty: nothing reads the old list, so it is cleared in place (length 0, every slot zero).
|
|
603
|
+
if (els.length === 0) {
|
|
604
|
+
this.line(`ts_memzero(&${target.lv}, sizeof(${target.lv}));`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
568
607
|
const tmp = this.tmp("fv");
|
|
569
608
|
this.line(`${fixedVecC(t)} ${tmp};`);
|
|
570
609
|
this.line(`ts_memzero(&${tmp}, sizeof(${tmp}));`);
|
|
@@ -655,24 +694,15 @@ export class StorageEmitter extends EmitterBase {
|
|
|
655
694
|
}
|
|
656
695
|
/** The kind of storage an expression names (struct, array, dataEnum…), without emitting code or failing. */
|
|
657
696
|
peekKind(node) {
|
|
658
|
-
|
|
659
|
-
try {
|
|
660
|
-
return this.reporter.guard(() => this.ref(node))?.type.kind;
|
|
661
|
-
}
|
|
662
|
-
finally {
|
|
663
|
-
this.lines.length = saved.lines;
|
|
664
|
-
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
665
|
-
this.temp = saved.temp;
|
|
666
|
-
this.reporter.diagnostics.length = saved.diagnostics;
|
|
667
|
-
}
|
|
697
|
+
return this.probe(() => this.ref(node))?.type.kind;
|
|
668
698
|
}
|
|
669
699
|
place(node) {
|
|
670
700
|
if (ts.isIdentifier(node)) {
|
|
671
701
|
const l = this.lookup(node.text);
|
|
672
702
|
if (!l)
|
|
673
703
|
this.fail(node, `"${node.text}" is not a local variable`);
|
|
674
|
-
if (l.type.kind === "array")
|
|
675
|
-
this.fail(node, `use ${node.text}.set(i, v) to change
|
|
704
|
+
if (l.type.kind === "array" || l.type.kind === "vec")
|
|
705
|
+
this.fail(node, `use ${node.text}.set(i, v) to change list elements`);
|
|
676
706
|
if (!l.mutable)
|
|
677
707
|
this.fail(node, `"${node.text}" is const or an argument and cannot be reassigned`);
|
|
678
708
|
if (l.type.kind === "pubkey")
|
package/dist/compiler/model.d.ts
CHANGED
|
@@ -38,9 +38,11 @@ export type ValueType = ScalarType | {
|
|
|
38
38
|
inner: ScalarType;
|
|
39
39
|
}
|
|
40
40
|
/** `Vec<T>` instruction argument: a read-only view into instruction data. T is a scalar or a fixed-size struct. */
|
|
41
|
+
/** A Vec argument (Borsh), read in place; `FixedVec<T, N>` arguments are the same with at most `cap` elements. */
|
|
41
42
|
| {
|
|
42
43
|
kind: "vecArg";
|
|
43
44
|
elem: FieldType;
|
|
45
|
+
cap?: number;
|
|
44
46
|
}
|
|
45
47
|
/** `FixedArray<T, N>` instruction argument (Borsh [T; N]), parsed into a read-only copy. */
|
|
46
48
|
| {
|
|
@@ -119,6 +121,8 @@ export type FieldType = ScalarType | {
|
|
|
119
121
|
export interface StructField {
|
|
120
122
|
name: string;
|
|
121
123
|
type: FieldType;
|
|
124
|
+
/** The declaration, while the struct is being analyzed (for messages). */
|
|
125
|
+
node?: ts.Node;
|
|
122
126
|
}
|
|
123
127
|
/** Every exported interface. Used as account data, nested in other structs, or emitted as an event. */
|
|
124
128
|
export interface StructDef {
|
|
@@ -129,12 +133,21 @@ export interface StructDef {
|
|
|
129
133
|
* or undefined when a field is a variable-length string (event-only structs).
|
|
130
134
|
*/
|
|
131
135
|
size: number | undefined;
|
|
132
|
-
/** The
|
|
136
|
+
/** The first growable Vec field, if any. Such structs can only be account data. */
|
|
133
137
|
vec?: {
|
|
134
138
|
name: string;
|
|
135
139
|
elem: FieldType;
|
|
136
140
|
elemSize: number;
|
|
137
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* Every growable Vec, in stored order (after the fixed-size fields). The first ends the C struct (its u32 length,
|
|
144
|
+
* then its elements); each further one follows the previous one's elements: a u32 length, then its elements.
|
|
145
|
+
*/
|
|
146
|
+
vecs?: {
|
|
147
|
+
name: string;
|
|
148
|
+
elem: FieldType;
|
|
149
|
+
elemSize: number;
|
|
150
|
+
}[];
|
|
138
151
|
accountDiscriminator: number[];
|
|
139
152
|
eventDiscriminator: number[];
|
|
140
153
|
node: ts.InterfaceDeclaration;
|
|
@@ -410,6 +423,14 @@ export interface ProgramModel {
|
|
|
410
423
|
helperDecls: Map<ts.Node, string>;
|
|
411
424
|
/** Local closures (they read the enclosing function's variables): their declaration -> name; expanded where called. */
|
|
412
425
|
closureDecls: Map<ts.Node, string>;
|
|
426
|
+
/**
|
|
427
|
+
* Functions that return a function (`function adder(n: u64): (x: u64) => u64 { return (x) => x + n; }`): no C
|
|
428
|
+
* function; a call binds the returned function, with the arguments, as a closure where it is made.
|
|
429
|
+
*/
|
|
430
|
+
factories: Map<string, {
|
|
431
|
+
node: HelperNode;
|
|
432
|
+
imports: Map<string, string>;
|
|
433
|
+
}>;
|
|
413
434
|
errors: ErrorDef[];
|
|
414
435
|
/** Enum name -> member name -> error */
|
|
415
436
|
errorEnums: Map<string, Map<string, ErrorDef>>;
|
|
@@ -436,6 +457,10 @@ export interface ProgramModel {
|
|
|
436
457
|
fixedVecs: Map<string, FieldType & {
|
|
437
458
|
kind: "vec";
|
|
438
459
|
}>;
|
|
460
|
+
/** FixedVec types of local lists (`const ys = xs.map(…)`), filled in while emitting: C typedefs only, not the IDL. */
|
|
461
|
+
localVecs: Map<string, FieldType & {
|
|
462
|
+
kind: "vec";
|
|
463
|
+
}>;
|
|
439
464
|
}
|
|
440
465
|
export declare function isWritable(k: ParamKind): boolean;
|
|
441
466
|
export declare function typeName(t: ValueType | FieldType): string;
|
package/dist/idlpublish.d.ts
CHANGED
|
@@ -25,7 +25,12 @@ export interface PublishOptions {
|
|
|
25
25
|
/** Write (create or update) the IDL metadata account; returns its address. */
|
|
26
26
|
export declare function publishIdl(chain: Chain, opts: PublishOptions): Promise<Address>;
|
|
27
27
|
export declare function metadataAddress(program: Address, authority: Address | null): Promise<Address>;
|
|
28
|
-
/**
|
|
28
|
+
/** The most an IDL read from the chain may inflate to: compressed data can expand far past the account's size. */
|
|
29
|
+
export declare const MAX_IDL_BYTES: number;
|
|
30
|
+
/**
|
|
31
|
+
* Read an IDL back (canonical, or the one `authority` wrote). Only an IDL stored in the metadata account itself is
|
|
32
|
+
* read: one that links to a URL or points at another account is refused (nothing is fetched from where it points).
|
|
33
|
+
*/
|
|
29
34
|
export declare function fetchIdl(rpc: Chain["rpc"], program: Address, authority?: Address | null): Promise<object>;
|
|
30
35
|
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
31
36
|
export declare function anchorIdlAddress(program: Address): Promise<Address>;
|
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 {
|
|
9
|
-
import {
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { gunzipSync, inflateSync } from "node:zlib";
|
|
10
|
+
import { Compression, DataSource, Encoding, Format, decodeData, fetchMaybeMetadata, findMetadataPda, packDirectData, writeMetadata, } from "@solana-program/program-metadata";
|
|
10
11
|
import { createAddressWithSeed, createClient, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, extendClient, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit";
|
|
11
12
|
export const PROGRAM_METADATA_ID = "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S";
|
|
12
13
|
/** A Chain over an RPC endpoint (polls signature status: no websocket needed). */
|
|
@@ -111,15 +112,54 @@ function planFailure(e) {
|
|
|
111
112
|
export async function metadataAddress(program, authority) {
|
|
112
113
|
return (await findMetadataPda({ program, authority, seed: "idl" }))[0];
|
|
113
114
|
}
|
|
114
|
-
/**
|
|
115
|
+
/** The most an IDL read from the chain may inflate to: compressed data can expand far past the account's size. */
|
|
116
|
+
export const MAX_IDL_BYTES = 16 * 1024 * 1024;
|
|
117
|
+
/** Decompress `data` (zlib or gzip), refusing output past MAX_IDL_BYTES. */
|
|
118
|
+
function inflateCapped(data, gzip, what) {
|
|
119
|
+
try {
|
|
120
|
+
return (gzip ? gunzipSync : inflateSync)(data, { maxOutputLength: MAX_IDL_BYTES });
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
if (e.code === "ERR_BUFFER_TOO_LARGE" || e instanceof RangeError)
|
|
124
|
+
throw new Error(`${what} inflates to more than ${MAX_IDL_BYTES / 1024 / 1024} MiB; refusing it`);
|
|
125
|
+
throw new Error(`${what} is not valid compressed data: ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read an IDL back (canonical, or the one `authority` wrote). Only an IDL stored in the metadata account itself is
|
|
130
|
+
* read: one that links to a URL or points at another account is refused (nothing is fetched from where it points).
|
|
131
|
+
*/
|
|
115
132
|
export async function fetchIdl(rpc, program, authority = null) {
|
|
116
|
-
|
|
133
|
+
const at = await metadataAddress(program, authority);
|
|
134
|
+
const account = await fetchMaybeMetadata(rpc, at);
|
|
135
|
+
if (!account.exists)
|
|
136
|
+
throw new Error(`no IDL metadata account at ${at}`);
|
|
137
|
+
const m = account.data;
|
|
138
|
+
if (m.dataLength > m.data.length)
|
|
139
|
+
throw new Error(`the IDL metadata at ${at} is truncated`);
|
|
140
|
+
const content = (what) => {
|
|
141
|
+
const raw = new Uint8Array(m.data.subarray(0, m.dataLength));
|
|
142
|
+
const bytes = m.compression === Compression.None ? raw : inflateCapped(raw, m.compression === Compression.Gzip, what);
|
|
143
|
+
// Base58 decodes in quadratic time: only small contents.
|
|
144
|
+
if (m.encoding === Encoding.Base58 && bytes.length > 64 * 1024)
|
|
145
|
+
throw new Error(`${what} is too large for base58`);
|
|
146
|
+
return decodeData(bytes, m.encoding);
|
|
147
|
+
};
|
|
148
|
+
if (m.dataSource === DataSource.Url) {
|
|
149
|
+
const url = content(`the IDL link at ${at}`);
|
|
150
|
+
throw new Error(`the IDL at ${at} is not stored on chain but linked: ${JSON.stringify(url.slice(0, 200))}. Fetch it yourself and pass the file (typesol client <idl.json>)`);
|
|
151
|
+
}
|
|
152
|
+
if (m.dataSource !== DataSource.Direct)
|
|
153
|
+
throw new Error(`the IDL at ${at} is stored in another account (external data), which is not read`);
|
|
154
|
+
return JSON.parse(content(`the IDL at ${at}`));
|
|
117
155
|
}
|
|
118
156
|
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
119
157
|
export async function anchorIdlAddress(program) {
|
|
120
158
|
const [base] = await getProgramDerivedAddress({ programAddress: program, seeds: [] });
|
|
121
159
|
return createAddressWithSeed({ baseAddress: base, programAddress: program, seed: "anchor:idl" });
|
|
122
160
|
}
|
|
161
|
+
/** Anchor's account discriminator for IdlAccount: sha256("account:IdlAccount")[0..8]. */
|
|
162
|
+
const IDL_ACCOUNT_DISCRIMINATOR = createHash("sha256").update("account:IdlAccount").digest().subarray(0, 8);
|
|
123
163
|
/** The IDL in Anchor's IDL account (`anchor idl init`), or null when the program has none. */
|
|
124
164
|
export async function fetchAnchorIdl(rpc, program) {
|
|
125
165
|
const { value } = await rpc.getAccountInfo(await anchorIdlAddress(program), { encoding: "base64" }).send();
|
|
@@ -129,10 +169,12 @@ export async function fetchAnchorIdl(rpc, program) {
|
|
|
129
169
|
// IdlAccount: 8-byte discriminator, authority (32 bytes), a u32 length, then the zlib-compressed JSON.
|
|
130
170
|
if (data.length < 44)
|
|
131
171
|
throw new Error("Anchor IDL account is too short");
|
|
172
|
+
if (!data.subarray(0, 8).equals(IDL_ACCOUNT_DISCRIMINATOR))
|
|
173
|
+
throw new Error("the account at Anchor's IDL address is not an IdlAccount");
|
|
132
174
|
const len = data.readUInt32LE(40);
|
|
133
175
|
if (44 + len > data.length)
|
|
134
176
|
throw new Error("Anchor IDL account is truncated");
|
|
135
|
-
return JSON.parse(
|
|
177
|
+
return JSON.parse(inflateCapped(data.subarray(44, 44 + len), false, "the Anchor IDL").toString("utf8"));
|
|
136
178
|
}
|
|
137
179
|
/**
|
|
138
180
|
* A program's IDL from the chain: the canonical Program Metadata IDL (`typesol idl publish`, Codama) first, else
|
package/dist/portgen.js
CHANGED
|
@@ -152,7 +152,7 @@ export function generatePort(idl) {
|
|
|
152
152
|
if (t.vec) {
|
|
153
153
|
const inner = tsType(t.vec, where, at);
|
|
154
154
|
if (where === "variant" || where === "struct" || (where === "account" && !last)) {
|
|
155
|
-
notes.push(`${at}: a Vec here (TypeSOL keeps a Vec only as an account's last field or as an argument); replaced by FixedArray, so the layout differs.`);
|
|
155
|
+
notes.push(`${at}: a Vec here (TypeSOL keeps Anchor's bytes for a growable Vec only as an account's last field or as an argument); replaced by FixedArray, so the layout differs.`);
|
|
156
156
|
return `${use("FixedArray")}<${inner}, 8> /* TODO: Anchor has a Vec here */`;
|
|
157
157
|
}
|
|
158
158
|
return `${use("Vec")}<${inner}>`;
|
package/dist/svmrpc.d.ts
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* await counter.accounts.Counter.fetch(rpc, address);
|
|
9
9
|
*
|
|
10
10
|
* Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
|
|
11
|
-
* the accounts transactions sent through this connection named, plus those passed to `track`.
|
|
11
|
+
* the accounts transactions sent through this connection named, plus those passed to `track`. Subscriptions see the
|
|
12
|
+
* transactions sent through this connection: `logsNotifications` replays those sent so far (failed ones too, with
|
|
13
|
+
* `err` set), then each new one, as `{ mentions: [address] }` filters them; `accountNotifications` yields the account
|
|
14
|
+
* after each later transaction that names it. Errors are in the JSON shape RPC nodes return
|
|
15
|
+
* (`{ InstructionError: [0, { Custom: 6000 }] }`).
|
|
12
16
|
*/
|
|
13
17
|
import { type Address, type Rpc, type RpcSubscriptions, type SolanaRpcApi, type SolanaRpcSubscriptionsApi } from "@solana/kit";
|
|
14
18
|
import { type LiteSVM, type TransactionMetadata } from "litesvm";
|
|
@@ -23,4 +27,6 @@ export interface SvmConnection {
|
|
|
23
27
|
/** Add accounts for `getProgramAccounts` to scan (accounts the transactions named are scanned already). */
|
|
24
28
|
track(...addresses: Address[]): void;
|
|
25
29
|
}
|
|
30
|
+
/** A LiteSVM transaction error in the JSON shape RPC nodes return: `{ InstructionError: [0, { Custom: 6000 }] }`. */
|
|
31
|
+
export declare function rpcError(err: unknown): unknown;
|
|
26
32
|
export declare function svmConnection(svm: LiteSVM): SvmConnection;
|
package/dist/svmrpc.js
CHANGED
|
@@ -8,15 +8,81 @@
|
|
|
8
8
|
* await counter.accounts.Counter.fetch(rpc, address);
|
|
9
9
|
*
|
|
10
10
|
* Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
|
|
11
|
-
* the accounts transactions sent through this connection named, plus those passed to `track`.
|
|
11
|
+
* the accounts transactions sent through this connection named, plus those passed to `track`. Subscriptions see the
|
|
12
|
+
* transactions sent through this connection: `logsNotifications` replays those sent so far (failed ones too, with
|
|
13
|
+
* `err` set), then each new one, as `{ mentions: [address] }` filters them; `accountNotifications` yields the account
|
|
14
|
+
* after each later transaction that names it. Errors are in the JSON shape RPC nodes return
|
|
15
|
+
* (`{ InstructionError: [0, { Custom: 6000 }] }`).
|
|
12
16
|
*/
|
|
13
17
|
import { getAddressDecoder, getBase58Decoder, getBase58Encoder, getBase64Decoder, getBase64Encoder, getCompiledTransactionMessageDecoder, getTransactionDecoder, } from "@solana/kit";
|
|
14
18
|
import { FailedTransactionMetadata } from "litesvm";
|
|
15
19
|
const b64 = (bytes) => getBase64Decoder().decode(bytes);
|
|
16
20
|
const reply = (value) => ({ send: async () => value });
|
|
21
|
+
// LiteSVM's fieldless error enums by value (const enums: their names are not there at run time).
|
|
22
|
+
const TX_ERRORS = ("AccountInUse AccountLoadedTwice AccountNotFound ProgramAccountNotFound InsufficientFundsForFee InvalidAccountForFee " +
|
|
23
|
+
"AlreadyProcessed BlockhashNotFound CallChainTooDeep MissingSignatureForFee InvalidAccountIndex SignatureFailure " +
|
|
24
|
+
"InvalidProgramForExecution SanitizeFailure ClusterMaintenance AccountBorrowOutstanding WouldExceedMaxBlockCostLimit " +
|
|
25
|
+
"UnsupportedVersion InvalidWritableAccount WouldExceedMaxAccountCostLimit WouldExceedAccountDataBlockLimit " +
|
|
26
|
+
"TooManyAccountLocks AddressLookupTableNotFound InvalidAddressLookupTableOwner InvalidAddressLookupTableData " +
|
|
27
|
+
"InvalidAddressLookupTableIndex InvalidRentPayingAccount WouldExceedMaxVoteCostLimit WouldExceedAccountDataTotalLimit " +
|
|
28
|
+
"MaxLoadedAccountsDataSizeExceeded ResanitizationNeeded InvalidLoadedAccountsDataSizeLimit UnbalancedTransaction " +
|
|
29
|
+
"ProgramCacheHitMaxLimit CommitCancelled").split(" ");
|
|
30
|
+
const IX_ERRORS = ("GenericError InvalidArgument InvalidInstructionData InvalidAccountData AccountDataTooSmall InsufficientFunds " +
|
|
31
|
+
"IncorrectProgramId MissingRequiredSignature AccountAlreadyInitialized UninitializedAccount UnbalancedInstruction " +
|
|
32
|
+
"ModifiedProgramId ExternalAccountLamportSpend ExternalAccountDataModified ReadonlyLamportChange ReadonlyDataModified " +
|
|
33
|
+
"DuplicateAccountIndex ExecutableModified RentEpochModified NotEnoughAccountKeys AccountDataSizeChanged " +
|
|
34
|
+
"AccountNotExecutable AccountBorrowFailed AccountBorrowOutstanding DuplicateAccountOutOfSync InvalidError " +
|
|
35
|
+
"ExecutableDataModified ExecutableLamportChange ExecutableAccountNotRentExempt UnsupportedProgramId CallDepth " +
|
|
36
|
+
"MissingAccount ReentrancyNotAllowed MaxSeedLengthExceeded InvalidSeeds InvalidRealloc ComputationalBudgetExceeded " +
|
|
37
|
+
"PrivilegeEscalation ProgramEnvironmentSetupFailure ProgramFailedToComplete ProgramFailedToCompile Immutable " +
|
|
38
|
+
"IncorrectAuthority AccountNotRentExempt InvalidAccountOwner ArithmeticOverflow UnsupportedSysvar IllegalOwner " +
|
|
39
|
+
"MaxAccountsDataAllocationsExceeded MaxAccountsExceeded MaxInstructionTraceLengthExceeded " +
|
|
40
|
+
"BuiltinProgramsMustConsumeComputeUnits BorshIoError").split(" ");
|
|
41
|
+
/** A LiteSVM transaction error in the JSON shape RPC nodes return: `{ InstructionError: [0, { Custom: 6000 }] }`. */
|
|
42
|
+
export function rpcError(err) {
|
|
43
|
+
if (typeof err === "number")
|
|
44
|
+
return TX_ERRORS[err] ?? String(err);
|
|
45
|
+
if (typeof err !== "object" || err === null)
|
|
46
|
+
return String(err);
|
|
47
|
+
const o = err;
|
|
48
|
+
if (typeof o.err === "function" && typeof o.index === "number") {
|
|
49
|
+
const inner = o.err.call(err);
|
|
50
|
+
const fields = (typeof inner === "object" && inner !== null ? inner : {});
|
|
51
|
+
const ix = typeof inner === "number"
|
|
52
|
+
? (IX_ERRORS[inner] ?? String(inner))
|
|
53
|
+
: typeof fields.code === "number"
|
|
54
|
+
? { Custom: fields.code }
|
|
55
|
+
: typeof fields.msg === "string"
|
|
56
|
+
? { BorshIoError: fields.msg }
|
|
57
|
+
: String(inner);
|
|
58
|
+
return { InstructionError: [o.index, ix] };
|
|
59
|
+
}
|
|
60
|
+
if (typeof o.index === "number")
|
|
61
|
+
return { DuplicateInstruction: o.index };
|
|
62
|
+
if (typeof o.accountIndex === "number")
|
|
63
|
+
return /Restricted/.test(String(err))
|
|
64
|
+
? { ProgramExecutionTemporarilyRestricted: { account_index: o.accountIndex } }
|
|
65
|
+
: { InsufficientFundsForRent: { account_index: o.accountIndex } };
|
|
66
|
+
return String(err);
|
|
67
|
+
}
|
|
17
68
|
export function svmConnection(svm) {
|
|
18
69
|
const transactions = [];
|
|
19
70
|
const known = new Set();
|
|
71
|
+
// Every transaction sent (failed ones too) for log subscriptions, the account keys of each, and live subscribers.
|
|
72
|
+
const sent = [];
|
|
73
|
+
const accountKeys = new Map();
|
|
74
|
+
const logListeners = new Set();
|
|
75
|
+
const accountListeners = new Map();
|
|
76
|
+
const record = (n) => {
|
|
77
|
+
sent.push(n);
|
|
78
|
+
for (const l of [...logListeners])
|
|
79
|
+
l(n);
|
|
80
|
+
// A failed transaction changed no account.
|
|
81
|
+
if (n.err === null)
|
|
82
|
+
for (const k of new Set(n.keys))
|
|
83
|
+
for (const l of [...(accountListeners.get(k) ?? [])])
|
|
84
|
+
l();
|
|
85
|
+
};
|
|
20
86
|
const decode = (wire) => {
|
|
21
87
|
const tx = getTransactionDecoder().decode(getBase64Encoder().encode(wire));
|
|
22
88
|
return { tx, keys: getCompiledTransactionMessageDecoder().decode(tx.messageBytes).staticAccounts };
|
|
@@ -50,12 +116,17 @@ export function svmConnection(svm) {
|
|
|
50
116
|
const { tx, keys } = decode(wire);
|
|
51
117
|
const res = svm.sendTransaction(tx);
|
|
52
118
|
svm.expireBlockhash();
|
|
53
|
-
if (res instanceof FailedTransactionMetadata)
|
|
54
|
-
|
|
119
|
+
if (res instanceof FailedTransactionMetadata) {
|
|
120
|
+
const meta = res.meta();
|
|
121
|
+
record({ signature: getBase58Decoder().decode(meta.signature()), logs: meta.logs(), err: rpcError(res.err()), keys });
|
|
122
|
+
throw new Error(`transaction failed: ${String(res.err())}\n${meta.logs().join("\n")}`);
|
|
123
|
+
}
|
|
55
124
|
for (const k of keys)
|
|
56
125
|
known.add(k);
|
|
57
126
|
const signature = getBase58Decoder().decode(res.signature());
|
|
58
127
|
transactions.push({ signature, meta: res });
|
|
128
|
+
accountKeys.set(signature, keys);
|
|
129
|
+
record({ signature, logs: res.logs(), err: null, keys });
|
|
59
130
|
return signature;
|
|
60
131
|
},
|
|
61
132
|
}),
|
|
@@ -77,7 +148,7 @@ export function svmConnection(svm) {
|
|
|
77
148
|
if (res instanceof FailedTransactionMetadata)
|
|
78
149
|
return {
|
|
79
150
|
context: { slot: 0n },
|
|
80
|
-
value: { err:
|
|
151
|
+
value: { err: rpcError(res.err()), logs: res.meta().logs(), unitsConsumed: res.meta().computeUnitsConsumed(), returnData: null },
|
|
81
152
|
};
|
|
82
153
|
const meta = res.meta();
|
|
83
154
|
const ret = meta.returnData();
|
|
@@ -108,38 +179,81 @@ export function svmConnection(svm) {
|
|
|
108
179
|
return want.every((b, i) => data[Number(memcmp.offset) + i] === b);
|
|
109
180
|
});
|
|
110
181
|
})),
|
|
111
|
-
// The json shape: log messages, and inner instructions with base58 data (what
|
|
182
|
+
// The json shape: account keys, log messages, and inner instructions with their program and base58 data (what
|
|
183
|
+
// `eventsOf` reads).
|
|
112
184
|
getTransaction: (signature) => {
|
|
113
185
|
const t = transactions.find((x) => x.signature === signature);
|
|
114
186
|
return reply(t
|
|
115
187
|
? {
|
|
116
188
|
slot: 0n,
|
|
117
189
|
blockTime: null,
|
|
190
|
+
transaction: { signatures: [signature], message: { accountKeys: accountKeys.get(signature) ?? [] } },
|
|
118
191
|
meta: {
|
|
119
192
|
err: null,
|
|
120
193
|
logMessages: t.meta.logs(),
|
|
121
194
|
computeUnitsConsumed: t.meta.computeUnitsConsumed(),
|
|
122
|
-
innerInstructions: t.meta
|
|
123
|
-
|
|
124
|
-
|
|
195
|
+
innerInstructions: t.meta.innerInstructions().map((group, index) => ({
|
|
196
|
+
index,
|
|
197
|
+
instructions: group.map((i) => ({
|
|
198
|
+
programIdIndex: i.instruction().programIdIndex(),
|
|
199
|
+
accounts: [...i.instruction().accounts()],
|
|
200
|
+
data: getBase58Decoder().decode(i.instruction().data()),
|
|
201
|
+
stackHeight: i.stackHeight(),
|
|
202
|
+
})),
|
|
203
|
+
})),
|
|
125
204
|
},
|
|
126
205
|
}
|
|
127
206
|
: null);
|
|
128
207
|
},
|
|
129
208
|
};
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Notifications: `backlog` first, then each item `listen` pushes, until the subscriber aborts. The listener is in
|
|
211
|
+
* place once `subscribe` is called, so nothing sent after that call is missed.
|
|
212
|
+
*/
|
|
213
|
+
const stream = (backlog, listen) => ({
|
|
214
|
+
subscribe: async ({ abortSignal }) => {
|
|
215
|
+
const queue = [...backlog()];
|
|
216
|
+
let wake;
|
|
217
|
+
const stop = listen((item) => {
|
|
218
|
+
queue.push(item);
|
|
219
|
+
wake?.();
|
|
220
|
+
});
|
|
221
|
+
const aborted = new Promise((resolve) => abortSignal.addEventListener("abort", () => resolve(), { once: true }));
|
|
222
|
+
void aborted.then(stop);
|
|
223
|
+
return (async function* () {
|
|
224
|
+
while (!abortSignal.aborted) {
|
|
225
|
+
if (queue.length) {
|
|
226
|
+
yield queue.shift();
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
await Promise.race([aborted, new Promise((resolve) => (wake = resolve))]);
|
|
230
|
+
wake = undefined;
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
},
|
|
137
234
|
});
|
|
235
|
+
const none = () => () => undefined;
|
|
138
236
|
const rpcSubscriptions = {
|
|
139
|
-
signatureNotifications: () => stream(() => [{ context: { slot: 0n }, value: { err: null } }]),
|
|
140
|
-
slotNotifications: () => stream(() => []),
|
|
141
|
-
logsNotifications: (
|
|
142
|
-
|
|
237
|
+
signatureNotifications: () => stream(() => [{ context: { slot: 0n }, value: { err: null } }], none),
|
|
238
|
+
slotNotifications: () => stream(() => [], none),
|
|
239
|
+
logsNotifications: (filter = "all") => {
|
|
240
|
+
const wanted = (n) => typeof filter !== "object" || filter.mentions.every((a) => n.keys.includes(a));
|
|
241
|
+
const shape = (n) => ({ context: { slot: 0n }, value: { signature: n.signature, logs: n.logs, err: n.err } });
|
|
242
|
+
return stream(() => sent.filter(wanted).map(shape), (push) => {
|
|
243
|
+
const l = (n) => {
|
|
244
|
+
if (wanted(n))
|
|
245
|
+
push(shape(n));
|
|
246
|
+
};
|
|
247
|
+
logListeners.add(l);
|
|
248
|
+
return () => logListeners.delete(l);
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
accountNotifications: (a) => stream(() => [], (push) => {
|
|
252
|
+
const l = () => push({ context: { slot: 0n }, value: encoded(a) ?? { data: ["", "base64"], lamports: 0n } });
|
|
253
|
+
const set = accountListeners.get(a) ?? new Set();
|
|
254
|
+
accountListeners.set(a, set.add(l));
|
|
255
|
+
return () => set.delete(l);
|
|
256
|
+
}),
|
|
143
257
|
};
|
|
144
258
|
return {
|
|
145
259
|
rpc: rpc,
|
package/dist/testing.js
CHANGED
|
@@ -313,13 +313,33 @@ export function programHarness(svm, programId, idl) {
|
|
|
313
313
|
throw new Error(`${instruction} returns nothing`);
|
|
314
314
|
const data = Buffer.from(harness.ok(res).returnData().data());
|
|
315
315
|
const t = def.returns;
|
|
316
|
+
const short = (n) => {
|
|
317
|
+
if (data.length < n)
|
|
318
|
+
throw new Error(`${instruction} returned ${data.length} bytes; a ${String(t)} needs ${n}`);
|
|
319
|
+
};
|
|
316
320
|
if (typeof t === "string") {
|
|
317
321
|
if (t === "pubkey")
|
|
318
|
-
return getAddressDecoder().decode(data);
|
|
322
|
+
return (short(32), getAddressDecoder().decode(data));
|
|
319
323
|
if (t === "bool")
|
|
320
|
-
return (data[0] !== 0);
|
|
324
|
+
return (short(1), data[0] !== 0);
|
|
325
|
+
if (t === "f32")
|
|
326
|
+
return (short(4), data.readFloatLE(0));
|
|
327
|
+
if (t === "f64")
|
|
328
|
+
return (short(8), data.readDoubleLE(0));
|
|
329
|
+
if (t === "string" || t === "bytes") {
|
|
330
|
+
// Borsh: a u32 length, then the bytes (UTF-8 for a string).
|
|
331
|
+
short(4);
|
|
332
|
+
const len = data.readUInt32LE(0);
|
|
333
|
+
short(4 + len);
|
|
334
|
+
const body = data.subarray(4, 4 + len);
|
|
335
|
+
return (t === "string" ? body.toString("utf8") : new Uint8Array(body));
|
|
336
|
+
}
|
|
337
|
+
const int = /^[ui](8|16|32|64|128)$/.exec(t);
|
|
338
|
+
if (!int)
|
|
339
|
+
throw new Error(`decode ${instruction}'s return data yourself (${t})`);
|
|
321
340
|
const signed = t.startsWith("i");
|
|
322
|
-
const bytes = Number(
|
|
341
|
+
const bytes = Number(int[1]) / 8;
|
|
342
|
+
short(bytes);
|
|
323
343
|
let n = 0n;
|
|
324
344
|
for (let i = bytes - 1; i >= 0; i--)
|
|
325
345
|
n = (n << 8n) | BigInt(data[i]);
|