From c01e0a8a94b5aaebb9098ceaca8ee65b5d998aa7 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 2 Jul 2025 15:58:25 +0530 Subject: [PATCH 01/13] erc721 template --- .../src/cli/commands/stylus/create.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 23c40804ac3..95bd9807ee7 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -43,6 +43,7 @@ export async function createStylusProject() { choices: [ { title: "Default", value: "default" }, { title: "ERC20", value: "erc20" }, + { title: "ERC721", value: "erc721" }, ], message: "Select a template:", name: "projectType", @@ -50,25 +51,29 @@ export async function createStylusProject() { }); // Step 5: Create the project + let newProject; if (projectType === "default") { spinner.start(`Creating new Stylus project: ${projectName}...`); - const newProject = spawnSync("cargo", ["stylus", "new", projectName], { + newProject = spawnSync("cargo", ["stylus", "new", projectName], { stdio: "inherit", }); - if (newProject.status !== 0) { - spinner.fail("Failed to create Stylus project."); - process.exit(1); - } } else if (projectType === "erc20") { const repoUrl = "git@github.com:thirdweb-example/stylus-erc20-template.git"; spinner.start(`Creating new ERC20 Stylus project: ${projectName}...`); - const clone = spawnSync("git", ["clone", repoUrl, projectName], { + newProject = spawnSync("git", ["clone", repoUrl, projectName], { stdio: "inherit", }); - if (clone.status !== 0) { - spinner.fail("Failed to create Stylus project."); - process.exit(1); - } + } else if (projectType === "erc721") { + const repoUrl = "git@github.com:thirdweb-example/stylus-erc721-template.git"; + spinner.start(`Creating new ERC721 Stylus project: ${projectName}...`); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } + + if (!newProject?.status || newProject.status !== 0) { + spinner.fail("Failed to create Stylus project."); + process.exit(1); } spinner.succeed("Project created successfully."); From 54826fabfe27a0d23b184afa36c137a8acfd2d51 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 2 Jul 2025 21:37:01 +0530 Subject: [PATCH 02/13] detect and parse constructor --- .../src/cli/commands/stylus/builder.ts | 451 ++++++++++-------- .../src/cli/commands/stylus/create.ts | 136 +++--- 2 files changed, 316 insertions(+), 271 deletions(-) diff --git a/packages/thirdweb/src/cli/commands/stylus/builder.ts b/packages/thirdweb/src/cli/commands/stylus/builder.ts index 6a12fc29c19..6da306b5da0 100644 --- a/packages/thirdweb/src/cli/commands/stylus/builder.ts +++ b/packages/thirdweb/src/cli/commands/stylus/builder.ts @@ -11,226 +11,269 @@ import { upload } from "../../../storage/upload.js"; const THIRDWEB_URL = "https://thirdweb.com"; export async function publishStylus(secretKey?: string) { - const spinner = ora("Checking if this is a Stylus project...").start(); - const uri = await buildStylus(spinner, secretKey); + const spinner = ora("Checking if this is a Stylus project...").start(); + const uri = await buildStylus(spinner, secretKey); - const url = getUrl(uri, "publish").toString(); - spinner.succeed(`Upload complete, navigate to ${url}`); - await open(url); + const url = getUrl(uri, "publish").toString(); + spinner.succeed(`Upload complete, navigate to ${url}`); + await open(url); } export async function deployStylus(secretKey?: string) { - const spinner = ora("Checking if this is a Stylus project...").start(); - const uri = await buildStylus(spinner, secretKey); + const spinner = ora("Checking if this is a Stylus project...").start(); + const uri = await buildStylus(spinner, secretKey); - const url = getUrl(uri, "deploy").toString(); - spinner.succeed(`Upload complete, navigate to ${url}`); - await open(url); + const url = getUrl(uri, "deploy").toString(); + spinner.succeed(`Upload complete, navigate to ${url}`); + await open(url); } async function buildStylus(spinner: Ora, secretKey?: string) { - if (!secretKey) { - spinner.fail( - "Error: Secret key is required. Please pass it via the -k parameter.", - ); - process.exit(1); - } - - try { - // Step 1: Validate stylus project - const root = process.cwd(); - if (!root) { - spinner.fail("Error: No package directory found."); - process.exit(1); - } - - const cargoTomlPath = join(root, "Cargo.toml"); - if (!existsSync(cargoTomlPath)) { - spinner.fail("Error: No Cargo.toml found. Not a Stylus/Rust project."); - process.exit(1); - } - - const cargoToml = readFileSync(cargoTomlPath, "utf8"); - const parsedCargoToml = parse(cargoToml); - if (!parsedCargoToml.dependencies?.["stylus-sdk"]) { - spinner.fail( - "Error: Not a Stylus project. Missing stylus-sdk dependency.", - ); - process.exit(1); - } - - spinner.succeed("Stylus project detected."); - - // Step 2: Run stylus command to generate initcode - spinner.start("Generating initcode..."); - const initcodeResult = spawnSync("cargo", ["stylus", "get-initcode"], { - encoding: "utf-8", - }); - if (initcodeResult.status !== 0) { - spinner.fail("Failed to generate initcode."); - process.exit(1); - } - - const initcode = extractBytecode(initcodeResult.stdout); - if (!initcode) { - spinner.fail("Failed to generate initcode."); - process.exit(1); - } - spinner.succeed("Initcode generated."); - - // Step 3: Run stylus command to generate abi - spinner.start("Generating ABI..."); - const abiResult = spawnSync("cargo", ["stylus", "export-abi", "--json"], { - encoding: "utf-8", - }); - if (abiResult.status !== 0) { - spinner.fail("Failed to generate ABI."); - process.exit(1); - } - - const abiContent = abiResult.stdout.trim(); - if (!abiContent) { - spinner.fail("Failed to generate ABI."); - process.exit(1); - } - spinner.succeed("ABI generated."); - - // Step 4: Process the output - const parts = abiContent.split(/======= :/g).filter(Boolean); - const contractNames = extractContractNamesFromExportAbi(abiContent); - - let selectedContractName: string | undefined; - let selectedAbiContent: string | undefined; - - if (contractNames.length === 1) { - selectedContractName = contractNames[0]?.replace(/^I/, ""); - selectedAbiContent = parts[0]; - } else { - const response = await prompts({ - choices: contractNames.map((name, idx) => ({ - title: name, - value: idx, - })), - message: "Select entrypoint:", - name: "contract", - type: "select", - }); - - const selectedIndex = response.contract; - - if (typeof selectedIndex !== "number") { - spinner.fail("No contract selected."); - process.exit(1); - } - - selectedContractName = contractNames[selectedIndex]?.replace(/^I/, ""); - selectedAbiContent = parts[selectedIndex]; - } - - 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); - process.exit(1); - } - - const metadata = { - compiler: {}, - language: "rust", - output: { - abi: JSON.parse(cleanedAbi), - devdoc: {}, - userdoc: {}, - }, - settings: { - compilationTarget: { - "src/main.rs": selectedContractName, - }, - }, - sources: {}, - }; - spinner.succeed("Stylus contract exported successfully."); - - // Step 5: Upload to IPFS - spinner.start("Uploading to IPFS..."); - const client = createThirdwebClient({ - secretKey, - }); - - const metadataUri = await upload({ - client, - files: [metadata], - }); - - const bytecodeUri = await upload({ - client, - files: [initcode], - }); - - const uri = await upload({ - client, - files: [ - { - analytics: { - cli_version: "", - command: "publish-stylus", - contract_name: selectedContractName, - project_type: "stylus", - }, - bytecodeUri, - compilers: { - stylus: [ - { bytecodeUri, compilerVersion: "", evmVersion: "", metadataUri }, - ], - }, - metadataUri, - name: selectedContractName, - }, - ], - }); - spinner.succeed("Upload complete"); - - return uri; - } catch (error) { - spinner.fail(`Error: ${error}`); - process.exit(1); - } + if (!secretKey) { + spinner.fail( + "Error: Secret key is required. Please pass it via the -k parameter.", + ); + process.exit(1); + } + + try { + // Step 1: Validate stylus project + const root = process.cwd(); + if (!root) { + spinner.fail("Error: No package directory found."); + process.exit(1); + } + + const cargoTomlPath = join(root, "Cargo.toml"); + if (!existsSync(cargoTomlPath)) { + spinner.fail("Error: No Cargo.toml found. Not a Stylus/Rust project."); + process.exit(1); + } + + const cargoToml = readFileSync(cargoTomlPath, "utf8"); + const parsedCargoToml = parse(cargoToml); + if (!parsedCargoToml.dependencies?.["stylus-sdk"]) { + spinner.fail( + "Error: Not a Stylus project. Missing stylus-sdk dependency.", + ); + process.exit(1); + } + + spinner.succeed("Stylus project detected."); + + // Step 2: Run stylus command to generate initcode + spinner.start("Generating initcode..."); + const initcodeResult = spawnSync("cargo", ["stylus", "get-initcode"], { + encoding: "utf-8", + }); + if (initcodeResult.status !== 0) { + spinner.fail("Failed to generate initcode."); + process.exit(1); + } + + const initcode = extractBytecode(initcodeResult.stdout); + if (!initcode) { + spinner.fail("Failed to generate initcode."); + process.exit(1); + } + spinner.succeed("Initcode generated."); + + // Step 3: Run stylus command to generate abi + spinner.start("Generating ABI..."); + const abiResult = spawnSync("cargo", ["stylus", "export-abi", "--json"], { + encoding: "utf-8", + }); + if (abiResult.status !== 0) { + spinner.fail("Failed to generate ABI."); + process.exit(1); + } + + const abiContent = abiResult.stdout.trim(); + if (!abiContent) { + spinner.fail("Failed to generate ABI."); + process.exit(1); + } + spinner.succeed("ABI generated."); + + // Step 3.5: detect the constructor + spinner.start("Detecting constructor…"); + const constructorResult = spawnSync("cargo", ["stylus", "constructor"], { + encoding: "utf-8", + }); + + if (constructorResult.status !== 0) { + spinner.fail("Failed to get constructor signature."); + process.exit(1); + } + + const constructorSigRaw = constructorResult.stdout.trim(); // e.g. "constructor(address owner)" + 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; + + if (contractNames.length === 1) { + selectedContractName = contractNames[0]?.replace(/^I/, ""); + selectedAbiContent = parts[0]; + } else { + const response = await prompts({ + choices: contractNames.map((name, idx) => ({ + title: name, + value: idx, + })), + message: "Select entrypoint:", + name: "contract", + type: "select", + }); + + const selectedIndex = response.contract; + + if (typeof selectedIndex !== "number") { + spinner.fail("No contract selected."); + process.exit(1); + } + + selectedContractName = contractNames[selectedIndex]?.replace(/^I/, ""); + selectedAbiContent = parts[selectedIndex]; + } + + 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); + process.exit(1); + } + + // biome-ignore lint/suspicious/noExplicitAny: <> + const abiArray: any[] = JSON.parse(cleanedAbi); + + const constructorAbi = constructorSigToAbi(constructorSigRaw); + if (constructorAbi && !abiArray.some((e) => e.type === "constructor")) { + abiArray.unshift(constructorAbi); // put it at the top for readability + } + + const metadata = { + compiler: {}, + language: "rust", + output: { + abi: abiArray, + devdoc: {}, + userdoc: {}, + }, + settings: { + compilationTarget: { + "src/main.rs": selectedContractName, + }, + }, + sources: {}, + }; + spinner.succeed("Stylus contract exported successfully."); + + // Step 5: Upload to IPFS + spinner.start("Uploading to IPFS..."); + const client = createThirdwebClient({ + secretKey, + }); + + const metadataUri = await upload({ + client, + files: [metadata], + }); + + const bytecodeUri = await upload({ + client, + files: [initcode], + }); + + const uri = await upload({ + client, + files: [ + { + analytics: { + cli_version: "", + command: "publish-stylus", + contract_name: selectedContractName, + project_type: "stylus", + }, + bytecodeUri, + compilers: { + stylus: [ + { bytecodeUri, compilerVersion: "", evmVersion: "", metadataUri }, + ], + }, + metadataUri, + name: selectedContractName, + }, + ], + }); + spinner.succeed("Upload complete"); + + return uri; + } catch (error) { + spinner.fail(`Error: ${error}`); + process.exit(1); + } } 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"); + return [...abiRawOutput.matchAll(/:(I?[A-Za-z0-9_]+)/g)] + .map((m) => m[1]) + .filter((name): name is string => typeof name === "string"); } function getUrl(hash: string, command: string) { - const url = new URL( - `${THIRDWEB_URL}/contracts/${command}/${encodeURIComponent(hash.replace("ipfs://", ""))}`, - ); + const url = new URL( + `${THIRDWEB_URL}/contracts/${command}/${encodeURIComponent(hash.replace("ipfs://", ""))}`, + ); - return url; + return url; } function extractBytecode(rawOutput: string): string { - const hexStart = rawOutput.indexOf("7f000000"); - if (hexStart === -1) { - throw new Error("Could not find start of bytecode"); - } - return rawOutput.slice(hexStart).trim(); + const hexStart = rawOutput.indexOf("7f000000"); + if (hexStart === -1) { + throw new Error("Could not find start of bytecode"); + } + return rawOutput.slice(hexStart).trim(); +} + +function constructorSigToAbi(sig: string) { + if (!sig || !sig.startsWith("constructor")) return undefined; + + const sigClean = sig + .replace(/^constructor\s*\(?/, "") + .replace(/\)\s*$/, "") + .replace(/\s+(payable|nonpayable)\s*$/, ""); + + const mutability = sig.includes("payable") ? "payable" : "nonpayable"; + + const inputs = + sigClean === "" + ? [] + : sigClean.split(",").map((p) => { + const [type, name = ""] = p.trim().split(/\s+/); + return { internalType: type, type, name }; + }); + + return { type: "constructor", stateMutability: mutability, inputs }; } diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 95bd9807ee7..5b704ebf290 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -3,79 +3,81 @@ import ora from "ora"; import prompts from "prompts"; export async function createStylusProject() { - const spinner = ora(); + const spinner = ora(); - // Step 1: Ensure cargo is installed - const cargoCheck = spawnSync("cargo", ["--version"]); - if (cargoCheck.status !== 0) { - console.error("Error: `cargo` is not installed"); - process.exit(1); - } + // Step 1: Ensure cargo is installed + const cargoCheck = spawnSync("cargo", ["--version"]); + if (cargoCheck.status !== 0) { + console.error("Error: `cargo` is not installed"); + process.exit(1); + } - // Step 2: Install stylus etc. - spinner.start("Installing Stylus..."); - const install = spawnSync("cargo", ["install", "--force", "cargo-stylus"], { - stdio: "inherit", - }); - if (install.status !== 0) { - spinner.fail("Failed to install Stylus."); - process.exit(1); - } - spinner.succeed("Stylus installed."); + // Step 2: Install stylus etc. + spinner.start("Installing Stylus..."); + const install = spawnSync("cargo", ["install", "--force", "cargo-stylus"], { + stdio: "inherit", + }); + if (install.status !== 0) { + spinner.fail("Failed to install Stylus."); + process.exit(1); + } + spinner.succeed("Stylus installed."); - spawnSync("rustup", ["default", "1.83"], { - stdio: "inherit", - }); - spawnSync("rustup", ["target", "add", "wasm32-unknown-unknown"], { - stdio: "inherit", - }); + spawnSync("rustup", ["default", "1.83"], { + stdio: "inherit", + }); + spawnSync("rustup", ["target", "add", "wasm32-unknown-unknown"], { + stdio: "inherit", + }); - // Step 3: Create the project - const { projectName } = await prompts({ - initial: "my-stylus-project", - message: "Project name:", - name: "projectName", - type: "text", - }); + // Step 3: Create the project + const { projectName } = await prompts({ + initial: "my-stylus-project", + message: "Project name:", + name: "projectName", + type: "text", + }); - // Step 4: Select project type - const { projectType } = await prompts({ - choices: [ - { title: "Default", value: "default" }, - { title: "ERC20", value: "erc20" }, - { title: "ERC721", value: "erc721" }, - ], - message: "Select a template:", - name: "projectType", - type: "select", - }); + // Step 4: Select project type + const { projectType } = await prompts({ + choices: [ + { title: "Default", value: "default" }, + { title: "ERC20", value: "erc20" }, + { title: "ERC721", value: "erc721" }, + ], + message: "Select a template:", + name: "projectType", + type: "select", + }); - // Step 5: Create the project - let newProject; - if (projectType === "default") { - spinner.start(`Creating new Stylus project: ${projectName}...`); - newProject = spawnSync("cargo", ["stylus", "new", projectName], { - stdio: "inherit", - }); - } else if (projectType === "erc20") { - const repoUrl = "git@github.com:thirdweb-example/stylus-erc20-template.git"; - spinner.start(`Creating new ERC20 Stylus project: ${projectName}...`); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } else if (projectType === "erc721") { - const repoUrl = "git@github.com:thirdweb-example/stylus-erc721-template.git"; - spinner.start(`Creating new ERC721 Stylus project: ${projectName}...`); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } + // Step 5: Create the project + // biome-ignore lint/suspicious/noImplicitAnyLet: <> + let newProject; + if (projectType === "default") { + spinner.start(`Creating new Stylus project: ${projectName}...`); + newProject = spawnSync("cargo", ["stylus", "new", projectName], { + stdio: "inherit", + }); + } else if (projectType === "erc20") { + const repoUrl = "git@github.com:thirdweb-example/stylus-erc20-template.git"; + spinner.start(`Creating new ERC20 Stylus project: ${projectName}...`); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } else if (projectType === "erc721") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-erc721-template.git"; + spinner.start(`Creating new ERC721 Stylus project: ${projectName}...`); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } - if (!newProject?.status || newProject.status !== 0) { - spinner.fail("Failed to create Stylus project."); - process.exit(1); - } + if (!newProject || newProject.status !== 0) { + spinner.fail("Failed to create Stylus project."); + process.exit(1); + } - spinner.succeed("Project created successfully."); - console.log(`\n✅ cd into your project: ${projectName}`); + spinner.succeed("Project created successfully."); + console.log(`\n✅ cd into your project: ${projectName}`); } From 6e9a9ad5b16ea7bd2dae8205aaa71c502751f5a8 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 2 Jul 2025 23:03:33 +0530 Subject: [PATCH 03/13] erc1155 --- packages/thirdweb/src/cli/commands/stylus/create.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 5b704ebf290..51915dfa22e 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -44,6 +44,7 @@ export async function createStylusProject() { { title: "Default", value: "default" }, { title: "ERC20", value: "erc20" }, { title: "ERC721", value: "erc721" }, + { title: "ERC1155", value: "erc1155" }, ], message: "Select a template:", name: "projectType", @@ -71,6 +72,13 @@ export async function createStylusProject() { newProject = spawnSync("git", ["clone", repoUrl, projectName], { stdio: "inherit", }); + } else if (projectType === "erc1155") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-erc1155-template.git"; + spinner.start(`Creating new ERC1155 Stylus project: ${projectName}...`); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); } if (!newProject || newProject.status !== 0) { From 3fdb5be1188b897da255999e5dc6b6894805dfde Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 3 Jul 2025 00:02:26 +0530 Subject: [PATCH 04/13] airdrop erc20 --- packages/thirdweb/src/cli/commands/stylus/create.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 51915dfa22e..3f26526d9aa 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -45,6 +45,7 @@ export async function createStylusProject() { { title: "ERC20", value: "erc20" }, { title: "ERC721", value: "erc721" }, { title: "ERC1155", value: "erc1155" }, + { title: "Airdrop ERC20", value: "airdrop20" }, ], message: "Select a template:", name: "projectType", @@ -79,6 +80,15 @@ export async function createStylusProject() { newProject = spawnSync("git", ["clone", repoUrl, projectName], { stdio: "inherit", }); + } else if (projectType === "airdrop20") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-airdrop-erc20-template.git"; + spinner.start( + `Creating new Airdrop ERC20 Stylus project: ${projectName}...`, + ); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); } if (!newProject || newProject.status !== 0) { From feb0e4de60332d33eef31403269104a286575b77 Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 3 Jul 2025 00:24:55 +0530 Subject: [PATCH 05/13] airdrop erc721 --- packages/thirdweb/src/cli/commands/stylus/create.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 3f26526d9aa..2a144fc9d23 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -46,6 +46,7 @@ export async function createStylusProject() { { title: "ERC721", value: "erc721" }, { title: "ERC1155", value: "erc1155" }, { title: "Airdrop ERC20", value: "airdrop20" }, + { title: "Airdrop ERC721", value: "airdrop721" }, ], message: "Select a template:", name: "projectType", @@ -89,6 +90,15 @@ export async function createStylusProject() { newProject = spawnSync("git", ["clone", repoUrl, projectName], { stdio: "inherit", }); + } else if (projectType === "airdrop721") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-airdrop-erc721-template.git"; + spinner.start( + `Creating new Airdrop ERC721 Stylus project: ${projectName}...`, + ); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); } if (!newProject || newProject.status !== 0) { From 99b77642aa1b7cfa5b3591237886dcfa3b26d5ed Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 3 Jul 2025 00:26:01 +0530 Subject: [PATCH 06/13] airdrop erc1155 --- packages/thirdweb/src/cli/commands/stylus/create.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 2a144fc9d23..12ddfc76e93 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -47,6 +47,7 @@ export async function createStylusProject() { { title: "ERC1155", value: "erc1155" }, { title: "Airdrop ERC20", value: "airdrop20" }, { title: "Airdrop ERC721", value: "airdrop721" }, + { title: "Airdrop ERC1155", value: "airdrop1155" }, ], message: "Select a template:", name: "projectType", @@ -99,6 +100,15 @@ export async function createStylusProject() { newProject = spawnSync("git", ["clone", repoUrl, projectName], { stdio: "inherit", }); + } else if (projectType === "airdrop1155") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-airdrop-erc1155-template.git"; + spinner.start( + `Creating new Airdrop ERC1155 Stylus project: ${projectName}...`, + ); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); } if (!newProject || newProject.status !== 0) { From c9dfa002b69cba54bcca39bc3723bcfcfc128085 Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 3 Jul 2025 05:07:17 +0530 Subject: [PATCH 07/13] stylus deployer for deployment with constructor --- .../abis/stylus/IStylusConstructor.json | 3 + .../generate/abis/stylus/IStylusDeployer.json | 4 + .../contract/deployment/deploy-with-abi.ts | 62 +++++- .../write/stylus_constructor.ts | 50 +++++ .../events/ContractDeployed.ts | 24 +++ .../IStylusDeployer/write/deploy.ts | 178 ++++++++++++++++++ 6 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 packages/thirdweb/scripts/generate/abis/stylus/IStylusConstructor.json create mode 100644 packages/thirdweb/scripts/generate/abis/stylus/IStylusDeployer.json create mode 100644 packages/thirdweb/src/extensions/stylus/__generated__/IStylusConstructor/write/stylus_constructor.ts create mode 100644 packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.ts create mode 100644 packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/write/deploy.ts diff --git a/packages/thirdweb/scripts/generate/abis/stylus/IStylusConstructor.json b/packages/thirdweb/scripts/generate/abis/stylus/IStylusConstructor.json new file mode 100644 index 00000000000..7fc202d729f --- /dev/null +++ b/packages/thirdweb/scripts/generate/abis/stylus/IStylusConstructor.json @@ -0,0 +1,3 @@ +[ + "function stylus_constructor()" +] \ No newline at end of file diff --git a/packages/thirdweb/scripts/generate/abis/stylus/IStylusDeployer.json b/packages/thirdweb/scripts/generate/abis/stylus/IStylusDeployer.json new file mode 100644 index 00000000000..0d851cc8fc0 --- /dev/null +++ b/packages/thirdweb/scripts/generate/abis/stylus/IStylusDeployer.json @@ -0,0 +1,4 @@ +[ + "function deploy(bytes calldata bytecode,bytes calldata initData,uint256 initValue,bytes32 salt) public payable returns (address)", + "event ContractDeployed(address deployedContract)" +] \ No newline at end of file diff --git a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts index 0c409874981..6b6f36f0dfa 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts @@ -1,5 +1,11 @@ import type { Abi, AbiConstructor } from "abitype"; +import { parseEventLogs } from "../../event/actions/parse-logs.js"; +import { FN_SELECTOR } from "../../extensions/stylus/__generated__/IStylusConstructor/write/stylus_constructor.js"; +import { contractDeployedEvent } from "../../extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.js"; +import { deploy } from "../../extensions/stylus/__generated__/IStylusDeployer/write/deploy.js"; import { activateStylusContract } from "../../extensions/stylus/write/activateStylusContract.js"; +import { eth_blockNumber } from "../../rpc/actions/eth_blockNumber.js"; +import { getRpcClient } from "../../rpc/rpc.js"; import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js"; import { sendTransaction } from "../../transaction/actions/send-transaction.js"; import { prepareTransaction } from "../../transaction/prepare-transaction.js"; @@ -11,7 +17,7 @@ import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js"; import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js"; import { ensureBytecodePrefix } from "../../utils/bytecode/prefix.js"; import { concatHex } from "../../utils/encoding/helpers/concat-hex.js"; -import { type Hex, isHex } from "../../utils/encoding/hex.js"; +import { type Hex, isHex, toHex } from "../../utils/encoding/hex.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { ClientAndChain } from "../../utils/types.js"; import type { Account } from "../../wallets/interfaces/wallet.js"; @@ -171,6 +177,58 @@ export async function deployContract( to: info.create2FactoryAddress, }), }); + } else if (options.isStylus && options.constructorParams) { + const STYLUS_DEPLOYER = "0xCeCbA2F1dC234F70Dd89f2041029807F8D03A990"; + const stylusDeployer = getContract({ + address: STYLUS_DEPLOYER, + chain: options.chain, + client: options.client, + }); + + const constructorAbi = options.abi.find( + (abi) => abi.type === "constructor", + ) as AbiConstructor | undefined; + const constructorCalldata = (FN_SELECTOR + + encodeAbiParameters( + constructorAbi?.inputs || [], // Leave an empty array if there's no constructor + normalizeFunctionParams( + constructorAbi, + options.constructorParams, + ).slice(2), + )) as `${typeof FN_SELECTOR}${string}`; + + const rpcRequest = getRpcClient({ + ...options, + }); + const blockNumber = await eth_blockNumber(rpcRequest); + const salt = toHex(blockNumber, { + size: 32, + }); + + const deployTx = deploy({ + bytecode: options.bytecode, + contract: stylusDeployer, + initData: constructorCalldata, + initValue: 0n, + salt, + }); + + const receipt = await sendAndConfirmTransaction({ + account: options.account, + transaction: deployTx, + }); + + const deployEvent = contractDeployedEvent(); + const decodedEvent = parseEventLogs({ + events: [deployEvent], + logs: receipt.logs, + }); + if (decodedEvent.length === 0 || !decodedEvent[0]) { + throw new Error( + `No ContractDeployed event found in transaction: ${receipt.transactionHash}`, + ); + } + address = decodedEvent[0]?.args.deployedContract; } else { const deployTx = prepareDirectDeployTransaction(options); const receipt = await sendAndConfirmTransaction({ @@ -180,7 +238,7 @@ export async function deployContract( address = receipt.contractAddress; if (!address) { throw new Error( - `Could not find deployed contract address in transaction: ${receipt.transactionHash}`, + `Could not find deployed contract address in transaction: $receipt.transactionHash`, ); } } diff --git a/packages/thirdweb/src/extensions/stylus/__generated__/IStylusConstructor/write/stylus_constructor.ts b/packages/thirdweb/src/extensions/stylus/__generated__/IStylusConstructor/write/stylus_constructor.ts new file mode 100644 index 00000000000..ec91e868b68 --- /dev/null +++ b/packages/thirdweb/src/extensions/stylus/__generated__/IStylusConstructor/write/stylus_constructor.ts @@ -0,0 +1,50 @@ +import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js"; +import type { BaseTransactionOptions } from "../../../../../transaction/types.js"; + +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; + +export const FN_SELECTOR = "0x5585258d" as const; +const FN_INPUTS = [] as const; +const FN_OUTPUTS = [] as const; + +/** + * Checks if the `stylus_constructor` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `stylus_constructor` method is supported. + * @extension STYLUS + * @example + * ```ts + * import { isStylus_constructorSupported } from "thirdweb/extensions/stylus"; + * + * const supported = isStylus_constructorSupported(["0x..."]); + * ``` + */ +export function isStylus_constructorSupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Prepares a transaction to call the "stylus_constructor" function on the contract. + * @param options - The options for the "stylus_constructor" function. + * @returns A prepared transaction object. + * @extension STYLUS + * @example + * ```ts + * import { sendTransaction } from "thirdweb"; + * import { stylus_constructor } from "thirdweb/extensions/stylus"; + * + * const transaction = stylus_constructor(); + * + * // Send the transaction + * await sendTransaction({ transaction, account }); + * ``` + */ +export function stylus_constructor(options: BaseTransactionOptions) { + return prepareContractCall({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} diff --git a/packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.ts b/packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.ts new file mode 100644 index 00000000000..547454085c3 --- /dev/null +++ b/packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.ts @@ -0,0 +1,24 @@ +import { prepareEvent } from "../../../../../event/prepare-event.js"; + +/** + * Creates an event object for the ContractDeployed event. + * @returns The prepared event object. + * @extension STYLUS + * @example + * ```ts + * import { getContractEvents } from "thirdweb"; + * import { contractDeployedEvent } from "thirdweb/extensions/stylus"; + * + * const events = await getContractEvents({ + * contract, + * events: [ + * contractDeployedEvent() + * ], + * }); + * ``` + */ +export function contractDeployedEvent() { + return prepareEvent({ + signature: "event ContractDeployed(address deployedContract)", + }); +} diff --git a/packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/write/deploy.ts b/packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/write/deploy.ts new file mode 100644 index 00000000000..bf1c111839f --- /dev/null +++ b/packages/thirdweb/src/extensions/stylus/__generated__/IStylusDeployer/write/deploy.ts @@ -0,0 +1,178 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js"; +import type { + BaseTransactionOptions, + WithOverrides, +} from "../../../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; +import { once } from "../../../../../utils/promise/once.js"; + +/** + * Represents the parameters for the "deploy" function. + */ +export type DeployParams = WithOverrides<{ + bytecode: AbiParameterToPrimitiveType<{ type: "bytes"; name: "bytecode" }>; + initData: AbiParameterToPrimitiveType<{ type: "bytes"; name: "initData" }>; + initValue: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "initValue"; + }>; + salt: AbiParameterToPrimitiveType<{ type: "bytes32"; name: "salt" }>; +}>; + +export const FN_SELECTOR = "0xa9a8e4e9" as const; +const FN_INPUTS = [ + { + name: "bytecode", + type: "bytes", + }, + { + name: "initData", + type: "bytes", + }, + { + name: "initValue", + type: "uint256", + }, + { + name: "salt", + type: "bytes32", + }, +] as const; +const FN_OUTPUTS = [ + { + type: "address", + }, +] as const; + +/** + * Checks if the `deploy` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `deploy` method is supported. + * @extension STYLUS + * @example + * ```ts + * import { isDeploySupported } from "thirdweb/extensions/stylus"; + * + * const supported = isDeploySupported(["0x..."]); + * ``` + */ +export function isDeploySupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "deploy" function. + * @param options - The options for the deploy function. + * @returns The encoded ABI parameters. + * @extension STYLUS + * @example + * ```ts + * import { encodeDeployParams } from "thirdweb/extensions/stylus"; + * const result = encodeDeployParams({ + * bytecode: ..., + * initData: ..., + * initValue: ..., + * salt: ..., + * }); + * ``` + */ +export function encodeDeployParams(options: DeployParams) { + return encodeAbiParameters(FN_INPUTS, [ + options.bytecode, + options.initData, + options.initValue, + options.salt, + ]); +} + +/** + * Encodes the "deploy" function into a Hex string with its parameters. + * @param options - The options for the deploy function. + * @returns The encoded hexadecimal string. + * @extension STYLUS + * @example + * ```ts + * import { encodeDeploy } from "thirdweb/extensions/stylus"; + * const result = encodeDeploy({ + * bytecode: ..., + * initData: ..., + * initValue: ..., + * salt: ..., + * }); + * ``` + */ +export function encodeDeploy(options: DeployParams) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeDeployParams(options).slice(2)) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Prepares a transaction to call the "deploy" function on the contract. + * @param options - The options for the "deploy" function. + * @returns A prepared transaction object. + * @extension STYLUS + * @example + * ```ts + * import { sendTransaction } from "thirdweb"; + * import { deploy } from "thirdweb/extensions/stylus"; + * + * const transaction = deploy({ + * contract, + * bytecode: ..., + * initData: ..., + * initValue: ..., + * salt: ..., + * overrides: { + * ... + * } + * }); + * + * // Send the transaction + * await sendTransaction({ transaction, account }); + * ``` + */ +export function deploy( + options: BaseTransactionOptions< + | DeployParams + | { + asyncParams: () => Promise; + } + >, +) { + const asyncOptions = once(async () => { + return "asyncParams" in options ? await options.asyncParams() : options; + }); + + return prepareContractCall({ + accessList: async () => (await asyncOptions()).overrides?.accessList, + authorizationList: async () => + (await asyncOptions()).overrides?.authorizationList, + contract: options.contract, + erc20Value: async () => (await asyncOptions()).overrides?.erc20Value, + extraGas: async () => (await asyncOptions()).overrides?.extraGas, + gas: async () => (await asyncOptions()).overrides?.gas, + gasPrice: async () => (await asyncOptions()).overrides?.gasPrice, + maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas, + maxPriorityFeePerGas: async () => + (await asyncOptions()).overrides?.maxPriorityFeePerGas, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + nonce: async () => (await asyncOptions()).overrides?.nonce, + params: async () => { + const resolvedOptions = await asyncOptions(); + return [ + resolvedOptions.bytecode, + resolvedOptions.initData, + resolvedOptions.initValue, + resolvedOptions.salt, + ] as const; + }, + value: async () => (await asyncOptions()).overrides?.value, + }); +} From 11149a3d9b5db5042c17737607817ea7ed22e628 Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 3 Jul 2025 05:22:36 +0530 Subject: [PATCH 08/13] fix lint --- .../src/cli/commands/stylus/builder.ts | 480 +++++++++--------- .../src/cli/commands/stylus/create.ts | 214 ++++---- 2 files changed, 347 insertions(+), 347 deletions(-) diff --git a/packages/thirdweb/src/cli/commands/stylus/builder.ts b/packages/thirdweb/src/cli/commands/stylus/builder.ts index 6da306b5da0..b0f89bb6064 100644 --- a/packages/thirdweb/src/cli/commands/stylus/builder.ts +++ b/packages/thirdweb/src/cli/commands/stylus/builder.ts @@ -11,269 +11,269 @@ import { upload } from "../../../storage/upload.js"; const THIRDWEB_URL = "https://thirdweb.com"; export async function publishStylus(secretKey?: string) { - const spinner = ora("Checking if this is a Stylus project...").start(); - const uri = await buildStylus(spinner, secretKey); + const spinner = ora("Checking if this is a Stylus project...").start(); + const uri = await buildStylus(spinner, secretKey); - const url = getUrl(uri, "publish").toString(); - spinner.succeed(`Upload complete, navigate to ${url}`); - await open(url); + const url = getUrl(uri, "publish").toString(); + spinner.succeed(`Upload complete, navigate to ${url}`); + await open(url); } export async function deployStylus(secretKey?: string) { - const spinner = ora("Checking if this is a Stylus project...").start(); - const uri = await buildStylus(spinner, secretKey); + const spinner = ora("Checking if this is a Stylus project...").start(); + const uri = await buildStylus(spinner, secretKey); - const url = getUrl(uri, "deploy").toString(); - spinner.succeed(`Upload complete, navigate to ${url}`); - await open(url); + const url = getUrl(uri, "deploy").toString(); + spinner.succeed(`Upload complete, navigate to ${url}`); + await open(url); } async function buildStylus(spinner: Ora, secretKey?: string) { - if (!secretKey) { - spinner.fail( - "Error: Secret key is required. Please pass it via the -k parameter.", - ); - process.exit(1); - } - - try { - // Step 1: Validate stylus project - const root = process.cwd(); - if (!root) { - spinner.fail("Error: No package directory found."); - process.exit(1); - } - - const cargoTomlPath = join(root, "Cargo.toml"); - if (!existsSync(cargoTomlPath)) { - spinner.fail("Error: No Cargo.toml found. Not a Stylus/Rust project."); - process.exit(1); - } - - const cargoToml = readFileSync(cargoTomlPath, "utf8"); - const parsedCargoToml = parse(cargoToml); - if (!parsedCargoToml.dependencies?.["stylus-sdk"]) { - spinner.fail( - "Error: Not a Stylus project. Missing stylus-sdk dependency.", - ); - process.exit(1); - } - - spinner.succeed("Stylus project detected."); - - // Step 2: Run stylus command to generate initcode - spinner.start("Generating initcode..."); - const initcodeResult = spawnSync("cargo", ["stylus", "get-initcode"], { - encoding: "utf-8", - }); - if (initcodeResult.status !== 0) { - spinner.fail("Failed to generate initcode."); - process.exit(1); - } - - const initcode = extractBytecode(initcodeResult.stdout); - if (!initcode) { - spinner.fail("Failed to generate initcode."); - process.exit(1); - } - spinner.succeed("Initcode generated."); - - // Step 3: Run stylus command to generate abi - spinner.start("Generating ABI..."); - const abiResult = spawnSync("cargo", ["stylus", "export-abi", "--json"], { - encoding: "utf-8", - }); - if (abiResult.status !== 0) { - spinner.fail("Failed to generate ABI."); - process.exit(1); - } - - const abiContent = abiResult.stdout.trim(); - if (!abiContent) { - spinner.fail("Failed to generate ABI."); - process.exit(1); - } - spinner.succeed("ABI generated."); - - // Step 3.5: detect the constructor - spinner.start("Detecting constructor…"); - const constructorResult = spawnSync("cargo", ["stylus", "constructor"], { - encoding: "utf-8", - }); - - if (constructorResult.status !== 0) { - spinner.fail("Failed to get constructor signature."); - process.exit(1); - } - - const constructorSigRaw = constructorResult.stdout.trim(); // e.g. "constructor(address owner)" - 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; - - if (contractNames.length === 1) { - selectedContractName = contractNames[0]?.replace(/^I/, ""); - selectedAbiContent = parts[0]; - } else { - const response = await prompts({ - choices: contractNames.map((name, idx) => ({ - title: name, - value: idx, - })), - message: "Select entrypoint:", - name: "contract", - type: "select", - }); - - const selectedIndex = response.contract; - - if (typeof selectedIndex !== "number") { - spinner.fail("No contract selected."); - process.exit(1); - } - - selectedContractName = contractNames[selectedIndex]?.replace(/^I/, ""); - selectedAbiContent = parts[selectedIndex]; - } - - 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); - process.exit(1); - } - - // biome-ignore lint/suspicious/noExplicitAny: <> - const abiArray: any[] = JSON.parse(cleanedAbi); - - const constructorAbi = constructorSigToAbi(constructorSigRaw); - if (constructorAbi && !abiArray.some((e) => e.type === "constructor")) { - abiArray.unshift(constructorAbi); // put it at the top for readability - } - - const metadata = { - compiler: {}, - language: "rust", - output: { - abi: abiArray, - devdoc: {}, - userdoc: {}, - }, - settings: { - compilationTarget: { - "src/main.rs": selectedContractName, - }, - }, - sources: {}, - }; - spinner.succeed("Stylus contract exported successfully."); - - // Step 5: Upload to IPFS - spinner.start("Uploading to IPFS..."); - const client = createThirdwebClient({ - secretKey, - }); - - const metadataUri = await upload({ - client, - files: [metadata], - }); - - const bytecodeUri = await upload({ - client, - files: [initcode], - }); - - const uri = await upload({ - client, - files: [ - { - analytics: { - cli_version: "", - command: "publish-stylus", - contract_name: selectedContractName, - project_type: "stylus", - }, - bytecodeUri, - compilers: { - stylus: [ - { bytecodeUri, compilerVersion: "", evmVersion: "", metadataUri }, - ], - }, - metadataUri, - name: selectedContractName, - }, - ], - }); - spinner.succeed("Upload complete"); - - return uri; - } catch (error) { - spinner.fail(`Error: ${error}`); - process.exit(1); - } + if (!secretKey) { + spinner.fail( + "Error: Secret key is required. Please pass it via the -k parameter.", + ); + process.exit(1); + } + + try { + // Step 1: Validate stylus project + const root = process.cwd(); + if (!root) { + spinner.fail("Error: No package directory found."); + process.exit(1); + } + + const cargoTomlPath = join(root, "Cargo.toml"); + if (!existsSync(cargoTomlPath)) { + spinner.fail("Error: No Cargo.toml found. Not a Stylus/Rust project."); + process.exit(1); + } + + const cargoToml = readFileSync(cargoTomlPath, "utf8"); + const parsedCargoToml = parse(cargoToml); + if (!parsedCargoToml.dependencies?.["stylus-sdk"]) { + spinner.fail( + "Error: Not a Stylus project. Missing stylus-sdk dependency.", + ); + process.exit(1); + } + + spinner.succeed("Stylus project detected."); + + // Step 2: Run stylus command to generate initcode + spinner.start("Generating initcode..."); + const initcodeResult = spawnSync("cargo", ["stylus", "get-initcode"], { + encoding: "utf-8", + }); + if (initcodeResult.status !== 0) { + spinner.fail("Failed to generate initcode."); + process.exit(1); + } + + const initcode = extractBytecode(initcodeResult.stdout); + if (!initcode) { + spinner.fail("Failed to generate initcode."); + process.exit(1); + } + spinner.succeed("Initcode generated."); + + // Step 3: Run stylus command to generate abi + spinner.start("Generating ABI..."); + const abiResult = spawnSync("cargo", ["stylus", "export-abi", "--json"], { + encoding: "utf-8", + }); + if (abiResult.status !== 0) { + spinner.fail("Failed to generate ABI."); + process.exit(1); + } + + const abiContent = abiResult.stdout.trim(); + if (!abiContent) { + spinner.fail("Failed to generate ABI."); + process.exit(1); + } + spinner.succeed("ABI generated."); + + // Step 3.5: detect the constructor + spinner.start("Detecting constructor…"); + const constructorResult = spawnSync("cargo", ["stylus", "constructor"], { + encoding: "utf-8", + }); + + if (constructorResult.status !== 0) { + spinner.fail("Failed to get constructor signature."); + process.exit(1); + } + + const constructorSigRaw = constructorResult.stdout.trim(); // e.g. "constructor(address owner)" + 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; + + if (contractNames.length === 1) { + selectedContractName = contractNames[0]?.replace(/^I/, ""); + selectedAbiContent = parts[0]; + } else { + const response = await prompts({ + choices: contractNames.map((name, idx) => ({ + title: name, + value: idx, + })), + message: "Select entrypoint:", + name: "contract", + type: "select", + }); + + const selectedIndex = response.contract; + + if (typeof selectedIndex !== "number") { + spinner.fail("No contract selected."); + process.exit(1); + } + + selectedContractName = contractNames[selectedIndex]?.replace(/^I/, ""); + selectedAbiContent = parts[selectedIndex]; + } + + 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); + process.exit(1); + } + + // biome-ignore lint/suspicious/noExplicitAny: <> + const abiArray: any[] = JSON.parse(cleanedAbi); + + const constructorAbi = constructorSigToAbi(constructorSigRaw); + if (constructorAbi && !abiArray.some((e) => e.type === "constructor")) { + abiArray.unshift(constructorAbi); // put it at the top for readability + } + + const metadata = { + compiler: {}, + language: "rust", + output: { + abi: abiArray, + devdoc: {}, + userdoc: {}, + }, + settings: { + compilationTarget: { + "src/main.rs": selectedContractName, + }, + }, + sources: {}, + }; + spinner.succeed("Stylus contract exported successfully."); + + // Step 5: Upload to IPFS + spinner.start("Uploading to IPFS..."); + const client = createThirdwebClient({ + secretKey, + }); + + const metadataUri = await upload({ + client, + files: [metadata], + }); + + const bytecodeUri = await upload({ + client, + files: [initcode], + }); + + const uri = await upload({ + client, + files: [ + { + analytics: { + cli_version: "", + command: "publish-stylus", + contract_name: selectedContractName, + project_type: "stylus", + }, + bytecodeUri, + compilers: { + stylus: [ + { bytecodeUri, compilerVersion: "", evmVersion: "", metadataUri }, + ], + }, + metadataUri, + name: selectedContractName, + }, + ], + }); + spinner.succeed("Upload complete"); + + return uri; + } catch (error) { + spinner.fail(`Error: ${error}`); + process.exit(1); + } } 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"); + return [...abiRawOutput.matchAll(/:(I?[A-Za-z0-9_]+)/g)] + .map((m) => m[1]) + .filter((name): name is string => typeof name === "string"); } function getUrl(hash: string, command: string) { - const url = new URL( - `${THIRDWEB_URL}/contracts/${command}/${encodeURIComponent(hash.replace("ipfs://", ""))}`, - ); + const url = new URL( + `${THIRDWEB_URL}/contracts/${command}/${encodeURIComponent(hash.replace("ipfs://", ""))}`, + ); - return url; + return url; } function extractBytecode(rawOutput: string): string { - const hexStart = rawOutput.indexOf("7f000000"); - if (hexStart === -1) { - throw new Error("Could not find start of bytecode"); - } - return rawOutput.slice(hexStart).trim(); + const hexStart = rawOutput.indexOf("7f000000"); + if (hexStart === -1) { + throw new Error("Could not find start of bytecode"); + } + return rawOutput.slice(hexStart).trim(); } function constructorSigToAbi(sig: string) { - if (!sig || !sig.startsWith("constructor")) return undefined; + if (!sig || !sig.startsWith("constructor")) return undefined; - const sigClean = sig - .replace(/^constructor\s*\(?/, "") - .replace(/\)\s*$/, "") - .replace(/\s+(payable|nonpayable)\s*$/, ""); + const sigClean = sig + .replace(/^constructor\s*\(?/, "") + .replace(/\)\s*$/, "") + .replace(/\s+(payable|nonpayable)\s*$/, ""); - const mutability = sig.includes("payable") ? "payable" : "nonpayable"; + const mutability = sig.includes("payable") ? "payable" : "nonpayable"; - const inputs = - sigClean === "" - ? [] - : sigClean.split(",").map((p) => { - const [type, name = ""] = p.trim().split(/\s+/); - return { internalType: type, type, name }; - }); + const inputs = + sigClean === "" + ? [] + : sigClean.split(",").map((p) => { + const [type, name = ""] = p.trim().split(/\s+/); + return { internalType: type, name, type }; + }); - return { type: "constructor", stateMutability: mutability, inputs }; + return { inputs, stateMutability: mutability, type: "constructor" }; } diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 12ddfc76e93..07762e994e5 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -3,119 +3,119 @@ import ora from "ora"; import prompts from "prompts"; export async function createStylusProject() { - const spinner = ora(); + const spinner = ora(); - // Step 1: Ensure cargo is installed - const cargoCheck = spawnSync("cargo", ["--version"]); - if (cargoCheck.status !== 0) { - console.error("Error: `cargo` is not installed"); - process.exit(1); - } + // Step 1: Ensure cargo is installed + const cargoCheck = spawnSync("cargo", ["--version"]); + if (cargoCheck.status !== 0) { + console.error("Error: `cargo` is not installed"); + process.exit(1); + } - // Step 2: Install stylus etc. - spinner.start("Installing Stylus..."); - const install = spawnSync("cargo", ["install", "--force", "cargo-stylus"], { - stdio: "inherit", - }); - if (install.status !== 0) { - spinner.fail("Failed to install Stylus."); - process.exit(1); - } - spinner.succeed("Stylus installed."); + // Step 2: Install stylus etc. + spinner.start("Installing Stylus..."); + const install = spawnSync("cargo", ["install", "--force", "cargo-stylus"], { + stdio: "inherit", + }); + if (install.status !== 0) { + spinner.fail("Failed to install Stylus."); + process.exit(1); + } + spinner.succeed("Stylus installed."); - spawnSync("rustup", ["default", "1.83"], { - stdio: "inherit", - }); - spawnSync("rustup", ["target", "add", "wasm32-unknown-unknown"], { - stdio: "inherit", - }); + spawnSync("rustup", ["default", "1.83"], { + stdio: "inherit", + }); + spawnSync("rustup", ["target", "add", "wasm32-unknown-unknown"], { + stdio: "inherit", + }); - // Step 3: Create the project - const { projectName } = await prompts({ - initial: "my-stylus-project", - message: "Project name:", - name: "projectName", - type: "text", - }); + // Step 3: Create the project + const { projectName } = await prompts({ + initial: "my-stylus-project", + message: "Project name:", + name: "projectName", + type: "text", + }); - // Step 4: Select project type - const { projectType } = await prompts({ - choices: [ - { title: "Default", value: "default" }, - { title: "ERC20", value: "erc20" }, - { title: "ERC721", value: "erc721" }, - { title: "ERC1155", value: "erc1155" }, - { title: "Airdrop ERC20", value: "airdrop20" }, - { title: "Airdrop ERC721", value: "airdrop721" }, - { title: "Airdrop ERC1155", value: "airdrop1155" }, - ], - message: "Select a template:", - name: "projectType", - type: "select", - }); + // Step 4: Select project type + const { projectType } = await prompts({ + choices: [ + { title: "Default", value: "default" }, + { title: "ERC20", value: "erc20" }, + { title: "ERC721", value: "erc721" }, + { title: "ERC1155", value: "erc1155" }, + { title: "Airdrop ERC20", value: "airdrop20" }, + { title: "Airdrop ERC721", value: "airdrop721" }, + { title: "Airdrop ERC1155", value: "airdrop1155" }, + ], + message: "Select a template:", + name: "projectType", + type: "select", + }); - // Step 5: Create the project - // biome-ignore lint/suspicious/noImplicitAnyLet: <> - let newProject; - if (projectType === "default") { - spinner.start(`Creating new Stylus project: ${projectName}...`); - newProject = spawnSync("cargo", ["stylus", "new", projectName], { - stdio: "inherit", - }); - } else if (projectType === "erc20") { - const repoUrl = "git@github.com:thirdweb-example/stylus-erc20-template.git"; - spinner.start(`Creating new ERC20 Stylus project: ${projectName}...`); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } else if (projectType === "erc721") { - const repoUrl = - "git@github.com:thirdweb-example/stylus-erc721-template.git"; - spinner.start(`Creating new ERC721 Stylus project: ${projectName}...`); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } else if (projectType === "erc1155") { - const repoUrl = - "git@github.com:thirdweb-example/stylus-erc1155-template.git"; - spinner.start(`Creating new ERC1155 Stylus project: ${projectName}...`); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } else if (projectType === "airdrop20") { - const repoUrl = - "git@github.com:thirdweb-example/stylus-airdrop-erc20-template.git"; - spinner.start( - `Creating new Airdrop ERC20 Stylus project: ${projectName}...`, - ); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } else if (projectType === "airdrop721") { - const repoUrl = - "git@github.com:thirdweb-example/stylus-airdrop-erc721-template.git"; - spinner.start( - `Creating new Airdrop ERC721 Stylus project: ${projectName}...`, - ); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } else if (projectType === "airdrop1155") { - const repoUrl = - "git@github.com:thirdweb-example/stylus-airdrop-erc1155-template.git"; - spinner.start( - `Creating new Airdrop ERC1155 Stylus project: ${projectName}...`, - ); - newProject = spawnSync("git", ["clone", repoUrl, projectName], { - stdio: "inherit", - }); - } + // Step 5: Create the project + // biome-ignore lint/suspicious/noImplicitAnyLet: <> + let newProject; + if (projectType === "default") { + spinner.start(`Creating new Stylus project: ${projectName}...`); + newProject = spawnSync("cargo", ["stylus", "new", projectName], { + stdio: "inherit", + }); + } else if (projectType === "erc20") { + const repoUrl = "git@github.com:thirdweb-example/stylus-erc20-template.git"; + spinner.start(`Creating new ERC20 Stylus project: ${projectName}...`); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } else if (projectType === "erc721") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-erc721-template.git"; + spinner.start(`Creating new ERC721 Stylus project: ${projectName}...`); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } else if (projectType === "erc1155") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-erc1155-template.git"; + spinner.start(`Creating new ERC1155 Stylus project: ${projectName}...`); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } else if (projectType === "airdrop20") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-airdrop-erc20-template.git"; + spinner.start( + `Creating new Airdrop ERC20 Stylus project: ${projectName}...`, + ); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } else if (projectType === "airdrop721") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-airdrop-erc721-template.git"; + spinner.start( + `Creating new Airdrop ERC721 Stylus project: ${projectName}...`, + ); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } else if (projectType === "airdrop1155") { + const repoUrl = + "git@github.com:thirdweb-example/stylus-airdrop-erc1155-template.git"; + spinner.start( + `Creating new Airdrop ERC1155 Stylus project: ${projectName}...`, + ); + newProject = spawnSync("git", ["clone", repoUrl, projectName], { + stdio: "inherit", + }); + } - if (!newProject || newProject.status !== 0) { - spinner.fail("Failed to create Stylus project."); - process.exit(1); - } + if (!newProject || newProject.status !== 0) { + spinner.fail("Failed to create Stylus project."); + process.exit(1); + } - spinner.succeed("Project created successfully."); - console.log(`\n✅ cd into your project: ${projectName}`); + spinner.succeed("Project created successfully."); + console.log(`\n✅ cd into your project: ${projectName}`); } From a041415dee0cff329fa541fa35a7f200cc84c01b Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 4 Jul 2025 01:29:29 +0530 Subject: [PATCH 09/13] deploy with stylus constructor --- .../contract/deployment/deploy-with-abi.ts | 42 ++--------- .../write/deployWithStylusConstructor.ts | 70 +++++++++++++++++++ 2 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts diff --git a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts index 6b6f36f0dfa..407074b662a 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts @@ -1,11 +1,8 @@ import type { Abi, AbiConstructor } from "abitype"; import { parseEventLogs } from "../../event/actions/parse-logs.js"; -import { FN_SELECTOR } from "../../extensions/stylus/__generated__/IStylusConstructor/write/stylus_constructor.js"; import { contractDeployedEvent } from "../../extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.js"; -import { deploy } from "../../extensions/stylus/__generated__/IStylusDeployer/write/deploy.js"; import { activateStylusContract } from "../../extensions/stylus/write/activateStylusContract.js"; -import { eth_blockNumber } from "../../rpc/actions/eth_blockNumber.js"; -import { getRpcClient } from "../../rpc/rpc.js"; +import { deployWithStylusConstructor } from "../../extensions/stylus/write/deployWithStylusConstructor.js"; import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js"; import { sendTransaction } from "../../transaction/actions/send-transaction.js"; import { prepareTransaction } from "../../transaction/prepare-transaction.js"; @@ -17,7 +14,7 @@ import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js"; import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js"; import { ensureBytecodePrefix } from "../../utils/bytecode/prefix.js"; import { concatHex } from "../../utils/encoding/helpers/concat-hex.js"; -import { type Hex, isHex, toHex } from "../../utils/encoding/hex.js"; +import { type Hex, isHex } from "../../utils/encoding/hex.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { ClientAndChain } from "../../utils/types.js"; import type { Account } from "../../wallets/interfaces/wallet.js"; @@ -178,39 +175,12 @@ export async function deployContract( }), }); } else if (options.isStylus && options.constructorParams) { - const STYLUS_DEPLOYER = "0xCeCbA2F1dC234F70Dd89f2041029807F8D03A990"; - const stylusDeployer = getContract({ - address: STYLUS_DEPLOYER, + const deployTx = deployWithStylusConstructor({ + abi: options.abi, + bytecode: options.bytecode, chain: options.chain, client: options.client, - }); - - const constructorAbi = options.abi.find( - (abi) => abi.type === "constructor", - ) as AbiConstructor | undefined; - const constructorCalldata = (FN_SELECTOR + - encodeAbiParameters( - constructorAbi?.inputs || [], // Leave an empty array if there's no constructor - normalizeFunctionParams( - constructorAbi, - options.constructorParams, - ).slice(2), - )) as `${typeof FN_SELECTOR}${string}`; - - const rpcRequest = getRpcClient({ - ...options, - }); - const blockNumber = await eth_blockNumber(rpcRequest); - const salt = toHex(blockNumber, { - size: 32, - }); - - const deployTx = deploy({ - bytecode: options.bytecode, - contract: stylusDeployer, - initData: constructorCalldata, - initValue: 0n, - salt, + constructorParams: options.constructorParams, }); const receipt = await sendAndConfirmTransaction({ diff --git a/packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts b/packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts new file mode 100644 index 00000000000..ea5c60aebf3 --- /dev/null +++ b/packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts @@ -0,0 +1,70 @@ +import type { Abi, AbiConstructor } from "abitype"; +import type { Chain } from "../../../chains/types.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { getContract } from "../../../contract/contract.js"; +import { FN_SELECTOR } from "../../../extensions/stylus/__generated__/IStylusConstructor/write/stylus_constructor.js"; +import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js"; +import { normalizeFunctionParams } from "../../../utils/abi/normalizeFunctionParams.js"; +import { toHex } from "../../../utils/encoding/hex.js"; +import { deploy } from "../__generated__/IStylusDeployer/write/deploy.js"; + +const STYLUS_DEPLOYER = "0xcEcba2F1DC234f70Dd89F2041029807F8D03A990"; + +export type DeployWithStylusConstructorOptions = { + chain: Chain; + client: ThirdwebClient; + bytecode: `0x${string}`; + constructorParams: Record; + abi: Abi; +}; + +/** + * Deploy stylus contract with constructor params + * @param options - The options deploying contract with constructor + * @returns Prepared transaction to call stylus deployer + * @example + * ```ts + * import { deployWithStylusConstructor } from "thirdweb/stylus"; + * const transaction = deployWithStylusConstructor({ + * client, + * chain, + * bytecode, + * constructorParams, + * abi + * }); + * await sendTransaction({ transaction, account }); + * ``` + */ +export function deployWithStylusConstructor( + options: DeployWithStylusConstructorOptions, +) { + const { chain, client, constructorParams, abi, bytecode } = options; + const bytecodeHex = bytecode.startsWith("0x") + ? bytecode + : (`0x${bytecode}` as `0x${string}`); + + const stylusDeployer = getContract({ + address: STYLUS_DEPLOYER, + chain, + client, + }); + + const constructorAbi = abi.find((a) => a.type === "constructor") as + | AbiConstructor + | undefined; + + const normalized = normalizeFunctionParams(constructorAbi, constructorParams); + const constructorCalldata = (FN_SELECTOR + + encodeAbiParameters( + constructorAbi?.inputs || [], // Leave an empty array if there's no constructor + normalized, + ).slice(2)) as `${typeof FN_SELECTOR}${string}`; + + return deploy({ + bytecode: bytecodeHex, + contract: stylusDeployer, + initData: constructorCalldata, + initValue: 0n, + salt: toHex(0, { size: 32 }), + }); +} From 16cabef5b9c3df0fa693d5e6cfda8a955daab02f Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 4 Jul 2025 03:11:29 +0530 Subject: [PATCH 10/13] check prerequisites in cli --- .../src/cli/commands/stylus/builder.ts | 21 ++++++++++++ .../commands/stylus/check-prerequisites.ts | 34 +++++++++++++++++++ .../src/cli/commands/stylus/create.ts | 12 ++++++- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 packages/thirdweb/src/cli/commands/stylus/check-prerequisites.ts diff --git a/packages/thirdweb/src/cli/commands/stylus/builder.ts b/packages/thirdweb/src/cli/commands/stylus/builder.ts index b0f89bb6064..9f9e9a09341 100644 --- a/packages/thirdweb/src/cli/commands/stylus/builder.ts +++ b/packages/thirdweb/src/cli/commands/stylus/builder.ts @@ -7,11 +7,22 @@ import prompts from "prompts"; import { parse } from "toml"; import { createThirdwebClient } from "../../../client/client.js"; import { upload } from "../../../storage/upload.js"; +import { checkPrerequisites } from "./check-prerequisites.js"; const THIRDWEB_URL = "https://thirdweb.com"; export async function publishStylus(secretKey?: string) { const spinner = ora("Checking if this is a Stylus project...").start(); + + 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); const url = getUrl(uri, "publish").toString(); @@ -21,6 +32,16 @@ export async function publishStylus(secretKey?: string) { export async function deployStylus(secretKey?: string) { const spinner = ora("Checking if this is a Stylus project...").start(); + + 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); const url = getUrl(uri, "deploy").toString(); diff --git a/packages/thirdweb/src/cli/commands/stylus/check-prerequisites.ts b/packages/thirdweb/src/cli/commands/stylus/check-prerequisites.ts new file mode 100644 index 00000000000..f548f2bc0ce --- /dev/null +++ b/packages/thirdweb/src/cli/commands/stylus/check-prerequisites.ts @@ -0,0 +1,34 @@ +import { spawnSync } from "node:child_process"; +import type { Ora } from "ora"; + +export function checkPrerequisites( + spinner: Ora, + cmd: string, + args: string[] = ["--version"], + name = cmd, +) { + try { + const res = spawnSync(cmd, args, { encoding: "utf-8" }); + + if (res.error && (res.error as NodeJS.ErrnoException).code === "ENOENT") { + spinner.fail( + `Error: ${name} is not installed or not in PATH.\n` + + `Install it and try again.`, + ); + process.exit(1); + } + + if (res.status !== 0) { + spinner.fail( + `Error: ${name} returned a non-zero exit code (${res.status}).`, + ); + process.exit(1); + } + + const ver = res.stdout.trim().split("\n")[0]; + spinner.succeed(`${name} detected (${ver}).`); + } catch (err) { + spinner.fail(`Error while checking ${name}: ${err}`); + process.exit(1); + } +} diff --git a/packages/thirdweb/src/cli/commands/stylus/create.ts b/packages/thirdweb/src/cli/commands/stylus/create.ts index 07762e994e5..107c8333bd4 100644 --- a/packages/thirdweb/src/cli/commands/stylus/create.ts +++ b/packages/thirdweb/src/cli/commands/stylus/create.ts @@ -1,10 +1,20 @@ import { spawnSync } from "node:child_process"; import ora from "ora"; import prompts from "prompts"; +import { checkPrerequisites } from "./check-prerequisites.js"; export async function createStylusProject() { const spinner = ora(); + 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"]); if (cargoCheck.status !== 0) { @@ -23,7 +33,7 @@ export async function createStylusProject() { } spinner.succeed("Stylus installed."); - spawnSync("rustup", ["default", "1.83"], { + spawnSync("rustup", ["default", "1.87"], { stdio: "inherit", }); spawnSync("rustup", ["target", "add", "wasm32-unknown-unknown"], { From 58fda78f82d6a5e6de01842de6e2f271267edfb1 Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 4 Jul 2025 07:10:27 +0530 Subject: [PATCH 11/13] activate codehash, import metadata --- .../generate/abis/stylus/IArbWasm.json | 3 +- .../contract/deployment/deploy-with-abi.ts | 23 ++++ .../IArbWasm/read/codehashVersion.ts | 126 ++++++++++++++++++ .../stylus/write/activateStylusContract.ts | 2 +- .../stylus/write/isContractActivated.ts | 102 ++++++++++++++ 5 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 packages/thirdweb/src/extensions/stylus/__generated__/IArbWasm/read/codehashVersion.ts create mode 100644 packages/thirdweb/src/extensions/stylus/write/isContractActivated.ts diff --git a/packages/thirdweb/scripts/generate/abis/stylus/IArbWasm.json b/packages/thirdweb/scripts/generate/abis/stylus/IArbWasm.json index 44e1a961f48..8d7e7f4c76d 100644 --- a/packages/thirdweb/scripts/generate/abis/stylus/IArbWasm.json +++ b/packages/thirdweb/scripts/generate/abis/stylus/IArbWasm.json @@ -1,3 +1,4 @@ [ - "function activateProgram(address program) returns (uint16,uint256)" + "function activateProgram(address program) returns (uint16,uint256)", + "function codehashVersion(bytes32 codehash) external view returns (uint16 version)" ] \ No newline at end of file diff --git a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts index 407074b662a..8a88e91df96 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts @@ -3,6 +3,7 @@ import { parseEventLogs } from "../../event/actions/parse-logs.js"; import { contractDeployedEvent } from "../../extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.js"; import { activateStylusContract } from "../../extensions/stylus/write/activateStylusContract.js"; import { deployWithStylusConstructor } from "../../extensions/stylus/write/deployWithStylusConstructor.js"; +import { isContractActivated } from "../../extensions/stylus/write/isContractActivated.js"; import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js"; import { sendTransaction } from "../../transaction/actions/send-transaction.js"; import { prepareTransaction } from "../../transaction/prepare-transaction.js"; @@ -175,6 +176,28 @@ export async function deployContract( }), }); } else if (options.isStylus && options.constructorParams) { + const isActivated = await isContractActivated(options); + + if (!isActivated) { + // one time deploy to activate the new codehash + const impl = await deployContract({ + ...options, + abi: [], + constructorParams: undefined, + }); + + // fetch metadata + await fetch( + `https://contract.thirdweb.com/metadata/${options.chain.id}/${impl}`, + { + headers: { + "Content-Type": "application/json", + }, + method: "GET", + }, + ); + } + const deployTx = deployWithStylusConstructor({ abi: options.abi, bytecode: options.bytecode, diff --git a/packages/thirdweb/src/extensions/stylus/__generated__/IArbWasm/read/codehashVersion.ts b/packages/thirdweb/src/extensions/stylus/__generated__/IArbWasm/read/codehashVersion.ts new file mode 100644 index 00000000000..4dec920df54 --- /dev/null +++ b/packages/thirdweb/src/extensions/stylus/__generated__/IArbWasm/read/codehashVersion.ts @@ -0,0 +1,126 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import { decodeAbiParameters } from "viem"; +import { readContract } from "../../../../../transaction/read-contract.js"; +import type { BaseTransactionOptions } from "../../../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; +import type { Hex } from "../../../../../utils/encoding/hex.js"; + +/** + * Represents the parameters for the "codehashVersion" function. + */ +export type CodehashVersionParams = { + codehash: AbiParameterToPrimitiveType<{ type: "bytes32"; name: "codehash" }>; +}; + +export const FN_SELECTOR = "0xd70c0ca7" as const; +const FN_INPUTS = [ + { + name: "codehash", + type: "bytes32", + }, +] as const; +const FN_OUTPUTS = [ + { + name: "version", + type: "uint16", + }, +] as const; + +/** + * Checks if the `codehashVersion` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `codehashVersion` method is supported. + * @extension STYLUS + * @example + * ```ts + * import { isCodehashVersionSupported } from "thirdweb/extensions/stylus"; + * const supported = isCodehashVersionSupported(["0x..."]); + * ``` + */ +export function isCodehashVersionSupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "codehashVersion" function. + * @param options - The options for the codehashVersion function. + * @returns The encoded ABI parameters. + * @extension STYLUS + * @example + * ```ts + * import { encodeCodehashVersionParams } from "thirdweb/extensions/stylus"; + * const result = encodeCodehashVersionParams({ + * codehash: ..., + * }); + * ``` + */ +export function encodeCodehashVersionParams(options: CodehashVersionParams) { + return encodeAbiParameters(FN_INPUTS, [options.codehash]); +} + +/** + * Encodes the "codehashVersion" function into a Hex string with its parameters. + * @param options - The options for the codehashVersion function. + * @returns The encoded hexadecimal string. + * @extension STYLUS + * @example + * ```ts + * import { encodeCodehashVersion } from "thirdweb/extensions/stylus"; + * const result = encodeCodehashVersion({ + * codehash: ..., + * }); + * ``` + */ +export function encodeCodehashVersion(options: CodehashVersionParams) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeCodehashVersionParams(options).slice( + 2, + )) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Decodes the result of the codehashVersion function call. + * @param result - The hexadecimal result to decode. + * @returns The decoded result as per the FN_OUTPUTS definition. + * @extension STYLUS + * @example + * ```ts + * import { decodeCodehashVersionResult } from "thirdweb/extensions/stylus"; + * const result = decodeCodehashVersionResultResult("..."); + * ``` + */ +export function decodeCodehashVersionResult(result: Hex) { + return decodeAbiParameters(FN_OUTPUTS, result)[0]; +} + +/** + * Calls the "codehashVersion" function on the contract. + * @param options - The options for the codehashVersion function. + * @returns The parsed result of the function call. + * @extension STYLUS + * @example + * ```ts + * import { codehashVersion } from "thirdweb/extensions/stylus"; + * + * const result = await codehashVersion({ + * contract, + * codehash: ..., + * }); + * + * ``` + */ +export async function codehashVersion( + options: BaseTransactionOptions, +) { + return readContract({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + params: [options.codehash], + }); +} diff --git a/packages/thirdweb/src/extensions/stylus/write/activateStylusContract.ts b/packages/thirdweb/src/extensions/stylus/write/activateStylusContract.ts index f7593d03d6a..30e5fb58bf8 100644 --- a/packages/thirdweb/src/extensions/stylus/write/activateStylusContract.ts +++ b/packages/thirdweb/src/extensions/stylus/write/activateStylusContract.ts @@ -12,7 +12,7 @@ import { encode } from "../../../transaction/actions/encode.js"; import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js"; import { activateProgram } from "../__generated__/IArbWasm/write/activateProgram.js"; -const ARB_WASM_ADDRESS = "0x0000000000000000000000000000000000000071"; +export const ARB_WASM_ADDRESS = "0x0000000000000000000000000000000000000071"; export type ActivateStylusContractOptions = { chain: Chain; diff --git a/packages/thirdweb/src/extensions/stylus/write/isContractActivated.ts b/packages/thirdweb/src/extensions/stylus/write/isContractActivated.ts new file mode 100644 index 00000000000..06d0a1c932f --- /dev/null +++ b/packages/thirdweb/src/extensions/stylus/write/isContractActivated.ts @@ -0,0 +1,102 @@ +import { keccak256 } from "viem"; +import type { Chain } from "../../../chains/types.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { getContract } from "../../../contract/contract.js"; +import { codehashVersion } from "../__generated__/IArbWasm/read/codehashVersion.js"; +import { ARB_WASM_ADDRESS } from "./activateStylusContract.js"; + +export type IsContractActivatedOptions = { + chain: Chain; + client: ThirdwebClient; + bytecode: `0x${string}`; +}; + +export async function isContractActivated( + options: IsContractActivatedOptions, +): Promise { + const { chain, client, bytecode } = options; + const arbWasmPrecompile = getContract({ + address: ARB_WASM_ADDRESS, + chain, + client, + }); + + try { + await codehashVersion({ + codehash: keccak256(extractRuntimeBytecode(bytecode)), + contract: arbWasmPrecompile, + }); + return true; + } catch { + return false; + } +} + +function extractRuntimeBytecode(deployInput: string | Uint8Array): Uint8Array { + // normalise input + const deploy: Uint8Array = + typeof deployInput === "string" ? hexToBytes(deployInput) : deployInput; + + // the contract_deployment_calldata helper emits 42-byte prelude + 1-byte version => 43 bytes total + // ref: https://github.com/OffchainLabs/cargo-stylus/blob/main/main/src/deploy/mod.rs#L305 + const PRELUDE_LEN = 42; + const TOTAL_FIXED = PRELUDE_LEN + 1; // +1 version byte + + if (deploy.length < TOTAL_FIXED) { + throw new Error("Deployment bytecode too short"); + } + if (deploy[0] !== 0x7f) { + throw new Error( + "Missing 0x7f PUSH32 - not produced by contract_deployment_calldata", + ); + } + + // read length + const codeLenBytes = deploy.slice(1, 33); + let codeLen = 0n; + for (const b of codeLenBytes) codeLen = (codeLen << 8n) | BigInt(b); + + if (codeLen > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error("Runtime code length exceeds JS safe integer range"); + } + + // pattern sanity-check + const EXPECTED = [ + 0x80, // DUP1 + 0x60, + 0x2b, // PUSH1 0x2b (42 + 1) + 0x60, + 0x00, // PUSH1 0 + 0x39, // CODECOPY + 0x60, + 0x00, // PUSH1 0 + 0xf3, // RETURN + 0x00, // version + ] as const; + for (let i = 0; i < EXPECTED.length; i++) { + if (deploy[33 + i] !== EXPECTED[i]) { + throw new Error("Prelude bytes do not match expected pattern"); + } + } + + // slice out runtime code + const start = TOTAL_FIXED; + const end = start + Number(codeLen); + if (deploy.length < end) { + throw new Error("Deployment bytecode truncated - runtime code incomplete"); + } + + return deploy.slice(start, end); +} + +function hexToBytes(hex: string): Uint8Array { + const normalized = hex.startsWith("0x") ? hex.slice(2) : hex; + if (normalized.length % 2 !== 0) { + throw new Error("Hex string must have an even length"); + } + const bytes = new Uint8Array(normalized.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(normalized.slice(i * 2, i * 2 + 2), 16); + } + return bytes; +} From 2479b4cb47729c443fe29240b11d5447093bf0fd Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 4 Jul 2025 17:03:28 +0530 Subject: [PATCH 12/13] fix error msg --- packages/thirdweb/src/contract/deployment/deploy-with-abi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts index 8a88e91df96..0505509c2ff 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts @@ -231,7 +231,7 @@ export async function deployContract( address = receipt.contractAddress; if (!address) { throw new Error( - `Could not find deployed contract address in transaction: $receipt.transactionHash`, + `Could not find deployed contract address in transaction: ${receipt.transactionHash}`, ); } } From 62af2fc98477a21efa0c3c9c4bdf7da578ff0a2a Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 4 Jul 2025 20:54:46 +0530 Subject: [PATCH 13/13] exports --- packages/thirdweb/src/exports/extensions/stylus.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/thirdweb/src/exports/extensions/stylus.ts b/packages/thirdweb/src/exports/extensions/stylus.ts index 6579ad8c0e4..ee8b364bf65 100644 --- a/packages/thirdweb/src/exports/extensions/stylus.ts +++ b/packages/thirdweb/src/exports/extensions/stylus.ts @@ -5,3 +5,11 @@ export { type ActivateStylusContractOptions, activateStylusContract, } from "../../extensions/stylus/write/activateStylusContract.js"; +export { + type DeployWithStylusConstructorOptions, + deployWithStylusConstructor, +} from "../../extensions/stylus/write/deployWithStylusConstructor.js"; +export { + type IsContractActivatedOptions, + isContractActivated, +} from "../../extensions/stylus/write/isContractActivated.js";