diff --git a/.changeset/poor-laws-live.md b/.changeset/poor-laws-live.md new file mode 100644 index 00000000000..bfa187067cb --- /dev/null +++ b/.changeset/poor-laws-live.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +[CLI] fix stylus abi exports diff --git a/packages/thirdweb/src/cli/commands/stylus/builder.ts b/packages/thirdweb/src/cli/commands/stylus/builder.ts index 9f9e9a09341..23de428c5ba 100644 --- a/packages/thirdweb/src/cli/commands/stylus/builder.ts +++ b/packages/thirdweb/src/cli/commands/stylus/builder.ts @@ -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(/======= :/g).filter(Boolean); - const contractNames = extractContractNamesFromExportAbi(abiContent); - - let selectedContractName: string | undefined; - let selectedAbiContent: string | undefined; + let selectedIndex = 0; - 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."); - 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); + const selectedInterface = interfaces[selectedIndex]; + if (!selectedInterface) { + spinner.fail("No interface found."); 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(/:(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(); + 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) { diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 270e26e3fc7..b7e34f7bc98 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -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"]);