Skip to content

Commit f3dadca

Browse files
committed
add constructor inputs
1 parent b84125f commit f3dadca

File tree

6 files changed

+380
-319
lines changed

6 files changed

+380
-319
lines changed

src/lib/models/deploy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,11 @@ export interface UpdateDeploymentRequest {
7070
deploymentId: string;
7171
address: string;
7272
hash: string;
73-
}
73+
}
74+
75+
export interface DeploymentResult {
76+
deploymentId?: string;
77+
address: string;
78+
hash: string;
79+
sender?: string;
80+
}

src/lib/state/state.svelte.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ApprovalProcess } from "$lib/models/approval-process";
22
import type { GlobalState } from "$lib/models/ui";
3-
import type { ContractSources } from "$lib/models/solc";
3+
import { isDeploymentEnvironment, isSameNetwork } from "$lib/utils/helpers";
44

55
/**
66
* Global application state
@@ -84,3 +84,13 @@ export const addAPToDropdown = (approvalProcess: ApprovalProcess) => {
8484
export function setDeploymentCompleted(completed: boolean) {
8585
globalState.form.completed = completed;
8686
}
87+
88+
export function findDeploymentEnvironment(via?: string, network?: string) {
89+
if (!via || !network) return undefined;
90+
return globalState.approvalProcesses.find((ap) =>
91+
ap.network &&
92+
isDeploymentEnvironment(ap) &&
93+
isSameNetwork(ap.network, network) &&
94+
ap.via?.toLocaleLowerCase() === via.toLocaleLowerCase()
95+
);
96+
}

src/lib/utils/contracts.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ABIDescription, ABIParameter, CompilationResult } from "@remixproject/plugin-api";
22
import { AbiCoder } from "ethers";
33
import { attempt } from "./attempt";
4+
import type { Artifact } from "$lib/models/deploy";
45

56
export function getContractFeatures(
67
path: string,
@@ -28,6 +29,23 @@ export function getConstructorInputs(
2829
return constructor.inputs as ABIParameter[];
2930
}
3031

32+
export function getConstructorInputsWizard(
33+
path: string | undefined,
34+
contracts: Artifact['output']['contracts'],
35+
): ABIParameter[] {
36+
// if no compiled contracts found, then return empty inputs.
37+
if (!contracts || !path) return [];
38+
39+
const contractName =
40+
Object.keys(contracts[path]).length > 0
41+
? Object.keys(contracts[path])[0]
42+
: "";
43+
const abi: Array<ABIDescription> = contracts[path][contractName].abi;
44+
const constructor = abi.find((fragment) => fragment.type === "constructor");
45+
if (!constructor || !constructor.inputs) return [];
46+
return constructor.inputs as ABIParameter[];
47+
}
48+
3149
export async function encodeConstructorArgs(
3250
inputs: ABIParameter[],
3351
inputsWithValue: Record<string, string | number | boolean>
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
<script lang="ts">
2+
import { API } from "$lib/api";
3+
import { deployContract, switchToNetwork } from "$lib/ethereum";
4+
import type { ApprovalProcess, CreateApprovalProcessRequest } from "$lib/models/approval-process";
5+
import type { Artifact, DeployContractRequest, DeploymentResult, UpdateDeploymentRequest } from "$lib/models/deploy";
6+
import { getNetworkLiteral, isProductionNetwork } from "$lib/models/network";
7+
import { buildCompilerInput, type ContractSources } from "$lib/models/solc";
8+
import type { APIResponse } from "$lib/models/ui";
9+
import { addAPToDropdown, findDeploymentEnvironment, globalState } from "$lib/state/state.svelte";
10+
import { attempt } from "$lib/utils/attempt";
11+
import { encodeConstructorArgs, getConstructorInputsWizard, getContractBytecode } from "$lib/utils/contracts";
12+
import Button from "./shared/Button.svelte";
13+
import Input from "./shared/Input.svelte";
14+
import Message from "./shared/Message.svelte";
15+
16+
17+
let inputsWithValue = $state<Record<string, string | number | boolean>>({});
18+
let busy = $state(false);
19+
let successMessage = $state<string>("");
20+
let errorMessage = $state<string>("");
21+
let compilationError = $state<string>("");
22+
let compilationResult = $state<{ output: Artifact['output'] }>();
23+
let deploymentId = $state<string | undefined>(undefined);
24+
let deploymentResult = $state<DeploymentResult | undefined>(undefined);
25+
26+
let contractBytecode = $derived.by(() => {
27+
if (!globalState.contract?.target || !compilationResult) return;
28+
29+
const name = globalState.contract.target;
30+
const sources = compilationResult.output.contracts;
31+
32+
return getContractBytecode(name, name, sources);
33+
});
34+
35+
let deploymentArtifact = $derived.by(() => {
36+
if (!compilationResult || !globalState.contract?.target || !globalState.contract.source?.sources) return;
37+
38+
return {
39+
input: buildCompilerInput(globalState.contract.source?.sources as ContractSources),
40+
output: compilationResult.output
41+
}
42+
});
43+
44+
let inputs = $derived.by(() => {
45+
if (!compilationResult) return [];
46+
return getConstructorInputsWizard(globalState.contract?.target, compilationResult.output.contracts);
47+
});
48+
49+
const deploymentUrl = $derived(
50+
deploymentId && globalState.form.network
51+
? `https://defender.openzeppelin.com/#/deploy/environment/${
52+
isProductionNetwork(globalState.form.network) ? 'production' : 'test'
53+
}?deploymentId=${deploymentId}`
54+
: undefined
55+
);
56+
57+
$effect(() => {
58+
if (globalState.contract?.source?.sources) {
59+
compile();
60+
}
61+
});
62+
63+
function handleInputChange(event: Event) {
64+
const target = event.target as HTMLInputElement;
65+
inputsWithValue[target.name] = target.value;
66+
}
67+
68+
async function compile() {
69+
const sources = globalState.contract?.source?.sources;
70+
if (!sources) {
71+
return;
72+
}
73+
74+
const [res, error] = await attempt(async () => API.compile(buildCompilerInput(
75+
globalState.contract!.source!.sources as ContractSources
76+
)));
77+
78+
if (error) {
79+
compilationError = `Compilation failed: ${error.msg}`;
80+
return;
81+
}
82+
compilationResult = res.data;
83+
}
84+
85+
const displayMessage = (message: string, type: "success" | "error") => {
86+
successMessage = "";
87+
errorMessage = "";
88+
if (type === "success") {
89+
successMessage = message;
90+
} else {
91+
errorMessage = message;
92+
}
93+
}
94+
95+
export async function handleInjectedProviderDeployment(bytecode: string) {
96+
// Switch network if needed
97+
const [, networkError] = await attempt(async () => switchToNetwork(globalState.form.network!));
98+
if (networkError) {
99+
throw new Error(`Error switching network: ${networkError.msg}`);
100+
}
101+
102+
const [result, error] = await attempt(async () => deployContract(bytecode));
103+
if (error) {
104+
throw new Error(`Error deploying contract: ${error.msg}`);
105+
}
106+
107+
if (!result) {
108+
throw new Error("Deployment result not found");
109+
}
110+
111+
displayMessage(`Contract deployed successfully, hash: ${result?.hash}`, "success");
112+
113+
return {
114+
address: result.address,
115+
hash: result.hash,
116+
sender: result.sender
117+
};
118+
}
119+
120+
async function getOrCreateApprovalProcess(): Promise<ApprovalProcess | undefined> {
121+
const ap = globalState.form.approvalProcessToCreate;
122+
if (!ap || !ap.via || !ap.viaType) {
123+
displayMessage("Must select an approval process to create", "error");
124+
return;
125+
}
126+
127+
if (!globalState.form.network) {
128+
displayMessage("Must select a network", "error");
129+
return;
130+
}
131+
132+
const existing = findDeploymentEnvironment(ap.via, ap.network);
133+
if (existing) {
134+
return existing;
135+
}
136+
137+
const apRequest: CreateApprovalProcessRequest = {
138+
name: `Deploy From Remix - ${ap.viaType}`,
139+
via: ap.via,
140+
viaType: ap.viaType,
141+
network: getNetworkLiteral(globalState.form.network),
142+
relayerId: ap.relayerId,
143+
component: ["deploy"],
144+
};
145+
const result: APIResponse<{ approvalProcess: ApprovalProcess }> =
146+
await API.createApprovalProcess(apRequest);
147+
148+
if (!result.success) {
149+
displayMessage(`Approval process creation failed, error: ${JSON.stringify(result.error)}`, "error");
150+
return;
151+
}
152+
153+
displayMessage("Deployment Environment successfully created", "success");
154+
if (!result.data) return;
155+
156+
addAPToDropdown(result.data.approvalProcess)
157+
return result.data.approvalProcess;
158+
}
159+
160+
export async function createDefenderDeployment(request: DeployContractRequest) {
161+
const result: APIResponse<{ deployment: { deploymentId: string } }> =
162+
await API.createDeployment(request);
163+
164+
if (!result.success || !result.data) {
165+
throw new Error(`Contract deployment creation failed: ${JSON.stringify(result.error)}`);
166+
}
167+
168+
return result.data.deployment.deploymentId;
169+
}
170+
171+
export async function updateDeploymentStatus(
172+
deploymentId: string,
173+
address: string,
174+
hash: string
175+
) {
176+
const updateRequest: UpdateDeploymentRequest = {
177+
deploymentId,
178+
hash,
179+
address,
180+
};
181+
182+
const result = await API.updateDeployment(updateRequest);
183+
if (!result.success) {
184+
throw new Error(`Failed to update deployment status: ${JSON.stringify(result.error)}`);
185+
}
186+
}
187+
188+
async function deploy() {
189+
if (!globalState.form.network) {
190+
displayMessage("No network selected", "error");
191+
return;
192+
}
193+
194+
if (!globalState.contract?.target || !globalState.contract.source?.sources) {
195+
displayMessage("No contract selected", "error");
196+
return;
197+
}
198+
199+
if (!deploymentArtifact || !contractBytecode) {
200+
displayMessage("No artifact found", "error");
201+
return;
202+
}
203+
204+
const [constructorBytecode, constructorError] = await encodeConstructorArgs(inputs, inputsWithValue);
205+
if (constructorError) {
206+
displayMessage(`Error encoding constructor arguments: ${constructorError.msg}`, "error");
207+
return;
208+
}
209+
210+
// contract deployment requires contract bytecode
211+
// and constructor bytecode to be concatenated.
212+
const bytecode = contractBytecode + constructorBytecode?.slice(2);
213+
214+
const shouldUseInjectedProvider = globalState.form.approvalType === "injected";
215+
if (shouldUseInjectedProvider) {
216+
const [result, error] = await attempt(async () =>
217+
handleInjectedProviderDeployment(bytecode),
218+
);
219+
if (error) {
220+
displayMessage(`Error deploying contract: ${error.msg}`, "error");
221+
return;
222+
}
223+
224+
deploymentResult = result;
225+
226+
// loads global state with EOA approval process to create
227+
globalState.form.approvalProcessToCreate = {
228+
viaType: "EOA",
229+
via: deploymentResult?.sender,
230+
network: getNetworkLiteral(globalState.form.network),
231+
};
232+
globalState.form.approvalProcessSelected = undefined;
233+
}
234+
235+
const approvalProcess = globalState.form.approvalProcessSelected ?? await getOrCreateApprovalProcess();
236+
if (!approvalProcess) {
237+
displayMessage("No Approval Process selected", "error");
238+
return;
239+
};
240+
241+
const deployRequest: DeployContractRequest = {
242+
network: getNetworkLiteral(globalState.form.network),
243+
approvalProcessId: approvalProcess.approvalProcessId,
244+
contractName: globalState.contract!.target,
245+
contractPath: globalState.contract!.target,
246+
verifySourceCode: true,
247+
licenseType: 'MIT',
248+
artifactPayload: JSON.stringify(deploymentArtifact),
249+
// TODO: Implement constructor arguments + salt
250+
constructorBytecode: '',
251+
salt: '',
252+
}
253+
254+
const [newDeploymentId, deployError] = await attempt(async () => createDefenderDeployment(deployRequest));
255+
if (deployError || !newDeploymentId) {
256+
displayMessage(`Deployment failed to create: ${deployError?.msg}`, "error");
257+
return;
258+
}
259+
260+
if (shouldUseInjectedProvider && deploymentResult) {
261+
const [, updateError] = await attempt(async () => updateDeploymentStatus(
262+
newDeploymentId,
263+
deploymentResult!.address,
264+
deploymentResult!.hash
265+
));
266+
if (updateError) {
267+
displayMessage(`Error updating deployment status: ${updateError.msg}`, "error");
268+
return;
269+
}
270+
} else {
271+
// If we're not using an injected provider
272+
// we need to listen for the deployment to be finished.
273+
// listenForDeployment(newDeploymentId);
274+
}
275+
276+
deploymentId = newDeploymentId;
277+
displayMessage("Deployment successfuly created in Defender", "success");
278+
};
279+
280+
async function triggerDeploy() {
281+
busy = true;
282+
await deploy();
283+
busy = false;
284+
}
285+
286+
</script>
287+
288+
<div class="px-4 flex flex-col gap-2">
289+
{#if compilationError}
290+
<Message message={compilationError} type="error" />
291+
{/if}
292+
293+
{#if inputs.length > 0}
294+
<h6 class="text-sm">Constructor Arguments</h6>
295+
{#each inputs as input}
296+
<Input name={input.name} placeholder={input.name} onchange={handleInputChange} value={''} type="text"/>
297+
{/each}
298+
{/if}
299+
300+
<Button disabled={!globalState.authenticated || busy} loading={busy} label="Deploy" onClick={triggerDeploy} />
301+
302+
{#if successMessage || errorMessage}
303+
<Message message={successMessage || errorMessage} type={successMessage ? "success" : "error"} />
304+
305+
{#if deploymentUrl}
306+
<Button label={"View Deployment"} onClick={() => window.open(deploymentUrl, "_blank")} type="secondary" />
307+
{/if}
308+
{/if}
309+
</div>

0 commit comments

Comments
 (0)