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

Commit

Permalink
Closes 5afe#3: Trigger Safe transaction to enable protocol and module (
Browse files Browse the repository at this point in the history
…5afe#18)

* Closes 5afe#3: Trigger Safe transaction to enable protocol and module

* Add docs for the plugin commands
  • Loading branch information
rmeissner authored Jul 16, 2023
1 parent 4e7ef5f commit c8cda23
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 15 deletions.
12 changes: 12 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,16 @@ yarn test

```bash
yarn deploy <network>
```

### Interact with registry

- Register the Sample Plugin on the Registry
```bash
yarn register-plugin <network>
```

- List all Plugins in the Registry
```bash
yarn list-plugins <network>
```
2 changes: 2 additions & 0 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"typechain": "TS_NODE_TRANSPILE_ONLY=true hardhat typechain",
"postinstall": "yarn typechain",
"deploy": "hardhat deploy --network",
"register-plugin": "hardhat register-plugin --network",
"list-plugins": "hardhat list-plugins --network",
"prepack": "yarn build"
},
"devDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/utils/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const getProtocolManagerAddress = async(hre: HardhatRuntimeEnvironment):

// For the tests we deploy a mock for the manager
if (chainId === "31337") return deployMock(hre, "ManagerMock")

if (chainId === "5") return "0xb4Dc1B282706aB473cdD1b9899c57baD2BD3e2f3"

if (!(chainId in protocolDeployments)) throw Error("Unsupported Chain")
const manager = (protocolDeployments as any)[chainId][0].contracts.SafeProtocolManager.address
Expand All @@ -33,6 +35,8 @@ export const getProtocolRegistryAddress = async(hre: HardhatRuntimeEnvironment):

// For the tests we deploy a mock for the registry
if (chainId === "31337") return deployMock(hre, "RegistryMock")

if (chainId === "5") return "0x9EFbBcAD12034BC310581B9837D545A951761F5A"

if (!(chainId in protocolDeployments)) throw Error("Unsupported Chain")
// We use the unrestricted registry for the demo
Expand Down
86 changes: 81 additions & 5 deletions web/src/logic/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { EventLog } from "ethers";
import { ZeroAddress, EventLog } from "ethers";
import { BaseTransaction } from '@safe-global/safe-apps-sdk';
import { PluginMetadata, loadPluginMetadata } from "./metadata";
import { getPlugin, getRegistry } from "./protocol";
import { getManager, getPlugin, getRegistry } from "./protocol";
import { getSafeInfo, isConnectToSafe, submitTxs } from "./safeapp";
import { isModuleEnabled, buildEnableModule } from "./safe";

export const loadPluginDetails = async(pluginAddress: string): Promise<PluginMetadata> => {
const SENTINEL_MODULES = "0x0000000000000000000000000000000000000001"

export interface PluginDetails {
metadata: PluginMetadata,
enabled?: boolean
}

export const loadPluginDetails = async(pluginAddress: string): Promise<PluginDetails> => {
const plugin = await getPlugin(pluginAddress)
const metadata = loadPluginMetadata(plugin)
return metadata
const metadata = await loadPluginMetadata(plugin)
if (!await isConnectToSafe()) return { metadata }
const enabled = await isPluginEnabled(pluginAddress)
return { metadata, enabled }
}

export const loadPlugins = async(filterFlagged: boolean = true): Promise<string[]> => {
Expand All @@ -16,4 +28,68 @@ export const loadPlugins = async(filterFlagged: boolean = true): Promise<string[
const flaggedEvents = (await registry.queryFilter(registry.filters.IntegrationFlagged)) as EventLog[]
const flaggedIntegrations = flaggedEvents.map((event: EventLog) => event.args.integration)
return addedIntegrations.filter((integration) => flaggedIntegrations.indexOf(integration) < 0)
}

export const isPluginEnabled = async(plugin: string) => {
if (!await isConnectToSafe()) throw Error("Not connected to a Safe")
const manager = await getManager()
const safeInfo = await getSafeInfo()
const pluginInfo = await manager.enabledPlugins(safeInfo.safeAddress, plugin)
return pluginInfo.nextPluginPointer !== ZeroAddress
}

export const loadEnabledPlugins = async(): Promise<string[]> => {
if (!await isConnectToSafe()) throw Error("Not connected to a Safe")
const manager = await getManager()
const safeInfo = await getSafeInfo()
const paginatedPlugins = await manager.getPluginsPaginated(SENTINEL_MODULES, 10, safeInfo.safeAddress)
return paginatedPlugins.array
}

const buildEnablePlugin = async(plugin: string, requiresRootAccess: boolean): Promise<BaseTransaction> => {
const manager = await getManager()
return {
to: await manager.getAddress(),
value: "0",
data: (await manager.enablePlugin.populateTransaction(plugin, requiresRootAccess)).data
}
}

export const enablePlugin = async(plugin: string, requiresRootAccess: boolean) => {
if (!await isConnectToSafe()) throw Error("Not connected to a Safe")
const manager = await getManager()
const managerAddress = await manager.getAddress()
const info = await getSafeInfo()
const txs: BaseTransaction[] = []
if (!await isModuleEnabled(info.safeAddress, managerAddress)) {
txs.push(await buildEnableModule(info.safeAddress, managerAddress))
}
if (!await isPluginEnabled(plugin)) {
txs.push(await buildEnablePlugin(plugin, requiresRootAccess))
}
if (txs.length == 0) return
await submitTxs(txs)
}

const buildDisablePlugin = async(pointer: string, plugin: string): Promise<BaseTransaction> => {
const manager = await getManager()
return {
to: await manager.getAddress(),
value: "0",
data: (await manager.disablePlugin.populateTransaction(pointer, plugin)).data
}
}

export const disablePlugin = async(plugin: string) => {
if (!await isConnectToSafe()) throw Error("Not connected to a Safe")
const manager = await getManager()
const txs: BaseTransaction[] = []
const enabledPlugins = await loadEnabledPlugins()
const index = enabledPlugins.indexOf(plugin)
// Plugin is not enabled
if (index < 0) return
// If the plugin is not the first element in the linked list use previous element as pointer
// Otherwise use sentinel as pointer
const pointer = index > 0 ? enabledPlugins[index - 1] : SENTINEL_MODULES;
await submitTxs([await buildDisablePlugin(pointer, plugin)])
}
15 changes: 14 additions & 1 deletion web/src/logic/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,24 @@ const PLUGIN_ABI = [
"function metadataProvider() external view returns (uint256 providerType, bytes location)"
]

export const getManager = async() => {
const provider = await getProvider()
const registryInfo = {
address: "0xb4Dc1B282706aB473cdD1b9899c57baD2BD3e2f3",
abi: protocolDeployments[5][0].contracts.SafeProtocolManager.abi
};
return new ethers.Contract(
registryInfo.address,
registryInfo.abi,
provider
)
}

export const getRegistry = async() => {
const provider = await getProvider()
const registryInfo = protocolDeployments[5][0].contracts.TestSafeProtocolRegistryUnrestricted;
return new ethers.Contract(
registryInfo.address,
"0x9EFbBcAD12034BC310581B9837D545A951761F5A",
registryInfo.abi,
provider
)
Expand Down
32 changes: 32 additions & 0 deletions web/src/logic/safe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ethers } from "ethers"
import { getProvider } from "./web3";
import { BaseTransaction } from '@safe-global/safe-apps-sdk';

const SAFE_ABI = [
"function isModuleEnabled(address module) public view returns (bool)",
"function enableModule(address module) public"
]

// TODO: use safe-core-sdk here
const getSafe = async(address: string) => {
const provider = await getProvider()
return new ethers.Contract(
address,
SAFE_ABI,
provider
)
}

export const isModuleEnabled = async(safeAddress: string, module: string): Promise<boolean> => {
const safe = await getSafe(safeAddress)
return await safe.isModuleEnabled(module)
}

export const buildEnableModule = async(safeAddress: string, module: string): Promise<BaseTransaction> => {
const safe = await getSafe(safeAddress)
return {
to: safeAddress,
value: "0",
data: (await safe.enableModule.populateTransaction(module)).data
}
}
24 changes: 22 additions & 2 deletions web/src/logic/safeapp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { AbstractProvider, ethers } from "ethers"
import SafeAppsSDK, { SafeInfo } from '@safe-global/safe-apps-sdk';
import { ethers } from "ethers"
import SafeAppsSDK, { SafeInfo, BaseTransaction } from '@safe-global/safe-apps-sdk';
import { SafeAppProvider } from '@safe-global/safe-apps-provider';
import { PROTOCOL_CHAIN_ID } from "./constants";

Expand Down Expand Up @@ -32,4 +32,24 @@ export const getSafeAppsProvider = async() => {
const info = await getSafeInfo()
if (info.chainId != Number(PROTOCOL_CHAIN_ID)) throw Error("Unsupported chain")
return new ethers.BrowserProvider(new SafeAppProvider(info, safeAppsSDK))
}

export const submitTxs = async(txs: BaseTransaction[]): Promise<string> => {
const response = await safeAppsSDK.txs.send({ txs })
return response.safeTxHash
}

export const openSafeApp = async(appUrl: string) => {
if (!isConnectToSafe()) return
const safe = await getSafeInfo()
const environmentInfo = await safeAppsSDK.safe.getEnvironmentInfo()
const origin = environmentInfo.origin;
const chainInfo = await safeAppsSDK.safe.getChainInfo()
const networkPrefix = chainInfo.shortName
if (origin?.length) {
window.open(
`${origin}/apps/open?safe=${networkPrefix}:${safe.safeAddress}&appUrl=${appUrl}`,
'_blank',
)
}
}
29 changes: 22 additions & 7 deletions web/src/routes/plugins/Plugin.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { FunctionComponent, useEffect, useState } from "react";
import { FunctionComponent, useCallback, useEffect, useState } from "react";
import WarningIcon from '@mui/icons-material/Warning';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import * as blockies from 'blockies-ts';
import "./Plugins.css";
import { PluginMetadata } from "../../logic/metadata";
import { loadPluginDetails } from "../../logic/plugins";
import { PluginDetails, disablePlugin, enablePlugin, loadPluginDetails } from "../../logic/plugins";
import { openSafeApp } from "../../logic/safeapp";
import { Button } from '@mui/material';

type PluginMetaProps = {
metadata: PluginMetadata;
Expand All @@ -23,24 +25,37 @@ type PluginProps = {
};

export const Plugin: FunctionComponent<PluginProps> = ({ address }) => {
const [metadata, setMetadata] = useState<PluginMetadata|undefined>(undefined);
const [details, setDetails] = useState<PluginDetails|undefined>(undefined);
const blocky = blockies.create({ seed: address }).toDataURL();
useEffect(() => {
const fetchData = async() => {
try {
setMetadata(await loadPluginDetails(address))
setDetails(await loadPluginDetails(address))
} catch(e) {
console.warn(e)
}
}
fetchData();
}, [address])

const handleToggle = useCallback(async () => {
if (details?.enabled === undefined) return
try {
if (details.enabled)
await disablePlugin(address)
else
await enablePlugin(address, details.metadata.requiresRootAccess)
} catch (e) {
console.warn(e)
}
}, [details])
return (
<div className="Plugin">
<img className="AddressIcon" src={blocky} />
<div className="Plugin-title">{!metadata ? "Loading Metadata" : <PluginMeta metadata={metadata} />}</div>
{metadata?.requiresRootAccess == true && <WarningIcon color="warning" />}
{(metadata?.appUrl?.length ?? 0) > 0 && <a href={metadata?.appUrl} target="_blank"><OpenInNewIcon /></a>}
<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>
);
};

0 comments on commit c8cda23

Please sign in to comment.