@typesol/cli 0.24.0 → 0.25.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 +46 -2
- package/README.md +10 -7
- package/dist/clientgen.js +36 -21
- package/dist/compiler/analyze.js +32 -2
- package/dist/compiler/emit/base.d.ts +10 -0
- package/dist/compiler/emit/base.js +8 -0
- package/dist/compiler/emit/calls.js +10 -10
- package/dist/compiler/emit/cpi.d.ts +10 -1
- package/dist/compiler/emit/cpi.js +182 -166
- package/dist/compiler/emit/function.d.ts +9 -0
- package/dist/compiler/emit/function.js +169 -139
- package/dist/compiler/emit/program.js +3 -0
- package/dist/compiler/emit/shared.d.ts +1 -0
- package/dist/compiler/emit/shared.js +1 -0
- package/dist/compiler/emit/statements.d.ts +6 -0
- package/dist/compiler/emit/statements.js +35 -5
- package/docs/architecture.md +26 -2
- package/docs/case-study-escrow.md +224 -0
- package/docs/errors.md +26 -1
- package/docs/language.md +12 -8
- package/docs/migrate-from-anchor.md +3 -1
- package/docs/runtime.md +59 -0
- package/lib/typesol.d.ts +35 -13
- package/package.json +1 -1
- package/runtime/typesol_rt.h +85 -14
|
@@ -38,8 +38,10 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
38
38
|
const k = a.kind;
|
|
39
39
|
if (k.kind === "signer")
|
|
40
40
|
head.push(` TS_TRY(ts_check_signer(${v}));`);
|
|
41
|
+
// Mut<…>: Anchor's `mut` (2000). Anchor checks it with the constraints, after seeds and associated token; here it
|
|
42
|
+
// sits with the type checks, which keeps binaries smaller (only a transaction breaking both sees the difference).
|
|
41
43
|
if (isWritable(k))
|
|
42
|
-
head.push(` TS_TRY(
|
|
44
|
+
head.push(` TS_TRY(ts_check_mut(${v}));`);
|
|
43
45
|
// A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
|
|
44
46
|
if (k.kind === "account")
|
|
45
47
|
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : emptySizeC(k.struct)}));`);
|
|
@@ -160,7 +162,9 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
160
162
|
pairs.push(`if (${present}a_${writable[i].name} == a_${writable[j].name}) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: ${writable[i].name} and ${writable[j].name} must be different accounts");`);
|
|
161
163
|
}
|
|
162
164
|
head.push(...pairs.map((p) => ` ${p}`));
|
|
163
|
-
// Arguments: Borsh, read sequentially (strings make later offsets dynamic).
|
|
165
|
+
// Arguments: Borsh, read sequentially (strings make later offsets dynamic). Anchor decodes them before the accounts;
|
|
166
|
+
// here they come after the account types (decoding first keeps them live across the checks: larger binaries), so
|
|
167
|
+
// only a transaction wrong in both ways sees a different error.
|
|
164
168
|
if (ix.args.length)
|
|
165
169
|
head.push(` uint64_t off = ${ix.discriminator.length};`);
|
|
166
170
|
const need = (bytes) => head.push(` if (params->data_len < off + ${bytes}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
@@ -365,6 +369,11 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
365
369
|
}),
|
|
366
370
|
];
|
|
367
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Each account's constraints, account by account in parameter order and, within one account, in Anchor's order
|
|
374
|
+
* (anchor-syn's `linearize`): seeds, associated token, has_one, owner, executable, address, token::, mint::.
|
|
375
|
+
* A transaction that breaks several checks then fails with the same error code as under Anchor.
|
|
376
|
+
*/
|
|
368
377
|
constraintChecks(ix) {
|
|
369
378
|
const out = [];
|
|
370
379
|
// Bumps found by Seeds<…> checks, declared ahead of the (possibly optional-guarded) checks for `acct.pdaBump`.
|
|
@@ -375,21 +384,40 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
375
384
|
const k = a.kind.kind;
|
|
376
385
|
if (c.hasOne.length && k !== "account" && k !== "token" && k !== "mint")
|
|
377
386
|
this.reporter.fail(a.node, "HasOne applies to existing accounts (Account<T> / Mut<T>), TokenAccount and Mint");
|
|
378
|
-
|
|
387
|
+
if (c.closeTo) {
|
|
388
|
+
const dest = ix.accounts.find((o) => o.name === c.closeTo);
|
|
389
|
+
if (!isWritable(dest.kind))
|
|
390
|
+
this.reporter.fail(a.node, `CloseTo<…, "${c.closeTo}">: ${c.closeTo} must be writable (Mut<…>)`);
|
|
391
|
+
}
|
|
392
|
+
if (c.seeds)
|
|
393
|
+
out.push(...this.seedsCheck(ix, a, bumpDecls));
|
|
394
|
+
if (c.ata)
|
|
395
|
+
out.push(...this.ataCheck(ix, a));
|
|
396
|
+
// has_one on a program account; on a token account or mint the same entries are Anchor's token:: / mint::
|
|
397
|
+
// constraints, which come last.
|
|
398
|
+
const hasOne = [];
|
|
399
|
+
// Anchor's order inside token:: (authority, mint, token program) and mint:: (authorities, token program).
|
|
400
|
+
const rank = (e) => ["owner=", "mint=", "mintAuthority=", "freezeAuthority=", "tokenProgram="].findIndex((x) => e.startsWith(x));
|
|
401
|
+
const entries = k === "token" || k === "mint" ? [...c.hasOne].sort((x, y) => rank(x) - rank(y)) : c.hasOne;
|
|
402
|
+
for (const entry of entries) {
|
|
379
403
|
// "field=param" (TokenOf / MintOf) or a field checked against the parameter of the same name (HasOne).
|
|
380
404
|
const [f, p = f] = entry.split("=");
|
|
381
|
-
if (
|
|
382
|
-
|
|
405
|
+
if (f === "tokenProgram")
|
|
406
|
+
hasOne.push(` if (!ts_key_eq(&a_${a.name}->owner, &a_${p}->key)) TS_FAIL(${k === "mint" ? "TS_ERR_MINT_TOKEN_PROGRAM" : "TS_ERR_TOKEN_TOKEN_PROGRAM"}, "TypeSOL: ${a.name} is not owned by ${p}");`);
|
|
407
|
+
else if (k === "token")
|
|
408
|
+
hasOne.push(f === "mint"
|
|
383
409
|
? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${p}");`
|
|
384
410
|
: ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${p}");`);
|
|
385
411
|
else if (k === "mint") {
|
|
386
412
|
const at = f === "mintAuthority" ? 0 : 46;
|
|
387
413
|
const code = f === "mintAuthority" ? "TS_ERR_MINT_AUTHORITY" : "TS_ERR_FREEZE_AUTHORITY";
|
|
388
|
-
|
|
414
|
+
hasOne.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${p}->key)) TS_FAIL(${code}, "TypeSOL: ${p} is not the ${f} of ${a.name}");`);
|
|
389
415
|
}
|
|
390
416
|
else
|
|
391
|
-
|
|
417
|
+
hasOne.push(` if (!ts_key_eq(&d_${a.name}->${f}, &a_${f}->key)) TS_FAIL(TS_ERR_CONSTRAINT, "TypeSOL: ${a.name}.${f} must be the ${f} account");`);
|
|
392
418
|
}
|
|
419
|
+
if (k === "account")
|
|
420
|
+
out.push(...hasOne);
|
|
393
421
|
if (c.owner) {
|
|
394
422
|
out.push(` static const SolPubkey co_${a.name} = {${cBytes(c.owner.bytes)}};`);
|
|
395
423
|
out.push(` if (!ts_key_eq(&a_${a.name}->owner, &co_${a.name})) TS_FAIL(TS_ERR_CONSTRAINT_OWNER, "TypeSOL: ${a.name} is not owned by the expected program");`);
|
|
@@ -400,138 +428,8 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
400
428
|
out.push(` static const SolPubkey ca_${a.name} = {${cBytes(c.address.bytes)}};`);
|
|
401
429
|
out.push(` if (!ts_key_eq(&a_${a.name}->key, &ca_${a.name})) TS_FAIL(TS_ERR_ADDRESS, "TypeSOL: ${a.name} is not the expected address");`);
|
|
402
430
|
}
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
// existing one there. Only the IDL entry is recorded here.
|
|
406
|
-
const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
|
|
407
|
-
if (tp && !ix.pdas.has(a.name)) {
|
|
408
|
-
ix.pdas.set(a.name, [
|
|
409
|
-
{ kind: "account", path: snakeCase(c.ata.owner) },
|
|
410
|
-
{ kind: "account", path: snakeCase(tp.name) },
|
|
411
|
-
{ kind: "account", path: snakeCase(c.ata.mint) },
|
|
412
|
-
]);
|
|
413
|
-
ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
else if (c.ata) {
|
|
417
|
-
// The token program is the account's owner; an AccountInfo not created yet takes the instruction's token program.
|
|
418
|
-
let program = `&a_${a.name}->owner`;
|
|
419
|
-
if (k !== "token") {
|
|
420
|
-
const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
|
|
421
|
-
if (!tp)
|
|
422
|
-
this.reporter.fail(a.node, "Ata<AccountInfo, …> needs a token program parameter to derive the address");
|
|
423
|
-
program = `&a_${tp.name}->key`;
|
|
424
|
-
}
|
|
425
|
-
out.push(` { SolPubkey ata_${a.name};`);
|
|
426
|
-
out.push(` TS_TRY(ts_ata_address(&a_${c.ata.owner}->key, &a_${c.ata.mint}->key, ${program}, &ata_${a.name}));`);
|
|
427
|
-
out.push(` if (!ts_key_eq(&ata_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_ASSOCIATED, "TypeSOL: ${a.name} is not the associated token account of ${c.ata.owner} and ${c.ata.mint}"); }`);
|
|
428
|
-
// The address alone is not enough: legacy SPL Token lets an ATA's owner be reassigned.
|
|
429
|
-
if (k === "token") {
|
|
430
|
-
out.push(` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${c.ata.mint}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${c.ata.mint}");`);
|
|
431
|
-
out.push(` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${c.ata.owner}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${c.ata.owner}");`);
|
|
432
|
-
}
|
|
433
|
-
const tp = ix.accounts.find((o) => o.kind.kind === "tokenProgram");
|
|
434
|
-
if (tp && !ix.pdas.has(a.name)) {
|
|
435
|
-
ix.pdas.set(a.name, [
|
|
436
|
-
{ kind: "account", path: snakeCase(c.ata.owner) },
|
|
437
|
-
{ kind: "account", path: snakeCase(tp.name) },
|
|
438
|
-
{ kind: "account", path: snakeCase(c.ata.mint) },
|
|
439
|
-
]);
|
|
440
|
-
ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
if (c.closeTo) {
|
|
444
|
-
const dest = ix.accounts.find((o) => o.name === c.closeTo);
|
|
445
|
-
if (!isWritable(dest.kind))
|
|
446
|
-
this.reporter.fail(a.node, `CloseTo<…, "${c.closeTo}">: ${c.closeTo} must be writable (Mut<…>)`);
|
|
447
|
-
}
|
|
448
|
-
if (c.seeds) {
|
|
449
|
-
const parts = [];
|
|
450
|
-
const idl = [];
|
|
451
|
-
for (const raw of c.seeds.names) {
|
|
452
|
-
// "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
|
|
453
|
-
const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
|
|
454
|
-
const n = ref ?? raw;
|
|
455
|
-
// "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
|
|
456
|
-
// `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
|
|
457
|
-
if (ref?.includes(".")) {
|
|
458
|
-
const [owner, field, ...rest] = ref.split(".");
|
|
459
|
-
const o = ix.accounts.find((x) => x.name === owner);
|
|
460
|
-
if (!o || o.kind.kind !== "account" || rest.length)
|
|
461
|
-
this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
|
|
462
|
-
const st = o.kind.struct;
|
|
463
|
-
const f = st.fields.find((x) => x.name === field);
|
|
464
|
-
if (!f)
|
|
465
|
-
this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
|
|
466
|
-
const t = f.type;
|
|
467
|
-
if (t.kind === "pubkey")
|
|
468
|
-
parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
|
|
469
|
-
else if (t.kind === "int" || t.kind === "enum")
|
|
470
|
-
parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
|
|
471
|
-
else
|
|
472
|
-
this.reporter.fail(a.node, `Seeds: field "${ref}" of type ${typeName(t)} cannot be a seed (use a Pubkey, an integer or a unit enum)`);
|
|
473
|
-
idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
|
|
477
|
-
const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
|
|
478
|
-
if (ref && !acct && !arg)
|
|
479
|
-
this.reporter.fail(a.node, `Seeds: "@${ref}" is neither an account parameter nor an argument`);
|
|
480
|
-
if (acct) {
|
|
481
|
-
parts.push(`{(const uint8_t *)&a_${n}->key, 32}`);
|
|
482
|
-
idl.push({ kind: "account", path: snakeCase(n) });
|
|
483
|
-
}
|
|
484
|
-
else if (arg) {
|
|
485
|
-
const t = arg.type;
|
|
486
|
-
if (t.kind === "string")
|
|
487
|
-
parts.push(`{arg_${n}.ptr, arg_${n}.len}`);
|
|
488
|
-
else if (t.kind === "pubkey")
|
|
489
|
-
parts.push(`{(const uint8_t *)&arg_${n}, 32}`);
|
|
490
|
-
else if (t.kind === "int" || t.kind === "enum")
|
|
491
|
-
parts.push(`{(const uint8_t *)&arg_${n}, ${scalarSize(t)}}`);
|
|
492
|
-
else
|
|
493
|
-
this.reporter.fail(a.node, `Seeds: argument "${n}" of type ${typeName(t)} cannot be a seed`);
|
|
494
|
-
idl.push({ kind: "arg", path: snakeCase(n) });
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
if (Buffer.byteLength(n, "utf8") > 32)
|
|
498
|
-
this.reporter.fail(a.node, "a seed can be at most 32 bytes");
|
|
499
|
-
parts.push(`{(const uint8_t *)${cString(n)}, ${Buffer.byteLength(n, "utf8")}}`);
|
|
500
|
-
idl.push({ kind: "const", value: [...Buffer.from(n, "utf8")] });
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
if (!ix.pdas.has(a.name))
|
|
504
|
-
ix.pdas.set(a.name, idl);
|
|
505
|
-
// The deriving program: this one, another account parameter ("@name"), or a fixed address.
|
|
506
|
-
let program = "params->program_id";
|
|
507
|
-
const p = c.seeds.program;
|
|
508
|
-
if (p?.startsWith("@")) {
|
|
509
|
-
const pa = ix.accounts.find((o) => o.name === p.slice(1));
|
|
510
|
-
if (!pa)
|
|
511
|
-
this.reporter.fail(a.node, `Seeds program: "${p}" is not an account parameter`);
|
|
512
|
-
program = `&a_${pa.name}->key`;
|
|
513
|
-
ix.pdaPrograms.set(a.name, { kind: "account", path: snakeCase(pa.name) });
|
|
514
|
-
}
|
|
515
|
-
else if (p) {
|
|
516
|
-
const bytes = [...decodeAddress(p)];
|
|
517
|
-
out.push(` static const SolPubkey sp_${a.name} = {${cBytes(bytes)}};`);
|
|
518
|
-
program = `&sp_${a.name}`;
|
|
519
|
-
ix.pdaPrograms.set(a.name, { kind: "const", value: bytes });
|
|
520
|
-
}
|
|
521
|
-
const seeds = `cs_${a.name}`;
|
|
522
|
-
out.push(` SolSignerSeed ${seeds}[${parts.length + 1}] = {${parts.join(", ")}};`);
|
|
523
|
-
if (c.seeds.bump) {
|
|
524
|
-
out.push(` ${seeds}[${parts.length}] = (SolSignerSeed){&d_${a.name}->${c.seeds.bump}, 1};`);
|
|
525
|
-
out.push(` TS_TRY(ts_require_pda(a_${a.name}, ${seeds}, ${parts.length + 1}, ${program}));`);
|
|
526
|
-
this.bumps.set(a.name, `d_${a.name}->${c.seeds.bump}`);
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
|
|
530
|
-
out.push(` { SolPubkey pda_${a.name};`);
|
|
531
|
-
out.push(` if (ts_find_pda(${seeds}, ${parts.length}, ${program}, &pda_${a.name}, &sb_${a.name}) != SUCCESS || !ts_key_eq(&pda_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: ${a.name} is not the expected PDA"); }`);
|
|
532
|
-
this.bumps.set(a.name, `sb_${a.name}`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
431
|
+
if (k !== "account")
|
|
432
|
+
out.push(...hasOne);
|
|
535
433
|
// Constraints run only when every optional account they mention was passed.
|
|
536
434
|
const lines = out.splice(start);
|
|
537
435
|
const optional = this.optionalIn(lines.join("\n"));
|
|
@@ -542,6 +440,138 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
542
440
|
}
|
|
543
441
|
return [...bumpDecls, ...out];
|
|
544
442
|
}
|
|
443
|
+
/** Ata<…>: the account's owner (2015) and address (2009) as Anchor checks them, then its mint. */
|
|
444
|
+
ataCheck(ix, a) {
|
|
445
|
+
const out = [];
|
|
446
|
+
const c = a.constraints;
|
|
447
|
+
const k = a.kind.kind;
|
|
448
|
+
const ata = c.ata;
|
|
449
|
+
// With two token programs the address depends on which the mint uses: no IDL derivation, and an Ata<AccountInfo>
|
|
450
|
+
// (derived here) is refused.
|
|
451
|
+
const tps = ix.accounts.filter((o) => o.kind.kind === "tokenProgram");
|
|
452
|
+
const tp = tps.length === 1 ? tps[0] : undefined;
|
|
453
|
+
if (tp && !ix.pdas.has(a.name)) {
|
|
454
|
+
ix.pdas.set(a.name, [
|
|
455
|
+
{ kind: "account", path: snakeCase(ata.owner) },
|
|
456
|
+
{ kind: "account", path: snakeCase(tp.name) },
|
|
457
|
+
{ kind: "account", path: snakeCase(ata.mint) },
|
|
458
|
+
]);
|
|
459
|
+
ix.pdaPrograms.set(a.name, { kind: "const", value: ATA_PROGRAM_BYTES });
|
|
460
|
+
}
|
|
461
|
+
// Created in the body: the ATA program checks the address when it creates it, and createIfNeeded checks an
|
|
462
|
+
// existing one there. Only the IDL entry is recorded.
|
|
463
|
+
if (k === "token" && a.kind.init)
|
|
464
|
+
return out;
|
|
465
|
+
// The address alone is not enough: legacy SPL Token lets an ATA's owner be reassigned.
|
|
466
|
+
if (k === "token")
|
|
467
|
+
out.push(` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${ata.owner}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${ata.owner}");`);
|
|
468
|
+
// The token program is the account's owner; an AccountInfo not created yet takes the instruction's token program.
|
|
469
|
+
let program = `&a_${a.name}->owner`;
|
|
470
|
+
if (k !== "token") {
|
|
471
|
+
if (!tp)
|
|
472
|
+
this.reporter.fail(a.node, tps.length
|
|
473
|
+
? "Ata<AccountInfo, …> needs exactly one token program parameter to derive the address; use TokenInterface"
|
|
474
|
+
: "Ata<AccountInfo, …> needs a token program parameter to derive the address");
|
|
475
|
+
program = `&a_${tp.name}->key`;
|
|
476
|
+
}
|
|
477
|
+
out.push(` { SolPubkey ata_${a.name};`);
|
|
478
|
+
out.push(` TS_TRY(ts_ata_address(&a_${ata.owner}->key, &a_${ata.mint}->key, ${program}, &ata_${a.name}));`);
|
|
479
|
+
out.push(` if (!ts_key_eq(&ata_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_ASSOCIATED, "TypeSOL: ${a.name} is not the associated token account of ${ata.owner} and ${ata.mint}"); }`);
|
|
480
|
+
if (k === "token")
|
|
481
|
+
out.push(` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${ata.mint}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${ata.mint}");`);
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
/** Seeds<…>: the account is the PDA of its seeds (with the stored bump, or the canonical one found here). */
|
|
485
|
+
seedsCheck(ix, a, bumpDecls) {
|
|
486
|
+
const out = [];
|
|
487
|
+
const c = a.constraints;
|
|
488
|
+
const parts = [];
|
|
489
|
+
const idl = [];
|
|
490
|
+
for (const raw of c.seeds.names) {
|
|
491
|
+
// "@name" refers to an account parameter (its key) or an argument (its bytes); anything else is a literal.
|
|
492
|
+
const ref = raw.startsWith("@") ? raw.slice(1) : undefined;
|
|
493
|
+
const n = ref ?? raw;
|
|
494
|
+
// "@acct.field": a field of a struct account parameter (its data was checked in the head), Anchor's
|
|
495
|
+
// `seeds = [pool.amm.as_ref()]`. The account may be this one (`seeds = [offer.id.to_le_bytes()]`).
|
|
496
|
+
if (ref?.includes(".")) {
|
|
497
|
+
const [owner, field, ...rest] = ref.split(".");
|
|
498
|
+
const o = ix.accounts.find((x) => x.name === owner);
|
|
499
|
+
if (!o || o.kind.kind !== "account" || rest.length)
|
|
500
|
+
this.reporter.fail(a.node, `Seeds: "@${ref}" must name a field of a struct account parameter ("@vault.owner")`);
|
|
501
|
+
const st = o.kind.struct;
|
|
502
|
+
const f = st.fields.find((x) => x.name === field);
|
|
503
|
+
if (!f)
|
|
504
|
+
this.reporter.fail(a.node, `Seeds: ${st.name} has no field "${field}"`);
|
|
505
|
+
const t = f.type;
|
|
506
|
+
if (t.kind === "pubkey")
|
|
507
|
+
parts.push(`{(const uint8_t *)&d_${owner}->${field}, 32}`);
|
|
508
|
+
else if (t.kind === "int" || t.kind === "enum")
|
|
509
|
+
parts.push(`{(const uint8_t *)&d_${owner}->${field}, ${scalarSize(t)}}`);
|
|
510
|
+
else
|
|
511
|
+
this.reporter.fail(a.node, `Seeds: field "${ref}" of type ${typeName(t)} cannot be a seed (use a Pubkey, an integer or a unit enum)`);
|
|
512
|
+
idl.push({ kind: "account", path: `${snakeCase(owner)}.${snakeCase(field)}`, account: st.name });
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const acct = ref ? ix.accounts.find((o) => o.name === ref) : undefined;
|
|
516
|
+
const arg = ref ? ix.args.find((o) => o.name === ref) : undefined;
|
|
517
|
+
if (ref && !acct && !arg)
|
|
518
|
+
this.reporter.fail(a.node, `Seeds: "@${ref}" is neither an account parameter nor an argument`);
|
|
519
|
+
if (acct) {
|
|
520
|
+
parts.push(`{(const uint8_t *)&a_${n}->key, 32}`);
|
|
521
|
+
idl.push({ kind: "account", path: snakeCase(n) });
|
|
522
|
+
}
|
|
523
|
+
else if (arg) {
|
|
524
|
+
const t = arg.type;
|
|
525
|
+
if (t.kind === "string")
|
|
526
|
+
parts.push(`{arg_${n}.ptr, arg_${n}.len}`);
|
|
527
|
+
else if (t.kind === "pubkey")
|
|
528
|
+
parts.push(`{(const uint8_t *)&arg_${n}, 32}`);
|
|
529
|
+
else if (t.kind === "int" || t.kind === "enum")
|
|
530
|
+
parts.push(`{(const uint8_t *)&arg_${n}, ${scalarSize(t)}}`);
|
|
531
|
+
else
|
|
532
|
+
this.reporter.fail(a.node, `Seeds: argument "${n}" of type ${typeName(t)} cannot be a seed`);
|
|
533
|
+
idl.push({ kind: "arg", path: snakeCase(n) });
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
if (Buffer.byteLength(n, "utf8") > 32)
|
|
537
|
+
this.reporter.fail(a.node, "a seed can be at most 32 bytes");
|
|
538
|
+
parts.push(`{(const uint8_t *)${cString(n)}, ${Buffer.byteLength(n, "utf8")}}`);
|
|
539
|
+
idl.push({ kind: "const", value: [...Buffer.from(n, "utf8")] });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (!ix.pdas.has(a.name))
|
|
543
|
+
ix.pdas.set(a.name, idl);
|
|
544
|
+
// The deriving program: this one, another account parameter ("@name"), or a fixed address.
|
|
545
|
+
let program = "params->program_id";
|
|
546
|
+
const p = c.seeds.program;
|
|
547
|
+
if (p?.startsWith("@")) {
|
|
548
|
+
const pa = ix.accounts.find((o) => o.name === p.slice(1));
|
|
549
|
+
if (!pa)
|
|
550
|
+
this.reporter.fail(a.node, `Seeds program: "${p}" is not an account parameter`);
|
|
551
|
+
program = `&a_${pa.name}->key`;
|
|
552
|
+
ix.pdaPrograms.set(a.name, { kind: "account", path: snakeCase(pa.name) });
|
|
553
|
+
}
|
|
554
|
+
else if (p) {
|
|
555
|
+
const bytes = [...decodeAddress(p)];
|
|
556
|
+
out.push(` static const SolPubkey sp_${a.name} = {${cBytes(bytes)}};`);
|
|
557
|
+
program = `&sp_${a.name}`;
|
|
558
|
+
ix.pdaPrograms.set(a.name, { kind: "const", value: bytes });
|
|
559
|
+
}
|
|
560
|
+
const seeds = `cs_${a.name}`;
|
|
561
|
+
out.push(` SolSignerSeed ${seeds}[${parts.length + 1}] = {${parts.join(", ")}};`);
|
|
562
|
+
if (c.seeds.bump) {
|
|
563
|
+
out.push(` ${seeds}[${parts.length}] = (SolSignerSeed){&d_${a.name}->${c.seeds.bump}, 1};`);
|
|
564
|
+
out.push(` TS_TRY(ts_require_pda(a_${a.name}, ${seeds}, ${parts.length + 1}, ${program}));`);
|
|
565
|
+
this.bumps.set(a.name, `d_${a.name}->${c.seeds.bump}`);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
bumpDecls.push(` uint8_t sb_${a.name}; (void)sb_${a.name};`);
|
|
569
|
+
out.push(` { SolPubkey pda_${a.name};`);
|
|
570
|
+
out.push(` if (ts_find_pda(${seeds}, ${parts.length}, ${program}, &pda_${a.name}, &sb_${a.name}) != SUCCESS || !ts_key_eq(&pda_${a.name}, &a_${a.name}->key)) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: ${a.name} is not the expected PDA"); }`);
|
|
571
|
+
this.bumps.set(a.name, `sb_${a.name}`);
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
545
575
|
/** Optional account parameters a piece of generated C refers to (a_name / d_name). */
|
|
546
576
|
optionalIn(code) {
|
|
547
577
|
return (this.ix?.accounts ?? []).filter((a) => a.optional && new RegExp(`\\b[ad]_${a.name}\\b`).test(code)).map((a) => a.name);
|
|
@@ -137,6 +137,9 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
137
137
|
// refuse more than the array holds instead (ts_deserialize relies on this bound).
|
|
138
138
|
out.push(` if (*(const uint64_t *)input > TS_MAX_ACCOUNTS) TS_FAIL(TS_ERR_TOO_MANY_ACCOUNTS, "TypeSOL: at most ${MAX_ACCOUNTS} accounts per instruction");`);
|
|
139
139
|
out.push(` ts_deserialize(input, ¶ms);`);
|
|
140
|
+
// loadAccountMut's record of the accounts it loaded: set up only in programs that use it (one store otherwise wasted).
|
|
141
|
+
if ([...helperBodies, ...ixBodies].some((b) => b.includes("ts_claim_mut(")))
|
|
142
|
+
out.push(` params.loaded_mut = 0;`);
|
|
140
143
|
const minDisc = Math.min(8, ...model.instructions.map((ix) => ix.discriminator.length));
|
|
141
144
|
out.push(` if (params.data_len < ${minDisc}) TS_FAIL(TS_ERR_INSTRUCTION_MISSING, "TypeSOL: instruction data too short");`);
|
|
142
145
|
// Self-CPI events (emitCpi): accept the event instruction when its authority PDA signed it, like Anchor.
|
|
@@ -11,6 +11,7 @@ export declare const ATA_PROGRAM_BYTES: number[];
|
|
|
11
11
|
export declare const U64: ScalarType;
|
|
12
12
|
export declare const U8: ScalarType;
|
|
13
13
|
export declare const I64: ScalarType;
|
|
14
|
+
export declare const I32: ScalarType;
|
|
14
15
|
export declare const BOOL: ScalarType;
|
|
15
16
|
export declare const PUBKEY: ScalarType;
|
|
16
17
|
export declare const STRING: ValueType;
|
|
@@ -13,6 +13,7 @@ export const ATA_PROGRAM_BYTES = [
|
|
|
13
13
|
export const U64 = { kind: "int", name: "u64" };
|
|
14
14
|
export const U8 = { kind: "int", name: "u8" };
|
|
15
15
|
export const I64 = { kind: "int", name: "i64" };
|
|
16
|
+
export const I32 = { kind: "int", name: "i32" };
|
|
16
17
|
export const BOOL = { kind: "bool" };
|
|
17
18
|
export const PUBKEY = { kind: "pubkey" };
|
|
18
19
|
export const STRING = { kind: "string" };
|
|
@@ -93,4 +93,10 @@ export declare abstract class StatementEmitter extends CpiEmitter {
|
|
|
93
93
|
/** Whether a place goes through a Vec element (`v.get(i).x`, `v.get(i)`). */
|
|
94
94
|
protected writesThroughVec(node: ts.Expression): boolean;
|
|
95
95
|
protected helperTakesMutAccounts(node: ts.CallExpression): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Whether evaluating `node` may resize a growable Vec (and so move the Vecs after it): a push / pop / clear, a
|
|
98
|
+
* realloc or close, a helper taking a writable account or a function argument, or a closure (its body is not
|
|
99
|
+
* looked into: it may capture the account). Conservative: it only decides whether a Vec element may be the target.
|
|
100
|
+
*/
|
|
101
|
+
protected mayResizeVecs(node: ts.Node): boolean;
|
|
96
102
|
}
|
|
@@ -1085,6 +1085,10 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1085
1085
|
this.fail(e, "this expression has no effect; only assignments and calls can be statements");
|
|
1086
1086
|
}
|
|
1087
1087
|
assign(target, valueNode, op, topLevel = false) {
|
|
1088
|
+
// The place is resolved before the right side runs (JS order). A right side that can resize a Vec of the account
|
|
1089
|
+
// would move the element out from under it: the write would land in the bytes that follow.
|
|
1090
|
+
if (this.writesThroughVec(target) && this.mayResizeVecs(valueNode))
|
|
1091
|
+
this.fail(target, "the right side may change this Vec (a helper, closure or push/pop/clear); store its result in a local first");
|
|
1088
1092
|
if (op !== undefined) {
|
|
1089
1093
|
const place = this.place(target);
|
|
1090
1094
|
let current = { c: place.read, type: place.type };
|
|
@@ -1143,8 +1147,6 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1143
1147
|
const structCall = isCall && ts.isIdentifier(valueNode.expression)
|
|
1144
1148
|
? this.helperOf(valueNode)?.returns?.kind === "struct"
|
|
1145
1149
|
: false;
|
|
1146
|
-
if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
|
|
1147
|
-
this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
|
|
1148
1150
|
// Whole arrays / structs: copy between identical layouts.
|
|
1149
1151
|
const targetRef = isCall && !structCall ? undefined : this.ref(target);
|
|
1150
1152
|
if (targetRef &&
|
|
@@ -1161,9 +1163,6 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1161
1163
|
}
|
|
1162
1164
|
const place = this.place(target);
|
|
1163
1165
|
if (isCall && !createsFirst) {
|
|
1164
|
-
// A helper that may replace a Vec element would leave `place` pointing at the new one (JS writes the old object).
|
|
1165
|
-
if (this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
|
|
1166
|
-
this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
|
|
1167
1166
|
raw = this.call(valueNode, topLevel);
|
|
1168
1167
|
if (!raw)
|
|
1169
1168
|
this.fail(valueNode, "this call does not produce a value");
|
|
@@ -1194,4 +1193,35 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1194
1193
|
const h = this.helperOf(node);
|
|
1195
1194
|
return !!h?.params.some((p) => p.account && isWritable(p.account));
|
|
1196
1195
|
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Whether evaluating `node` may resize a growable Vec (and so move the Vecs after it): a push / pop / clear, a
|
|
1198
|
+
* realloc or close, a helper taking a writable account or a function argument, or a closure (its body is not
|
|
1199
|
+
* looked into: it may capture the account). Conservative: it only decides whether a Vec element may be the target.
|
|
1200
|
+
*/
|
|
1201
|
+
mayResizeVecs(node) {
|
|
1202
|
+
let found = false;
|
|
1203
|
+
const isClosure = (e) => ts.isArrowFunction(e) || ts.isFunctionExpression(e) || (ts.isIdentifier(e) && !!this.lookup(e.text)?.closure);
|
|
1204
|
+
const visit = (n) => {
|
|
1205
|
+
if (found)
|
|
1206
|
+
return;
|
|
1207
|
+
if (ts.isCallExpression(n)) {
|
|
1208
|
+
const callee = n.expression;
|
|
1209
|
+
if (ts.isPropertyAccessExpression(callee) && ["push", "pop", "clear"].includes(callee.name.text))
|
|
1210
|
+
found = true;
|
|
1211
|
+
else if (ts.isCallExpression(callee))
|
|
1212
|
+
found = true; // a function a function returns
|
|
1213
|
+
else if (ts.isIdentifier(callee)) {
|
|
1214
|
+
if (this.lookup(callee.text)?.closure)
|
|
1215
|
+
found = true;
|
|
1216
|
+
else if (["realloc", "close"].includes(this.exportName(callee) ?? ""))
|
|
1217
|
+
found = true;
|
|
1218
|
+
else if (this.helperOf(n) && (this.helperTakesMutAccounts(n) || n.arguments.some(isClosure)))
|
|
1219
|
+
found = true;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
ts.forEachChild(n, visit);
|
|
1223
|
+
};
|
|
1224
|
+
visit(node);
|
|
1225
|
+
return found;
|
|
1226
|
+
}
|
|
1197
1227
|
}
|
package/docs/architecture.md
CHANGED
|
@@ -22,6 +22,21 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
22
22
|
`compiler/index.ts` wires the stages together. `generate()` runs analyze, emit and IDL generation in memory, and
|
|
23
23
|
`build()` also compiles and writes the output files.
|
|
24
24
|
|
|
25
|
+
## Repository layout
|
|
26
|
+
|
|
27
|
+
| Path | Contents |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `src/` | The compiler (`src/compiler/`) and the tools around it: CLI, workspace, client, CPI and port generators, testing, IDL publishing (source map below) |
|
|
30
|
+
| `runtime/` | `typesol_rt.h`, the C runtime every program includes ([runtime.md](runtime.md)) |
|
|
31
|
+
| `lib/` | `typesol.d.ts`, the language surface programs typecheck against |
|
|
32
|
+
| `ts-plugin/` | The editor plugin (`typesol/ts-plugin`) |
|
|
33
|
+
| `examples/` | Example programs; the `typesol init` templates are copies of three of them |
|
|
34
|
+
| `test/` | The test suite: `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated clients), `programs/` (third-party program binaries, see its SOURCES.md) |
|
|
35
|
+
| `bench/anchor/` | Anchor twins of the counter, vault and escrow examples, for the differential tests (`pnpm bench:anchor`) and the zero-copy client test; the benchmarks themselves are in [typesol/benchmarks](https://github.com/typesol/benchmarks) |
|
|
36
|
+
| `scripts/` | `smoke.ts` and `localnet.sh` (a local validator run), `fetch-programs.sh` (third-party binaries), `readme-bench.ts` (the README numbers) |
|
|
37
|
+
| `docs/` | User and contributor documentation (also published on typesol.com) |
|
|
38
|
+
| `assets/` | The CLI's logo and the README images (`readme/`, generated) |
|
|
39
|
+
|
|
25
40
|
## Source map
|
|
26
41
|
|
|
27
42
|
| Path | Role |
|
|
@@ -34,11 +49,17 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
|
|
|
34
49
|
| `src/compiler/idl.ts` | Turns the model into an Anchor IDL: accounts with `pda`, `relations`, `address` and `optional`, plus types, events, errors and docs. |
|
|
35
50
|
| `src/compiler/toolchain.ts` | Finds the Agave SDK and runs clang and lld with the SDK's flags, plus per-function sections, `--gc-sections` and `--strip-all` so unused code and symbol tables stay out of the binary. |
|
|
36
51
|
| `src/compiler/diagnostics.ts` | `Reporter` and `CompileError`: diagnostics with file, line and column. |
|
|
52
|
+
| `src/compiler/anchor-errors.ts` | Anchor's own error codes (`AnchorError.X`), shared by `require` and the generated clients. |
|
|
53
|
+
| `src/compiler/index.ts` | The compiler's API (`generate`, `build`, `verifyBinary`, the build manifest). |
|
|
37
54
|
| `runtime/typesol_rt.h` | The C runtime: error codes, word-wise memory helpers, the entry point's input parser (`ts_deserialize`), account checks, checked math, Borsh helpers, CPIs (System, Token, Token-2022, ATA, Metaplex), oracles and the heap allocator. Header-only and `static inline`, so unused code is dropped. |
|
|
38
55
|
| `src/workspace.ts` | `typesol.json` projects, program keypairs, `typesol init` templates and deploy arguments. |
|
|
39
56
|
| `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
|
|
40
57
|
| `src/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
|
|
41
|
-
| `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program
|
|
58
|
+
| `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program, and reading a program's IDL back from the chain (Program Metadata or Anchor's IDL account) for `typesol client --address`. |
|
|
59
|
+
| `src/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
|
|
60
|
+
| `src/templates.ts` | The programs and tests `typesol init --template` writes (copies of `examples/`; a test checks they match). |
|
|
61
|
+
| `src/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
|
|
62
|
+
| `src/keys.ts` | Base58 and program keypair helpers. |
|
|
42
63
|
| `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
|
|
43
64
|
| `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
|
|
44
65
|
| `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
|
|
@@ -54,7 +75,7 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
|
|
|
54
75
|
|
|
55
76
|
| File | Layer | Contents |
|
|
56
77
|
| --- | --- | --- |
|
|
57
|
-
| `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`) |
|
|
78
|
+
| `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`), growable Vec placement (`vecPlace`, `vecLenCheck`) |
|
|
58
79
|
| `base.ts` | `EmitterBase` | function state; `line()`, `tmp()`, `settle()`, scopes, `fail()`, account lookup |
|
|
59
80
|
| `storage.ts` | `StorageEmitter` | references into account data and locals, reads and writes, indexing, aliasing, argument validation |
|
|
60
81
|
| `expressions.ts` | `ExpressionEmitter` | literals, operators and checked arithmetic, property reads, conditionals, text, casts |
|
|
@@ -63,6 +84,9 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
|
|
|
63
84
|
| `statements.ts` | `StatementEmitter` | variables, assignment, `if`/loops/`switch`, `return` |
|
|
64
85
|
| `function.ts` | `FunctionEmitter` | the instruction prologue (account checks, constraints, argument decoding) and epilogue |
|
|
65
86
|
|
|
87
|
+
`borsh.ts` writes the Borsh pack/unpack codecs of `@layout borsh` structs, and `program.ts` assembles the C file
|
|
88
|
+
(typedefs, codecs, helpers, instructions, the entry point). `index.ts` is the emitter's export.
|
|
89
|
+
|
|
66
90
|
## Rules the emitter keeps
|
|
67
91
|
|
|
68
92
|
These invariants are easy to break. Each one has tests.
|