diff --git a/src/lib/models/deploy.ts b/src/lib/models/deploy.ts index 70e956e..c1e1296 100644 --- a/src/lib/models/deploy.ts +++ b/src/lib/models/deploy.ts @@ -70,4 +70,11 @@ export interface UpdateDeploymentRequest { deploymentId: string; address: string; hash: string; -} \ No newline at end of file +} + +export interface DeploymentResult { + deploymentId?: string; + address: string; + hash: string; + sender?: string; +} diff --git a/src/lib/state/state.svelte.ts b/src/lib/state/state.svelte.ts index 704d71e..76f1a2a 100644 --- a/src/lib/state/state.svelte.ts +++ b/src/lib/state/state.svelte.ts @@ -1,6 +1,6 @@ import type { ApprovalProcess } from "$lib/models/approval-process"; import type { GlobalState } from "$lib/models/ui"; -import type { ContractSources } from "$lib/models/solc"; +import { isDeploymentEnvironment, isSameNetwork } from "$lib/utils/helpers"; /** * Global application state @@ -84,3 +84,13 @@ export const addAPToDropdown = (approvalProcess: ApprovalProcess) => { export function setDeploymentCompleted(completed: boolean) { globalState.form.completed = completed; } + +export function findDeploymentEnvironment(via?: string, network?: string) { + if (!via || !network) return undefined; + return globalState.approvalProcesses.find((ap) => + ap.network && + isDeploymentEnvironment(ap) && + isSameNetwork(ap.network, network) && + ap.via?.toLocaleLowerCase() === via.toLocaleLowerCase() + ); +} \ No newline at end of file diff --git a/src/lib/utils/contracts.ts b/src/lib/utils/contracts.ts index 821ba38..78b0a9c 100644 --- a/src/lib/utils/contracts.ts +++ b/src/lib/utils/contracts.ts @@ -1,6 +1,7 @@ import type { ABIDescription, ABIParameter, CompilationResult } from "@remixproject/plugin-api"; import { AbiCoder } from "ethers"; import { attempt } from "./attempt"; +import type { Artifact } from "$lib/models/deploy"; export function getContractFeatures( path: string, @@ -28,6 +29,23 @@ export function getConstructorInputs( return constructor.inputs as ABIParameter[]; } +export function getConstructorInputsWizard( + path: string | undefined, + contracts: Artifact['output']['contracts'], +): ABIParameter[] { + // if no compiled contracts found, then return empty inputs. + if (!contracts || !path) return []; + + const contractName = + Object.keys(contracts[path]).length > 0 + ? Object.keys(contracts[path])[0] + : ""; + const abi: Array = contracts[path][contractName].abi; + const constructor = abi.find((fragment) => fragment.type === "constructor"); + if (!constructor || !constructor.inputs) return []; + return constructor.inputs as ABIParameter[]; +} + export async function encodeConstructorArgs( inputs: ABIParameter[], inputsWithValue: Record diff --git a/src/lib/utils/helpers.ts b/src/lib/utils/helpers.ts index d32442a..558b4b1 100644 --- a/src/lib/utils/helpers.ts +++ b/src/lib/utils/helpers.ts @@ -1,5 +1,6 @@ import type { ApprovalProcess } from "$lib/models/approval-process"; import { getNetworkLiteral, type TenantNetworkResponse } from "$lib/models/network"; +import type { ContractSources } from "$lib/models/solc"; export const abbreviateAddress = (address: string, size = 6) => { return `${address.slice(0, size)}...${address.slice(-size)}`; @@ -14,3 +15,14 @@ export const isSameNetwork = (a: string | TenantNetworkResponse, b: string | Ten export const isDeploymentEnvironment = (approvalProcess: ApprovalProcess) => { return approvalProcess.component?.includes('deploy'); } + +export const isMultisig = (viaType?: ApprovalProcess['viaType']) => { + if (!viaType) return false; + const multisigTypes = ['Safe', 'Multisig', 'Gnosis Safe', 'Gnosis Multisig']; + return multisigTypes.includes(viaType); +} + +export const isUpgradeable = (sources?: ContractSources) => { + if (!sources) return false; + return Object.keys(sources).some((path) => path.includes('@openzeppelin/contracts-upgradeable')); +} diff --git a/src/lib/wizard/components/ApprovalProcess.svelte b/src/lib/wizard/components/ApprovalProcess.svelte index 5450f97..1319a68 100644 --- a/src/lib/wizard/components/ApprovalProcess.svelte +++ b/src/lib/wizard/components/ApprovalProcess.svelte @@ -7,6 +7,7 @@ import type { Relayer } from "$lib/models/relayer"; import { getNetworkLiteral } from "$lib/models/network"; import Input from "./shared/Input.svelte"; + import Message from "./shared/Message.svelte"; let address = $state(globalState.form.approvalProcessToCreate?.via || ""); @@ -167,11 +168,8 @@ {:else if approvalProcessType === "Relayer"} {#if disableRelayers} -
- -

- API Key not allowed to manage Relayers -

+
+
{:else} + import { API } from "$lib/api"; + import { deployContract, switchToNetwork } from "$lib/ethereum"; + import type { ApprovalProcess, CreateApprovalProcessRequest } from "$lib/models/approval-process"; + import type { Artifact, DeployContractRequest, DeploymentResult, UpdateDeploymentRequest } from "$lib/models/deploy"; + import { getNetworkLiteral, isProductionNetwork } from "$lib/models/network"; + import { buildCompilerInput, type ContractSources } from "$lib/models/solc"; + import type { APIResponse } from "$lib/models/ui"; + import { addAPToDropdown, findDeploymentEnvironment, globalState } from "$lib/state/state.svelte"; + import { attempt } from "$lib/utils/attempt"; + import { encodeConstructorArgs, getConstructorInputsWizard, getContractBytecode } from "$lib/utils/contracts"; + import { isMultisig, isUpgradeable } from "$lib/utils/helpers"; + import Button from "./shared/Button.svelte"; + import Input from "./shared/Input.svelte"; + import Message from "./shared/Message.svelte"; + + let inputsWithValue = $state>({}); + let busy = $state(false); + let successMessage = $state(""); + let errorMessage = $state(""); + let compilationError = $state(""); + let compilationResult = $state<{ output: Artifact['output'] }>(); + let deploymentId = $state(undefined); + let deploymentResult = $state(undefined); + let isDeterministic = $state(false); + let salt: string = $state(""); + + let contractBytecode = $derived.by(() => { + if (!globalState.contract?.target || !compilationResult) return; + + const name = globalState.contract.target; + const sources = compilationResult.output.contracts; + + return getContractBytecode(name, name, sources); + }); + + let deploymentArtifact = $derived.by(() => { + if (!compilationResult || !globalState.contract?.target || !globalState.contract.source?.sources) return; + + return { + input: buildCompilerInput(globalState.contract.source?.sources as ContractSources), + output: compilationResult.output + } + }); + + let inputs = $derived.by(() => { + if (!compilationResult) return []; + return getConstructorInputsWizard(globalState.contract?.target, compilationResult.output.contracts); + }); + + let displayUpgradeableWarning = $derived.by(() => { + return isUpgradeable(globalState.contract?.source?.sources as ContractSources); + }); + + let enforceDeterministic = $derived.by(() => { + const selectedMultisig = globalState.form.approvalType === 'existing' && isMultisig(globalState.form.approvalProcessSelected?.viaType); + const toCreateMultisig = globalState.form.approvalType === 'new' && isMultisig(globalState.form.approvalProcessToCreate?.viaType); + return selectedMultisig || toCreateMultisig; + }); + + const deploymentUrl = $derived( + deploymentId && globalState.form.network + ? `https://defender.openzeppelin.com/#/deploy/environment/${ + isProductionNetwork(globalState.form.network) ? 'production' : 'test' + }?deploymentId=${deploymentId}` + : undefined + ); + + $effect(() => { + if (globalState.contract?.source?.sources) { + compile(); + } + }); + + function handleInputChange(event: Event) { + const target = event.target as HTMLInputElement; + inputsWithValue[target.name] = target.value; + } + + async function compile() { + const sources = globalState.contract?.source?.sources; + if (!sources) { + return; + } + + const [res, error] = await attempt(async () => API.compile(buildCompilerInput( + globalState.contract!.source!.sources as ContractSources + ))); + + if (error) { + compilationError = `Compilation failed: ${error.msg}`; + return; + } + compilationResult = res.data; + } + + function displayMessage(message: string, type: "success" | "error") { + successMessage = ""; + errorMessage = ""; + if (type === "success") { + successMessage = message; + } else { + errorMessage = message; + } + } + + function handleSaltChanged(event: Event) { + const target = event.target as HTMLInputElement; + salt = target.value; + } + + export async function handleInjectedProviderDeployment(bytecode: string) { + // Switch network if needed + const [, networkError] = await attempt(async () => switchToNetwork(globalState.form.network!)); + if (networkError) { + throw new Error(`Error switching network: ${networkError.msg}`); + } + + const [result, error] = await attempt(async () => deployContract(bytecode)); + if (error) { + throw new Error(`Error deploying contract: ${error.msg}`); + } + + if (!result) { + throw new Error("Deployment result not found"); + } + + displayMessage(`Contract deployed successfully, hash: ${result?.hash}`, "success"); + + return { + address: result.address, + hash: result.hash, + sender: result.sender + }; + } + + async function getOrCreateApprovalProcess(): Promise { + const ap = globalState.form.approvalProcessToCreate; + if (!ap || !ap.via || !ap.viaType) { + displayMessage("Must select an approval process to create", "error"); + return; + } + + if (!globalState.form.network) { + displayMessage("Must select a network", "error"); + return; + } + + const existing = findDeploymentEnvironment(ap.via, ap.network); + if (existing) { + return existing; + } + + const apRequest: CreateApprovalProcessRequest = { + name: `Deploy From Remix - ${ap.viaType}`, + via: ap.via, + viaType: ap.viaType, + network: getNetworkLiteral(globalState.form.network), + relayerId: ap.relayerId, + component: ["deploy"], + }; + const result: APIResponse<{ approvalProcess: ApprovalProcess }> = + await API.createApprovalProcess(apRequest); + + if (!result.success) { + displayMessage(`Approval process creation failed, error: ${JSON.stringify(result.error)}`, "error"); + return; + } + + displayMessage("Deployment Environment successfully created", "success"); + if (!result.data) return; + + addAPToDropdown(result.data.approvalProcess) + return result.data.approvalProcess; + } + + export async function createDefenderDeployment(request: DeployContractRequest) { + const result: APIResponse<{ deployment: { deploymentId: string } }> = + await API.createDeployment(request); + + if (!result.success || !result.data) { + throw new Error(`Contract deployment creation failed: ${JSON.stringify(result.error)}`); + } + + return result.data.deployment.deploymentId; + } + + export async function updateDeploymentStatus( + deploymentId: string, + address: string, + hash: string + ) { + const updateRequest: UpdateDeploymentRequest = { + deploymentId, + hash, + address, + }; + + const result = await API.updateDeployment(updateRequest); + if (!result.success) { + throw new Error(`Failed to update deployment status: ${JSON.stringify(result.error)}`); + } + } + + async function deploy() { + if (!globalState.form.network) { + displayMessage("No network selected", "error"); + return; + } + + if (!globalState.contract?.target || !globalState.contract.source?.sources) { + displayMessage("No contract selected", "error"); + return; + } + + if (!deploymentArtifact || !contractBytecode) { + displayMessage("No artifact found", "error"); + return; + } + + if ((isDeterministic || enforceDeterministic) && !salt) { + displayMessage("Salt is required", "error"); + return; + } + + errorMessage = ""; + successMessage = ""; + + const [constructorBytecode, constructorError] = await encodeConstructorArgs(inputs, inputsWithValue); + if (constructorError) { + displayMessage(`Error encoding constructor arguments: ${constructorError.msg}`, "error"); + return; + } + + // contract deployment requires contract bytecode + // and constructor bytecode to be concatenated. + const bytecode = contractBytecode + constructorBytecode?.slice(2); + + const shouldUseInjectedProvider = globalState.form.approvalType === "injected"; + if (shouldUseInjectedProvider) { + const [result, error] = await attempt(async () => + handleInjectedProviderDeployment(bytecode), + ); + if (error) { + displayMessage(`Error deploying contract: ${error.msg}`, "error"); + return; + } + + deploymentResult = result; + + // loads global state with EOA approval process to create + globalState.form.approvalProcessToCreate = { + viaType: "EOA", + via: deploymentResult?.sender, + network: getNetworkLiteral(globalState.form.network), + }; + globalState.form.approvalProcessSelected = undefined; + } + + const approvalProcess = globalState.form.approvalProcessSelected ?? await getOrCreateApprovalProcess(); + if (!approvalProcess) { + displayMessage("No Approval Process selected", "error"); + return; + }; + + const deployRequest: DeployContractRequest = { + network: getNetworkLiteral(globalState.form.network), + approvalProcessId: approvalProcess.approvalProcessId, + contractName: globalState.contract!.target, + contractPath: globalState.contract!.target, + verifySourceCode: true, + licenseType: 'MIT', + artifactPayload: JSON.stringify(deploymentArtifact), + constructorBytecode, + salt, + } + + const [newDeploymentId, deployError] = await attempt(async () => createDefenderDeployment(deployRequest)); + if (deployError || !newDeploymentId) { + displayMessage(`Deployment failed to create: ${deployError?.msg}`, "error"); + return; + } + + if (shouldUseInjectedProvider && deploymentResult) { + const [, updateError] = await attempt(async () => updateDeploymentStatus( + newDeploymentId, + deploymentResult!.address, + deploymentResult!.hash + )); + if (updateError) { + displayMessage(`Error updating deployment status: ${updateError.msg}`, "error"); + return; + } + } else { + // If we're not using an injected provider + // we need to listen for the deployment to be finished. + // listenForDeployment(newDeploymentId); + } + + deploymentId = newDeploymentId; + displayMessage("Deployment successfuly created in Defender", "success"); + }; + + async function triggerDeploy() { + busy = true; + await deploy(); + busy = false; + } + + + +
+ + {#if displayUpgradeableWarning} + + {/if} + +
+ (isDeterministic = !isDeterministic)} + disabled={enforceDeterministic} + > + +
+ + {#if isDeterministic || enforceDeterministic} + + {/if} + + {#if compilationError} + + {/if} + + {#if inputs.length > 0} +
Constructor Arguments
+ {#each inputs as input} + + {/each} + {:else} + + {/if} + +
\ No newline at end of file diff --git a/src/lib/wizard/components/shared/Button.svelte b/src/lib/wizard/components/shared/Button.svelte index 7bfaaf8..ceda8b3 100644 --- a/src/lib/wizard/components/shared/Button.svelte +++ b/src/lib/wizard/components/shared/Button.svelte @@ -1,14 +1,25 @@ -
- @@ -328,7 +43,13 @@ {}} /> - @@ -337,12 +58,20 @@ - -
- +
+
+ +
+