Skip to content

Commit b49c191

Browse files
authored
Support stylus contracts publish and deploy (#6495)
1 parent 06f0765 commit b49c191

File tree

19 files changed

+867
-101
lines changed

19 files changed

+867
-101
lines changed

.changeset/sharp-bugs-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Support Stylus contracts in CLI and SDK

packages/thirdweb/package.json

Lines changed: 25 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -137,66 +137,26 @@
137137
},
138138
"typesVersions": {
139139
"*": {
140-
"adapters/*": [
141-
"./dist/types/exports/adapters/*.d.ts"
142-
],
143-
"auth": [
144-
"./dist/types/exports/auth.d.ts"
145-
],
146-
"chains": [
147-
"./dist/types/exports/chains.d.ts"
148-
],
149-
"contract": [
150-
"./dist/types/exports/contract.d.ts"
151-
],
152-
"deploys": [
153-
"./dist/types/exports/deploys.d.ts"
154-
],
155-
"event": [
156-
"./dist/types/exports/event.d.ts"
157-
],
158-
"extensions/*": [
159-
"./dist/types/exports/extensions/*.d.ts"
160-
],
161-
"pay": [
162-
"./dist/types/exports/pay.d.ts"
163-
],
164-
"react": [
165-
"./dist/types/exports/react.d.ts"
166-
],
167-
"react-native": [
168-
"./dist/types/exports/react-native.d.ts"
169-
],
170-
"rpc": [
171-
"./dist/types/exports/rpc.d.ts"
172-
],
173-
"storage": [
174-
"./dist/types/exports/storage.d.ts"
175-
],
176-
"transaction": [
177-
"./dist/types/exports/transaction.d.ts"
178-
],
179-
"utils": [
180-
"./dist/types/exports/utils.d.ts"
181-
],
182-
"wallets": [
183-
"./dist/types/exports/wallets.d.ts"
184-
],
185-
"wallets/*": [
186-
"./dist/types/exports/wallets/*.d.ts"
187-
],
188-
"modules": [
189-
"./dist/types/exports/modules.d.ts"
190-
],
191-
"social": [
192-
"./dist/types/exports/social.d.ts"
193-
],
194-
"ai": [
195-
"./dist/types/exports/ai.d.ts"
196-
],
197-
"bridge": [
198-
"./dist/types/exports/bridge.d.ts"
199-
]
140+
"adapters/*": ["./dist/types/exports/adapters/*.d.ts"],
141+
"auth": ["./dist/types/exports/auth.d.ts"],
142+
"chains": ["./dist/types/exports/chains.d.ts"],
143+
"contract": ["./dist/types/exports/contract.d.ts"],
144+
"deploys": ["./dist/types/exports/deploys.d.ts"],
145+
"event": ["./dist/types/exports/event.d.ts"],
146+
"extensions/*": ["./dist/types/exports/extensions/*.d.ts"],
147+
"pay": ["./dist/types/exports/pay.d.ts"],
148+
"react": ["./dist/types/exports/react.d.ts"],
149+
"react-native": ["./dist/types/exports/react-native.d.ts"],
150+
"rpc": ["./dist/types/exports/rpc.d.ts"],
151+
"storage": ["./dist/types/exports/storage.d.ts"],
152+
"transaction": ["./dist/types/exports/transaction.d.ts"],
153+
"utils": ["./dist/types/exports/utils.d.ts"],
154+
"wallets": ["./dist/types/exports/wallets.d.ts"],
155+
"wallets/*": ["./dist/types/exports/wallets/*.d.ts"],
156+
"modules": ["./dist/types/exports/modules.d.ts"],
157+
"social": ["./dist/types/exports/social.d.ts"],
158+
"ai": ["./dist/types/exports/ai.d.ts"],
159+
"bridge": ["./dist/types/exports/bridge.d.ts"]
200160
}
201161
},
202162
"browser": {
@@ -233,7 +193,11 @@
233193
"fuse.js": "7.1.0",
234194
"input-otp": "^1.4.1",
235195
"mipd": "0.0.7",
196+
"open": "10.1.0",
197+
"ora": "8.2.0",
236198
"ox": "0.6.10",
199+
"prompts": "2.4.2",
200+
"toml": "3.0.0",
237201
"uqr": "0.1.2",
238202
"viem": "2.23.10"
239203
},
@@ -358,6 +322,7 @@
358322
"@testing-library/react": "^16.2.0",
359323
"@testing-library/user-event": "^14.6.1",
360324
"@types/cross-spawn": "^6.0.6",
325+
"@types/prompts": "2.4.9",
361326
"@types/react": "19.0.10",
362327
"@viem/anvil": "0.0.10",
363328
"@vitejs/plugin-react": "^4.3.4",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[
2+
"function activateProgram(address program) returns (uint16,uint256)"
3+
]

packages/thirdweb/src/cli/bin.ts

100644100755
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@ import {
44
generate,
55
isValidChainIdAndContractAddress,
66
} from "./commands/generate/generate.js";
7+
import { deployStylus, publishStylus } from "./commands/stylus/builder.js";
8+
import { createStylusProject } from "./commands/stylus/create.js";
9+
710
// skip the first two args?
811
const [, , command = "", ...rest] = process.argv;
912

13+
let secretKey: string | undefined;
14+
const keyIndex = rest.indexOf("-k");
15+
if (keyIndex !== -1 && rest.length > keyIndex + 1) {
16+
secretKey = rest[keyIndex + 1];
17+
rest.splice(keyIndex, 2);
18+
}
19+
1020
async function main() {
1121
switch (command) {
1222
case "generate": {
@@ -20,6 +30,21 @@ async function main() {
2030
break;
2131
}
2232

33+
case "publish-stylus": {
34+
await publishStylus(secretKey);
35+
break;
36+
}
37+
38+
case "deploy-stylus": {
39+
await deployStylus(secretKey);
40+
break;
41+
}
42+
43+
case "create-stylus": {
44+
await createStylusProject();
45+
break;
46+
}
47+
2348
case "login": {
2449
// Not implemented yet
2550
console.info(
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { spawnSync } from "node:child_process";
2+
import { existsSync, readFileSync } from "node:fs";
3+
import { join } from "node:path";
4+
import open from "open";
5+
import ora, { type Ora } from "ora";
6+
import { parse } from "toml";
7+
import { createThirdwebClient } from "../../../client/client.js";
8+
import { upload } from "../../../storage/upload.js";
9+
10+
const THIRDWEB_URL = "https://thirdweb.com";
11+
12+
export async function publishStylus(secretKey?: string) {
13+
const spinner = ora("Checking if this is a Stylus project...").start();
14+
const uri = await buildStylus(spinner, secretKey);
15+
16+
const url = getUrl(uri, "publish").toString();
17+
spinner.succeed(`Upload complete, navigate to ${url}`);
18+
await open(url);
19+
}
20+
21+
export async function deployStylus(secretKey?: string) {
22+
const spinner = ora("Checking if this is a Stylus project...").start();
23+
const uri = await buildStylus(spinner, secretKey);
24+
25+
const url = getUrl(uri, "deploy").toString();
26+
spinner.succeed(`Upload complete, navigate to ${url}`);
27+
await open(url);
28+
}
29+
30+
async function buildStylus(spinner: Ora, secretKey?: string) {
31+
if (!secretKey) {
32+
spinner.fail("Error: Secret key is required.");
33+
process.exit(1);
34+
}
35+
36+
try {
37+
// Step 1: Validate stylus project
38+
const root = process.cwd();
39+
if (!root) {
40+
spinner.fail("Error: No package directory found.");
41+
process.exit(1);
42+
}
43+
44+
const cargoTomlPath = join(root, "Cargo.toml");
45+
if (!existsSync(cargoTomlPath)) {
46+
spinner.fail("Error: No Cargo.toml found. Not a Stylus/Rust project.");
47+
process.exit(1);
48+
}
49+
50+
const cargoToml = readFileSync(cargoTomlPath, "utf8");
51+
const parsedCargoToml = parse(cargoToml);
52+
if (!parsedCargoToml.dependencies?.["stylus-sdk"]) {
53+
spinner.fail(
54+
"Error: Not a Stylus project. Missing stylus-sdk dependency.",
55+
);
56+
process.exit(1);
57+
}
58+
59+
spinner.succeed("Stylus project detected.");
60+
61+
// Step 2: Run stylus command to generate initcode
62+
spinner.start("Generating initcode...");
63+
const initcodeResult = spawnSync("cargo", ["stylus", "get-initcode"], {
64+
encoding: "utf-8",
65+
});
66+
if (initcodeResult.status !== 0) {
67+
spinner.fail("Failed to generate initcode.");
68+
process.exit(1);
69+
}
70+
71+
const initcode = extractBytecode(initcodeResult.stdout);
72+
if (!initcode) {
73+
spinner.fail("Failed to generate initcode.");
74+
process.exit(1);
75+
}
76+
spinner.succeed("Initcode generated.");
77+
78+
// Step 3: Run stylus command to generate abi
79+
spinner.start("Generating ABI...");
80+
const abiResult = spawnSync("cargo", ["stylus", "export-abi", "--json"], {
81+
encoding: "utf-8",
82+
});
83+
if (abiResult.status !== 0) {
84+
spinner.fail("Failed to generate ABI.");
85+
process.exit(1);
86+
}
87+
88+
const abiContent = abiResult.stdout.trim();
89+
if (!abiContent) {
90+
spinner.fail("Failed to generate ABI.");
91+
process.exit(1);
92+
}
93+
spinner.succeed("ABI generated.");
94+
95+
// Step 4: Process the output
96+
const contractName = extractContractNameFromExportAbi(abiContent);
97+
if (!contractName) {
98+
spinner.fail("Error: Could not determine contract name from ABI output.");
99+
process.exit(1);
100+
}
101+
102+
let cleanedAbi = "";
103+
try {
104+
const jsonMatch = abiContent.match(/\[.*\]/s);
105+
if (jsonMatch) {
106+
cleanedAbi = jsonMatch[0];
107+
} else {
108+
throw new Error("No valid JSON ABI found in the file.");
109+
}
110+
} catch (error) {
111+
spinner.fail("Error: ABI file contains invalid format.");
112+
console.error(error);
113+
process.exit(1);
114+
}
115+
116+
const metadata = {
117+
compiler: {},
118+
language: "rust",
119+
output: {
120+
abi: JSON.parse(cleanedAbi),
121+
devdoc: {},
122+
userdoc: {},
123+
},
124+
settings: {
125+
compilationTarget: {
126+
"src/main.rs": contractName,
127+
},
128+
},
129+
sources: {},
130+
};
131+
spinner.succeed("Stylus contract exported successfully.");
132+
133+
// Step 5: Upload to IPFS
134+
spinner.start("Uploading to IPFS...");
135+
const client = createThirdwebClient({
136+
secretKey,
137+
});
138+
139+
const metadataUri = await upload({
140+
client,
141+
files: [metadata],
142+
});
143+
144+
const bytecodeUri = await upload({
145+
client,
146+
files: [initcode],
147+
});
148+
149+
const uri = await upload({
150+
client,
151+
files: [
152+
{
153+
name: contractName,
154+
metadataUri,
155+
bytecodeUri,
156+
analytics: {
157+
command: "publish-stylus",
158+
contract_name: contractName,
159+
cli_version: "",
160+
project_type: "stylus",
161+
},
162+
compilers: {
163+
stylus: [
164+
{ compilerVersion: "", evmVersion: "", metadataUri, bytecodeUri },
165+
],
166+
},
167+
},
168+
],
169+
});
170+
spinner.succeed("Upload complete");
171+
172+
return uri;
173+
} catch (error) {
174+
spinner.fail(`Error: ${error}`);
175+
process.exit(1);
176+
}
177+
}
178+
179+
function extractContractNameFromExportAbi(abiRawOutput: string): string | null {
180+
const match = abiRawOutput.match(/<stdin>:(I[A-Za-z0-9_]+)/);
181+
if (match?.[1]) {
182+
return match[1].replace(/^I/, "");
183+
}
184+
return null;
185+
}
186+
187+
function getUrl(hash: string, command: string) {
188+
const url = new URL(
189+
`${THIRDWEB_URL}/contracts/${command}/${encodeURIComponent(hash.replace("ipfs://", ""))}`,
190+
);
191+
192+
return url;
193+
}
194+
195+
function extractBytecode(rawOutput: string): string {
196+
const hexStart = rawOutput.indexOf("7f000000");
197+
if (hexStart === -1) {
198+
throw new Error("Could not find start of bytecode");
199+
}
200+
return rawOutput.slice(hexStart).trim();
201+
}

0 commit comments

Comments
 (0)