@typesol/cli 0.15.0 → 0.17.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 +107 -0
- package/README.md +11 -7
- package/dist/cli.js +147 -20
- package/dist/clientgen.js +37 -0
- package/dist/compiler/analyze.d.ts +11 -1
- package/dist/compiler/analyze.js +307 -37
- package/dist/compiler/emit/base.d.ts +7 -0
- package/dist/compiler/emit/base.js +62 -2
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +232 -0
- package/dist/compiler/emit/calls.js +50 -15
- package/dist/compiler/emit/cpi.d.ts +32 -8
- package/dist/compiler/emit/cpi.js +706 -25
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +126 -4
- package/dist/compiler/emit/function.js +45 -12
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +12 -0
- package/dist/compiler/emit/shared.js +22 -4
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +141 -7
- package/dist/compiler/emit/storage.d.ts +6 -1
- package/dist/compiler/emit/storage.js +82 -17
- package/dist/compiler/idl.js +61 -18
- package/dist/compiler/model.d.ts +56 -4
- package/dist/compiler/model.js +8 -2
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +367 -0
- package/dist/templates.js +49 -154
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.js +11 -1
- package/docs/architecture.md +3 -0
- package/docs/cli.md +16 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +19 -14
- package/docs/mainnet.md +5 -4
- package/docs/migrate-from-anchor.md +113 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +7 -0
- package/lib/typesol.d.ts +325 -13
- package/package.json +19 -1
- package/runtime/typesol_rt.h +54 -9
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
package/dist/portgen.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
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. Account structs holding a String, Vec or Option get `@layout borsh`, so they keep
|
|
8
|
+
* Anchor's bytes (existing accounts stay readable); where a layout still differs (a String or Vec nested in a struct, a
|
|
9
|
+
* data enum, a COption) it says so in a note and a comment.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { cNameProblem } from "./compiler/analyze.js";
|
|
13
|
+
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(" "));
|
|
14
|
+
const camel = (s) => s.replace(/[_-]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
15
|
+
const pascal = (s) => camel(s).replace(/^[a-z]/, (c) => c.toUpperCase());
|
|
16
|
+
const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
17
|
+
/** Names TypeSOL source cannot use as written: TypeScript keywords, the generated C's reserved names, the builtins
|
|
18
|
+
* a skeleton imports, names starting with "_". */
|
|
19
|
+
const BUILTINS = new Set(["require"]); // what a skeleton imports as a value
|
|
20
|
+
/** A function, parameter or local name; `member`: a struct field or variant, which also becomes a C member name. */
|
|
21
|
+
const ident = (s, member = false) => {
|
|
22
|
+
let n = camel(s).replace(/^_+/, "") || "value";
|
|
23
|
+
if (/^\d/.test(n))
|
|
24
|
+
n = `v${n}`;
|
|
25
|
+
while (RESERVED.has(n) || BUILTINS.has(n) || (member && cNameProblem(n)))
|
|
26
|
+
n = `${n}Value`;
|
|
27
|
+
return n;
|
|
28
|
+
};
|
|
29
|
+
/** A type name: kept as written when it is a valid one (account names decide account discriminators). */
|
|
30
|
+
const typeName = (s) => (/^[A-Za-z][A-Za-z0-9_]*$/.test(s) && !s.includes("__") && !RESERVED.has(s) ? s : pascal(s).replace(/^(\d)/, "T$1"));
|
|
31
|
+
const sighash = (preimage) => [...createHash("sha256").update(preimage).digest().subarray(0, 8)];
|
|
32
|
+
const sameBytes = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
33
|
+
const INTS = new Set(["u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128"]);
|
|
34
|
+
const PROGRAMS = {
|
|
35
|
+
"11111111111111111111111111111111": "SystemProgram",
|
|
36
|
+
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "TokenProgram",
|
|
37
|
+
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: "Token2022Program",
|
|
38
|
+
ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: "AssociatedTokenProgram",
|
|
39
|
+
};
|
|
40
|
+
export function generatePort(idl) {
|
|
41
|
+
const notes = [];
|
|
42
|
+
const imports = new Set();
|
|
43
|
+
const use = (n) => (imports.add(n), n);
|
|
44
|
+
const accountNames = new Set((idl.accounts ?? []).map((a) => a.name));
|
|
45
|
+
const eventNames = new Set((idl.events ?? []).map((e) => e.name));
|
|
46
|
+
const definedName = (d) => (typeof d === "string" ? d : d.name);
|
|
47
|
+
const defs = new Map((idl.types ?? []).map((t) => [t.name, t]));
|
|
48
|
+
// The fixed layouts a TypeSOL-built IDL spells out as helper types map back to TypeSOL's own types.
|
|
49
|
+
const helperDefs = new Map((idl.types ?? []).filter((t) => /^(FixedString\d+|FixedOption_|FixedEnum_)/.test(t.name)).map((t) => [t.name, t]));
|
|
50
|
+
const isTuple = (fields) => !!fields?.length && (typeof fields[0] !== "object" || fields[0] === null || !("name" in fields[0]));
|
|
51
|
+
// Types the skeleton cannot declare: references to them become a placeholder with a note.
|
|
52
|
+
const skipped = new Set((idl.types ?? [])
|
|
53
|
+
.filter((d) => !helperDefs.has(d.name) &&
|
|
54
|
+
(d.generics?.length || (d.type.kind === "struct" && isTuple(d.type.fields)) || (d.type.kind !== "struct" && d.type.kind !== "enum")))
|
|
55
|
+
.map((d) => d.name));
|
|
56
|
+
const dataEnum = (name) => defs.get(name)?.type.kind === "enum" && (defs.get(name)?.type.variants ?? []).some((v) => v.fields?.length);
|
|
57
|
+
// Where each struct is used decides what a field can be: account data and struct arguments keep fixed layouts.
|
|
58
|
+
const usage = new Map();
|
|
59
|
+
const mark = (t, as) => {
|
|
60
|
+
if (!t || typeof t === "string")
|
|
61
|
+
return;
|
|
62
|
+
const inner = t.vec ?? t.option ?? t.coption ?? t.array?.[0];
|
|
63
|
+
if (inner)
|
|
64
|
+
return mark(inner, as);
|
|
65
|
+
if (!t.defined)
|
|
66
|
+
return;
|
|
67
|
+
const name = definedName(t.defined);
|
|
68
|
+
const set = usage.get(name) ?? new Set();
|
|
69
|
+
if (set.has(as))
|
|
70
|
+
return;
|
|
71
|
+
usage.set(name, set.add(as));
|
|
72
|
+
const def = defs.get(name);
|
|
73
|
+
for (const f of (def?.type.fields ?? []))
|
|
74
|
+
if (typeof f === "object" && f && "type" in f)
|
|
75
|
+
mark(f.type, as);
|
|
76
|
+
for (const v of def?.type.variants ?? [])
|
|
77
|
+
for (const f of v.fields ?? [])
|
|
78
|
+
mark(typeof f === "object" && f && "type" in f ? f.type : f, as);
|
|
79
|
+
};
|
|
80
|
+
for (const a of accountNames)
|
|
81
|
+
mark({ defined: a }, "account");
|
|
82
|
+
for (const e of eventNames)
|
|
83
|
+
mark({ defined: e }, "event");
|
|
84
|
+
for (const ix of idl.instructions)
|
|
85
|
+
for (const g of ix.args)
|
|
86
|
+
mark(g.type, "arg");
|
|
87
|
+
const fixed = (w) => w === "account" || w === "struct" || w === "variant";
|
|
88
|
+
const describe = (w) => (w === "account" ? "account data" : w === "struct" ? "a struct argument" : "a data-enum variant");
|
|
89
|
+
const tsType = (t, where, at, last = false) => {
|
|
90
|
+
if (typeof t === "string") {
|
|
91
|
+
if (INTS.has(t))
|
|
92
|
+
return use(t);
|
|
93
|
+
if (t === "bool")
|
|
94
|
+
return "boolean";
|
|
95
|
+
if (t === "pubkey" || t === "publicKey")
|
|
96
|
+
return use("Pubkey");
|
|
97
|
+
if (t === "string") {
|
|
98
|
+
if (!fixed(where))
|
|
99
|
+
return "string";
|
|
100
|
+
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.`);
|
|
101
|
+
return `${use("FixedString")}<32> /* TODO: capacity; Anchor has a variable String */`;
|
|
102
|
+
}
|
|
103
|
+
if (t === "bytes")
|
|
104
|
+
return tsType({ vec: "u8" }, where, at, last);
|
|
105
|
+
notes.push(`${at}: IDL type "${t}" has no TypeSOL equivalent; left as u64`);
|
|
106
|
+
return `${use("u64")} /* TODO: ${t} */`;
|
|
107
|
+
}
|
|
108
|
+
if (t.array) {
|
|
109
|
+
const n = Number(t.array[1]);
|
|
110
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
111
|
+
notes.push(`${at}: an array of length ${JSON.stringify(t.array[1])}; set it`);
|
|
112
|
+
return `${use("FixedArray")}<${tsType(t.array[0], where, at)}, 1> /* TODO: length */`;
|
|
113
|
+
}
|
|
114
|
+
return `${use("FixedArray")}<${tsType(t.array[0], where, at)}, ${n}>`;
|
|
115
|
+
}
|
|
116
|
+
if (t.coption)
|
|
117
|
+
notes.push(`${at}: a COption (4-byte tag in Anchor); TypeSOL's Option has a 1-byte tag, so this layout differs.`);
|
|
118
|
+
if (t.option || t.coption) {
|
|
119
|
+
const inner = tsType((t.option ?? t.coption), where === "variant" ? "variant" : where, at);
|
|
120
|
+
if (fixed(where))
|
|
121
|
+
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.`);
|
|
122
|
+
return `${use("Option")}<${inner}>`;
|
|
123
|
+
}
|
|
124
|
+
if (t.vec) {
|
|
125
|
+
const inner = tsType(t.vec, where, at);
|
|
126
|
+
if (where === "variant" || where === "struct" || (where === "account" && !last)) {
|
|
127
|
+
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.`);
|
|
128
|
+
return `${use("FixedArray")}<${inner}, 8> /* TODO: Anchor has a Vec here */`;
|
|
129
|
+
}
|
|
130
|
+
return `${use("Vec")}<${inner}>`;
|
|
131
|
+
}
|
|
132
|
+
if (t.defined) {
|
|
133
|
+
const name = definedName(t.defined);
|
|
134
|
+
const fs = /^FixedString(\d+)$/.exec(name);
|
|
135
|
+
if (fs)
|
|
136
|
+
return `${use("FixedString")}<${fs[1]}>`;
|
|
137
|
+
const helper = helperDefs.get(name);
|
|
138
|
+
if (helper && name.startsWith("FixedOption_")) {
|
|
139
|
+
const value = (helper.type.fields ?? []).find((f) => f.name === "value");
|
|
140
|
+
if (value)
|
|
141
|
+
return `${use("Option")}<${tsType(value.type, "arg", at)}>`;
|
|
142
|
+
}
|
|
143
|
+
if (name.startsWith("FixedEnum_"))
|
|
144
|
+
return typeName(name.slice("FixedEnum_".length));
|
|
145
|
+
if (skipped.has(name)) {
|
|
146
|
+
notes.push(`${at}: ${name} could not be ported (generic, tuple or unsupported type); replaced by a placeholder`);
|
|
147
|
+
return `${use("u8")} /* TODO: ${name} */`;
|
|
148
|
+
}
|
|
149
|
+
if ((where === "account" || where === "struct") && dataEnum(name))
|
|
150
|
+
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.`);
|
|
151
|
+
return typeName(name);
|
|
152
|
+
}
|
|
153
|
+
notes.push(`${at}: unsupported IDL type ${JSON.stringify(t)}; left as u64`);
|
|
154
|
+
return `${use("u64")} /* TODO */`;
|
|
155
|
+
};
|
|
156
|
+
/** A String, Vec or Option at a struct's top level: Borsh-sized in Anchor. */
|
|
157
|
+
const borshOnly = (t) => t === "string" || t === "bytes" || (typeof t === "object" && (!!t.vec || !!t.option));
|
|
158
|
+
/** A field's type in a `@layout borsh` struct, or undefined when that layout cannot hold it. Notes the capacities. */
|
|
159
|
+
const borshField = (t, at) => {
|
|
160
|
+
const quiet = at === "";
|
|
161
|
+
const fixedOk = (u) => {
|
|
162
|
+
if (typeof u === "string")
|
|
163
|
+
return INTS.has(u) || u === "bool" || u === "pubkey" || u === "publicKey";
|
|
164
|
+
if (u.array)
|
|
165
|
+
return Number.isInteger(Number(u.array[1])) && fixedOk(u.array[0]);
|
|
166
|
+
if (u.defined) {
|
|
167
|
+
const name = definedName(u.defined);
|
|
168
|
+
const d = defs.get(name);
|
|
169
|
+
if (!d || skipped.has(name) || helperDefs.has(name))
|
|
170
|
+
return false;
|
|
171
|
+
if (d.type.kind === "enum")
|
|
172
|
+
return (d.type.variants ?? []).every((v) => !v.fields?.length);
|
|
173
|
+
return (d.type.fields ?? []).every((f) => fixedOk(f.type));
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
if (t === "string") {
|
|
178
|
+
if (!quiet)
|
|
179
|
+
notes.push(`${at}: set the String's capacity to Anchor's #[max_len] (Borsh bytes are kept: @layout borsh)`);
|
|
180
|
+
return `${use("FixedString")}<32> /* TODO: #[max_len] */`;
|
|
181
|
+
}
|
|
182
|
+
if (t === "bytes")
|
|
183
|
+
return borshField({ vec: "u8" }, at);
|
|
184
|
+
if (typeof t === "object" && t.vec) {
|
|
185
|
+
const elem = t.vec === "string"
|
|
186
|
+
? `${use("FixedString")}<32> /* TODO: #[max_len] */`
|
|
187
|
+
: fixedOk(t.vec) && !quiet
|
|
188
|
+
? tsType(t.vec, "account", at)
|
|
189
|
+
: fixedOk(t.vec)
|
|
190
|
+
? "x"
|
|
191
|
+
: undefined;
|
|
192
|
+
if (elem === undefined)
|
|
193
|
+
return undefined;
|
|
194
|
+
if (!quiet)
|
|
195
|
+
notes.push(`${at}: set the Vec's capacity to Anchor's #[max_len] (Borsh bytes are kept: @layout borsh)`);
|
|
196
|
+
return `${use("FixedVec")}<${elem}, 8> /* TODO: #[max_len] */`;
|
|
197
|
+
}
|
|
198
|
+
if (typeof t === "object" && t.option) {
|
|
199
|
+
if (!fixedOk(t.option))
|
|
200
|
+
return undefined;
|
|
201
|
+
return quiet ? "x" : `${use("Option")}<${tsType(t.option, "account", at)}>`;
|
|
202
|
+
}
|
|
203
|
+
if (!fixedOk(t))
|
|
204
|
+
return undefined;
|
|
205
|
+
return quiet ? "x" : tsType(t, "account", at);
|
|
206
|
+
};
|
|
207
|
+
const esc = (s) => s.replace(/\*\//g, "* /");
|
|
208
|
+
const docs = (lines, pad = "") => (lines?.length ? `${pad}/** ${esc(lines.join(" "))} */\n` : "");
|
|
209
|
+
const out = [];
|
|
210
|
+
// Constants: integer literals and strings; expressions are left to port by hand.
|
|
211
|
+
for (const c of idl.constants ?? []) {
|
|
212
|
+
const ty = typeof c.type === "string" && INTS.has(c.type) ? c.type : undefined;
|
|
213
|
+
const name = /^[A-Za-z_][A-Za-z0-9_]*$/.test(c.name) ? c.name : ident(c.name);
|
|
214
|
+
if (ty && /^-?\d[\d_]*$/.test(c.value.trim()))
|
|
215
|
+
out.push(`export const ${name}: ${use(ty)} = ${c.value.trim().replace(/_/g, "")}n;`);
|
|
216
|
+
else if (c.type === "string" && /^".*"$/.test(c.value.trim()))
|
|
217
|
+
out.push(`export const ${name} = ${c.value.trim()};`);
|
|
218
|
+
else
|
|
219
|
+
notes.push(`constant ${c.name} = ${c.value}: not a literal; port it by hand`);
|
|
220
|
+
}
|
|
221
|
+
if (out.length)
|
|
222
|
+
out.push("");
|
|
223
|
+
// Types: structs (account data, events or arguments), enums.
|
|
224
|
+
for (const def of idl.types ?? []) {
|
|
225
|
+
if (helperDefs.has(def.name))
|
|
226
|
+
continue;
|
|
227
|
+
if (skipped.has(def.name)) {
|
|
228
|
+
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`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const used = usage.get(def.name) ?? new Set(["account"]);
|
|
232
|
+
const where = used.has("account") ? "account" : used.has("arg") ? "struct" : used.has("event") ? "event" : "account";
|
|
233
|
+
if (def.type.kind === "struct") {
|
|
234
|
+
const fields = (def.type.fields ?? []);
|
|
235
|
+
// An account struct with a String, Vec or Option of its own: `@layout borsh` keeps Anchor's bytes.
|
|
236
|
+
const borsh = accountNames.has(def.name) && !isTuple(fields) && fields.some((f) => borshOnly(f.type)) && fields.every((f) => borshField(f.type, "") !== undefined);
|
|
237
|
+
const tag = accountNames.has(def.name) ? " (account)" : eventNames.has(def.name) ? ` (event: emit<${typeName(def.name)}>({ … }))` : "";
|
|
238
|
+
const doc = def.docs?.length ? esc(def.docs.join(" ")) : `${def.name}${tag}`;
|
|
239
|
+
out.push(borsh ? `/**\n * ${doc}\n * @layout borsh\n */` : `/** ${doc} */`);
|
|
240
|
+
out.push(`export interface ${typeName(def.name)} {`);
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
fields.forEach((f, i) => {
|
|
243
|
+
let n = ident(f.name, true);
|
|
244
|
+
while (seen.has(n))
|
|
245
|
+
n = `${n}2`;
|
|
246
|
+
seen.add(n);
|
|
247
|
+
const ty = borsh ? borshField(f.type, `${def.name}.${f.name}`) : tsType(f.type, where, `${def.name}.${f.name}`, i === fields.length - 1);
|
|
248
|
+
out.push(`${docs(f.docs, " ")} ${n}: ${ty};`);
|
|
249
|
+
});
|
|
250
|
+
out.push("}", "");
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const variants = def.type.variants ?? [];
|
|
254
|
+
if (variants.every((v) => !v.fields?.length)) {
|
|
255
|
+
out.push(`${docs(def.docs)}export enum ${typeName(def.name)} {`, ...variants.map((v) => ` ${ident(v.name, true).replace(/^[a-z]/, (c) => c.toUpperCase())},`), "}", "");
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Strings in variants are fine in arguments and events (Borsh); account data needs fixed-size variants.
|
|
259
|
+
const vw = used.has("account") || (!used.has("arg") && !used.has("event")) ? "variant" : "event";
|
|
260
|
+
const parts = variants.map((v) => {
|
|
261
|
+
const fs = (v.fields ?? []).map((f, i) => typeof f === "object" && f !== null && "name" in f
|
|
262
|
+
? `${ident(f.name, true)}: ${tsType(f.type, vw, `${def.name}.${v.name}`)}`
|
|
263
|
+
: `f${i}: ${tsType(f, vw, `${def.name}.${v.name}`)}`);
|
|
264
|
+
return `{ kind: "${pascal(v.name)}"${fs.map((x) => `; ${x}`).join("")} }`;
|
|
265
|
+
});
|
|
266
|
+
out.push(`${docs(def.docs)}export type ${typeName(def.name)} =\n | ${parts.join("\n | ")};`, "");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Errors: TypeSOL numbers them 6000 + their index. NotPorted, last, marks the instructions still to write, so the
|
|
271
|
+
// codes of the program's own errors do not move.
|
|
272
|
+
const errors = idl.errors ?? [];
|
|
273
|
+
const program = typeName(pascal(idl.metadata?.name ?? "program"));
|
|
274
|
+
const errorEnum = `${program}Error`;
|
|
275
|
+
if (!errors.every((e, i) => e.code === 6000 + i))
|
|
276
|
+
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`);
|
|
277
|
+
const memberNames = new Set();
|
|
278
|
+
const member = (name) => {
|
|
279
|
+
// RequireFailedN is TypeSOL's own name for require("message") errors; it is reserved in source.
|
|
280
|
+
let n = /^RequireFailed\d+$/.test(name) ? name.replace("RequireFailed", "Check") : pascal(name);
|
|
281
|
+
while (memberNames.has(n))
|
|
282
|
+
n += "_";
|
|
283
|
+
memberNames.add(n);
|
|
284
|
+
return n;
|
|
285
|
+
};
|
|
286
|
+
const errorLines = errors.map((e) => ` ${member(e.name)} = ${JSON.stringify(e.msg ?? e.name)},`);
|
|
287
|
+
const notPorted = member("NotPorted");
|
|
288
|
+
out.push(`export enum ${errorEnum} {`, ...errorLines, ` ${notPorted} = "This instruction is not ported yet",`, "}", "");
|
|
289
|
+
// Accounts of an instruction, nested groups flattened.
|
|
290
|
+
const flatten = (items, prefix = "") => items.flatMap((a) => (a.accounts ? flatten(a.accounts, `${prefix}${a.name}_`) : [{ ...a, name: `${prefix}${a.name}` }]));
|
|
291
|
+
for (const ix of idl.instructions) {
|
|
292
|
+
const fn = ident(ix.name);
|
|
293
|
+
const params = [];
|
|
294
|
+
const comments = [];
|
|
295
|
+
const taken = new Set();
|
|
296
|
+
const unique = (n, kind) => {
|
|
297
|
+
let m = n;
|
|
298
|
+
while (taken.has(m))
|
|
299
|
+
m = `${m}${kind}`;
|
|
300
|
+
if (m !== n)
|
|
301
|
+
notes.push(`${ix.name}: ${kind.toLowerCase()} "${n}" renamed to "${m}" (an account and an argument share the name)`);
|
|
302
|
+
taken.add(m);
|
|
303
|
+
return m;
|
|
304
|
+
};
|
|
305
|
+
for (const a of flatten(ix.accounts)) {
|
|
306
|
+
let ty;
|
|
307
|
+
if (a.address && PROGRAMS[a.address])
|
|
308
|
+
ty = use(PROGRAMS[a.address]);
|
|
309
|
+
else if (a.signer)
|
|
310
|
+
ty = a.writable ? `${use("Mut")}<${use("Signer")}>` : use("Signer");
|
|
311
|
+
else {
|
|
312
|
+
// The IDL does not say which struct an account holds: AccountInfo until you pick Account<T> / Mut<T> / Init<T>.
|
|
313
|
+
const base = a.writable ? `${use("Mut")}<${use("AccountInfo")}>` : use("AccountInfo");
|
|
314
|
+
ty = a.address ? `${use("Address")}<${base}, "${a.address}">` : base;
|
|
315
|
+
}
|
|
316
|
+
if (a.optional)
|
|
317
|
+
ty = `${ty} | null`;
|
|
318
|
+
params.push(`${unique(ident(a.name), "Account")}: ${ty}`);
|
|
319
|
+
if (a.pda) {
|
|
320
|
+
const seeds = a.pda.seeds.map((s) => s.kind === "const"
|
|
321
|
+
? JSON.stringify(Buffer.from(s.value ?? []).toString("utf8"))
|
|
322
|
+
: s.kind === "arg"
|
|
323
|
+
? `@${camel(s.path ?? "")} (argument)`
|
|
324
|
+
: `@${camel(s.path ?? "")}`);
|
|
325
|
+
comments.push(`${ident(a.name)}: PDA [${seeds.join(", ")}]`);
|
|
326
|
+
}
|
|
327
|
+
if (a.relations?.length)
|
|
328
|
+
comments.push(`${ident(a.name)}: has_one ${a.relations.map(camel).join(", ")}`);
|
|
329
|
+
}
|
|
330
|
+
for (const g of ix.args)
|
|
331
|
+
params.push(`${unique(ident(g.name), "Arg")}: ${tsType(g.type, "arg", `${ix.name}(${g.name})`)}`);
|
|
332
|
+
let ret = "void";
|
|
333
|
+
let body = ` ${use("require")}(false, ${errorEnum}.${notPorted}); // TODO: port ${ix.name}`;
|
|
334
|
+
if (ix.returns) {
|
|
335
|
+
const r = typeof ix.returns === "string" ? ix.returns : undefined;
|
|
336
|
+
if (r && INTS.has(r))
|
|
337
|
+
((ret = use(r)), (body += "\n return 0n;"));
|
|
338
|
+
else if (r === "bool")
|
|
339
|
+
((ret = "boolean"), (body += "\n return false;"));
|
|
340
|
+
else
|
|
341
|
+
notes.push(`${ix.name}: returns ${JSON.stringify(ix.returns)}; set the return type and value`);
|
|
342
|
+
}
|
|
343
|
+
const derived = sighash(`global:${snake(fn)}`);
|
|
344
|
+
const tags = sameBytes(derived, ix.discriminator) ? [] : [`@discriminator 0x${Buffer.from(ix.discriminator).toString("hex")}`];
|
|
345
|
+
const doc = [...(ix.docs ?? []), ...comments.map((c) => `- ${c}`), ...tags];
|
|
346
|
+
if (doc.length)
|
|
347
|
+
out.push(`/**\n${doc.map((l) => ` * ${esc(l)}`).join("\n")}\n */`);
|
|
348
|
+
out.push(`export function ${fn}(${params.join(", ")}): ${ret} {`, body, "}", "");
|
|
349
|
+
}
|
|
350
|
+
for (const a of idl.accounts ?? []) {
|
|
351
|
+
if (!sameBytes(sighash(`account:${a.name}`), a.discriminator))
|
|
352
|
+
notes.push(`account ${a.name}: its discriminator is not sha256("account:${a.name}"); TypeSOL derives that one, so existing accounts would not load`);
|
|
353
|
+
}
|
|
354
|
+
const header = [
|
|
355
|
+
`// Ported from the ${idl.metadata?.name ?? "program"} IDL by \`typesol port\`. Fill in the instruction bodies;`,
|
|
356
|
+
"// pick Account<T> / Mut<T> / Init<T> for the AccountInfo parameters that hold one of the structs below.",
|
|
357
|
+
`import { ${[
|
|
358
|
+
...[...imports]
|
|
359
|
+
.filter((n) => n !== "require")
|
|
360
|
+
.sort()
|
|
361
|
+
.map((n) => `type ${n}`),
|
|
362
|
+
...(imports.has("require") ? ["require"] : []),
|
|
363
|
+
].join(", ")} } from "typesol";`,
|
|
364
|
+
"",
|
|
365
|
+
];
|
|
366
|
+
return { source: [...header, ...out].join("\n").replace(/\n{3,}/g, "\n\n"), notes };
|
|
367
|
+
}
|
package/dist/templates.js
CHANGED
|
@@ -2,71 +2,16 @@
|
|
|
2
2
|
// main flow and at least one refused call, so a new project starts from something that passes and shows how to test.
|
|
3
3
|
// The programs are copies of examples/ (test/workspace.test.ts checks they still match).
|
|
4
4
|
export const TEMPLATE_NAMES = ["counter", "vault", "escrow"];
|
|
5
|
-
/**
|
|
5
|
+
/** Every test starts the same way: build the project's program into a LiteSVM with `typesol/testing`. */
|
|
6
6
|
const testFile = (name, o) => `// Runs the program in LiteSVM (an in-process Solana runtime): \`npm test\`.
|
|
7
|
-
import {
|
|
8
|
-
${o.imports}import {
|
|
9
|
-
${[
|
|
10
|
-
"AccountRole",
|
|
11
|
-
"appendTransactionMessageInstructions",
|
|
12
|
-
"createTransactionMessage",
|
|
13
|
-
...o.kit,
|
|
14
|
-
"pipe",
|
|
15
|
-
"setTransactionMessageFeePayerSigner",
|
|
16
|
-
"signTransactionMessageWithSigners",
|
|
17
|
-
"type Address",
|
|
18
|
-
"type Instruction",
|
|
19
|
-
"type KeyPairSigner",
|
|
20
|
-
]
|
|
21
|
-
.map((x) => ` ${x},`)
|
|
22
|
-
.join("\n")}
|
|
23
|
-
} from "@solana/kit";
|
|
24
|
-
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
|
|
25
|
-
import path from "node:path";
|
|
26
|
-
import { fileURLToPath } from "node:url";
|
|
27
|
-
import { buildWorkspace, findWorkspace } from "typesol/workspace";
|
|
7
|
+
${o.imports ?? ""}import { testProgram, type TestProgram } from "typesol/testing";
|
|
28
8
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
29
9
|
|
|
30
|
-
const SYSTEM = "11111111111111111111111111111111" as Address;
|
|
31
|
-
const ws = findWorkspace(path.dirname(fileURLToPath(import.meta.url)))!;
|
|
32
|
-
|
|
33
10
|
describe("${name}", () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
beforeAll(() => {
|
|
38
|
-
const [built] = buildWorkspace(ws, ["${name}"]);
|
|
39
|
-
programId = built.address as Address;
|
|
40
|
-
svm.addProgramFromFile(programId, built.result.soPath);
|
|
41
|
-
coder = new BorshCoder(built.result.idl as unknown as Idl);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const ix = (name: string, args: object, accounts: { address: Address; signer?: KeyPairSigner; writable?: boolean }[]): Instruction => ({
|
|
45
|
-
programAddress: programId,
|
|
46
|
-
accounts: accounts.map((a) => ({
|
|
47
|
-
address: a.address,
|
|
48
|
-
role: a.signer ? (a.writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER) : a.writable ? AccountRole.WRITABLE : AccountRole.READONLY,
|
|
49
|
-
...(a.signer ? { signer: a.signer } : {}),
|
|
50
|
-
})) as Instruction["accounts"],
|
|
51
|
-
data: new Uint8Array(coder.instruction.encode(name, args)),
|
|
11
|
+
let t: TestProgram;
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
t = await testProgram("${name}", import.meta.url);
|
|
52
14
|
});
|
|
53
|
-
const send = async (payer: KeyPairSigner, ixs: Instruction[]) => {
|
|
54
|
-
const tx = await pipe(
|
|
55
|
-
createTransactionMessage({ version: 0 }),
|
|
56
|
-
(t) => setTransactionMessageFeePayerSigner(payer, t),
|
|
57
|
-
(t) => svm.setTransactionMessageLifetimeUsingLatestBlockhash(t),
|
|
58
|
-
(t) => appendTransactionMessageInstructions(ixs, t),
|
|
59
|
-
(t) => signTransactionMessageWithSigners(t),
|
|
60
|
-
);
|
|
61
|
-
const res = svm.sendTransaction(tx);
|
|
62
|
-
svm.expireBlockhash();
|
|
63
|
-
return res;
|
|
64
|
-
};
|
|
65
|
-
/** The custom error code of a failed transaction: 6000 + the member's index in the program's error enum. */
|
|
66
|
-
const errorCode = (res: TransactionMetadata | FailedTransactionMetadata) => {
|
|
67
|
-
if (!(res instanceof FailedTransactionMetadata)) throw new Error("expected the transaction to fail");
|
|
68
|
-
return Number(/Custom\\D*(\\d+)/.exec(String(res.err()))?.[1]);
|
|
69
|
-
};
|
|
70
15
|
${o.body}});
|
|
71
16
|
`;
|
|
72
17
|
export const TEMPLATES = {
|
|
@@ -101,25 +46,17 @@ export function assertAtLeast(counter: Account<Counter>, min: u64): void {
|
|
|
101
46
|
}
|
|
102
47
|
`,
|
|
103
48
|
test: (name) => testFile(name, {
|
|
104
|
-
imports: "",
|
|
105
|
-
kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
|
|
106
49
|
body: `
|
|
107
50
|
it("initializes and increments the caller's counter; refuses anyone else", async () => {
|
|
108
|
-
const alice = await
|
|
109
|
-
const mallory = await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
expect(await send(alice, [ix("initialize", {}, [me, { address: counter, writable: true }, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
|
|
115
|
-
expect(await send(alice, [ix("increment", { by: new BN(5) }, [{ address: alice.address, signer: alice }, { address: counter, writable: true }])])).toBeInstanceOf(TransactionMetadata);
|
|
116
|
-
const account = svm.getAccount(counter);
|
|
117
|
-
if (!account.exists) throw new Error("counter was not created");
|
|
118
|
-
expect(coder.accounts.decode("Counter", Buffer.from(account.data)).count.toString()).toBe("5");
|
|
51
|
+
const alice = await t.wallet();
|
|
52
|
+
const mallory = await t.wallet();
|
|
53
|
+
const counter = await t.pda("counter", alice);
|
|
54
|
+
t.ok(await t.send(alice, t.ix("initialize", {}, { authority: alice, counter })));
|
|
55
|
+
t.ok(await t.send(alice, t.ix("increment", { by: 5n }, { authority: alice, counter })));
|
|
56
|
+
expect((await t.account<{ count: bigint }>("Counter", counter))?.count).toBe(5n);
|
|
119
57
|
|
|
120
58
|
// CounterError.Unauthorized is the enum's first member: 6000.
|
|
121
|
-
|
|
122
|
-
expect(errorCode(await send(mallory, [stranger]))).toBe(6000);
|
|
59
|
+
expect(t.errorCode(await t.send(mallory, t.ix("increment", { by: 1n }, { authority: mallory, counter })))).toBe(6000);
|
|
123
60
|
});
|
|
124
61
|
`,
|
|
125
62
|
}),
|
|
@@ -182,41 +119,30 @@ export function shut(owner: Mut<Signer>, vault: Mut<Vault>): void {
|
|
|
182
119
|
}
|
|
183
120
|
`,
|
|
184
121
|
test: (name) => testFile(name, {
|
|
185
|
-
imports: "",
|
|
186
|
-
kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
|
|
187
122
|
body: `
|
|
188
123
|
it("deposits and withdraws SOL, keeps the rent reserve, then closes", async () => {
|
|
189
|
-
const owner = await
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(
|
|
196
|
-
const rent = svm.getBalance(vault)!;
|
|
197
|
-
expect(await send(owner, [ix("deposit", { amount: new BN(3_000_000_000) }, [me, v, { address: SYSTEM }])])).toBeInstanceOf(TransactionMetadata);
|
|
198
|
-
expect(await send(owner, [ix("withdraw", { amount: new BN(1_000_000_000) }, [me, v])])).toBeInstanceOf(TransactionMetadata);
|
|
199
|
-
expect(svm.getBalance(vault)).toBe(rent + 2_000_000_000n);
|
|
124
|
+
const owner = await t.wallet();
|
|
125
|
+
const vault = await t.pda("vault", owner);
|
|
126
|
+
t.ok(await t.send(owner, t.ix("open", {}, { owner, vault })));
|
|
127
|
+
const rent = t.balance(vault);
|
|
128
|
+
t.ok(await t.send(owner, t.ix("deposit", { amount: 3_000_000_000n }, { owner, vault })));
|
|
129
|
+
t.ok(await t.send(owner, t.ix("withdraw", { amount: 1_000_000_000n }, { owner, vault })));
|
|
130
|
+
expect(t.balance(vault)).toBe(rent + 2_000_000_000n);
|
|
200
131
|
|
|
201
132
|
// VaultError.InsufficientFunds (third member, 6002): the rent reserve stays.
|
|
202
|
-
expect(errorCode(await send(owner,
|
|
133
|
+
expect(t.errorCode(await t.send(owner, t.ix("withdraw", { amount: 2_000_000_001n }, { owner, vault })))).toBe(6002);
|
|
203
134
|
|
|
204
|
-
|
|
205
|
-
expect(
|
|
135
|
+
t.ok(await t.send(owner, t.ix("shut", {}, { owner, vault })));
|
|
136
|
+
expect(await t.account("Vault", vault)).toBeNull();
|
|
206
137
|
});
|
|
207
138
|
|
|
208
139
|
it("refuses a stranger", async () => {
|
|
209
|
-
const owner = await
|
|
210
|
-
const thief = await
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const [vault] = await getProgramDerivedAddress({ programAddress: programId, seeds: ["vault", getAddressEncoder().encode(owner.address)] });
|
|
214
|
-
const v = { address: vault, writable: true };
|
|
215
|
-
expect(await send(owner, [ix("open", {}, [{ address: owner.address, signer: owner, writable: true }, v, { address: SYSTEM }])])).toBeInstanceOf(
|
|
216
|
-
TransactionMetadata,
|
|
217
|
-
);
|
|
140
|
+
const owner = await t.wallet();
|
|
141
|
+
const thief = await t.wallet();
|
|
142
|
+
const vault = await t.pda("vault", owner);
|
|
143
|
+
t.ok(await t.send(owner, t.ix("open", {}, { owner, vault })));
|
|
218
144
|
// VaultError.NotOwner: 6000.
|
|
219
|
-
expect(errorCode(await send(thief,
|
|
145
|
+
expect(t.errorCode(await t.send(thief, t.ix("shut", {}, { owner: thief, vault })))).toBe(6000);
|
|
220
146
|
});
|
|
221
147
|
`,
|
|
222
148
|
}),
|
|
@@ -330,18 +256,17 @@ export function refund(
|
|
|
330
256
|
}
|
|
331
257
|
`,
|
|
332
258
|
test: (name) => testFile(name, {
|
|
333
|
-
imports: `import { AccountState, getMintEncoder, getMintSize, getTokenDecoder, getTokenEncoder, getTokenSize } from "@solana-program/token";\n`,
|
|
334
|
-
kit: ["generateKeyPairSigner", "getAddressEncoder", "getProgramDerivedAddress", "lamports"],
|
|
259
|
+
imports: `import { AccountState, getMintEncoder, getMintSize, getTokenDecoder, getTokenEncoder, getTokenSize } from "@solana-program/token";\nimport { type Address, type KeyPairSigner, generateKeyPairSigner, lamports } from "@solana/kit";\n`,
|
|
335
260
|
body: `
|
|
336
261
|
const TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;
|
|
337
262
|
/** A mint, written straight into the VM (a real client sends System CreateAccount + InitializeMint2). */
|
|
338
263
|
const mint = async (authority: Address) => {
|
|
339
264
|
const address = (await generateKeyPairSigner()).address;
|
|
340
265
|
const data = getMintEncoder().encode({ mintAuthority: authority, supply: 0n, decimals: 6, isInitialized: true, freezeAuthority: null });
|
|
341
|
-
svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(1_461_600n), programAddress: TOKEN, space: BigInt(getMintSize()) });
|
|
266
|
+
t.svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(1_461_600n), programAddress: TOKEN, space: BigInt(getMintSize()) });
|
|
342
267
|
return address;
|
|
343
268
|
};
|
|
344
|
-
/** A token account of
|
|
269
|
+
/** A token account of owner for mint holding amount, written straight into the VM. */
|
|
345
270
|
const tokenAccount = async (owner: Address, mintAddress: Address, amount: bigint) => {
|
|
346
271
|
const address = (await generateKeyPairSigner()).address;
|
|
347
272
|
const data = getTokenEncoder().encode({
|
|
@@ -354,73 +279,43 @@ export function refund(
|
|
|
354
279
|
delegatedAmount: 0n,
|
|
355
280
|
closeAuthority: null,
|
|
356
281
|
});
|
|
357
|
-
svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(2_039_280n), programAddress: TOKEN, space: BigInt(getTokenSize()) });
|
|
282
|
+
t.svm.setAccount({ address, data: new Uint8Array(data), executable: false, lamports: lamports(2_039_280n), programAddress: TOKEN, space: BigInt(getTokenSize()) });
|
|
358
283
|
return address;
|
|
359
284
|
};
|
|
360
285
|
const balance = (address: Address) => {
|
|
361
|
-
const account = svm.getAccount(address);
|
|
286
|
+
const account = t.svm.getAccount(address);
|
|
362
287
|
return account.exists ? getTokenDecoder().decode(account.data).amount : null;
|
|
363
288
|
};
|
|
364
289
|
|
|
365
290
|
it("escrows mint A for mint B; the taker swaps, the maker can refund", async () => {
|
|
366
|
-
const maker = await
|
|
367
|
-
const taker = await
|
|
368
|
-
svm.airdrop(maker.address, lamports(1_000_000_000n));
|
|
369
|
-
svm.airdrop(taker.address, lamports(1_000_000_000n));
|
|
291
|
+
const maker = await t.wallet();
|
|
292
|
+
const taker = await t.wallet();
|
|
370
293
|
const mintA = await mint(maker.address);
|
|
371
294
|
const mintB = await mint(taker.address);
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
295
|
+
const makerTokenA = await tokenAccount(maker.address, mintA, 1_000n);
|
|
296
|
+
const makerTokenB = await tokenAccount(maker.address, mintB, 0n);
|
|
297
|
+
const takerTokenA = await tokenAccount(taker.address, mintA, 0n);
|
|
298
|
+
const takerTokenB = await tokenAccount(taker.address, mintB, 500n);
|
|
376
299
|
|
|
377
|
-
const open = async (seed:
|
|
378
|
-
const
|
|
379
|
-
programAddress: programId,
|
|
380
|
-
seeds: ["offer", getAddressEncoder().encode(maker.address), new Uint8Array(new BN(seed).toArray("le", 8))],
|
|
381
|
-
});
|
|
300
|
+
const open = async (seed: bigint) => {
|
|
301
|
+
const offer = await t.pda("offer", maker, seed);
|
|
382
302
|
// The client creates the vault: an empty token account for mint A owned by the offer PDA.
|
|
383
303
|
const vault = await tokenAccount(offer, mintA, 0n);
|
|
384
|
-
|
|
385
|
-
{ address: maker.address, signer: maker, writable: true },
|
|
386
|
-
{ address: offer, writable: true },
|
|
387
|
-
{ address: makerA, writable: true },
|
|
388
|
-
{ address: vault, writable: true },
|
|
389
|
-
{ address: SYSTEM },
|
|
390
|
-
{ address: TOKEN },
|
|
391
|
-
]);
|
|
392
|
-
expect(await send(maker, [make])).toBeInstanceOf(TransactionMetadata);
|
|
304
|
+
t.ok(await t.send(maker, t.ix("make", { seed, amountA: 400n, wantB: 300n, mintB }, { maker, offer, makerTokenA, vault })));
|
|
393
305
|
return { offer, vault };
|
|
394
306
|
};
|
|
395
307
|
|
|
396
|
-
const first = await open(
|
|
308
|
+
const first = await open(1n);
|
|
397
309
|
expect(balance(first.vault)).toBe(400n);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
{ address: maker.address, writable: true },
|
|
401
|
-
{ address: first.offer, writable: true },
|
|
402
|
-
{ address: takerA, writable: true },
|
|
403
|
-
{ address: takerB, writable: true },
|
|
404
|
-
{ address: makerB, writable: true },
|
|
405
|
-
{ address: first.vault, writable: true },
|
|
406
|
-
{ address: TOKEN },
|
|
407
|
-
]);
|
|
408
|
-
expect(await send(taker, [take])).toBeInstanceOf(TransactionMetadata);
|
|
409
|
-
expect([balance(takerA), balance(makerB), balance(first.vault)]).toEqual([400n, 300n, null]);
|
|
310
|
+
t.ok(await t.send(taker, t.ix("take", {}, { taker, maker: maker.address, offer: first.offer, takerTokenA, takerTokenB, makerTokenB, vault: first.vault })));
|
|
311
|
+
expect([balance(takerTokenA), balance(makerTokenB), balance(first.vault)]).toEqual([400n, 300n, null]);
|
|
410
312
|
|
|
411
|
-
const second = await open(
|
|
412
|
-
const refund = (
|
|
413
|
-
ix("refund", {}, [
|
|
414
|
-
{ address: signer.address, signer, writable: true },
|
|
415
|
-
{ address: second.offer, writable: true },
|
|
416
|
-
{ address: makerA, writable: true },
|
|
417
|
-
{ address: second.vault, writable: true },
|
|
418
|
-
{ address: TOKEN },
|
|
419
|
-
]);
|
|
313
|
+
const second = await open(2n);
|
|
314
|
+
const refund = (who: KeyPairSigner) => t.ix("refund", {}, { maker: who, offer: second.offer, makerTokenA, vault: second.vault });
|
|
420
315
|
// EscrowError.WrongMaker (third member, 6002): only the maker refunds.
|
|
421
|
-
expect(errorCode(await send(taker,
|
|
422
|
-
|
|
423
|
-
expect([balance(
|
|
316
|
+
expect(t.errorCode(await t.send(taker, refund(taker)))).toBe(6002);
|
|
317
|
+
t.ok(await t.send(maker, refund(maker)));
|
|
318
|
+
expect([balance(makerTokenA), await t.account("Offer", second.offer)]).toEqual([600n, null]);
|
|
424
319
|
});
|
|
425
320
|
`,
|
|
426
321
|
}),
|