@typesol/cli 0.24.1 → 0.26.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 +51 -0
- package/README.md +35 -16
- package/SECURITY.md +2 -0
- package/dist/cli.js +52 -37
- package/dist/compiler/analyze.js +11 -1
- package/dist/compiler/emit/calls.js +10 -10
- package/dist/compiler/emit/cpi.js +4 -0
- 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/dist/templates.js +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{clientgen.js → tools/clientgen.js} +4 -1
- package/dist/tools/doctor.d.ts +45 -0
- package/dist/tools/doctor.js +229 -0
- package/dist/{portgen.js → tools/portgen.js} +1 -1
- package/dist/workspace.js +1 -1
- package/docs/architecture.md +17 -17
- package/docs/audit.md +90 -0
- package/docs/case-study-escrow.md +224 -0
- package/docs/cli.md +3 -2
- package/docs/errors.md +26 -1
- package/docs/language.md +12 -8
- package/docs/migrate-from-anchor.md +3 -1
- package/docs/runtime.md +1 -1
- package/docs/security-model.md +97 -0
- package/docs/testing.md +5 -4
- package/lib/typesol.d.ts +35 -13
- package/package.json +13 -13
- package/runtime/typesol_rt.h +33 -4
- /package/dist/{clientgen.d.ts → tools/clientgen.d.ts} +0 -0
- /package/dist/{cpigen.d.ts → tools/cpigen.d.ts} +0 -0
- /package/dist/{cpigen.js → tools/cpigen.js} +0 -0
- /package/dist/{idlpublish.d.ts → tools/idlpublish.d.ts} +0 -0
- /package/dist/{idlpublish.js → tools/idlpublish.js} +0 -0
- /package/dist/{onchain.d.ts → tools/onchain.d.ts} +0 -0
- /package/dist/{onchain.js → tools/onchain.js} +0 -0
- /package/dist/{portgen.d.ts → tools/portgen.d.ts} +0 -0
- /package/dist/{profile.d.ts → tools/profile.d.ts} +0 -0
- /package/dist/{profile.js → tools/profile.js} +0 -0
- /package/dist/{svmrpc.d.ts → tools/svmrpc.d.ts} +0 -0
- /package/dist/{svmrpc.js → tools/svmrpc.js} +0 -0
|
@@ -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/dist/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Programs and tests that `typesol init --template <name>` writes. Each test runs its program in LiteSVM and covers its
|
|
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
|
-
// The programs are copies of examples/ (test/workspace.test.ts checks they still match).
|
|
3
|
+
// The programs are copies of examples/ (test/cli/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\`.
|
package/dist/testing.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import anchor, { type Idl } from "@anchor-lang/core";
|
|
20
20
|
import { type Address, type Instruction, type KeyPairSigner } from "@solana/kit";
|
|
21
21
|
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from "litesvm";
|
|
22
|
-
import { type SvmConnection, svmConnection } from "./svmrpc.js";
|
|
22
|
+
import { type SvmConnection, svmConnection } from "./tools/svmrpc.js";
|
|
23
23
|
export { type SvmConnection, svmConnection };
|
|
24
24
|
declare const BorshCoder: typeof anchor.BorshCoder;
|
|
25
25
|
type BorshCoder = InstanceType<typeof BorshCoder>;
|
package/dist/testing.js
CHANGED
|
@@ -22,7 +22,7 @@ import { FailedTransactionMetadata, LiteSVM } from "litesvm";
|
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { svmConnection } from "./svmrpc.js";
|
|
25
|
+
import { svmConnection } from "./tools/svmrpc.js";
|
|
26
26
|
import { buildWorkspace, findWorkspace } from "./workspace.js";
|
|
27
27
|
export { svmConnection };
|
|
28
28
|
// @anchor-lang/core is CommonJS: Node 22 cannot import its classes by name from an installed ES module.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* sanitized comments.
|
|
19
19
|
*/
|
|
20
20
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { ANCHOR_ERRORS } from "
|
|
21
|
+
import { ANCHOR_ERRORS } from "../compiler/anchor-errors.js";
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
/** An IDL the generator refuses: malformed, unsafe to embed, or older than the 0.30 IDL format. */
|
|
24
24
|
export class ClientIdlError extends Error {
|
|
@@ -32,6 +32,7 @@ export const TYPESOL_ERRORS = {
|
|
|
32
32
|
100: "InstructionMissing",
|
|
33
33
|
101: "InstructionFallbackNotFound",
|
|
34
34
|
102: "InstructionDidNotDeserialize",
|
|
35
|
+
2000: "ConstraintMut",
|
|
35
36
|
2001: "ConstraintHasOne",
|
|
36
37
|
2002: "ConstraintSigner",
|
|
37
38
|
2004: "ConstraintOwner",
|
|
@@ -45,6 +46,8 @@ export const TYPESOL_ERRORS = {
|
|
|
45
46
|
2016: "ConstraintMintMintAuthority",
|
|
46
47
|
2017: "ConstraintMintFreezeAuthority",
|
|
47
48
|
2018: "ConstraintMintDecimals",
|
|
49
|
+
2021: "ConstraintTokenTokenProgram",
|
|
50
|
+
2022: "ConstraintMintTokenProgram",
|
|
48
51
|
2040: "ConstraintDuplicateMutableAccount",
|
|
49
52
|
3000: "AccountDiscriminatorAlreadySet",
|
|
50
53
|
3002: "AccountDiscriminatorMismatch",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const INSTALL_AGAVE = "sh -c \"$(curl -sSfL https://release.anza.xyz/stable/install)\"";
|
|
2
|
+
export interface Check {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
/** A missing optional piece (the Solana CLI is only needed to deploy). */
|
|
5
|
+
optional?: boolean;
|
|
6
|
+
what: string;
|
|
7
|
+
detail: string;
|
|
8
|
+
fix?: string[];
|
|
9
|
+
}
|
|
10
|
+
/** The platform-tools the compiler needs: found, or what to run. */
|
|
11
|
+
export declare function toolchainCheck(): Check;
|
|
12
|
+
/** json_rpc_url / keypair_path from a Solana CLI config (the given file, or the CLI's default one). */
|
|
13
|
+
export declare function solanaConfig(file?: string): {
|
|
14
|
+
url?: string;
|
|
15
|
+
keypair?: string;
|
|
16
|
+
file: string;
|
|
17
|
+
exists: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** The Solana CLI's cluster monikers, as `solana --url` accepts them; anything else is used as given. */
|
|
20
|
+
export declare function clusterUrl(url: string): string;
|
|
21
|
+
/** Only the host: an RPC URL can carry an API key in its path or query, and these messages may be shared. */
|
|
22
|
+
export declare function rpcHost(url: string): string;
|
|
23
|
+
export declare function rpc<T>(url: string, method: string, params?: unknown[]): Promise<T>;
|
|
24
|
+
/** fetch's "fetch failed" says little: the cause's code (ECONNREFUSED, ENOTFOUND, a timeout) says more. */
|
|
25
|
+
export declare const errText: (e: unknown) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Before `solana program deploy`: the RPC answers, and the payer holds enough for the program's rent (a first deploy
|
|
28
|
+
* holds about twice the binary's rent while the buffer exists; an upgrade, the buffer's). Throws with what to do.
|
|
29
|
+
*/
|
|
30
|
+
export declare function deployPreflight(opts: {
|
|
31
|
+
url: string;
|
|
32
|
+
payer?: string;
|
|
33
|
+
programs: {
|
|
34
|
+
soPath: string;
|
|
35
|
+
address: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}[];
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/** Everything `typesol doctor` checks, in order. */
|
|
40
|
+
export declare function doctorChecks(opts: {
|
|
41
|
+
cwd: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
config?: string;
|
|
44
|
+
}): Promise<Check[]>;
|
|
45
|
+
export declare function formatChecks(checks: Check[]): string;
|