Skip to content

Commit

Permalink
Merge pull request #7 from OpenZeppelin/plat-5674-remix-plugin-fixes-…
Browse files Browse the repository at this point in the history
…from-feedback-2

Fixes from feedback
  • Loading branch information
MCarlomagno authored Nov 29, 2024
2 parents b26fdf5 + c332164 commit 71d1e7a
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 15 deletions.
12 changes: 12 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ class ApiClient {

return response.json();
}

async getDeployment(deploymentId: string) {
const response = await fetch(`/deploy?deploymentId=${deploymentId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": JSON.stringify(this.credentials)
},
});

return response.json();
}
}

export const API = new ApiClient();
79 changes: 70 additions & 9 deletions src/lib/components/Depoy.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script lang="ts">
import { addAPToDropdown, clearErrorBanner, globalState, setErrorBanner } from "$lib/state/state.svelte";
import { onDestroy } from "svelte";
import { addAPToDropdown, clearErrorBanner, globalState, setDeploymentCompleted, setErrorBanner } from "$lib/state/state.svelte";
import type {
ABIDescription,
ABIParameter,
CompilationResult,
} from "@remixproject/plugin-api";
import Button from "./shared/Button.svelte";
import { AbiCoder } from "ethers";
import { attempt } from "$lib/utils";
import { attempt, isDeploymentEnvironment, isSameNetwork } from "$lib/utils";
import { log, logError, logSuccess, logWarning } from "$lib/remix/logger";
import type {
ApprovalProcess,
Expand All @@ -28,6 +29,7 @@
let salt: string | undefined;
let inputsWithValue: Record<string, string | number | boolean> = {};
let contractPath: string | undefined;
let timeout: NodeJS.Timeout | undefined;
let deploymentId = $state<string | undefined>(undefined);
const deploymentUrl = $derived(
Expand All @@ -39,6 +41,7 @@
);
let deploying = $state(false);
let isDeterministic = $state(false);
let deploymentResult = $state<{ address: string, hash: string } | undefined>(undefined);
/**
* Finds constructor arguments and loads contract features.
Expand Down Expand Up @@ -93,11 +96,26 @@
const abi = contracts[path][contractName].abi;
return { name: contractName, abi };
}
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()
);
}
async function createApprovalProcess(): Promise<ApprovalProcess | undefined> {
async function getOrCreateApprovalProcess(): Promise<ApprovalProcess | undefined> {
const ap = globalState.form.approvalProcessToCreate;
if (!ap) return;
const existing = findDeploymentEnvironment(ap.via, ap.network);
if (existing) {
return existing;
}
if (!globalState.form.network) {
setErrorBanner("Please select a network");
return;
Expand Down Expand Up @@ -140,10 +158,32 @@
return result.data?.approvalProcess;
}
function logDeploymentResult(result: { address: string, hash: string }) {
logSuccess(`[Defender Deploy] Contract deployed, address: ${result.address}, tx hash: ${result.hash}`);
}
function listenForDeployment(deploymentId: string) {
// polls the deployment status every 10 seconds
log("[Defender Deploy] Waiting for deployment...");
const interval = 10000;
timeout = setInterval(async () => {
const result: APIResponse<{ address: string, hash: string }> = await API.getDeployment(deploymentId);
if (result.success && result.data?.address) {
deploymentResult = {
address: result.data.address,
hash: result.data.hash,
};
logDeploymentResult(deploymentResult);
clearInterval(timeout);
}
}, interval);
}
async function triggerDeployment() {
if (!globalState.form.network || !contractName || !contractPath) return;
clearErrorBanner();
setDeploymentCompleted(false);
deploying = true;
const [constructorBytecode, error] = await attempt<string>(async () => {
Expand All @@ -166,7 +206,6 @@
let contractAddress: string | undefined;
let hash: string | undefined;
if (shouldUseInjectedProvider) {
log("[Defender Deploy] Switching network.");
// ensures current network matches with the one selected.
const [, error] = await attempt(async () =>
switchToNetwork(globalState.form.network!),
Expand Down Expand Up @@ -199,20 +238,25 @@
`[Defender Deploy] Contract deployed, tx hash: ${result?.hash}`,
);
contractAddress = result?.address;
hash = result?.hash;
if (result) {
contractAddress = result.address;
hash = result.hash;
deploymentResult = { address: contractAddress, hash: hash };
logDeploymentResult(deploymentResult);
}
// loads global state with EOA approval process to create
globalState.form.approvalProcessToCreate = {
viaType: "EOA",
via: result?.sender,
network: getNetworkLiteral(globalState.form.network),
};
globalState.form.approvalProcessSelected = undefined;
}
const selectedApprovalProcess = globalState.form.approvalProcessSelected;
const approvalProcess: ApprovalProcess | undefined =
selectedApprovalProcess ?? (await createApprovalProcess());
selectedApprovalProcess ?? (await getOrCreateApprovalProcess());
if (!approvalProcess) {
deploying = false;
logError("[Defender Deploy] No Approval Process selected");
Expand Down Expand Up @@ -264,10 +308,16 @@
deploying = false;
return;
}
deploymentResult = { address: contractAddress, hash: hash };
logDeploymentResult(deploymentResult);
} else {
// If we're not using an injected provider
// we need to listen for the deployment to be finished.
listenForDeployment(deploymentId);
}
logSuccess("[Defender Deploy] Deployment submitted to Defender!");
globalState.form.completed = true;
setDeploymentCompleted(true);
deploying = false;
}
Expand All @@ -280,6 +330,12 @@
const target = event.target as HTMLInputElement;
salt = target.value;
}
onDestroy(() => {
if (timeout) {
clearInterval(timeout);
}
});
</script>

<input
Expand Down Expand Up @@ -334,7 +390,12 @@
<i class="fa fa-check-circle-o mr-2"></i>
<p class="m-0">
<small class="lh-sm">
Contract deployment submitted to Defender!<br>
{#if deploymentResult}
Contract deployed
{:else}
Contract deployment submitted to Defender!
{/if}
<br>
{#if deploymentUrl}
<a class="text-success" href={deploymentUrl} target="_blank">
View Deployment
Expand Down
8 changes: 7 additions & 1 deletion src/lib/defender/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,10 @@ export const updateDeployment = async (credentials: Credentials, updateReq: Upda
address: updateReq.address,
});
return response;
}
}

export const getDeployment = async (credentials: Credentials, deploymentId: string) => {
const client = getClient(credentials);
const response = await client.deploy.getDeployedContract(deploymentId);
return response;
}
3 changes: 3 additions & 0 deletions src/lib/ethereum/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Eip1193Provider, BrowserProvider, ContractFactory } from 'ethers';
import { chainIds, type TenantNetworkResponse } from "$lib/models/network";
import type { DeployContractResult } from '$lib/models/ethereum';
import { log } from '$lib/remix/logger';

function getEthereum(): Eip1193Provider {
if (!window.ethereum) throw new Error('Injected provider not found');
Expand All @@ -22,6 +23,8 @@ export async function switchToNetwork(network: string | TenantNetworkResponse) {
const current = await ethereum.request({ method: 'eth_chainId' });
if (parseInt(current, 16) === chainId) return;

log("[Defender Deploy] Switching network...");

await ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${chainId.toString(16)}` }],
Expand Down
4 changes: 3 additions & 1 deletion src/lib/models/approval-process.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { TenantNetworkResponse } from "./network";

/**
* Generic approval process model
*/
Expand All @@ -6,7 +8,7 @@ export type ApprovalProcess = {
createdAt: string;
name: string;
component?: ComponentType;
network?: string;
network?: string | TenantNetworkResponse;
via?: string;
viaType?: 'EOA' | 'Contract' | 'Multisig' | 'Gnosis Safe' | 'Safe' | 'Gnosis Multisig' | 'Relayer' | 'Relayer Group' | 'Unknown' | 'Relayer Group' | 'Timelock Controller' | 'ERC20' | 'Governor' | 'Fireblocks';
multisigSender?: string;
Expand Down
1 change: 1 addition & 0 deletions src/lib/models/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type GlobalState = {
viaType: 'EOA' | 'Safe' | 'Relayer';
via?: string;
relayerId?: string;
network?: string;
}
approvalType?: 'existing' | 'new' | 'injected';
completed?: boolean;
Expand Down
6 changes: 5 additions & 1 deletion src/lib/state/state.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,8 @@ export const setErrorBanner = (error?: string) => {

export const addAPToDropdown = (approvalProcess: ApprovalProcess) => {
globalState.approvalProcesses.push(approvalProcess);
};
}

export function setDeploymentCompleted(completed: boolean) {
globalState.form.completed = completed;
}
14 changes: 13 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* is the error from the catch statement.
*/

import { getNetworkLiteral } from "./models/network";
import type { ApprovalProcess } from "./models/approval-process";
import type { TenantNetworkResponse } from "./models/network";

type AttemptError = {
msg: string;
errorObject: any;
Expand Down Expand Up @@ -47,4 +51,12 @@ export const abbreviateAddress = (address: string, size = 6) => {
return `${address.slice(0, size)}...${address.slice(-size)}`;
}

export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export const isSameNetwork = (a: string | TenantNetworkResponse, b: string | TenantNetworkResponse) => {
return getNetworkLiteral(a) === getNetworkLiteral(b);
}

export const isDeploymentEnvironment = (approvalProcess: ApprovalProcess) => {
return approvalProcess.component?.includes('deploy');
}
29 changes: 27 additions & 2 deletions src/routes/deploy/+server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deployContract, updateDeployment } from "$lib/defender";
import { deployContract, getDeployment, updateDeployment } from "$lib/defender";
import type { Credentials } from "$lib/models/auth";
import type { DeployContractRequest, UpdateDeploymentRequest } from "$lib/models/deploy";
import { attempt } from "$lib/utils";
Expand All @@ -25,4 +25,29 @@ export async function PUT({ request }: { request: Request }) {
}

return json({ success: true, data: { result } });
}
}

export async function GET({ request }: { request: Request }) {
const url = new URL(request.url);
const deploymentId = url.searchParams.get('deploymentId');
const credentials = JSON.parse(request.headers.get('authorization') || '{}');

if (!deploymentId) {
return json({ success: false, error: "Missing deploymentId parameter" });
}

if (!credentials) {
return json({ success: false, error: "Missing Credentials" });
}

const [result, error] = await attempt(() => getDeployment(credentials, deploymentId));
if (error) {
return json({ success: false, error: error.msg });
}

if (!result) {
return json({ success: false, error: "Deployment not found" });
}

return json({ success: true, data: { address: result.address, hash: result.txHash } });
}

0 comments on commit 71d1e7a

Please sign in to comment.