|
5 | 5 | import { onMount } from "svelte";
|
6 | 6 | import Configuration from "$lib/wizard/components/Configuration.svelte";
|
7 | 7 | import Network from "$lib/wizard/components/Network.svelte";
|
8 |
| - import ApprovalProcess from "$lib/wizard/components/ApprovalProcess.svelte"; |
9 | 8 | import Button from "$lib/wizard/components/shared/Button.svelte";
|
10 |
| - import { globalState } from "$lib/state/state.svelte"; |
| 9 | + import { addAPToDropdown, globalState } from "$lib/state/state.svelte"; |
11 | 10 | import { API } from "$lib/api";
|
12 | 11 | import { buildCompilerInput, type ContractSources } from "$lib/models/solc";
|
13 | 12 | import Message from "$lib/wizard/components/shared/Message.svelte";
|
14 |
| - import type { Artifact, DeployContractRequest } from "$lib/models/deploy"; |
| 13 | + import type { Artifact, DeployContractRequest, UpdateDeploymentRequest } from "$lib/models/deploy"; |
15 | 14 | import { getNetworkLiteral } from "$lib/models/network";
|
| 15 | + import { attempt } from "$lib/utils/attempt"; |
| 16 | + import { getContractBytecode } from "$lib/utils/contracts"; |
| 17 | + import { deployContract, switchToNetwork } from "$lib/ethereum"; |
| 18 | + import type { ApprovalProcess as ApprovalProcessType, CreateApprovalProcessRequest } from "$lib/models/approval-process"; |
| 19 | + import type { APIResponse } from "$lib/models/ui"; |
| 20 | + import { isDeploymentEnvironment, isSameNetwork } from "$lib/utils/helpers"; |
| 21 | + import ApprovalProcess from "$lib/wizard/components/ApprovalProcess.svelte"; |
| 22 | +
|
| 23 | + interface DeploymentResult { |
| 24 | + deploymentId?: string; |
| 25 | + address: string; |
| 26 | + hash: string; |
| 27 | + sender?: string; |
| 28 | + } |
16 | 29 |
|
17 | 30 | let busy = $state(false);
|
18 | 31 | let successMessage = $state<string>("");
|
19 | 32 | let errorMessage = $state<string>("");
|
20 | 33 | let compilationResult = $state<{ output: Artifact['output'] }>();
|
21 |
| -
|
| 34 | + let deploymentId = $state<string | undefined>(undefined); |
22 | 35 | let deploymentArtifact = $derived.by(() => {
|
23 | 36 | if (!compilationResult || !globalState.contract?.target || !globalState.contract.source?.sources) return;
|
24 | 37 |
|
|
28 | 41 | }
|
29 | 42 | });
|
30 | 43 |
|
| 44 | + let contractBytecode = $derived.by(() => { |
| 45 | + if (!globalState.contract?.target || !compilationResult) return; |
| 46 | +
|
| 47 | + const name = globalState.contract.target; |
| 48 | + const sources = compilationResult.output.contracts; |
| 49 | +
|
| 50 | + return getContractBytecode(name, name, sources); |
| 51 | + }); |
| 52 | +
|
| 53 | + let constructorBytecode = $derived.by(() => { |
| 54 | + return "0x"; |
| 55 | + }); |
| 56 | +
|
| 57 | + let deploymentResult = $state<DeploymentResult | undefined>(undefined); |
| 58 | +
|
31 | 59 | let currentStep = $state(0);
|
32 | 60 | function toggleStep(step: number) {
|
33 | 61 | currentStep = step;
|
|
62 | 90 | compilationResult = res.data;
|
63 | 91 | }
|
64 | 92 |
|
| 93 | + export async function handleInjectedProviderDeployment(bytecode: string) { |
| 94 | + // Switch network if needed |
| 95 | + const [, networkError] = await attempt(async () => switchToNetwork(globalState.form.network!)); |
| 96 | + if (networkError) { |
| 97 | + throw new Error(`Error switching network: ${networkError.msg}`); |
| 98 | + } |
| 99 | +
|
| 100 | + const [result, error] = await attempt(async () => deployContract(bytecode)); |
| 101 | + if (error) { |
| 102 | + throw new Error(`Error deploying contract: ${error.msg}`); |
| 103 | + } |
| 104 | +
|
| 105 | + if (!result) { |
| 106 | + throw new Error("Deployment result not found"); |
| 107 | + } |
| 108 | +
|
| 109 | + displayMessage(`Contract deployed successfully, hash: ${result?.hash}`, "success"); |
| 110 | + |
| 111 | + return { |
| 112 | + address: result.address, |
| 113 | + hash: result.hash, |
| 114 | + sender: result.sender |
| 115 | + }; |
| 116 | + } |
| 117 | +
|
| 118 | + function findDeploymentEnvironment(via?: string, network?: string) { |
| 119 | + if (!via || !network) return undefined; |
| 120 | + return globalState.approvalProcesses.find((ap) => |
| 121 | + ap.network && |
| 122 | + isDeploymentEnvironment(ap) && |
| 123 | + isSameNetwork(ap.network, network) && |
| 124 | + ap.via?.toLocaleLowerCase() === via.toLocaleLowerCase() |
| 125 | + ); |
| 126 | + } |
| 127 | +
|
| 128 | +
|
| 129 | + async function getOrCreateApprovalProcess(): Promise<ApprovalProcessType | undefined> { |
| 130 | + const ap = globalState.form.approvalProcessToCreate; |
| 131 | + if (!ap || !ap.via || !ap.viaType) { |
| 132 | + displayMessage("Must select an approval process to create", "error"); |
| 133 | + return; |
| 134 | + } |
65 | 135 |
|
66 |
| - const deploy = async () => { |
67 | 136 | if (!globalState.form.network) {
|
68 |
| - displayMessage("No network selected", "error"); |
| 137 | + displayMessage("Must select a network", "error"); |
69 | 138 | return;
|
70 | 139 | }
|
71 | 140 |
|
72 |
| - if (!globalState.form.approvalProcessSelected) { |
73 |
| - displayMessage("No approval process selected", "error"); |
| 141 | + const existing = findDeploymentEnvironment(ap.via, ap.network); |
| 142 | + if (existing) { |
| 143 | + return existing; |
| 144 | + } |
| 145 | +
|
| 146 | + const apRequest: CreateApprovalProcessRequest = { |
| 147 | + name: `Deploy From Remix - ${ap.viaType}`, |
| 148 | + via: ap.via, |
| 149 | + viaType: ap.viaType, |
| 150 | + network: getNetworkLiteral(globalState.form.network), |
| 151 | + relayerId: ap.relayerId, |
| 152 | + component: ["deploy"], |
| 153 | + }; |
| 154 | + const result: APIResponse<{ approvalProcess: ApprovalProcessType }> = |
| 155 | + await API.createApprovalProcess(apRequest); |
| 156 | +
|
| 157 | + if (!result.success) { |
| 158 | + displayMessage(`Approval process creation failed, error: ${JSON.stringify(result.error)}`, "error"); |
| 159 | + return; |
| 160 | + } |
| 161 | +
|
| 162 | + displayMessage("Deployment Environment successfully created", "success"); |
| 163 | + if (!result.data) return; |
| 164 | +
|
| 165 | + addAPToDropdown(result.data.approvalProcess) |
| 166 | + return result.data.approvalProcess; |
| 167 | + } |
| 168 | +
|
| 169 | + export async function createDefenderDeployment(request: DeployContractRequest) { |
| 170 | + const result: APIResponse<{ deployment: { deploymentId: string } }> = |
| 171 | + await API.createDeployment(request); |
| 172 | +
|
| 173 | + if (!result.success || !result.data) { |
| 174 | + throw new Error(`Contract deployment creation failed: ${JSON.stringify(result.error)}`); |
| 175 | + } |
| 176 | +
|
| 177 | + return result.data.deployment.deploymentId; |
| 178 | + } |
| 179 | +
|
| 180 | + export async function updateDeploymentStatus( |
| 181 | + deploymentId: string, |
| 182 | + address: string, |
| 183 | + hash: string |
| 184 | + ) { |
| 185 | + const updateRequest: UpdateDeploymentRequest = { |
| 186 | + deploymentId, |
| 187 | + hash, |
| 188 | + address, |
| 189 | + }; |
| 190 | + |
| 191 | + const result = await API.updateDeployment(updateRequest); |
| 192 | + if (!result.success) { |
| 193 | + throw new Error(`Failed to update deployment status: ${JSON.stringify(result.error)}`); |
| 194 | + } |
| 195 | + } |
| 196 | +
|
| 197 | + // function listenForDeployment(deploymentId: string) { |
| 198 | + // // polls the deployment status every 10 seconds |
| 199 | + // const interval = 10000; |
| 200 | + // timeout = setInterval(async () => { |
| 201 | + // const result: APIResponse<{ address: string, hash: string }> = await API.getDeployment(deploymentId); |
| 202 | + // if (result.success && result.data?.address) { |
| 203 | + // deploymentResult = { |
| 204 | + // address: result.data.address, |
| 205 | + // hash: result.data.hash, |
| 206 | + // }; |
| 207 | + // logDeploymentResult(deploymentResult); |
| 208 | + // clearInterval(timeout); |
| 209 | + // } |
| 210 | + // }, interval); |
| 211 | + // } |
| 212 | +
|
| 213 | +
|
| 214 | + const deploy = async () => { |
| 215 | + if (!globalState.form.network) { |
| 216 | + displayMessage("No network selected", "error"); |
74 | 217 | return;
|
75 | 218 | }
|
76 | 219 |
|
|
84 | 227 | return;
|
85 | 228 | }
|
86 | 229 |
|
| 230 | + // contract deployment requires contract bytecode |
| 231 | + // and constructor bytecode to be concatenated. |
| 232 | + const bytecode = contractBytecode + constructorBytecode?.slice(2); |
| 233 | +
|
| 234 | + const shouldUseInjectedProvider = globalState.form.approvalType === "injected"; |
| 235 | + if (shouldUseInjectedProvider) { |
| 236 | + const [result, error] = await attempt(async () => |
| 237 | + handleInjectedProviderDeployment(bytecode), |
| 238 | + ); |
| 239 | + if (error) { |
| 240 | + displayMessage(`Error deploying contract: ${error.msg}`, "error"); |
| 241 | + return; |
| 242 | + } |
| 243 | +
|
| 244 | + deploymentResult = result; |
| 245 | +
|
| 246 | + // loads global state with EOA approval process to create |
| 247 | + globalState.form.approvalProcessToCreate = { |
| 248 | + viaType: "EOA", |
| 249 | + via: deploymentResult?.sender, |
| 250 | + network: getNetworkLiteral(globalState.form.network), |
| 251 | + }; |
| 252 | + globalState.form.approvalProcessSelected = undefined; |
| 253 | + } |
| 254 | +
|
| 255 | + const approvalProcess = globalState.form.approvalProcessSelected ?? await getOrCreateApprovalProcess(); |
| 256 | + if (!approvalProcess) { |
| 257 | + displayMessage("No Approval Process selected", "error"); |
| 258 | + return; |
| 259 | + }; |
| 260 | +
|
87 | 261 | const deployRequest: DeployContractRequest = {
|
88 |
| - artifactPayload: JSON.stringify(deploymentArtifact), |
89 | 262 | network: getNetworkLiteral(globalState.form.network),
|
90 |
| - // TODO: Implement approval process creation |
91 |
| - approvalProcessId: globalState.form.approvalProcessSelected.approvalProcessId, |
| 263 | + approvalProcessId: approvalProcess.approvalProcessId, |
92 | 264 | contractName: globalState.contract!.target,
|
93 | 265 | contractPath: globalState.contract!.target,
|
94 | 266 | verifySourceCode: true,
|
95 |
| - // TODO: Implement constructor arguments |
| 267 | + licenseType: 'MIT', |
| 268 | + artifactPayload: JSON.stringify(deploymentArtifact), |
| 269 | + // TODO: Implement constructor arguments + salt |
96 | 270 | constructorBytecode: '',
|
| 271 | + salt: '', |
97 | 272 | }
|
98 | 273 |
|
99 |
| - const res = await API.createDeployment(deployRequest); |
100 |
| - if (!res.success) { |
101 |
| - displayMessage(`Deployment failed: ${res.error}`, "error"); |
| 274 | + const [newDeploymentId, deployError] = await attempt(async () => createDefenderDeployment(deployRequest)); |
| 275 | + if (deployError || !newDeploymentId) { |
| 276 | + displayMessage(`Deployment failed to create: ${deployError?.msg}`, "error"); |
102 | 277 | return;
|
103 | 278 | }
|
104 | 279 |
|
| 280 | + if (shouldUseInjectedProvider && deploymentResult) { |
| 281 | + const [, updateError] = await attempt(async () => updateDeploymentStatus( |
| 282 | + newDeploymentId, |
| 283 | + deploymentResult!.address, |
| 284 | + deploymentResult!.hash |
| 285 | + )); |
| 286 | + if (updateError) { |
| 287 | + displayMessage(`Error updating deployment status: ${updateError.msg}`, "error"); |
| 288 | + return; |
| 289 | + } |
| 290 | + } else { |
| 291 | + // If we're not using an injected provider |
| 292 | + // we need to listen for the deployment to be finished. |
| 293 | + // listenForDeployment(newDeploymentId); |
| 294 | + } |
| 295 | +
|
| 296 | + deploymentId = newDeploymentId; |
105 | 297 | displayMessage("Deployment successful", "success");
|
106 | 298 | };
|
107 | 299 |
|
|
145 | 337 | <ApprovalProcess />
|
146 | 338 | </div>
|
147 | 339 | </div>
|
148 |
| - {#if compilationResult} |
149 |
| - {JSON.stringify(compilationResult, null, 2)} |
150 |
| - {/if} |
151 | 340 | </div>
|
152 | 341 | <div class="sticky px-4 flex flex-col gap-2">
|
153 | 342 | <Button disabled={!globalState.authenticated || busy} loading={busy} label="Deploy" onClick={compileAndDeploy} />
|
|
0 commit comments