@better-sol/cli 0.1.0-alpha.10 → 0.1.0-alpha.12

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/README.md CHANGED
@@ -48,7 +48,7 @@ Compile and deploy to Solana.
48
48
  npx @better-sol/cli@alpha deploy
49
49
  ```
50
50
 
51
- Parses your TypeScript, generates Anchor Rust, compiles it via the cloud API, and deploys the binary. On devnet and testnet, automatically funds your payer if the balance is low.
51
+ Parses your TypeScript, compiles it via the cloud API, and deploys the binary. On devnet and testnet, automatically funds your payer if the balance is low. Compiled binaries are cached locally in `.better-sol/cache/` for testing.
52
52
 
53
53
  | Flag | Default | Description |
54
54
  |---|---|---|
@@ -56,9 +56,9 @@ Parses your TypeScript, generates Anchor Rust, compiles it via the cloud API, an
56
56
  | `--program <name>` | all programs | Target a specific program |
57
57
  | `--payer <path>` | `keypair.json` | Payer keypair path |
58
58
  | `--cluster <cluster>` | `devnet` | `devnet`, `testnet`, `mainnet`, `localnet` |
59
- | `--dry-run` | `false` | Generate Rust without compiling or deploying |
60
- | `--verify` | `false` | Write Rust for verified builds |
61
- | `--output <dir>` | `generated` | Output directory for Rust files |
59
+ | `--dry-run` | `false` | Validate without compiling or deploying |
60
+ | `--verify` | `false` | Write Rust for OtterSec verified builds |
61
+ | `--output <dir>` | `generated` | Rust output directory (only used with `--verify`) |
62
62
 
63
63
  ### `generate idl`
64
64
 
package/dist/index.js CHANGED
@@ -388,13 +388,11 @@ function collectAccounts(source, program, rawStructZCs) {
388
388
  const firstArg = call.arguments[0];
389
389
  if (firstArg === void 0 || !isObjectExpression(firstArg)) continue;
390
390
  const fields = parseFields(source, firstArg);
391
- const chainText = nodeTextOf(source, decl.init);
392
- if (chainText.includes(".pda(")) throw new Error(".pda() was renamed to .derive(). Use .derive((seed) => ['literal', seed.fieldName]).");
393
- if (chainText.includes(".seeds(")) throw new Error(".seeds() was removed. Use .derive((seed) => ['literal', seed.fieldName]).");
394
- const zeroCopy = chainText.includes(".zeroCopy");
391
+ const zeroCopy = hasChainMethod(source, decl.init, "zeroCopy");
395
392
  if (zeroCopy) validateZeroCopyFields(name, fields, rawStructZCs);
396
- const seeds = parseSeeds(chainText);
397
- const hasOneFields = parseHasOneFields(chainText);
393
+ const deriveCall = findChainCall(source, decl.init, "derive");
394
+ const seeds = deriveCall !== void 0 ? parseSeedsFromAst(source, deriveCall) : [];
395
+ const hasOneFields = parseHasOneFieldsFromAst(source, decl.init);
398
396
  accounts.push({
399
397
  name,
400
398
  fields,
@@ -578,6 +576,17 @@ function resolveConstraint(source, prop, accountName, rawAccounts) {
578
576
  };
579
577
  }
580
578
  case "remaining": {
579
+ const argNode = init.arguments[0];
580
+ if (argNode !== void 0 && isIdentifier(argNode)) {
581
+ if (argNode.name === "tokenAccount" || argNode.name.includes("TokenAccount") || argNode.name.includes("tokenAccount")) return {
582
+ kind: "remaining",
583
+ itemType: "tokenAccount"
584
+ };
585
+ if (argNode.name === "signer" || argNode.name.includes("Signer")) return {
586
+ kind: "remaining",
587
+ itemType: "signer"
588
+ };
589
+ }
581
590
  const argText = getCallArgText(source, init, 0) ?? "";
582
591
  if (argText.includes("tokenAccount")) return {
583
592
  kind: "remaining",
@@ -704,50 +713,73 @@ function tryResolvePrimitive(name) {
704
713
  "bytes"
705
714
  ].includes(name) ? name : void 0;
706
715
  }
707
- function parseSeeds(chainText) {
708
- const args = extractPdaArgs(chainText);
709
- if (args === void 0) return [];
716
+ function hasChainMethod(source, node, methodName) {
717
+ return findChainCall(source, node, methodName) !== void 0;
718
+ }
719
+ function findChainCall(source, node, methodName) {
720
+ let current = node;
721
+ while (current !== void 0) {
722
+ if (!isCallExpression(current)) break;
723
+ if (isMemberExpression(current.callee)) {
724
+ if (getMemberPropertyName(source, current.callee) === methodName) return current;
725
+ current = current.callee.object;
726
+ } else break;
727
+ }
728
+ }
729
+ function parseSeedsFromAst(source, deriveCall) {
730
+ const firstArg = deriveCall.arguments[0];
731
+ if (firstArg === void 0 || !isArrowFunctionExpression(firstArg)) return [];
732
+ const body = unwrapParenthesized(firstArg.body);
733
+ if (!isArrayExpression(body)) return [];
710
734
  const seeds = [];
711
- const regex = /\b[A-Za-z_$][\w$]*\.([A-Za-z_$][\w$]*)|'([^']*)'|"([^"]*)"/g;
712
- let match;
713
- while ((match = regex.exec(args)) !== null) {
714
- const field = match[1];
715
- const singleQuotedLiteral = match[2];
716
- const doubleQuotedLiteral = match[3];
717
- if (field !== void 0) seeds.push({
718
- kind: "field",
719
- fieldName: field
720
- });
721
- else if (singleQuotedLiteral !== void 0 && singleQuotedLiteral !== "") seeds.push(parseLiteralSeed(singleQuotedLiteral));
722
- else if (doubleQuotedLiteral !== void 0 && doubleQuotedLiteral !== "") seeds.push(parseLiteralSeed(doubleQuotedLiteral));
735
+ for (const element of body.elements) {
736
+ if (element === void 0 || element === null || isSpreadElement(element)) continue;
737
+ const seed = parseSingleSeed(source, element);
738
+ if (seed !== void 0) seeds.push(seed);
723
739
  }
724
740
  return seeds;
725
741
  }
742
+ function parseSingleSeed(source, node) {
743
+ if (isStringLiteral(node)) return parseLiteralSeed(node.value);
744
+ if (isTemplateLiteral(node) && node.quasis.length === 1) {
745
+ const quasi = node.quasis[0];
746
+ if (quasi !== void 0) return parseLiteralSeed(quasi.value.cooked ?? quasi.value.raw);
747
+ }
748
+ if (isMemberExpression(node)) {
749
+ const property = getMemberPropertyName(source, node);
750
+ if (property !== void 0) return {
751
+ kind: "field",
752
+ fieldName: property
753
+ };
754
+ }
755
+ if (isCallExpression(node) && isMemberExpression(node.callee)) {
756
+ const property = getMemberPropertyName(source, node.callee);
757
+ if (property !== void 0) return {
758
+ kind: "field",
759
+ fieldName: property
760
+ };
761
+ }
762
+ }
726
763
  function parseLiteralSeed(value) {
727
- if (/^\{[A-Za-z_$][\w$]*\}$/.test(value)) throw new Error(`Dynamic PDA seed template '${value}' is not supported. Store the value as an account field and reference it with seed.${value.slice(1, -1)}.`);
728
764
  return {
729
765
  kind: "literal",
730
766
  value
731
767
  };
732
768
  }
733
- function parseHasOneFields(chainText) {
769
+ function parseHasOneFieldsFromAst(source, node) {
734
770
  const fields = [];
735
- const regex = /\.hasOne\(["']([^"']+)["']\)/g;
736
- let match;
737
- while ((match = regex.exec(chainText)) !== null) fields.push(match[1]);
738
- return fields;
739
- }
740
- function extractPdaArgs(chainText) {
741
- const start = chainText.indexOf(".derive(");
742
- if (start === -1) return void 0;
743
- const argsStart = start + 8;
744
- let depth = 1;
745
- for (let index = argsStart; index < chainText.length; index += 1) {
746
- const char = chainText[index];
747
- if (char === "(") depth += 1;
748
- else if (char === ")") depth -= 1;
749
- if (depth === 0) return chainText.slice(argsStart, index);
771
+ let current = node;
772
+ while (current !== void 0) {
773
+ if (!isCallExpression(current)) break;
774
+ if (isMemberExpression(current.callee)) {
775
+ if (getMemberPropertyName(source, current.callee) === "hasOne") {
776
+ const arg = current.arguments[0];
777
+ if (isStringLiteral(arg)) fields.push(arg.value);
778
+ }
779
+ current = current.callee.object;
780
+ } else break;
750
781
  }
782
+ return fields;
751
783
  }
752
784
  function validateZeroCopyFields(accountName, fields, structs) {
753
785
  for (const field of fields) try {
@@ -933,7 +965,7 @@ async function discoverProgramsWithSpinner(src) {
933
965
  const PAYER_KEYPAIR_PATH = "keypair.json";
934
966
  const GITIGNORE_ENTRIES = [
935
967
  ".better-sol/",
936
- "generated/**/*.so",
968
+ "generated/",
937
969
  "keypair.json",
938
970
  "node_modules/"
939
971
  ];
@@ -3134,21 +3166,18 @@ const DEFAULT_PAYER_PATH = "keypair.json";
3134
3166
  const AIRDROP_LAMPORTS = 2000000000n;
3135
3167
  const AIRDROP_RETRIES = 3;
3136
3168
  const MIN_DEPLOY_BALANCE = 1500000000n;
3169
+ const CACHE_DIR = `${BETTER_SOL_DIR}/cache`;
3137
3170
  async function deploy(options) {
3138
3171
  intro("better-sol deploy");
3139
3172
  const apiKey = await getStoredApiKey();
3140
3173
  const config = await loadConfig();
3141
3174
  const cluster = parseCluster(options.cluster, config.cluster);
3142
3175
  const src = options.src ?? config.programs;
3143
- const out = options.output ?? config.out;
3144
- const outDir = cwdPath(out);
3145
3176
  const payerPath = resolvePayerPath(options.payer, config.payer);
3146
3177
  const payer = await readKeypair(payerPath);
3147
3178
  const rpcUrl = clusterUrl(cluster);
3148
- const writesRust = options.verify || options.dryRun || options.output !== void 0;
3149
3179
  log.step(`Cluster: ${cluster}`);
3150
3180
  log.step(`Source: ${src}`);
3151
- if (writesRust) log.step(`Output: ${out}`);
3152
3181
  await ensureFunded(payer.publicKey, cluster, rpcUrl);
3153
3182
  const programs = await discoverProgramsWithSpinner(src);
3154
3183
  const matched = options.program !== void 0 ? programs.filter((p) => p.name === options.program) : programs;
@@ -3157,26 +3186,12 @@ async function deploy(options) {
3157
3186
  throw new Error(`No program named '${options.program}' found in ${src}.\nAvailable programs:\n${available}`);
3158
3187
  }
3159
3188
  const projects = matched.map((program) => generateAnchorProject(program));
3160
- await Promise.all(projects.map((project) => removeGeneratedSoFile(outDir, project.program.name)));
3161
- if (writesRust) {
3162
- const writeSpinner = spinner();
3163
- writeSpinner.start("Writing generated Anchor projects");
3164
- await ensureDirectory(outDir);
3165
- await Promise.all(projects.map(async (project) => {
3166
- const dir = join(outDir, project.program.name);
3167
- await ensureDirectory(join(dir, "src"));
3168
- writeFileSync(join(dir, "Cargo.toml"), project.cargoToml);
3169
- writeFileSync(join(dir, "src", "lib.rs"), project.libRs);
3170
- }));
3171
- writeSpinner.stop("Generated Anchor projects written");
3172
- }
3173
3189
  if (options.dryRun) {
3174
- for (const project of projects) printProgramSummary(project.program, cluster, outDir, true);
3175
- outro(`Dry run complete — Rust written to ${out}/. No compilation or deployment performed.`);
3190
+ outro("Dry run complete. No compilation or deployment performed.");
3176
3191
  return;
3177
3192
  }
3178
3193
  const compileSpinner = spinner();
3179
- compileSpinner.start(`Compiling ${matched.length === 1 ? matched[0]?.name : matched.length + " programs"} with Better Sol compiler`);
3194
+ compileSpinner.start(`Compiling ${matched.length === 1 ? matched[0]?.name : matched.length + " programs"}`);
3180
3195
  let compileResults;
3181
3196
  try {
3182
3197
  compileResults = await Promise.all(projects.map((project) => compileProgram({
@@ -3201,6 +3216,8 @@ async function deploy(options) {
3201
3216
  throw new Error(`Compilation failed for ${project.program.name}.${logs}`);
3202
3217
  }
3203
3218
  log.step(`Compiled: ${compileTime}`);
3219
+ if (result.bytecodeSha256) log.step(`Binary: sha256:${result.bytecodeSha256.slice(0, 16)}...`);
3220
+ writeBytecode(project.program.name, result.bytecode);
3204
3221
  const programKeypairPath = cwdJoin(BETTER_SOL_DIR, `${project.program.name}.json`);
3205
3222
  const solanaPath = ensureSolanaCli();
3206
3223
  const deploySpinner = spinner();
@@ -3223,14 +3240,27 @@ async function deploy(options) {
3223
3240
  }
3224
3241
  }
3225
3242
  if (options.verify) {
3243
+ const verifyDir = cwdPath(options.output ?? config.out);
3244
+ await writeRustForVerify(projects, verifyDir);
3226
3245
  log.info("To verify this build on-chain:");
3227
- log.step(`1. Commit and push the ${out}/ directory to a public repository`);
3246
+ log.step(`1. Commit and push the ${verifyDir}/ directory to a public repository`);
3228
3247
  log.step(`2. Run \`${CLI_COMMAND$1} verify ${matched[0]?.address ?? "<program-id>"}\``);
3229
3248
  }
3230
3249
  outro("Deploy complete.");
3231
3250
  }
3232
- async function removeGeneratedSoFile(outDir, programName) {
3233
- await rm(join(outDir, programName, "target", "deploy", `${programName}.so`), { force: true });
3251
+ function writeBytecode(programName, bytecode) {
3252
+ const cachePath = cwdPath(CACHE_DIR);
3253
+ ensureDirectory(cachePath);
3254
+ writeFileSync(join(cachePath, `${programName}.so`), Buffer.from(bytecode, "base64"));
3255
+ }
3256
+ async function writeRustForVerify(projects, outDir) {
3257
+ await ensureDirectory(outDir);
3258
+ await Promise.all(projects.map(async (project) => {
3259
+ const dir = join(outDir, project.program.name);
3260
+ await ensureDirectory(join(dir, "src"));
3261
+ writeFileSync(join(dir, "Cargo.toml"), project.cargoToml);
3262
+ writeFileSync(join(dir, "src", "lib.rs"), project.libRs);
3263
+ }));
3234
3264
  }
3235
3265
  async function deployCompiledProgram(params) {
3236
3266
  const deployDir = await mkdtemp(join(tmpdir(), "better-sol-deploy-"));
@@ -3307,12 +3337,6 @@ async function ensureFunded(address, cluster, rpcUrl) {
3307
3337
  s.stop("Airdrop failed");
3308
3338
  throw new Error(`Failed to airdrop SOL on ${cluster}. Fund ${address} manually or try again.`);
3309
3339
  }
3310
- function printProgramSummary(program, cluster, out, wroteRust) {
3311
- log.info(`Program: ${program.name}`);
3312
- log.step(`Address: ${program.address}`);
3313
- log.step(`Cluster: ${cluster}`);
3314
- if (wroteRust) log.step(`Rust: ${out}/${program.name}/`);
3315
- }
3316
3340
  //#endregion
3317
3341
  //#region src/generator/db.ts
3318
3342
  function isDbDialect(value) {
@@ -69336,11 +69360,11 @@ async function gitCommit() {
69336
69360
  //#endregion
69337
69361
  //#region src/index.ts
69338
69362
  const cli = new Command();
69339
- cli.name("better-sol").description("Write Solana programs in TypeScript. Run with npx @better-sol/cli@alpha").version("0.1.0-alpha.10");
69363
+ cli.name("better-sol").description("Write Solana programs in TypeScript. Run with npx @better-sol/cli@alpha").version("0.1.0-alpha.12");
69340
69364
  cli.command("init").description("Initialize a better-sol project").option("--force", "overwrite existing files", false).option("--skip-install", "skip installing dependencies", false).action((options) => run(() => init(options)));
69341
69365
  cli.command("create").description("Create a new better-sol program").argument("[name]", "program name").option("--dir <dir>", "program directory", "programs").option("--force", "overwrite existing files", false).action((name, options) => run(() => create(name, options)));
69342
69366
  cli.command("login").description("Save your compiler API key").argument("[apiKey]", "compiler API key").action((apiKey) => run(() => login(apiKey)));
69343
- cli.command("deploy").description("Generate Rust, compile, and deploy programs").option("--src <glob>", "program source glob").option("--program <name>", "target a specific program by name").option("--payer <path>", "payer keypair path").option("--cluster <cluster>", "devnet, testnet, mainnet, or localnet").option("--verify", "write generated Rust for verified builds", false).option("--dry-run", "generate and validate without compiling or deploying", false).option("--output <dir>", "generated Rust output directory").action((options) => run(() => deploy(options)));
69367
+ cli.command("deploy").description("Compile and deploy programs").option("--src <glob>", "program source glob").option("--program <name>", "target a specific program by name").option("--payer <path>", "payer keypair path").option("--cluster <cluster>", "devnet, testnet, mainnet, or localnet").option("--verify", "write generated Rust for verified builds", false).option("--dry-run", "generate and validate without compiling or deploying", false).option("--output <dir>", "output directory for verified build Rust", "generated").action((options) => run(() => deploy(options)));
69344
69368
  const generate = cli.command("generate").description("Generate derived artifacts");
69345
69369
  generate.command("db").description("Generate a database schema from account definitions").option("--dialect <dialect>", "postgres, mysql, or sqlite", "postgres").option("--out <path>", "output file", "src/db/better-sol.ts").option("--src <glob>", "program source glob").action((options) => run(() => generateDb(options)));
69346
69370
  generate.command("idl").description("Generate a typed Better Sol program from an IDL file or on-chain program address").argument("<source>", "path to IDL JSON file or on-chain program address").option("--out <path>", "output TypeScript file (default: generated/<name>.ts)").option("--name <name>", "program name override (default: derived from IDL)").option("--cluster <cluster>", "cluster for on-chain IDL fetch (mainnet, devnet, testnet, localnet)", "mainnet").action((source, options) => run(() => generateIdl(source, options)));