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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<img src="./src/docs/banner.png" width="100%" alt="OpenZeppelin Logo">
</p>

# Deploy with Defender - Remix Plugin
# Defender Deploy Plugin

Remix plugin to deploy smart contracts using OpenZeppelin Defender. For documentation about usage please visit the [Defender Docs](https://docs.openzeppelin.com/defender/remix-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).

## Getting Started

Expand All @@ -16,7 +16,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 a Remix iframe instead, and adopts its styles.
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.

## Testing in Remix

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.16",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
Expand Down
609 changes: 605 additions & 4 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
2 changes: 1 addition & 1 deletion src/lib/remix/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { globalState } from "$lib/remix/state/state.svelte";
import { globalState } from "$lib/state/state.svelte";
import type { PluginClient } from "@remixproject/plugin"
import type { CompilationFileSources, CompilationResult, lastCompilationResult } from "@remixproject/plugin-api";

Expand Down
2 changes: 1 addition & 1 deletion src/lib/remix/components/ApprovalProcess.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { globalState } from "$lib/remix/state/state.svelte";
import { globalState } from "$lib/state/state.svelte";
import Dropdown from "./shared/Dropdown.svelte";
import { abbreviateAddress } from "$lib/utils/helpers";
import { approvalProcessTypes, type ApprovalProcess, type ApprovalProcessType } from "$lib/models/approval-process";
Expand Down
2 changes: 1 addition & 1 deletion src/lib/remix/components/Deploy.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { onDestroy } from "svelte";

// Lib
import { addAPToDropdown, clearErrorBanner, globalState, setDeploymentCompleted, setErrorBanner } from "$lib/remix/state/state.svelte";
import { addAPToDropdown, clearErrorBanner, globalState, setDeploymentCompleted, setErrorBanner } from "$lib/state/state.svelte";
import { log, logError, logSuccess, logWarning } from "$lib/remix/logger";
import { deployContract, switchToNetwork } from "$lib/ethereum";
import { API } from "$lib/api";
Expand Down
2 changes: 1 addition & 1 deletion src/lib/remix/components/Network.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
type TenantNetworkResponse,
} from "$lib/models/network";
import type { DropdownItem } from "$lib/models/ui";
import { globalState } from "$lib/remix/state/state.svelte";
import { globalState } from "$lib/state/state.svelte";
import Dropdown from "./shared/Dropdown.svelte";

type Props = {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/remix/components/Setup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { AuthenticationResponse } from "$lib/models/auth";
import type { APIResponse } from "$lib/models/ui";
import { logError, logSuccess } from "$lib/remix/logger";
import { clearErrorBanner, globalState } from "$lib/remix/state/state.svelte";
import { clearErrorBanner, globalState } from "$lib/state/state.svelte";
import Button from "./shared/Button.svelte";

type Props = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ApprovalProcess } from "$lib/models/approval-process";
import type { GlobalState } from "$lib/models/ui";
import type { ContractSources } from "$lib/models/solc";

/**
* Global application state
Expand Down
206 changes: 206 additions & 0 deletions src/lib/wizard/components/ApprovalProcess.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<script lang="ts">
import { globalState } from "$lib/state/state.svelte";
import Dropdown from "./shared/Dropdown.svelte";
import { abbreviateAddress } from "$lib/utils/helpers";
import { approvalProcessTypes, type ApprovalProcess, type ApprovalProcessType } from "$lib/models/approval-process";
import type { DropdownItem, GlobalState } from "$lib/models/ui";
import type { Relayer } from "$lib/models/relayer";
import { getNetworkLiteral } from "$lib/models/network";
import Input from "./shared/Input.svelte";

let address = $state<string>("");

function approvalProcessByNetworkAndComponent(ap: ApprovalProcess) {
const networkName = typeof globalState.form.network === 'string'
? globalState.form.network
: globalState.form.network?.name;

return ap.network === networkName && ap.component?.includes("deploy");
}

// Approval processes load logic
const toDisplayName = (ap: ApprovalProcess) => `${ap.name} (${ap.viaType})`;
const approvalProcessToDropdownItem = (ap: ApprovalProcess) => ({
label: toDisplayName(ap),
value: ap,
});

// Approval process selection logic
const onSelectApprovalProcess = (ap: DropdownItem) => {
globalState.form.approvalProcessSelected = ap.value as ApprovalProcess;
};

// Approval process creation logic
const approvalProcessTypeToDropdownItem = (type: ApprovalProcessType) => ({
label: type,
value: type,
});

let approvalProcessType = $state<ApprovalProcessType>("EOA");
const onSelectApprovalProcessType = (type: DropdownItem) => {
if (type.value) {
approvalProcessType = type.value;

// Save the type to create the approval process.
globalState.form.approvalProcessToCreate = {
...globalState.form.approvalProcessToCreate,
viaType: approvalProcessType as "EOA" | "Safe" | "Relayer",
};
}
};

// Relayer selection logic
const relayerByNetwork = (relayer: Relayer) =>
relayer.network === globalState.form.network;
const relayerToDropdownItem = (relayer: Relayer) => ({
label: `${relayer.name} (${abbreviateAddress(relayer.address)})`,
value: relayer,
});
const onSelectRelayer = (relayer: DropdownItem) => {
if (relayer.value) {
globalState.form.approvalProcessToCreate = {
viaType: "Relayer",
via: relayer.value.address,
relayerId: relayer.value.relayerId,
network: globalState.form.network && getNetworkLiteral(globalState.form.network),
};
}
};

const onAddressChange = (e: Event) => {
const element = e.target as HTMLInputElement;

// Save the type to create the approval process.
globalState.form.approvalProcessToCreate = {
viaType: approvalProcessType as "EOA" | "Safe" | "Relayer",
via: element.value,
network: globalState.form.network && getNetworkLiteral(globalState.form.network),
};
};

// Radio logic
let radioSelected = $state<GlobalState["form"]["approvalType"]>("existing");
const onRadioChange = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
radioSelected = target.id as GlobalState["form"]["approvalType"];

globalState.form.approvalType = radioSelected;
}
};

let disableCreation = $derived.by(() =>
globalState.approvalProcesses.some(approvalProcessByNetworkAndComponent),
);

let disableRelayers = $derived.by(() =>
!globalState.permissions?.includes("manage-relayers")
);
</script>

<div class="form-check">
<input
class="form-check-input"
type="radio"
name="flexRadioDefault"
id="existing"
onclick={(e) => onRadioChange(e)}
checked
/>
<label class="text-sm" for="flexRadioDefault1">
Use existing Approval Process
</label>

{#key globalState.form.approvalProcessSelected}
<Dropdown
items={globalState.approvalProcesses
.filter(approvalProcessByNetworkAndComponent)
.map(approvalProcessToDropdownItem)}
placeholder="Select Approval Process"
on:select={(e) => onSelectApprovalProcess(e.detail)}
disabled={radioSelected !== "existing"}
defaultItem={globalState.form.approvalProcessSelected
? {
label: toDisplayName(globalState.form.approvalProcessSelected),
value: globalState.form.approvalProcessSelected,
}
: undefined}
emptyLabel="No Approval Processes Available"
/>
{/key}
</div>
<div
class="form-check mt-3"
title={disableCreation ? "Deploy Environment already exists" : undefined}
>
<input
class="text-xs"
type="radio"
name="flexRadioDefault"
id="new"
onclick={(e) => onRadioChange(e)}
disabled={disableCreation}
title={disableCreation ? "Deploy Environment already exists" : undefined}
/>
<label
class="text-sm"
for="flexRadioDefault2"
title={disableCreation ? "Deploy Environment already exists" : undefined}
>
Create new Approval Process
</label>

<Dropdown
items={approvalProcessTypes.map(approvalProcessTypeToDropdownItem)}
placeholder="Approval Process Type"
on:select={(e) => onSelectApprovalProcessType(e.detail)}
disabled={radioSelected !== "new" || disableCreation}
defaultItem={{
label: approvalProcessType,
value: approvalProcessType,
}}
/>

{#if approvalProcessType === "EOA" || approvalProcessType === "Safe"}
<div class="mt-2">
<Input value={address} placeholder="* Address" type="text" />
</div>
{:else if approvalProcessType === "Relayer"}
{#if disableRelayers}
<div class="alert alert-warning d-flex align-items-center mt-2">
<i class="fa fa-exclamation-triangle mr-2"></i>
<p class="m-0 lh-1">
<small class="lh-sm">API Key not allowed to manage Relayers</small>
</p>
</div>
{:else}
<Dropdown
name="relayer"
items={globalState.relayers
.filter(relayerByNetwork)
.map(relayerToDropdownItem)}
placeholder="* Select Relayer"
on:select={(e) => onSelectRelayer(e.detail)}
disabled={radioSelected !== "new" || disableCreation}
/>
{/if}
{/if}
</div>
<div class="form-check mt-3">
<input
class="form-check-input"
type="radio"
name="flexRadioDefault"
id="injected"
onclick={(e) => onRadioChange(e)}
title={disableCreation ? "Deploy Environment already exists" : undefined}
disabled={disableCreation}
/>
<label
class="text-sm"
for="flexRadioDefault2"
title={disableCreation ? "Deploy Environment already exists" : undefined}
>
Approve using injected provider
</label>
</div>
81 changes: 81 additions & 0 deletions src/lib/wizard/components/Configuration.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script lang="ts">
import { API } from "$lib/api";
import type { AuthenticationResponse } from "$lib/models/auth";
import type { APIResponse } from "$lib/models/ui";
import { globalState } from "$lib/state/state.svelte";
import Button from "./shared/Button.svelte";
import Input from "./shared/Input.svelte";

let loading = $state(false);
let successMessage = $state<string | undefined>(undefined);
let errorMessage = $state<string | undefined>(undefined);
let apiKey = $state("");
let apiSecret = $state("");

function handleGetApiKey() {
// TODO: Implement
}

async function authenticate() {

loading = true;

const result: APIResponse<AuthenticationResponse> = await API.authenticate({ apiKey, apiSecret });

if (result.success) {
globalState.authenticated = true;
successMessage = "API Key Authenticated";
} else {
errorMessage = result.error ?? "Defender Authentication Failed";
}

if (result?.data?.credentials) {
globalState.credentials = {
apiKey: result?.data?.credentials.apiKey,
apiSecret: result?.data?.credentials.apiSecret,
};
}

if (result?.data?.permissions) {
globalState.permissions = result?.data?.permissions;
}

if (result?.data?.networks) {
globalState.networks = result?.data?.networks;
}

if (result?.data?.approvalProcesses) {
globalState.approvalProcesses = result?.data?.approvalProcesses;
}

if (result?.data?.relayers) {
globalState.relayers = result?.data?.relayers;
}

loading = false;
}

</script>

<div class="flex flex-col gap-2">
<div class="flex flex-row justify-between">
<div>
<label class="text-xs" for="apiKey">API Key</label>
<i class="fa fa-info-circle text-xs text-gray-500" title="Get your API key from the Defender Dashboard"></i>
</div>
<button onclick={handleGetApiKey} class="text-xs text-blue-600 font-bold">Get API Key</button>
</div>
<Input value={apiKey} placeholder="Enter your API key" type="text" />

<Input label="Secret" value={apiSecret} placeholder="Enter your API secret" type="password" />

<Button loading={loading} label="Authenticate" onClick={authenticate} />

{#if successMessage}
<div class="text-green-600">{successMessage}</div>
{/if}

{#if errorMessage}
<div class="text-red-600">{errorMessage}</div>
{/if}
</div>
Loading
Loading