Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

# Defender Deploy Plugin

Plugin to deploy smart contracts using OpenZeppelin Defender. For documentation about usage in Remix please visit the [Defender Docs](https://docs.openzeppelin.com/defender/remix-plugin).
Plugin to deploy smart contracts using OpenZeppelin Defender. Currently supported in:

- [Remix IDE](https://remix.ethereum.org/) - As a plugin listed in plugins directory, for more information please visit [our docs](https://docs.openzeppelin.com/defender/remix-plugin).
- [Contracts Wizard](https://wizard.openzeppelin.com/) - Integrated in code editor, for more information please visit [our docs](https://docs.openzeppelin.com/defender/remix-plugin).

## Getting Started

Expand All @@ -16,7 +19,7 @@ pnpm install
pnpm dev
```

The interface is ugly, but don't worry! it's not meant to be used directly, it's used embedded in an iframe instead, and adopts the parent styles.
NOTE: This project is meant to be embedded in other UIs, just running the project won't be enough to see and debug it. You must embed the UI on an external iframe.

## Testing in Remix

Expand All @@ -31,4 +34,38 @@ Url: http://localhost:5173 # or live version https://defeder-remix-deploy.netlif
Type of connection: Iframe
Location in Remix: Side Panel
```
5. You should see the plugin added to the sidebar (new icon with ? symbol).
5. You should see the plugin added to the sidebar (new icon with ? symbol).

## Testing in Contracts Wizard

For testing in Contracts Wizard, you must also run the Contracts Wizard UI locally to point to your local plugin.

1. Run Contracts Wizard locally.
- a. Go to [https://github.com/OpenZeppelin/contracts-wizard](Contracts Wizard Repo).
- b. Clone the latest `master` branch and follow steps to setup the project.
- c. Move to `pacakges/ui` and run it with `yarn dev`.
2. In another terminal, run this project using `pnpm dev`, make sure the app is served in `http://localhost:5173`.
3. Open Contracts Wizard local UI, generally in `http://localhost:8080`.
4. Click on "Deploy with Defender" button, you should be able to see embedded the local plugin.

## Development

Many parts of codebase are shared across plugins (server side code, state definition, ethereum interactions, etc.), but UI components have a separated implementation to make them more flexible and prevent side-effects.

We have some bootstrap logic to expose one UI or another depending on the parent iframe domain.

### Remix
The entrypoint for Remix plugin is `src/routes/remix.svelte`. Its components mainly use [bootstrap](https://getbootstrap.com/) for styling, Remix UI injects bootstrap dependency as a <link> html tag when embedded.

### Wizard
The entrypoint for Contracts Wizard plugin is `src/routes/wizard.svelte`. Its components mainly use [tailwind CSS](https://tailwindcss.com/) for styling, this is mainly for convenience, since Contracts Wizard was made using this CSS framework.

## Release

The repo has a CI/CD connected to our netlify account, when we merge `main` to some of the release branches, a new version of the plugin is released to live. Branches:

- Remix IDE Plugin - `release-remix`
- Contracts Wizard Plugin - `release-wizard`

> [!WARNING]
> We use `main` branch as the single source of truth and it's the only branch allowed to be merged to release branches. It should be tested carefully before triggering a new release.
10 changes: 10 additions & 0 deletions src/lib/models/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,13 @@ export interface DeploymentResult {
hash: string;
sender?: string;
}

export type ABITypeParameter = 'uint' | 'uint[]' | 'int' | 'int[]' | 'address' | 'address[]' | 'bool' | 'bool[]' | 'fixed' | 'fixed[]' | 'ufixed' | 'ufixed[]' | 'bytes' | 'bytes[]' | 'function' | 'function[]' | 'tuple' | 'tuple[]' | string;
export interface ABIParameter {
/** The name of the parameter */
name: string;
/** The canonical type of the parameter */
type: ABITypeParameter;
/** Used for tuple types */
components?: ABIParameter[];
}
2 changes: 1 addition & 1 deletion src/lib/remix/components/Deploy.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
verifySourceCode: true,
artifactPayload,
constructorBytecode,
salt,
salt: isDeterministic || enforceDeterministic ? salt : undefined,
};
const [newDeploymentId, deployError] = await attempt(async () => createDefenderDeployment(deployRequest));
if (deployError) {
Expand Down
8 changes: 8 additions & 0 deletions src/lib/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ export const isUpgradeable = (sources?: ContractSources) => {
if (!sources) return false;
return Object.keys(sources).some((path) => path.includes('@openzeppelin/contracts-upgradeable'));
}

export const debouncer = (fn: (...args: any[]) => void, delay: number) => {
let timeout: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
60 changes: 33 additions & 27 deletions src/lib/wizard/components/Deploy.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@
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 type { ABIParameter, 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 { debouncer, isMultisig, isUpgradeable } from "$lib/utils/helpers";
import Button from "./shared/Button.svelte";
import Input from "./shared/Input.svelte";
import Message from "./shared/Message.svelte";

// debounce the compile call to avoid sending too many requests while the user is editing.
const compileDebounced = debouncer(compile, 600);

let inputsWithValue = $state<Record<string, string | number | boolean>>({});
let busy = $state(false);
let isDeploying = $state(false);
let successMessage = $state<string>("");
let errorMessage = $state<string>("");
let compilationError = $state<string>("");
Expand All @@ -24,6 +27,7 @@
let deploymentResult = $state<DeploymentResult | undefined>(undefined);
let isDeterministic = $state(false);
let salt: string = $state("");
let isCompiling = $state(false);

let contractBytecode = $derived.by(() => {
if (!globalState.contract?.target || !compilationResult) return;
Expand All @@ -43,11 +47,6 @@
}
});

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);
});
Expand All @@ -66,9 +65,12 @@
: undefined
);

let inputs: ABIParameter[] = $state([]);

$effect(() => {
if (globalState.contract?.source?.sources) {
compile();
isCompiling = true;
compileDebounced();
}
});

Expand All @@ -77,7 +79,7 @@
inputsWithValue[target.name] = target.value;
}

async function compile() {
async function compile(): Promise<void> {
const sources = globalState.contract?.source?.sources;
if (!sources) {
return;
Expand All @@ -92,6 +94,11 @@
return;
}
compilationResult = res.data;

if (globalState.contract?.target && compilationResult) {
inputs = getConstructorInputsWizard(globalState.contract.target, compilationResult.output.contracts);
}
isCompiling = false;
}

function displayMessage(message: string, type: "success" | "error") {
Expand Down Expand Up @@ -266,13 +273,13 @@
const deployRequest: DeployContractRequest = {
network: getNetworkLiteral(globalState.form.network),
approvalProcessId: approvalProcess.approvalProcessId,
contractName: globalState.contract!.target,
contractPath: globalState.contract!.target,
contractName: globalState.contract.target,
contractPath: globalState.contract.target,
verifySourceCode: true,
licenseType: 'MIT',
artifactPayload: JSON.stringify(deploymentArtifact),
constructorBytecode,
salt,
salt: isDeterministic || enforceDeterministic ? salt : undefined,
}

const [newDeploymentId, deployError] = await attempt(async () => createDefenderDeployment(deployRequest));
Expand Down Expand Up @@ -302,27 +309,28 @@
};

async function triggerDeploy() {
busy = true;
isDeploying = true;
await deploy();
busy = false;
isDeploying = false;
}

</script>

<div class="flex flex-col gap-2">

{#if displayUpgradeableWarning}
<Message type="warn" message="Upgradable contracts are not yet fully supported. This action will only deploy the implementation contract without initializing. <br />We recommend using <u><a href='https://github.com/OpenZeppelin/openzeppelin-upgrades' target='_blank'>openzeppelin-upgrades</a></u> package instead." />
{/if}

{#if inputs.length > 0}
<h6 class="text-sm">Constructor Arguments</h6>
{#each inputs as input}
<Input name={input.name} placeholder={`${input.name} (${input.type})`} onchange={handleInputChange} value={''} type="text"/>
{/each}
{:else}
<Message type="info" message="No constructor arguments found" />
{/if}
{#if isCompiling}
<Message type="loading" message="Compiling..." />
{:else if inputs.length > 0}
<h6 class="text-sm">Constructor Arguments</h6>
{#each inputs as input}
<Input name={input.name} placeholder={`${input.name} (${input.type})`} onchange={handleInputChange} value={''} type="text"/>
{/each}
{:else}
<Message type="info" message="No constructor arguments found" />
{/if}

<div class="pt-2 flex">
<input
Expand Down Expand Up @@ -351,9 +359,7 @@
<Message message={compilationError} type="error" />
{/if}



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

{#if successMessage || errorMessage}
<Message message={successMessage || errorMessage} type={successMessage ? "success" : "error"} />
Expand Down
7 changes: 6 additions & 1 deletion src/lib/wizard/components/shared/Message.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
type Props = {
message: string;
type: 'success' | 'error' | 'warn' | 'info';
type: 'success' | 'error' | 'warn' | 'info' | 'loading';
};

let { message, type }: Props = $props();
Expand All @@ -27,5 +27,10 @@
<i class={`fa fa-info-circle text-blue-600`}></i>
<div class="text-xs text-blue-600">{@html message}</div>
</div>
{:else if type === 'loading'}
<div class="flex flex-row items-center gap-2">
<i class={"fa fa-circle-o-notch fa-spin text-blue-600"}></i>
<div class="text-xs text-blue-600">{@html message}</div>
</div>
{/if}

2 changes: 1 addition & 1 deletion src/routes/remix.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
</button>

{#if currentTab === index}
<div transition:slide class="collapse show">
<div transition:slide class="show">
<div class="card-body">
{#if index === 0}<Setup onSuccess={() => wait(1000).then(() => toggle(1))}/>{/if}
{#if index === 1}<Network onSelected={() => wait(1000).then(() => toggle(2))}/>{/if}
Expand Down
Loading