[CLI] fix stylus abi exports (#8726)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"thirdweb": patch
|
||||
---
|
||||
|
||||
[CLI] fix stylus abi exports
|
||||
@@ -1,6 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parseAbiItem } from "abitype";
|
||||
import open from "open";
|
||||
import ora, { type Ora } from "ora";
|
||||
import prompts from "prompts";
|
||||
@@ -16,12 +17,6 @@ export async function publishStylus(secretKey?: string) {
|
||||
|
||||
checkPrerequisites(spinner, "cargo", ["--version"], "Rust (cargo)");
|
||||
checkPrerequisites(spinner, "rustc", ["--version"], "Rust compiler (rustc)");
|
||||
checkPrerequisites(
|
||||
spinner,
|
||||
"solc",
|
||||
["--version"],
|
||||
"Solidity compiler (solc)",
|
||||
);
|
||||
|
||||
const uri = await buildStylus(spinner, secretKey);
|
||||
|
||||
@@ -35,12 +30,6 @@ export async function deployStylus(secretKey?: string) {
|
||||
|
||||
checkPrerequisites(spinner, "cargo", ["--version"], "Rust (cargo)");
|
||||
checkPrerequisites(spinner, "rustc", ["--version"], "Rust compiler (rustc)");
|
||||
checkPrerequisites(
|
||||
spinner,
|
||||
"solc",
|
||||
["--version"],
|
||||
"Solidity compiler (solc)",
|
||||
);
|
||||
|
||||
const uri = await buildStylus(spinner, secretKey);
|
||||
|
||||
@@ -99,9 +88,9 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
|
||||
}
|
||||
spinner.succeed("Initcode generated.");
|
||||
|
||||
// Step 3: Run stylus command to generate abi
|
||||
// Step 3: Run stylus command to generate abi (plain Solidity, no solc needed)
|
||||
spinner.start("Generating ABI...");
|
||||
const abiResult = spawnSync("cargo", ["stylus", "export-abi", "--json"], {
|
||||
const abiResult = spawnSync("cargo", ["stylus", "export-abi"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (abiResult.status !== 0) {
|
||||
@@ -109,15 +98,21 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const abiContent = abiResult.stdout.trim();
|
||||
if (!abiContent) {
|
||||
const solidityOutput = abiResult.stdout.trim();
|
||||
if (!solidityOutput) {
|
||||
spinner.fail("Failed to generate ABI.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const interfaces = parseSolidityInterfaces(solidityOutput);
|
||||
if (interfaces.length === 0) {
|
||||
spinner.fail("No interfaces found in ABI output.");
|
||||
process.exit(1);
|
||||
}
|
||||
spinner.succeed("ABI generated.");
|
||||
|
||||
// Step 3.5: detect the constructor
|
||||
spinner.start("Detecting constructor…");
|
||||
spinner.start("Detecting constructor\u2026");
|
||||
const constructorResult = spawnSync("cargo", ["stylus", "constructor"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
@@ -127,23 +122,16 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const constructorSigRaw = constructorResult.stdout.trim(); // e.g. "constructor(address owner)"
|
||||
const constructorSigRaw = constructorResult.stdout.trim();
|
||||
spinner.succeed(`Constructor found: ${constructorSigRaw || "none"}`);
|
||||
|
||||
// Step 4: Process the output
|
||||
const parts = abiContent.split(/======= <stdin>:/g).filter(Boolean);
|
||||
const contractNames = extractContractNamesFromExportAbi(abiContent);
|
||||
let selectedIndex = 0;
|
||||
|
||||
let selectedContractName: string | undefined;
|
||||
let selectedAbiContent: string | undefined;
|
||||
|
||||
if (contractNames.length === 1) {
|
||||
selectedContractName = contractNames[0]?.replace(/^I/, "");
|
||||
selectedAbiContent = parts[0];
|
||||
} else {
|
||||
if (interfaces.length > 1) {
|
||||
const response = await prompts({
|
||||
choices: contractNames.map((name, idx) => ({
|
||||
title: name,
|
||||
choices: interfaces.map((iface, idx) => ({
|
||||
title: iface.name,
|
||||
value: idx,
|
||||
})),
|
||||
message: "Select entrypoint:",
|
||||
@@ -151,46 +139,31 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
|
||||
type: "select",
|
||||
});
|
||||
|
||||
const selectedIndex = response.contract;
|
||||
|
||||
if (typeof selectedIndex !== "number") {
|
||||
if (typeof response.contract !== "number") {
|
||||
spinner.fail("No contract selected.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
selectedContractName = contractNames[selectedIndex]?.replace(/^I/, "");
|
||||
selectedAbiContent = parts[selectedIndex];
|
||||
selectedIndex = response.contract;
|
||||
}
|
||||
|
||||
if (!selectedAbiContent) {
|
||||
throw new Error("Entrypoint not found");
|
||||
}
|
||||
|
||||
if (!selectedContractName) {
|
||||
spinner.fail("Error: Could not determine contract name from ABI output.");
|
||||
const selectedInterface = interfaces[selectedIndex];
|
||||
if (!selectedInterface) {
|
||||
spinner.fail("No interface found.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let cleanedAbi = "";
|
||||
try {
|
||||
const jsonMatch = selectedAbiContent.match(/\[.*\]/s);
|
||||
if (jsonMatch) {
|
||||
cleanedAbi = jsonMatch[0];
|
||||
} else {
|
||||
throw new Error("No valid JSON ABI found in the file.");
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail("Error: ABI file contains invalid format.");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <>
|
||||
const abiArray: any[] = JSON.parse(cleanedAbi);
|
||||
const selectedContractName = selectedInterface.name.replace(/^I/, "");
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ABI is untyped JSON from parseAbiItem
|
||||
const abiArray: any[] = selectedInterface.abi;
|
||||
|
||||
const constructorAbi = constructorSigToAbi(constructorSigRaw);
|
||||
if (constructorAbi && !abiArray.some((e) => e.type === "constructor")) {
|
||||
abiArray.unshift(constructorAbi); // put it at the top for readability
|
||||
if (
|
||||
constructorAbi &&
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ABI entries have varying shapes
|
||||
!abiArray.some((e: any) => e.type === "constructor")
|
||||
) {
|
||||
abiArray.unshift(constructorAbi);
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
@@ -256,10 +229,94 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function extractContractNamesFromExportAbi(abiRawOutput: string): string[] {
|
||||
return [...abiRawOutput.matchAll(/<stdin>:(I?[A-Za-z0-9_]+)/g)]
|
||||
.map((m) => m[1])
|
||||
.filter((name): name is string => typeof name === "string");
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ABI items from parseAbiItem are untyped
|
||||
type AbiEntry = any;
|
||||
type ParsedInterface = { name: string; abi: AbiEntry[] };
|
||||
|
||||
function parseSolidityInterfaces(source: string): ParsedInterface[] {
|
||||
const results: ParsedInterface[] = [];
|
||||
|
||||
const ifaceRegex = /interface\s+(I?[A-Za-z0-9_]+)\s*\{([\s\S]*?)\n\}/g;
|
||||
for (
|
||||
let ifaceMatch = ifaceRegex.exec(source);
|
||||
ifaceMatch !== null;
|
||||
ifaceMatch = ifaceRegex.exec(source)
|
||||
) {
|
||||
const name = ifaceMatch[1] ?? "";
|
||||
const body = ifaceMatch[2] ?? "";
|
||||
const abi: AbiEntry[] = [];
|
||||
|
||||
// Build struct lookup: name -> tuple type string
|
||||
const structs = new Map<string, string>();
|
||||
const structRegex = /struct\s+(\w+)\s*\{([^}]*)\}/g;
|
||||
for (
|
||||
let structMatch = structRegex.exec(body);
|
||||
structMatch !== null;
|
||||
structMatch = structRegex.exec(body)
|
||||
) {
|
||||
const fields = (structMatch[2] ?? "")
|
||||
.split(";")
|
||||
.map((f) => f.trim())
|
||||
.filter(Boolean)
|
||||
.map((f) => f.split(/\s+/)[0] ?? "");
|
||||
structs.set(structMatch[1] ?? "", `(${fields.join(",")})`);
|
||||
}
|
||||
|
||||
// Resolve struct references in a type string (iterative for nested structs)
|
||||
const resolveStructs = (sig: string): string => {
|
||||
let resolved = sig;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
let changed = false;
|
||||
for (const [sName, sTuple] of structs) {
|
||||
const re = new RegExp(`\\b${sName}\\b(\\[\\])?`, "g");
|
||||
const next = resolved.replace(
|
||||
re,
|
||||
(_, arr) => `${sTuple}${arr ?? ""}`,
|
||||
);
|
||||
if (next !== resolved) {
|
||||
resolved = next;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) break;
|
||||
}
|
||||
return resolved;
|
||||
};
|
||||
|
||||
// Extract each statement (function/error/event) delimited by ;
|
||||
const statements = body
|
||||
.split(";")
|
||||
.map((s) => s.replace(/\n/g, " ").trim())
|
||||
.filter(
|
||||
(s) =>
|
||||
s.startsWith("function ") ||
|
||||
s.startsWith("error ") ||
|
||||
s.startsWith("event "),
|
||||
);
|
||||
|
||||
for (const stmt of statements) {
|
||||
// Strip Solidity qualifiers that abitype doesn't expect
|
||||
let cleaned = stmt
|
||||
.replace(/\b(external|public|internal|private)\b/g, "")
|
||||
.replace(/\b(memory|calldata|storage)\b/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
// Resolve struct type names to tuple types
|
||||
cleaned = resolveStructs(cleaned);
|
||||
|
||||
try {
|
||||
const parsed = parseAbiItem(cleaned);
|
||||
abi.push(parsed);
|
||||
} catch {
|
||||
// Skip unparseable items
|
||||
}
|
||||
}
|
||||
|
||||
results.push({ abi, name });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function getUrl(hash: string, command: string) {
|
||||
|
||||
@@ -8,12 +8,6 @@ export async function createStylusProject() {
|
||||
|
||||
checkPrerequisites(spinner, "cargo", ["--version"], "Rust (cargo)");
|
||||
checkPrerequisites(spinner, "rustc", ["--version"], "Rust compiler (rustc)");
|
||||
checkPrerequisites(
|
||||
spinner,
|
||||
"solc",
|
||||
["--version"],
|
||||
"Solidity compiler (solc)",
|
||||
);
|
||||
|
||||
// Step 1: Ensure cargo is installed
|
||||
const cargoCheck = spawnSync("cargo", ["--version"]);
|
||||
|
||||
Reference in New Issue
Block a user