Skip to content
This repository has been archived by the owner on Oct 27, 2023. It is now read-only.

Commit

Permalink
Improve web design (5afe#21)
Browse files Browse the repository at this point in the history
* Use Cards to make everything nicer

* Update favico

* Formatting
  • Loading branch information
rmeissner authored Jul 18, 2023
1 parent ad69bc3 commit 83c312b
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 144 deletions.
11 changes: 8 additions & 3 deletions contracts/contracts/Imports.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {TestSafeProtocolRegistryUnrestricted} from "@safe-global/safe-core-proto
// ExecutableMockContract for testing

contract ExecutableMockContract is MockContract {
function executeCallViaMock(address payable to, uint256 value, bytes memory data, uint256 gas) external returns (bool success, bytes memory response) {
(success, response) = to.call{ value: value, gas: gas}(data);
function executeCallViaMock(
address payable to,
uint256 value,
bytes memory data,
uint256 gas
) external returns (bool success, bytes memory response) {
(success, response) = to.call{value: value, gas: gas}(data);
}
}
}
146 changes: 82 additions & 64 deletions contracts/test/RelayPlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,23 @@ describe("RelayPlugin", async () => {
const setup = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture();
const manager = await ethers.getContractAt("MockContract", await getProtocolManagerAddress(hre));
const account = await (await ethers.getContractFactory("ExecutableMockContract")).deploy()
const account = await (await ethers.getContractFactory("ExecutableMockContract")).deploy();
const plugin = await getRelayPlugin(hre);
return {
account,
plugin,
manager
manager,
};
});

const addRelayContext = (data: string, fee: string, feeToken: string = ZeroAddress, decimals: number = 18) => {
return data +
relayer.address.slice(2) +
getAddress(feeToken).slice(2) +
return (
data +
relayer.address.slice(2) +
getAddress(feeToken).slice(2) +
abiEncoder.encode(["uint256"], [ethers.parseUnits(fee, decimals)]).slice(2)
}
);
};

it("should be inititalized correctly", async () => {
const { plugin } = await setup();
Expand All @@ -56,96 +58,112 @@ describe("RelayPlugin", async () => {
it("should revert if invalid method selector is used", async () => {
const { account, plugin, manager } = await setup();
await expect(plugin.executeFromPlugin(await manager.getAddress(), await account.getAddress(), "0xbaddad42"))
.to.be.revertedWithCustomError(plugin, "InvalidRelayMethod").withArgs("0xbaddad42");
.to.be.revertedWithCustomError(plugin, "InvalidRelayMethod")
.withArgs("0xbaddad42");
});

it("should revert if target contract reverts", async () => {
const { account, plugin, manager } = await setup();
await account.givenMethodRevert("0x6a761202")
await account.givenMethodRevert("0x6a761202");
await expect(plugin.executeFromPlugin(await manager.getAddress(), await account.getAddress(), "0x6a761202"))
.to.be.revertedWithCustomError(plugin, "RelayExecutionFailure").withArgs("0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000");
.to.be.revertedWithCustomError(plugin, "RelayExecutionFailure")
.withArgs(
"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
);
});

it("should revert if fee is too high", async () => {
const { account, plugin, manager } = await setup();
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data
await expect(relayer.sendTransaction({to: plugin, data: addRelayContext(tx, "0.01")}))
.to.be.revertedWithCustomError(plugin, "FeeTooHigh(address,uint256)").withArgs(ZeroAddress, ethers.parseUnits("0.01", 18));
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data;
await expect(relayer.sendTransaction({ to: plugin, data: addRelayContext(tx, "0.01") }))
.to.be.revertedWithCustomError(plugin, "FeeTooHigh(address,uint256)")
.withArgs(ZeroAddress, ethers.parseUnits("0.01", 18));
});

it("should revert if fee payment fails", async () => {
const { account, plugin, manager } = await setup();
const setupTx = await plugin.setMaxFeePerToken.populateTransaction(ZeroAddress, ethers.parseUnits("0.01", 18))
await account.executeCallViaMock(setupTx.to, setupTx.value || 0, setupTx.data, MaxUint256)
expect(await plugin.maxFeePerToken(account, ZeroAddress)).to.be.eq(ethers.parseUnits("0.01", 18))
await manager.givenAnyRevert()
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data
await expect(relayer.sendTransaction({to: plugin, data: addRelayContext(tx, "0.01")}))
.to.be.revertedWithCustomError(plugin, "FeePaymentFailure").withArgs("0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000");
const setupTx = await plugin.setMaxFeePerToken.populateTransaction(ZeroAddress, ethers.parseUnits("0.01", 18));
await account.executeCallViaMock(setupTx.to, setupTx.value || 0, setupTx.data, MaxUint256);
expect(await plugin.maxFeePerToken(account, ZeroAddress)).to.be.eq(ethers.parseUnits("0.01", 18));
await manager.givenAnyRevert();
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data;
await expect(relayer.sendTransaction({ to: plugin, data: addRelayContext(tx, "0.01") }))
.to.be.revertedWithCustomError(plugin, "FeePaymentFailure")
.withArgs(
"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
);
});

it("should be a success with native token", async () => {
const { account, plugin, manager } = await setup();
const setupTx = await plugin.setMaxFeePerToken.populateTransaction(ZeroAddress, ethers.parseUnits("0.01", 18))
await account.executeCallViaMock(setupTx.to, setupTx.value || 0, setupTx.data, MaxUint256)
expect(await plugin.maxFeePerToken(account, ZeroAddress)).to.be.eq(ethers.parseUnits("0.01", 18))
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data
await expect(relayer.sendTransaction({to: plugin, data: addRelayContext(tx, "0.01")})).to.not.be.reverted;
expect(await account.invocationCount()).to.be.eq(1)
expect(await account.invocationCountForCalldata("0x6a761202")).to.be.eq(1)
const setupTx = await plugin.setMaxFeePerToken.populateTransaction(ZeroAddress, ethers.parseUnits("0.01", 18));
await account.executeCallViaMock(setupTx.to, setupTx.value || 0, setupTx.data, MaxUint256);
expect(await plugin.maxFeePerToken(account, ZeroAddress)).to.be.eq(ethers.parseUnits("0.01", 18));
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data;
await expect(relayer.sendTransaction({ to: plugin, data: addRelayContext(tx, "0.01") })).to.not.be.reverted;
expect(await account.invocationCount()).to.be.eq(1);
expect(await account.invocationCountForCalldata("0x6a761202")).to.be.eq(1);

const nonce = keccak256(abiEncoder.encode(
["address", "address", "address", "bytes"],
[await plugin.getAddress(), await manager.getAddress(), await account.getAddress(), "0x6a761202"]
))
const managerInterface = ISafeProtocolManager__factory.createInterface()
const nonce = keccak256(
abiEncoder.encode(
["address", "address", "address", "bytes"],
[await plugin.getAddress(), await manager.getAddress(), await account.getAddress(), "0x6a761202"],
),
);
const managerInterface = ISafeProtocolManager__factory.createInterface();
const expectedData = managerInterface.encodeFunctionData("executeTransaction", [
await account.getAddress(),
{
nonce,
metadataHash: ZeroHash,
actions: [{
to: relayer.address,
value: ethers.parseUnits("0.01", 18),
data: "0x"
}]
}
])
expect(await manager.invocationCount()).to.be.eq(1)
expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(1)
actions: [
{
to: relayer.address,
value: ethers.parseUnits("0.01", 18),
data: "0x",
},
],
},
]);
expect(await manager.invocationCount()).to.be.eq(1);
expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(1);
});

it("should be a success with fee token", async () => {
const { account, plugin, manager } = await setup();
const maxFee = ethers.parseUnits("0.02", 18)
const setupTx = await plugin.setMaxFeePerToken.populateTransaction(TOKEN_ADDRESS, maxFee)
await account.executeCallViaMock(setupTx.to, setupTx.value || 0, setupTx.data, MaxUint256)
expect(await plugin.maxFeePerToken(account, TOKEN_ADDRESS)).to.be.eq(maxFee)
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data
await expect(relayer.sendTransaction({to: plugin, data: addRelayContext(tx, "0.02", TOKEN_ADDRESS)})).to.not.be.reverted;
expect(await account.invocationCount()).to.be.eq(1)
expect(await account.invocationCountForCalldata("0x6a761202")).to.be.eq(1)
const maxFee = ethers.parseUnits("0.02", 18);
const setupTx = await plugin.setMaxFeePerToken.populateTransaction(TOKEN_ADDRESS, maxFee);
await account.executeCallViaMock(setupTx.to, setupTx.value || 0, setupTx.data, MaxUint256);
expect(await plugin.maxFeePerToken(account, TOKEN_ADDRESS)).to.be.eq(maxFee);
const tx = (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, "0x6a761202")).data;
await expect(relayer.sendTransaction({ to: plugin, data: addRelayContext(tx, "0.02", TOKEN_ADDRESS) })).to.not.be.reverted;
expect(await account.invocationCount()).to.be.eq(1);
expect(await account.invocationCountForCalldata("0x6a761202")).to.be.eq(1);

const tokenInterface = new Interface(["function transfer(address,uint256)"])
const encodedTransfer = tokenInterface.encodeFunctionData("transfer", [relayer.address, maxFee])
const managerInterface = ISafeProtocolManager__factory.createInterface()
const nonce = keccak256(abiEncoder.encode(
["address", "address", "address", "bytes"],
[await plugin.getAddress(), await manager.getAddress(), await account.getAddress(), "0x6a761202"]
))
const tokenInterface = new Interface(["function transfer(address,uint256)"]);
const encodedTransfer = tokenInterface.encodeFunctionData("transfer", [relayer.address, maxFee]);
const managerInterface = ISafeProtocolManager__factory.createInterface();
const nonce = keccak256(
abiEncoder.encode(
["address", "address", "address", "bytes"],
[await plugin.getAddress(), await manager.getAddress(), await account.getAddress(), "0x6a761202"],
),
);
const expectedData = managerInterface.encodeFunctionData("executeTransaction", [
await account.getAddress(),
{
nonce,
metadataHash: ZeroHash,
actions: [{
to: TOKEN_ADDRESS,
value: 0,
data: encodedTransfer
}]
}
])
expect(await manager.invocationCount()).to.be.eq(1)
expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(1)
actions: [
{
to: TOKEN_ADDRESS,
value: 0,
data: encodedTransfer,
},
],
},
]);
expect(await manager.invocationCount()).to.be.eq(1);
expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(1);
});
});
Binary file modified web/public/favicon.ico
Binary file not shown.
12 changes: 6 additions & 6 deletions web/src/routes/plugins/Plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "./Plugins.css";
import { PluginMetadata } from "../../logic/metadata";
import { PluginDetails, disablePlugin, enablePlugin, loadPluginDetails } from "../../logic/plugins";
import { openSafeApp } from "../../logic/safeapp";
import { Button } from '@mui/material';
import { Button, Card, Tooltip } from '@mui/material';

type PluginMetaProps = {
metadata: PluginMetadata;
Expand Down Expand Up @@ -50,12 +50,12 @@ export const Plugin: FunctionComponent<PluginProps> = ({ address }) => {
}
}, [details])
return (
<div className="Plugin">
<img className="AddressIcon" src={blocky} />
<Card className="Plugin">
<Tooltip title={address}><img className="AddressIcon" src={blocky} /></Tooltip>
<div className="Plugin-title">{!details ? "Loading Metadata" : <PluginMeta metadata={details.metadata} />}</div>
{details?.metadata?.requiresRootAccess == true && <WarningIcon color="warning" />}
{(details?.metadata?.appUrl?.length ?? 0) > 0 && <OpenInNewIcon onClick={() => openSafeApp(details?.metadata?.appUrl!!)} />}
{details?.enabled != undefined && <Button onClick={handleToggle}>{details?.enabled ? "Remove" : "Add"}</Button>}
</div>
{details?.enabled != undefined && <Button className="Plugin-toggle" onClick={handleToggle}>{details?.enabled ? "Disable" : "Enable"}</Button>}
{(details?.metadata?.appUrl?.length ?? 0) > 0 && <OpenInNewIcon className="Plugin-link" onClick={() => openSafeApp(details?.metadata?.appUrl!!)} />}
</Card>
);
};
31 changes: 17 additions & 14 deletions web/src/routes/plugins/PluginList.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import logo from '../../logo.svg';
import './Plugins.css';
import { loadPlugins } from '../../logic/plugins';
import { Plugin } from './Plugin';
import { Checkbox, FormControlLabel } from '@mui/material';
import { Button, Checkbox, FormControlLabel } from '@mui/material';

function PluginList() {
const [showFlagged, setFilterFlagged] = useState<boolean>(false);
const [plugins, setPlugins] = useState<string[]>([]);
const fetchData = useCallback(async () => {
try {
setPlugins([])
setPlugins(await loadPlugins(!showFlagged))
} catch(e) {
console.warn(e)
}
}, [showFlagged])
useEffect(() => {
const fetchData = async() => {
try {
setPlugins([])
setPlugins(await loadPlugins(!showFlagged))
} catch(e) {
console.warn(e)
}
}
fetchData();
}, [showFlagged])
}, [fetchData])
return (
<div className="Plugins">
<header className="Plugins-header">
Expand All @@ -27,9 +27,12 @@ function PluginList() {
Safe&#123;Core&#125; Protocol Demo
</p>
</header>
<FormControlLabel control={
<Checkbox checked={showFlagged} onChange={(_, checked) => setFilterFlagged(checked) } inputProps={{ 'aria-label': 'controlled' }} />
} label="Show Flagged PlugIns" />
<span>
<FormControlLabel control={
<Checkbox checked={showFlagged} onChange={(_, checked) => setFilterFlagged(checked) } inputProps={{ 'aria-label': 'controlled' }} />
} label="Show Flagged PlugIns" />
<Button onClick={fetchData}>Reload</Button>
</span>
<div className='Plugins-list'>
{plugins.map((plugin) => <Plugin address={plugin} />)}
</div>
Expand Down
17 changes: 14 additions & 3 deletions web/src/routes/plugins/Plugins.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
}

.Plugin {
width: 100%;
width: calc(100% - 16px);
margin: 8px;
padding: 8px;
text-align: start;
font-size: 20pt;
font-size: 16pt;
color: white;
display: flex;
flex-direction: row;
Expand All @@ -40,7 +42,7 @@
}

.Plugins-list {
width: 600pt;
width: min(calc(100% - 48px), 600px);
display: flex;
flex-direction: column;
align-items: start;
Expand All @@ -51,4 +53,13 @@
.AddressIcon {
border-radius: 50%;
margin-right: 10px;
}

.Plugin-link {
cursor: pointer;
margin-left: 8px;
}

.Plugin-toggle {
margin-left: 8px;
}
Loading

0 comments on commit 83c312b

Please sign in to comment.