Skip to content

Commit

Permalink
add constructor inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
MCarlomagno committed Dec 10, 2024
1 parent b84125f commit f3dadca
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 319 deletions.
9 changes: 8 additions & 1 deletion src/lib/models/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,11 @@ export interface UpdateDeploymentRequest {
deploymentId: string;
address: string;
hash: string;
}
}

export interface DeploymentResult {
deploymentId?: string;
address: string;
hash: string;
sender?: string;
}
12 changes: 11 additions & 1 deletion src/lib/state/state.svelte.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
);
}
18 changes: 18 additions & 0 deletions src/lib/utils/contracts.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<ABIDescription> = 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<string, string | number | boolean>
Expand Down
309 changes: 309 additions & 0 deletions src/lib/wizard/components/Deploy.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
<script lang="ts">
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 Button from "./shared/Button.svelte";
import Input from "./shared/Input.svelte";
import Message from "./shared/Message.svelte";
let inputsWithValue = $state<Record<string, string | number | boolean>>({});
let busy = $state(false);
let successMessage = $state<string>("");
let errorMessage = $state<string>("");
let compilationError = $state<string>("");
let compilationResult = $state<{ output: Artifact['output'] }>();
let deploymentId = $state<string | undefined>(undefined);
let deploymentResult = $state<DeploymentResult | undefined>(undefined);
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);
});
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;
}
const displayMessage = (message: string, type: "success" | "error") => {
successMessage = "";
errorMessage = "";
if (type === "success") {
successMessage = message;
} else {
errorMessage = message;
}
}
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<ApprovalProcess | undefined> {
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;
}
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),
// TODO: Implement constructor arguments + salt
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;
}
</script>

<div class="px-4 flex flex-col gap-2">
{#if compilationError}
<Message message={compilationError} type="error" />
{/if}

{#if inputs.length > 0}
<h6 class="text-sm">Constructor Arguments</h6>
{#each inputs as input}
<Input name={input.name} placeholder={input.name} onchange={handleInputChange} value={''} type="text"/>
{/each}
{/if}

<Button disabled={!globalState.authenticated || busy} loading={busy} label="Deploy" onClick={triggerDeploy} />

{#if successMessage || errorMessage}
<Message message={successMessage || errorMessage} type={successMessage ? "success" : "error"} />

{#if deploymentUrl}
<Button label={"View Deployment"} onClick={() => window.open(deploymentUrl, "_blank")} type="secondary" />
{/if}
{/if}
</div>
Loading

0 comments on commit f3dadca

Please sign in to comment.