Skip to content

Commit 8def4f3

Browse files
authored
Stylus CLI updates (#7502)
1 parent d92ab6f commit 8def4f3

File tree

15 files changed

+784
-14
lines changed

15 files changed

+784
-14
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[
2-
"function activateProgram(address program) returns (uint16,uint256)"
2+
"function activateProgram(address program) returns (uint16,uint256)",
3+
"function codehashVersion(bytes32 codehash) external view returns (uint16 version)"
34
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[
2+
"function stylus_constructor()"
3+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
"function deploy(bytes calldata bytecode,bytes calldata initData,uint256 initValue,bytes32 salt) public payable returns (address)",
3+
"event ContractDeployed(address deployedContract)"
4+
]

packages/thirdweb/src/cli/commands/stylus/builder.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,22 @@ import prompts from "prompts";
77
import { parse } from "toml";
88
import { createThirdwebClient } from "../../../client/client.js";
99
import { upload } from "../../../storage/upload.js";
10+
import { checkPrerequisites } from "./check-prerequisites.js";
1011

1112
const THIRDWEB_URL = "https://thirdweb.com";
1213

1314
export async function publishStylus(secretKey?: string) {
1415
const spinner = ora("Checking if this is a Stylus project...").start();
16+
17+
checkPrerequisites(spinner, "cargo", ["--version"], "Rust (cargo)");
18+
checkPrerequisites(spinner, "rustc", ["--version"], "Rust compiler (rustc)");
19+
checkPrerequisites(
20+
spinner,
21+
"solc",
22+
["--version"],
23+
"Solidity compiler (solc)",
24+
);
25+
1526
const uri = await buildStylus(spinner, secretKey);
1627

1728
const url = getUrl(uri, "publish").toString();
@@ -21,6 +32,16 @@ export async function publishStylus(secretKey?: string) {
2132

2233
export async function deployStylus(secretKey?: string) {
2334
const spinner = ora("Checking if this is a Stylus project...").start();
35+
36+
checkPrerequisites(spinner, "cargo", ["--version"], "Rust (cargo)");
37+
checkPrerequisites(spinner, "rustc", ["--version"], "Rust compiler (rustc)");
38+
checkPrerequisites(
39+
spinner,
40+
"solc",
41+
["--version"],
42+
"Solidity compiler (solc)",
43+
);
44+
2445
const uri = await buildStylus(spinner, secretKey);
2546

2647
const url = getUrl(uri, "deploy").toString();
@@ -95,6 +116,20 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
95116
}
96117
spinner.succeed("ABI generated.");
97118

119+
// Step 3.5: detect the constructor
120+
spinner.start("Detecting constructor…");
121+
const constructorResult = spawnSync("cargo", ["stylus", "constructor"], {
122+
encoding: "utf-8",
123+
});
124+
125+
if (constructorResult.status !== 0) {
126+
spinner.fail("Failed to get constructor signature.");
127+
process.exit(1);
128+
}
129+
130+
const constructorSigRaw = constructorResult.stdout.trim(); // e.g. "constructor(address owner)"
131+
spinner.succeed(`Constructor found: ${constructorSigRaw || "none"}`);
132+
98133
// Step 4: Process the output
99134
const parts = abiContent.split(/======= <stdin>:/g).filter(Boolean);
100135
const contractNames = extractContractNamesFromExportAbi(abiContent);
@@ -150,11 +185,19 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
150185
process.exit(1);
151186
}
152187

188+
// biome-ignore lint/suspicious/noExplicitAny: <>
189+
const abiArray: any[] = JSON.parse(cleanedAbi);
190+
191+
const constructorAbi = constructorSigToAbi(constructorSigRaw);
192+
if (constructorAbi && !abiArray.some((e) => e.type === "constructor")) {
193+
abiArray.unshift(constructorAbi); // put it at the top for readability
194+
}
195+
153196
const metadata = {
154197
compiler: {},
155198
language: "rust",
156199
output: {
157-
abi: JSON.parse(cleanedAbi),
200+
abi: abiArray,
158201
devdoc: {},
159202
userdoc: {},
160203
},
@@ -234,3 +277,24 @@ function extractBytecode(rawOutput: string): string {
234277
}
235278
return rawOutput.slice(hexStart).trim();
236279
}
280+
281+
function constructorSigToAbi(sig: string) {
282+
if (!sig || !sig.startsWith("constructor")) return undefined;
283+
284+
const sigClean = sig
285+
.replace(/^constructor\s*\(?/, "")
286+
.replace(/\)\s*$/, "")
287+
.replace(/\s+(payable|nonpayable)\s*$/, "");
288+
289+
const mutability = sig.includes("payable") ? "payable" : "nonpayable";
290+
291+
const inputs =
292+
sigClean === ""
293+
? []
294+
: sigClean.split(",").map((p) => {
295+
const [type, name = ""] = p.trim().split(/\s+/);
296+
return { internalType: type, name, type };
297+
});
298+
299+
return { inputs, stateMutability: mutability, type: "constructor" };
300+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { spawnSync } from "node:child_process";
2+
import type { Ora } from "ora";
3+
4+
export function checkPrerequisites(
5+
spinner: Ora,
6+
cmd: string,
7+
args: string[] = ["--version"],
8+
name = cmd,
9+
) {
10+
try {
11+
const res = spawnSync(cmd, args, { encoding: "utf-8" });
12+
13+
if (res.error && (res.error as NodeJS.ErrnoException).code === "ENOENT") {
14+
spinner.fail(
15+
`Error: ${name} is not installed or not in PATH.\n` +
16+
`Install it and try again.`,
17+
);
18+
process.exit(1);
19+
}
20+
21+
if (res.status !== 0) {
22+
spinner.fail(
23+
`Error: ${name} returned a non-zero exit code (${res.status}).`,
24+
);
25+
process.exit(1);
26+
}
27+
28+
const ver = res.stdout.trim().split("\n")[0];
29+
spinner.succeed(`${name} detected (${ver}).`);
30+
} catch (err) {
31+
spinner.fail(`Error while checking ${name}: ${err}`);
32+
process.exit(1);
33+
}
34+
}

packages/thirdweb/src/cli/commands/stylus/create.ts

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { spawnSync } from "node:child_process";
22
import ora from "ora";
33
import prompts from "prompts";
4+
import { checkPrerequisites } from "./check-prerequisites.js";
45

56
export async function createStylusProject() {
67
const spinner = ora();
78

9+
checkPrerequisites(spinner, "cargo", ["--version"], "Rust (cargo)");
10+
checkPrerequisites(spinner, "rustc", ["--version"], "Rust compiler (rustc)");
11+
checkPrerequisites(
12+
spinner,
13+
"solc",
14+
["--version"],
15+
"Solidity compiler (solc)",
16+
);
17+
818
// Step 1: Ensure cargo is installed
919
const cargoCheck = spawnSync("cargo", ["--version"]);
1020
if (cargoCheck.status !== 0) {
@@ -23,7 +33,7 @@ export async function createStylusProject() {
2333
}
2434
spinner.succeed("Stylus installed.");
2535

26-
spawnSync("rustup", ["default", "1.83"], {
36+
spawnSync("rustup", ["default", "1.87"], {
2737
stdio: "inherit",
2838
});
2939
spawnSync("rustup", ["target", "add", "wasm32-unknown-unknown"], {
@@ -43,32 +53,77 @@ export async function createStylusProject() {
4353
choices: [
4454
{ title: "Default", value: "default" },
4555
{ title: "ERC20", value: "erc20" },
56+
{ title: "ERC721", value: "erc721" },
57+
{ title: "ERC1155", value: "erc1155" },
58+
{ title: "Airdrop ERC20", value: "airdrop20" },
59+
{ title: "Airdrop ERC721", value: "airdrop721" },
60+
{ title: "Airdrop ERC1155", value: "airdrop1155" },
4661
],
4762
message: "Select a template:",
4863
name: "projectType",
4964
type: "select",
5065
});
5166

5267
// Step 5: Create the project
68+
// biome-ignore lint/suspicious/noImplicitAnyLet: <>
69+
let newProject;
5370
if (projectType === "default") {
5471
spinner.start(`Creating new Stylus project: ${projectName}...`);
55-
const newProject = spawnSync("cargo", ["stylus", "new", projectName], {
72+
newProject = spawnSync("cargo", ["stylus", "new", projectName], {
5673
stdio: "inherit",
5774
});
58-
if (newProject.status !== 0) {
59-
spinner.fail("Failed to create Stylus project.");
60-
process.exit(1);
61-
}
6275
} else if (projectType === "erc20") {
6376
const repoUrl = "[email protected]:thirdweb-example/stylus-erc20-template.git";
6477
spinner.start(`Creating new ERC20 Stylus project: ${projectName}...`);
65-
const clone = spawnSync("git", ["clone", repoUrl, projectName], {
78+
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
79+
stdio: "inherit",
80+
});
81+
} else if (projectType === "erc721") {
82+
const repoUrl =
83+
"[email protected]:thirdweb-example/stylus-erc721-template.git";
84+
spinner.start(`Creating new ERC721 Stylus project: ${projectName}...`);
85+
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
86+
stdio: "inherit",
87+
});
88+
} else if (projectType === "erc1155") {
89+
const repoUrl =
90+
"[email protected]:thirdweb-example/stylus-erc1155-template.git";
91+
spinner.start(`Creating new ERC1155 Stylus project: ${projectName}...`);
92+
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
93+
stdio: "inherit",
94+
});
95+
} else if (projectType === "airdrop20") {
96+
const repoUrl =
97+
"[email protected]:thirdweb-example/stylus-airdrop-erc20-template.git";
98+
spinner.start(
99+
`Creating new Airdrop ERC20 Stylus project: ${projectName}...`,
100+
);
101+
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
66102
stdio: "inherit",
67103
});
68-
if (clone.status !== 0) {
69-
spinner.fail("Failed to create Stylus project.");
70-
process.exit(1);
71-
}
104+
} else if (projectType === "airdrop721") {
105+
const repoUrl =
106+
"[email protected]:thirdweb-example/stylus-airdrop-erc721-template.git";
107+
spinner.start(
108+
`Creating new Airdrop ERC721 Stylus project: ${projectName}...`,
109+
);
110+
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
111+
stdio: "inherit",
112+
});
113+
} else if (projectType === "airdrop1155") {
114+
const repoUrl =
115+
"[email protected]:thirdweb-example/stylus-airdrop-erc1155-template.git";
116+
spinner.start(
117+
`Creating new Airdrop ERC1155 Stylus project: ${projectName}...`,
118+
);
119+
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
120+
stdio: "inherit",
121+
});
122+
}
123+
124+
if (!newProject || newProject.status !== 0) {
125+
spinner.fail("Failed to create Stylus project.");
126+
process.exit(1);
72127
}
73128

74129
spinner.succeed("Project created successfully.");

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Abi, AbiConstructor } from "abitype";
2+
import { parseEventLogs } from "../../event/actions/parse-logs.js";
3+
import { contractDeployedEvent } from "../../extensions/stylus/__generated__/IStylusDeployer/events/ContractDeployed.js";
24
import { activateStylusContract } from "../../extensions/stylus/write/activateStylusContract.js";
5+
import { deployWithStylusConstructor } from "../../extensions/stylus/write/deployWithStylusConstructor.js";
6+
import { isContractActivated } from "../../extensions/stylus/write/isContractActivated.js";
37
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
48
import { sendTransaction } from "../../transaction/actions/send-transaction.js";
59
import { prepareTransaction } from "../../transaction/prepare-transaction.js";
@@ -171,6 +175,53 @@ export async function deployContract(
171175
to: info.create2FactoryAddress,
172176
}),
173177
});
178+
} else if (options.isStylus && options.constructorParams) {
179+
const isActivated = await isContractActivated(options);
180+
181+
if (!isActivated) {
182+
// one time deploy to activate the new codehash
183+
const impl = await deployContract({
184+
...options,
185+
abi: [],
186+
constructorParams: undefined,
187+
});
188+
189+
// fetch metadata
190+
await fetch(
191+
`https://contract.thirdweb.com/metadata/${options.chain.id}/${impl}`,
192+
{
193+
headers: {
194+
"Content-Type": "application/json",
195+
},
196+
method: "GET",
197+
},
198+
);
199+
}
200+
201+
const deployTx = deployWithStylusConstructor({
202+
abi: options.abi,
203+
bytecode: options.bytecode,
204+
chain: options.chain,
205+
client: options.client,
206+
constructorParams: options.constructorParams,
207+
});
208+
209+
const receipt = await sendAndConfirmTransaction({
210+
account: options.account,
211+
transaction: deployTx,
212+
});
213+
214+
const deployEvent = contractDeployedEvent();
215+
const decodedEvent = parseEventLogs({
216+
events: [deployEvent],
217+
logs: receipt.logs,
218+
});
219+
if (decodedEvent.length === 0 || !decodedEvent[0]) {
220+
throw new Error(
221+
`No ContractDeployed event found in transaction: ${receipt.transactionHash}`,
222+
);
223+
}
224+
address = decodedEvent[0]?.args.deployedContract;
174225
} else {
175226
const deployTx = prepareDirectDeployTransaction(options);
176227
const receipt = await sendAndConfirmTransaction({

packages/thirdweb/src/exports/extensions/stylus.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,11 @@ export {
55
type ActivateStylusContractOptions,
66
activateStylusContract,
77
} from "../../extensions/stylus/write/activateStylusContract.js";
8+
export {
9+
type DeployWithStylusConstructorOptions,
10+
deployWithStylusConstructor,
11+
} from "../../extensions/stylus/write/deployWithStylusConstructor.js";
12+
export {
13+
type IsContractActivatedOptions,
14+
isContractActivated,
15+
} from "../../extensions/stylus/write/isContractActivated.js";

0 commit comments

Comments
 (0)