@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 +4 -4
- package/dist/index.js +94 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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` |
|
|
60
|
-
| `--verify` | `false` | Write Rust for verified builds |
|
|
61
|
-
| `--output <dir>` | `generated` |
|
|
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
|
|
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
|
|
397
|
-
const
|
|
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
|
|
708
|
-
|
|
709
|
-
|
|
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
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
769
|
+
function parseHasOneFieldsFromAst(source, node) {
|
|
734
770
|
const fields = [];
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
|
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
|
-
|
|
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"}
|
|
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 ${
|
|
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
|
-
|
|
3233
|
-
|
|
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.
|
|
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("
|
|
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)));
|