Skip to content

Stylus CLI updates #7502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"function stylus_constructor()"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"function deploy(bytes calldata bytecode,bytes calldata initData,uint256 initValue,bytes32 salt) public payable returns (address)",
"event ContractDeployed(address deployedContract)"
]
66 changes: 65 additions & 1 deletion packages/thirdweb/src/cli/commands/stylus/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -95,6 +116,20 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
}
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(/======= <stdin>:/g).filter(Boolean);
const contractNames = extractContractNamesFromExportAbi(abiContent);
Expand Down Expand Up @@ -150,11 +185,19 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
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: JSON.parse(cleanedAbi),
abi: abiArray,
devdoc: {},
userdoc: {},
},
Expand Down Expand Up @@ -234,3 +277,24 @@ function extractBytecode(rawOutput: string): string {
}
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, name, type };
});

return { inputs, stateMutability: mutability, type: "constructor" };
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
77 changes: 66 additions & 11 deletions packages/thirdweb/src/cli/commands/stylus/create.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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"], {
Expand All @@ -43,32 +53,77 @@ export async function createStylusProject() {
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add explicit type annotation to comply with coding guidelines.

The variable declaration violates the coding guidelines which require explicit types and avoiding any. The implicit any type should be replaced with the proper return type.

-  let newProject;
+  let newProject: ReturnType<typeof spawnSync>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let newProject;
let newProject: ReturnType<typeof spawnSync>;
🤖 Prompt for AI Agents
In packages/thirdweb/src/cli/commands/stylus/create.ts at line 54, the variable
newProject is declared without an explicit type, causing an implicit any type
violation. Add an explicit type annotation to newProject that matches the
expected return type of the value it will hold, ensuring compliance with coding
guidelines that forbid implicit any types.

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",
});
} 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",
});
if (clone.status !== 0) {
spinner.fail("Failed to create Stylus project.");
process.exit(1);
}
} 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);
}

spinner.succeed("Project created successfully.");
Expand Down
53 changes: 52 additions & 1 deletion packages/thirdweb/src/contract/deployment/deploy-with-abi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Abi, AbiConstructor } from "abitype";
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";
Expand Down Expand Up @@ -171,6 +175,53 @@
to: info.create2FactoryAddress,
}),
});
} else if (options.isStylus && options.constructorParams) {
const isActivated = await isContractActivated(options);

Check warning on line 179 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L179

Added line #L179 was not covered by tests

if (!isActivated) {

Check warning on line 181 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L181

Added line #L181 was not covered by tests
// one time deploy to activate the new codehash
const impl = await deployContract({
...options,
abi: [],
constructorParams: undefined,
});

Check warning on line 187 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L183-L187

Added lines #L183 - L187 were not covered by tests

// fetch metadata
await fetch(
`https://contract.thirdweb.com/metadata/${options.chain.id}/${impl}`,
{
headers: {
"Content-Type": "application/json",
},
method: "GET",
},
);
}

Check warning on line 199 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L190-L199

Added lines #L190 - L199 were not covered by tests

const deployTx = deployWithStylusConstructor({
abi: options.abi,
bytecode: options.bytecode,
chain: options.chain,
client: options.client,
constructorParams: options.constructorParams,
});

Check warning on line 207 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L201-L207

Added lines #L201 - L207 were not covered by tests

const receipt = await sendAndConfirmTransaction({
account: options.account,
transaction: deployTx,
});

Check warning on line 212 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L209-L212

Added lines #L209 - L212 were not covered by tests

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;

Check warning on line 224 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L214-L224

Added lines #L214 - L224 were not covered by tests
} else {
const deployTx = prepareDirectDeployTransaction(options);
const receipt = await sendAndConfirmTransaction({
Expand All @@ -180,7 +231,7 @@
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`,

Check warning on line 234 in packages/thirdweb/src/contract/deployment/deploy-with-abi.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/contract/deployment/deploy-with-abi.ts#L234

Added line #L234 was not covered by tests
);
}
}
Expand Down
Loading
Loading