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

Merged
merged 19 commits into from
Jul 4, 2025
Merged
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 = "[email protected]: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 =
"[email protected]: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 =
"[email protected]: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 =
"[email protected]: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 =
"[email protected]: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 =
"[email protected]: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
51 changes: 51 additions & 0 deletions 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 @@ export async function deployContract(
to: info.create2FactoryAddress,
}),
});
} 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,
chain: options.chain,
client: options.client,
constructorParams: options.constructorParams,
});

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({
Expand Down
8 changes: 8 additions & 0 deletions packages/thirdweb/src/exports/extensions/stylus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ export {
type ActivateStylusContractOptions,
activateStylusContract,
} from "../../extensions/stylus/write/activateStylusContract.js";
export {
type DeployWithStylusConstructorOptions,
deployWithStylusConstructor,
} from "../../extensions/stylus/write/deployWithStylusConstructor.js";
Comment on lines +8 to +11
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify TSDoc compliance for the new export.

The export structure follows the established pattern correctly. However, per the coding guidelines, every public symbol must have comprehensive TSDoc with at least one @example block and custom annotation tags.


🏁 Script executed:

#!/bin/bash
# Description: Check if deployWithStylusConstructor has proper TSDoc documentation
# Expected: TSDoc comments with @example block and annotation tags

rg -A 20 "export.*deployWithStylusConstructor" --type ts

Length of output: 2307


🏁 Script executed:

#!/bin/bash
# Check for existing TSDoc above the deployWithStylusConstructor function
rg -B5 -n "export function deployWithStylusConstructor" packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts

Length of output: 275


🏁 Script executed:

#!/bin/bash
# Display TSDoc for deployWithStylusConstructor function
echo "===== deployWithStylusConstructor.ts (first 50 lines) ====="
head -n 50 packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts

# Display exports file to check for file-level TSDoc
echo "===== stylus.ts exports file (first 20 lines) ====="
head -n 20 packages/thirdweb/src/exports/extensions/stylus.ts

Length of output: 2570


Add missing custom annotation tag to deployWithStylusConstructor TSDoc

The deployWithStylusConstructor function already has a description, @param, @returns and a working @example, but per our SDK docs guidelines every public symbol also needs a custom annotation tag (e.g. @beta or @experimental).

• In packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts, add an appropriate annotation to the existing TSDoc block above export function deployWithStylusConstructor(...).

/**
 * Deploy stylus contract with constructor params
 * @beta               ← add one of: @beta | @experimental | @internal
 * @param options – The options for deploying a contract with constructor
 * @returns Prepared transaction to call stylus deployer
 * @example
 * …
 */
export function deployWithStylusConstructor()

• (Optional) Enhance the file-level TSDoc in packages/thirdweb/src/exports/extensions/stylus.ts to describe the module’s purpose more thoroughly.

🤖 Prompt for AI Agents
In packages/thirdweb/src/extensions/stylus/write/deployWithStylusConstructor.ts
around the export function deployWithStylusConstructor, add a custom annotation
tag such as @beta, @experimental, or @internal to the existing TSDoc block above
the function to comply with SDK docs guidelines. Optionally, in
packages/thirdweb/src/exports/extensions/stylus.ts, enhance the file-level TSDoc
comment to provide a more detailed description of the module's purpose.

export {
type IsContractActivatedOptions,
isContractActivated,
} from "../../extensions/stylus/write/isContractActivated.js";
Comment on lines +12 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify TSDoc compliance for the new export.

The export structure is consistent with the existing pattern. Ensure the source function has proper TSDoc documentation as required.


🏁 Script executed:

#!/bin/bash
# Description: Check if isContractActivated has proper TSDoc documentation
# Expected: TSDoc comments with @example block and annotation tags

rg -A 20 "export.*isContractActivated" --type ts

Length of output: 1947


Add comprehensive TSDoc for isContractActivated export

The isContractActivated function in
packages/thirdweb/src/extensions/stylus/write/isContractActivated.ts
is currently missing a TSDoc comment with an @example block and the required custom annotation tags (@beta, @internal, @experimental, as applicable). Please add a documentation block immediately above the function declaration, for example:

/**
 * Checks whether a given contract’s bytecode has been activated.
 *
 * @remarks
 * This uses the ArbWasm precompile contract to verify the runtime bytecode version.
 *
 * @param options - Configuration options for the activation check.
 * @param options.chain - The target blockchain.
 * @param options.client - The RPC client instance.
 * @param options.bytecode - The contract’s runtime bytecode.
 *
 * @returns A promise that resolves to `true` if activated, otherwise `false`.
 *
 * @example
 * ```ts
 * const activated = await isContractActivated({
 *   chain: "arbitrum",
 *   client: myRpcClient,
 *   bytecode: myContractBytecode,
 * });
 * console.log(activated); // true
 * ```
 *
 * @beta
 */
export async function isContractActivated(
  options: IsContractActivatedOptions,
): Promise<boolean> {  }
  • Ensure the TSDoc compiles without errors.
  • Include at least one valid @example block.
  • Add the appropriate custom annotation tag.
🤖 Prompt for AI Agents
In packages/thirdweb/src/extensions/stylus/write/isContractActivated.ts around
lines 12 to 15, the isContractActivated function lacks a comprehensive TSDoc
comment. Add a detailed TSDoc block immediately above the function declaration
that describes its purpose, parameters, and return value. Include an @example
block demonstrating usage and add the appropriate custom annotation tag such as
@beta. Ensure the TSDoc is well-formed and compiles without errors.

Loading
Loading