@typesol/cli 0.14.0 → 0.16.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 +81 -0
- package/README.md +35 -5
- package/dist/cli.js +161 -24
- package/dist/compiler/analyze.d.ts +4 -0
- package/dist/compiler/analyze.js +51 -9
- package/dist/compiler/emit/base.js +1 -1
- package/dist/compiler/emit/calls.js +7 -0
- package/dist/compiler/emit/cpi.js +188 -9
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +107 -2
- package/dist/compiler/emit/function.js +11 -1
- package/dist/compiler/emit/shared.js +3 -2
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +131 -5
- package/dist/compiler/emit/storage.d.ts +2 -1
- package/dist/compiler/emit/storage.js +24 -7
- package/dist/compiler/idl.js +28 -14
- package/dist/compiler/model.d.ts +16 -3
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/idlpublish.js +43 -9
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +311 -0
- package/dist/templates.d.ts +9 -0
- package/dist/templates.js +324 -0
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.d.ts +3 -2
- package/dist/workspace.js +18 -106
- package/docs/architecture.md +3 -0
- package/docs/cli.md +17 -3
- package/docs/language.md +11 -10
- package/docs/mainnet.md +100 -0
- package/docs/migrate-from-anchor.md +104 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +10 -2
- package/lib/typesol.d.ts +68 -12
- package/package.json +19 -1
- package/runtime/typesol_rt.h +25 -5
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
type IdlType = string | {
|
|
2
|
+
vec?: IdlType;
|
|
3
|
+
option?: IdlType;
|
|
4
|
+
coption?: IdlType;
|
|
5
|
+
array?: [IdlType, number | unknown];
|
|
6
|
+
defined?: string | {
|
|
7
|
+
name: string;
|
|
8
|
+
generics?: unknown[];
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
interface IdlField {
|
|
12
|
+
name: string;
|
|
13
|
+
type: IdlType;
|
|
14
|
+
docs?: string[];
|
|
15
|
+
}
|
|
16
|
+
interface IdlTypeDef {
|
|
17
|
+
name: string;
|
|
18
|
+
docs?: string[];
|
|
19
|
+
type: {
|
|
20
|
+
kind: string;
|
|
21
|
+
fields?: (IdlField | IdlType)[];
|
|
22
|
+
variants?: {
|
|
23
|
+
name: string;
|
|
24
|
+
fields?: (IdlField | IdlType)[];
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
27
|
+
generics?: unknown[];
|
|
28
|
+
}
|
|
29
|
+
interface IdlAccountItem {
|
|
30
|
+
name: string;
|
|
31
|
+
writable?: boolean;
|
|
32
|
+
signer?: boolean;
|
|
33
|
+
optional?: boolean;
|
|
34
|
+
address?: string;
|
|
35
|
+
docs?: string[];
|
|
36
|
+
pda?: {
|
|
37
|
+
seeds: {
|
|
38
|
+
kind: string;
|
|
39
|
+
value?: number[];
|
|
40
|
+
path?: string;
|
|
41
|
+
}[];
|
|
42
|
+
program?: unknown;
|
|
43
|
+
};
|
|
44
|
+
relations?: string[];
|
|
45
|
+
accounts?: IdlAccountItem[];
|
|
46
|
+
}
|
|
47
|
+
interface IdlInstruction {
|
|
48
|
+
name: string;
|
|
49
|
+
docs?: string[];
|
|
50
|
+
discriminator: number[];
|
|
51
|
+
accounts: IdlAccountItem[];
|
|
52
|
+
args: IdlField[];
|
|
53
|
+
returns?: IdlType;
|
|
54
|
+
}
|
|
55
|
+
export interface PortIdl {
|
|
56
|
+
address?: string;
|
|
57
|
+
metadata?: {
|
|
58
|
+
name?: string;
|
|
59
|
+
};
|
|
60
|
+
instructions: IdlInstruction[];
|
|
61
|
+
accounts?: {
|
|
62
|
+
name: string;
|
|
63
|
+
discriminator: number[];
|
|
64
|
+
}[];
|
|
65
|
+
events?: {
|
|
66
|
+
name: string;
|
|
67
|
+
discriminator: number[];
|
|
68
|
+
}[];
|
|
69
|
+
errors?: {
|
|
70
|
+
code: number;
|
|
71
|
+
name: string;
|
|
72
|
+
msg?: string;
|
|
73
|
+
}[];
|
|
74
|
+
types?: IdlTypeDef[];
|
|
75
|
+
constants?: {
|
|
76
|
+
name: string;
|
|
77
|
+
type: IdlType;
|
|
78
|
+
value: string;
|
|
79
|
+
}[];
|
|
80
|
+
}
|
|
81
|
+
export interface PortResult {
|
|
82
|
+
source: string;
|
|
83
|
+
/** What the skeleton cannot carry over as is, and where Anchor clients would see a different layout. */
|
|
84
|
+
notes: string[];
|
|
85
|
+
}
|
|
86
|
+
export declare function generatePort(idl: PortIdl): PortResult;
|
|
87
|
+
export {};
|
package/dist/portgen.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `typesol port <idl.json>`: a TypeSOL program skeleton from an Anchor IDL (0.30+/1.x), to port an Anchor program.
|
|
3
|
+
*
|
|
4
|
+
* It writes the account structs, enums, errors, events and one function per instruction with its accounts and
|
|
5
|
+
* arguments, bodies left to fill in. Instructions keep their discriminators (an explicit `@discriminator` when the
|
|
6
|
+
* name would derive another), account structs keep their names (so their discriminators), errors keep their order —
|
|
7
|
+
* so existing clients keep working when the layout allows it. Where TypeSOL stores data differently from Anchor's Borsh
|
|
8
|
+
* layout (String, Option and a Vec that is not the last field in account data), it says so in a note and a comment.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { cNameProblem } from "./compiler/analyze.js";
|
|
12
|
+
const RESERVED = new Set("break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "));
|
|
13
|
+
const camel = (s) => s.replace(/[_-]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
14
|
+
const pascal = (s) => camel(s).replace(/^[a-z]/, (c) => c.toUpperCase());
|
|
15
|
+
const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
16
|
+
/** Names TypeSOL source cannot use as written: TypeScript keywords, the generated C's reserved names, the builtins
|
|
17
|
+
* a skeleton imports, names starting with "_". */
|
|
18
|
+
const BUILTINS = new Set(["require"]); // what a skeleton imports as a value
|
|
19
|
+
/** A function, parameter or local name; `member`: a struct field or variant, which also becomes a C member name. */
|
|
20
|
+
const ident = (s, member = false) => {
|
|
21
|
+
let n = camel(s).replace(/^_+/, "") || "value";
|
|
22
|
+
if (/^\d/.test(n))
|
|
23
|
+
n = `v${n}`;
|
|
24
|
+
while (RESERVED.has(n) || BUILTINS.has(n) || (member && cNameProblem(n)))
|
|
25
|
+
n = `${n}Value`;
|
|
26
|
+
return n;
|
|
27
|
+
};
|
|
28
|
+
/** A type name: kept as written when it is a valid one (account names decide account discriminators). */
|
|
29
|
+
const typeName = (s) => (/^[A-Za-z][A-Za-z0-9_]*$/.test(s) && !s.includes("__") && !RESERVED.has(s) ? s : pascal(s).replace(/^(\d)/, "T$1"));
|
|
30
|
+
const sighash = (preimage) => [...createHash("sha256").update(preimage).digest().subarray(0, 8)];
|
|
31
|
+
const sameBytes = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
32
|
+
const INTS = new Set(["u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128"]);
|
|
33
|
+
const PROGRAMS = {
|
|
34
|
+
"11111111111111111111111111111111": "SystemProgram",
|
|
35
|
+
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "TokenProgram",
|
|
36
|
+
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: "Token2022Program",
|
|
37
|
+
ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: "AssociatedTokenProgram",
|
|
38
|
+
};
|
|
39
|
+
export function generatePort(idl) {
|
|
40
|
+
const notes = [];
|
|
41
|
+
const imports = new Set();
|
|
42
|
+
const use = (n) => (imports.add(n), n);
|
|
43
|
+
const accountNames = new Set((idl.accounts ?? []).map((a) => a.name));
|
|
44
|
+
const eventNames = new Set((idl.events ?? []).map((e) => e.name));
|
|
45
|
+
const definedName = (d) => (typeof d === "string" ? d : d.name);
|
|
46
|
+
const defs = new Map((idl.types ?? []).map((t) => [t.name, t]));
|
|
47
|
+
// The fixed layouts a TypeSOL-built IDL spells out as helper types map back to TypeSOL's own types.
|
|
48
|
+
const helperDefs = new Map((idl.types ?? []).filter((t) => /^(FixedString\d+|FixedOption_|FixedEnum_)/.test(t.name)).map((t) => [t.name, t]));
|
|
49
|
+
const isTuple = (fields) => !!fields?.length && (typeof fields[0] !== "object" || fields[0] === null || !("name" in fields[0]));
|
|
50
|
+
// Types the skeleton cannot declare: references to them become a placeholder with a note.
|
|
51
|
+
const skipped = new Set((idl.types ?? [])
|
|
52
|
+
.filter((d) => !helperDefs.has(d.name) &&
|
|
53
|
+
(d.generics?.length || (d.type.kind === "struct" && isTuple(d.type.fields)) || (d.type.kind !== "struct" && d.type.kind !== "enum")))
|
|
54
|
+
.map((d) => d.name));
|
|
55
|
+
const dataEnum = (name) => defs.get(name)?.type.kind === "enum" && (defs.get(name)?.type.variants ?? []).some((v) => v.fields?.length);
|
|
56
|
+
// Where each struct is used decides what a field can be: account data and struct arguments keep fixed layouts.
|
|
57
|
+
const usage = new Map();
|
|
58
|
+
const mark = (t, as) => {
|
|
59
|
+
if (!t || typeof t === "string")
|
|
60
|
+
return;
|
|
61
|
+
const inner = t.vec ?? t.option ?? t.coption ?? t.array?.[0];
|
|
62
|
+
if (inner)
|
|
63
|
+
return mark(inner, as);
|
|
64
|
+
if (!t.defined)
|
|
65
|
+
return;
|
|
66
|
+
const name = definedName(t.defined);
|
|
67
|
+
const set = usage.get(name) ?? new Set();
|
|
68
|
+
if (set.has(as))
|
|
69
|
+
return;
|
|
70
|
+
usage.set(name, set.add(as));
|
|
71
|
+
const def = defs.get(name);
|
|
72
|
+
for (const f of (def?.type.fields ?? []))
|
|
73
|
+
if (typeof f === "object" && f && "type" in f)
|
|
74
|
+
mark(f.type, as);
|
|
75
|
+
for (const v of def?.type.variants ?? [])
|
|
76
|
+
for (const f of v.fields ?? [])
|
|
77
|
+
mark(typeof f === "object" && f && "type" in f ? f.type : f, as);
|
|
78
|
+
};
|
|
79
|
+
for (const a of accountNames)
|
|
80
|
+
mark({ defined: a }, "account");
|
|
81
|
+
for (const e of eventNames)
|
|
82
|
+
mark({ defined: e }, "event");
|
|
83
|
+
for (const ix of idl.instructions)
|
|
84
|
+
for (const g of ix.args)
|
|
85
|
+
mark(g.type, "arg");
|
|
86
|
+
const fixed = (w) => w === "account" || w === "struct" || w === "variant";
|
|
87
|
+
const describe = (w) => (w === "account" ? "account data" : w === "struct" ? "a struct argument" : "a data-enum variant");
|
|
88
|
+
const tsType = (t, where, at, last = false) => {
|
|
89
|
+
if (typeof t === "string") {
|
|
90
|
+
if (INTS.has(t))
|
|
91
|
+
return use(t);
|
|
92
|
+
if (t === "bool")
|
|
93
|
+
return "boolean";
|
|
94
|
+
if (t === "pubkey" || t === "publicKey")
|
|
95
|
+
return use("Pubkey");
|
|
96
|
+
if (t === "string") {
|
|
97
|
+
if (!fixed(where))
|
|
98
|
+
return "string";
|
|
99
|
+
notes.push(`${at}: Anchor encodes this String with its length (Borsh); TypeSOL holds FixedString<N> (u32 length + N bytes) in ${describe(where)}, so this layout differs. Set N.`);
|
|
100
|
+
return `${use("FixedString")}<32> /* TODO: capacity; Anchor has a variable String */`;
|
|
101
|
+
}
|
|
102
|
+
if (t === "bytes")
|
|
103
|
+
return tsType({ vec: "u8" }, where, at, last);
|
|
104
|
+
notes.push(`${at}: IDL type "${t}" has no TypeSOL equivalent; left as u64`);
|
|
105
|
+
return `${use("u64")} /* TODO: ${t} */`;
|
|
106
|
+
}
|
|
107
|
+
if (t.array) {
|
|
108
|
+
const n = Number(t.array[1]);
|
|
109
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
110
|
+
notes.push(`${at}: an array of length ${JSON.stringify(t.array[1])}; set it`);
|
|
111
|
+
return `${use("FixedArray")}<${tsType(t.array[0], where, at)}, 1> /* TODO: length */`;
|
|
112
|
+
}
|
|
113
|
+
return `${use("FixedArray")}<${tsType(t.array[0], where, at)}, ${n}>`;
|
|
114
|
+
}
|
|
115
|
+
if (t.coption)
|
|
116
|
+
notes.push(`${at}: a COption (4-byte tag in Anchor); TypeSOL's Option has a 1-byte tag, so this layout differs.`);
|
|
117
|
+
if (t.option || t.coption) {
|
|
118
|
+
const inner = tsType((t.option ?? t.coption), where === "variant" ? "variant" : where, at);
|
|
119
|
+
if (fixed(where))
|
|
120
|
+
notes.push(`${at}: an Option in ${describe(where)} is fixed-size in TypeSOL (1 + size of T), Borsh (1, or 1 + size) in Anchor: the layout differs.`);
|
|
121
|
+
return `${use("Option")}<${inner}>`;
|
|
122
|
+
}
|
|
123
|
+
if (t.vec) {
|
|
124
|
+
const inner = tsType(t.vec, where, at);
|
|
125
|
+
if (where === "variant" || where === "struct" || (where === "account" && !last)) {
|
|
126
|
+
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.`);
|
|
127
|
+
return `${use("FixedArray")}<${inner}, 8> /* TODO: Anchor has a Vec here */`;
|
|
128
|
+
}
|
|
129
|
+
return `${use("Vec")}<${inner}>`;
|
|
130
|
+
}
|
|
131
|
+
if (t.defined) {
|
|
132
|
+
const name = definedName(t.defined);
|
|
133
|
+
const fs = /^FixedString(\d+)$/.exec(name);
|
|
134
|
+
if (fs)
|
|
135
|
+
return `${use("FixedString")}<${fs[1]}>`;
|
|
136
|
+
const helper = helperDefs.get(name);
|
|
137
|
+
if (helper && name.startsWith("FixedOption_")) {
|
|
138
|
+
const value = (helper.type.fields ?? []).find((f) => f.name === "value");
|
|
139
|
+
if (value)
|
|
140
|
+
return `${use("Option")}<${tsType(value.type, "arg", at)}>`;
|
|
141
|
+
}
|
|
142
|
+
if (name.startsWith("FixedEnum_"))
|
|
143
|
+
return typeName(name.slice("FixedEnum_".length));
|
|
144
|
+
if (skipped.has(name)) {
|
|
145
|
+
notes.push(`${at}: ${name} could not be ported (generic, tuple or unsupported type); replaced by a placeholder`);
|
|
146
|
+
return `${use("u8")} /* TODO: ${name} */`;
|
|
147
|
+
}
|
|
148
|
+
if ((where === "account" || where === "struct") && dataEnum(name))
|
|
149
|
+
notes.push(`${at}: ${name} is a data enum: Anchor stores the variant's own size (Borsh), TypeSOL the largest variant's, so this layout differs.`);
|
|
150
|
+
return typeName(name);
|
|
151
|
+
}
|
|
152
|
+
notes.push(`${at}: unsupported IDL type ${JSON.stringify(t)}; left as u64`);
|
|
153
|
+
return `${use("u64")} /* TODO */`;
|
|
154
|
+
};
|
|
155
|
+
const esc = (s) => s.replace(/\*\//g, "* /");
|
|
156
|
+
const docs = (lines, pad = "") => (lines?.length ? `${pad}/** ${esc(lines.join(" "))} */\n` : "");
|
|
157
|
+
const out = [];
|
|
158
|
+
// Constants: integer literals and strings; expressions are left to port by hand.
|
|
159
|
+
for (const c of idl.constants ?? []) {
|
|
160
|
+
const ty = typeof c.type === "string" && INTS.has(c.type) ? c.type : undefined;
|
|
161
|
+
const name = /^[A-Za-z_][A-Za-z0-9_]*$/.test(c.name) ? c.name : ident(c.name);
|
|
162
|
+
if (ty && /^-?\d[\d_]*$/.test(c.value.trim()))
|
|
163
|
+
out.push(`export const ${name}: ${use(ty)} = ${c.value.trim().replace(/_/g, "")}n;`);
|
|
164
|
+
else if (c.type === "string" && /^".*"$/.test(c.value.trim()))
|
|
165
|
+
out.push(`export const ${name} = ${c.value.trim()};`);
|
|
166
|
+
else
|
|
167
|
+
notes.push(`constant ${c.name} = ${c.value}: not a literal; port it by hand`);
|
|
168
|
+
}
|
|
169
|
+
if (out.length)
|
|
170
|
+
out.push("");
|
|
171
|
+
// Types: structs (account data, events or arguments), enums.
|
|
172
|
+
for (const def of idl.types ?? []) {
|
|
173
|
+
if (helperDefs.has(def.name))
|
|
174
|
+
continue;
|
|
175
|
+
if (skipped.has(def.name)) {
|
|
176
|
+
notes.push(`type ${def.name}: ${def.generics?.length ? "generics are" : def.type.kind === "struct" ? "tuple structs are" : `kind "${def.type.kind}" is`} not supported; skipped`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const used = usage.get(def.name) ?? new Set(["account"]);
|
|
180
|
+
const where = used.has("account") ? "account" : used.has("arg") ? "struct" : used.has("event") ? "event" : "account";
|
|
181
|
+
if (def.type.kind === "struct") {
|
|
182
|
+
const fields = (def.type.fields ?? []);
|
|
183
|
+
const tag = accountNames.has(def.name) ? " (account)" : eventNames.has(def.name) ? ` (event: emit<${typeName(def.name)}>({ … }))` : "";
|
|
184
|
+
out.push(docs(def.docs).trimEnd() || `/** ${def.name}${tag} */`);
|
|
185
|
+
out.push(`export interface ${typeName(def.name)} {`);
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
fields.forEach((f, i) => {
|
|
188
|
+
let n = ident(f.name, true);
|
|
189
|
+
while (seen.has(n))
|
|
190
|
+
n = `${n}2`;
|
|
191
|
+
seen.add(n);
|
|
192
|
+
out.push(`${docs(f.docs, " ")} ${n}: ${tsType(f.type, where, `${def.name}.${f.name}`, i === fields.length - 1)};`);
|
|
193
|
+
});
|
|
194
|
+
out.push("}", "");
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const variants = def.type.variants ?? [];
|
|
198
|
+
if (variants.every((v) => !v.fields?.length)) {
|
|
199
|
+
out.push(`${docs(def.docs)}export enum ${typeName(def.name)} {`, ...variants.map((v) => ` ${ident(v.name, true).replace(/^[a-z]/, (c) => c.toUpperCase())},`), "}", "");
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Strings in variants are fine in arguments and events (Borsh); account data needs fixed-size variants.
|
|
203
|
+
const vw = used.has("account") || (!used.has("arg") && !used.has("event")) ? "variant" : "event";
|
|
204
|
+
const parts = variants.map((v) => {
|
|
205
|
+
const fs = (v.fields ?? []).map((f, i) => typeof f === "object" && f !== null && "name" in f
|
|
206
|
+
? `${ident(f.name, true)}: ${tsType(f.type, vw, `${def.name}.${v.name}`)}`
|
|
207
|
+
: `f${i}: ${tsType(f, vw, `${def.name}.${v.name}`)}`);
|
|
208
|
+
return `{ kind: "${pascal(v.name)}"${fs.map((x) => `; ${x}`).join("")} }`;
|
|
209
|
+
});
|
|
210
|
+
out.push(`${docs(def.docs)}export type ${typeName(def.name)} =\n | ${parts.join("\n | ")};`, "");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Errors: TypeSOL numbers them 6000 + their index. NotPorted, last, marks the instructions still to write, so the
|
|
215
|
+
// codes of the program's own errors do not move.
|
|
216
|
+
const errors = idl.errors ?? [];
|
|
217
|
+
const program = typeName(pascal(idl.metadata?.name ?? "program"));
|
|
218
|
+
const errorEnum = `${program}Error`;
|
|
219
|
+
if (!errors.every((e, i) => e.code === 6000 + i))
|
|
220
|
+
notes.push(`errors: TypeSOL numbers an error enum from 6000 in order; the IDL's codes (${errors.map((e) => e.code).join(", ")}) differ, so clients see other codes`);
|
|
221
|
+
const memberNames = new Set();
|
|
222
|
+
const member = (name) => {
|
|
223
|
+
// RequireFailedN is TypeSOL's own name for require("message") errors; it is reserved in source.
|
|
224
|
+
let n = /^RequireFailed\d+$/.test(name) ? name.replace("RequireFailed", "Check") : pascal(name);
|
|
225
|
+
while (memberNames.has(n))
|
|
226
|
+
n += "_";
|
|
227
|
+
memberNames.add(n);
|
|
228
|
+
return n;
|
|
229
|
+
};
|
|
230
|
+
const errorLines = errors.map((e) => ` ${member(e.name)} = ${JSON.stringify(e.msg ?? e.name)},`);
|
|
231
|
+
const notPorted = member("NotPorted");
|
|
232
|
+
out.push(`export enum ${errorEnum} {`, ...errorLines, ` ${notPorted} = "This instruction is not ported yet",`, "}", "");
|
|
233
|
+
// Accounts of an instruction, nested groups flattened.
|
|
234
|
+
const flatten = (items, prefix = "") => items.flatMap((a) => (a.accounts ? flatten(a.accounts, `${prefix}${a.name}_`) : [{ ...a, name: `${prefix}${a.name}` }]));
|
|
235
|
+
for (const ix of idl.instructions) {
|
|
236
|
+
const fn = ident(ix.name);
|
|
237
|
+
const params = [];
|
|
238
|
+
const comments = [];
|
|
239
|
+
const taken = new Set();
|
|
240
|
+
const unique = (n, kind) => {
|
|
241
|
+
let m = n;
|
|
242
|
+
while (taken.has(m))
|
|
243
|
+
m = `${m}${kind}`;
|
|
244
|
+
if (m !== n)
|
|
245
|
+
notes.push(`${ix.name}: ${kind.toLowerCase()} "${n}" renamed to "${m}" (an account and an argument share the name)`);
|
|
246
|
+
taken.add(m);
|
|
247
|
+
return m;
|
|
248
|
+
};
|
|
249
|
+
for (const a of flatten(ix.accounts)) {
|
|
250
|
+
let ty;
|
|
251
|
+
if (a.address && PROGRAMS[a.address])
|
|
252
|
+
ty = use(PROGRAMS[a.address]);
|
|
253
|
+
else if (a.signer)
|
|
254
|
+
ty = a.writable ? `${use("Mut")}<${use("Signer")}>` : use("Signer");
|
|
255
|
+
else {
|
|
256
|
+
// The IDL does not say which struct an account holds: AccountInfo until you pick Account<T> / Mut<T> / Init<T>.
|
|
257
|
+
const base = a.writable ? `${use("Mut")}<${use("AccountInfo")}>` : use("AccountInfo");
|
|
258
|
+
ty = a.address ? `${use("Address")}<${base}, "${a.address}">` : base;
|
|
259
|
+
}
|
|
260
|
+
if (a.optional)
|
|
261
|
+
ty = `${ty} | null`;
|
|
262
|
+
params.push(`${unique(ident(a.name), "Account")}: ${ty}`);
|
|
263
|
+
if (a.pda) {
|
|
264
|
+
const seeds = a.pda.seeds.map((s) => s.kind === "const"
|
|
265
|
+
? JSON.stringify(Buffer.from(s.value ?? []).toString("utf8"))
|
|
266
|
+
: s.kind === "arg"
|
|
267
|
+
? `@${camel(s.path ?? "")} (argument)`
|
|
268
|
+
: `@${camel(s.path ?? "")}`);
|
|
269
|
+
comments.push(`${ident(a.name)}: PDA [${seeds.join(", ")}]`);
|
|
270
|
+
}
|
|
271
|
+
if (a.relations?.length)
|
|
272
|
+
comments.push(`${ident(a.name)}: has_one ${a.relations.map(camel).join(", ")}`);
|
|
273
|
+
}
|
|
274
|
+
for (const g of ix.args)
|
|
275
|
+
params.push(`${unique(ident(g.name), "Arg")}: ${tsType(g.type, "arg", `${ix.name}(${g.name})`)}`);
|
|
276
|
+
let ret = "void";
|
|
277
|
+
let body = ` ${use("require")}(false, ${errorEnum}.${notPorted}); // TODO: port ${ix.name}`;
|
|
278
|
+
if (ix.returns) {
|
|
279
|
+
const r = typeof ix.returns === "string" ? ix.returns : undefined;
|
|
280
|
+
if (r && INTS.has(r))
|
|
281
|
+
((ret = use(r)), (body += "\n return 0n;"));
|
|
282
|
+
else if (r === "bool")
|
|
283
|
+
((ret = "boolean"), (body += "\n return false;"));
|
|
284
|
+
else
|
|
285
|
+
notes.push(`${ix.name}: returns ${JSON.stringify(ix.returns)}; set the return type and value`);
|
|
286
|
+
}
|
|
287
|
+
const derived = sighash(`global:${snake(fn)}`);
|
|
288
|
+
const tags = sameBytes(derived, ix.discriminator) ? [] : [`@discriminator 0x${Buffer.from(ix.discriminator).toString("hex")}`];
|
|
289
|
+
const doc = [...(ix.docs ?? []), ...comments.map((c) => `- ${c}`), ...tags];
|
|
290
|
+
if (doc.length)
|
|
291
|
+
out.push(`/**\n${doc.map((l) => ` * ${esc(l)}`).join("\n")}\n */`);
|
|
292
|
+
out.push(`export function ${fn}(${params.join(", ")}): ${ret} {`, body, "}", "");
|
|
293
|
+
}
|
|
294
|
+
for (const a of idl.accounts ?? []) {
|
|
295
|
+
if (!sameBytes(sighash(`account:${a.name}`), a.discriminator))
|
|
296
|
+
notes.push(`account ${a.name}: its discriminator is not sha256("account:${a.name}"); TypeSOL derives that one, so existing accounts would not load`);
|
|
297
|
+
}
|
|
298
|
+
const header = [
|
|
299
|
+
`// Ported from the ${idl.metadata?.name ?? "program"} IDL by \`typesol port\`. Fill in the instruction bodies;`,
|
|
300
|
+
"// pick Account<T> / Mut<T> / Init<T> for the AccountInfo parameters that hold one of the structs below.",
|
|
301
|
+
`import { ${[
|
|
302
|
+
...[...imports]
|
|
303
|
+
.filter((n) => n !== "require")
|
|
304
|
+
.sort()
|
|
305
|
+
.map((n) => `type ${n}`),
|
|
306
|
+
...(imports.has("require") ? ["require"] : []),
|
|
307
|
+
].join(", ")} } from "typesol";`,
|
|
308
|
+
"",
|
|
309
|
+
];
|
|
310
|
+
return { source: [...header, ...out].join("\n").replace(/\n{3,}/g, "\n\n"), notes };
|
|
311
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const TEMPLATE_NAMES: readonly ["counter", "vault", "escrow"];
|
|
2
|
+
export type TemplateName = (typeof TEMPLATE_NAMES)[number];
|
|
3
|
+
export interface Template {
|
|
4
|
+
program: string;
|
|
5
|
+
test: (name: string) => string;
|
|
6
|
+
/** devDependencies beyond the ones every project gets. */
|
|
7
|
+
devDependencies?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const TEMPLATES: Record<TemplateName, Template>;
|