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/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/cli/commands/stylus/builder.ts b/packages/thirdweb/src/cli/commands/stylus/builder.ts index 6a12fc29c19..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(); @@ -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(/======= :/g).filter(Boolean); const contractNames = extractContractNamesFromExportAbi(abiContent); @@ -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: {}, }, @@ -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" }; +} 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 23c40804ac3..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"], { @@ -43,6 +53,11 @@ 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", @@ -50,25 +65,65 @@ export async function createStylusProject() { }); // Step 5: Create the project + // biome-ignore lint/suspicious/noImplicitAnyLet: <> + 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", + }); + } 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."); diff --git a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts index 0c409874981..0505509c2ff 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts @@ -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"; @@ -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({ 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"; 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/__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, + }); +} 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/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 }), + }); +} 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; +}