diff --git a/README.md b/README.md new file mode 100644 index 0000000..297c794 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Safe{Core} Protocol Demo + +This repository contain a demo for developing and using [Plugins](https://github.com/5afe/safe-core-protocol-specs/tree/main/integrations#plugins) from the Safe{Core} Protocol. + +## Structure + +The repository is separated into two parts: + +- [Contracts](./contracts/) contains the sample contracts and scripts to deploy them +- [Web App](./web/) contains the web app to configure and use the sample contracts + +## Make it your own + +To get started with your own plugin you can fork/ copy this repository and adjust the existing code. + +Follow the instructions in the [Contracts](./contracts/) folder to create a Plugin for your use case. You can then register the plugin on a test registry and it will be visible on [Demo App](https://5afe.github.io/safe-core-protocol-demo). + +This [Demo App](https://5afe.github.io/safe-core-protocol-demo) can be used as a [Safe app in the Safe{Wallet} Web](https://app.safe.global/share/safe-app?appUrl=https%3A%2F%2F5afe.github.io%2Fsafe-core-protocol-demo&chain=gor) interface to add the Plugin to a Safe. + +To configure your Plugin it is necessary to create a web app. For this follow the instructions in the [Web App](./web/) folder. A simple way to host your web app is to use [GitHub pages](https://pages.github.com/). For this you can use the `yarn deploy` script. + +Important don't forget to update your [Plugin Metadata](./contracts/README.md#plugin-metadata). \ No newline at end of file diff --git a/contracts/README.md b/contracts/README.md index e7ac987..5c13071 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -2,6 +2,52 @@ This project shows the usage of [Safe{Core} protocol](https://github.com/5afe/safe-core-protocol) +## Structure + +The project contains [contracts](./contracts), [tests](./test) and [scripts](./src/) to build a Plugin. + +- [Contracts](./contracts) + - [Imports.sol](./contracts/Imports.sol) - Import that are only used for testing and by scripts + - [Base.sol](./contracts/Base.sol) - Types and base Plugin contracts that can be extended (i.e. to manage Plugin metadata) + - [Plugins.sol](./contracts/Plugins.sol) - A collection of example Plugins +- [Tests](./test) + - [RelayPlugin.spec.ts](./test/SamplePlugin.spec.ts) - Tests for the Relay example Plugin. +- [Scripts](./src) + - [Deployment](./src/deploy) - Deployment scripts for the example Plugins + - [Tasks](./src/tasks) - Tasks to register plugins + - [Utils](./src/utils) - Utility method to interfact with Plugins + +## Plugin Metadata + +The metadata of a Plugin is used to provide information to users when enabling the Plugin. Currently the information required is: +- `name` - Name of the Plugin that should be displayed +- `version` - Version of the Plugin that is shown to the user +- `requiresRootAccess` - Indicates if the Plugin require root access (i.e. perform `delegatecall` or change the account config). +- `iconUrl` - Icon that should be displayed +- `appUrl` - App to configure and use the Plugin + +Note: The format and type of metadata currently required by Plugins is just for this demo. This will change in the future and a proper format will be proposed in the [specificiations](https://github.com/5afe/safe-core-protocol-specs) + +## Contracts + +### Base Contracts + +The base contracts include two base contracts: + +- `BasePluginWithStoredMetadata` - A plugin that stores the metadata onchain +- `BasePluginWithEventMetadata` - A plugin that stores the metadata in an event + +Both allow that the web app can retrieve this metadata to display it to the user. + +It is also possible to provide other storage means (i.e. `ipfs` or `url`). For this it is necessary to extend the `BasePlugin` contract and add the require utility script. + +### Example Plugins + +The following example are included int his repository: + +- `RelayPlugin` - A plugin that allows to relay Safe transactions and pay a fee for it which is capped by the user. + + ## Useful commands ### Install @@ -30,6 +76,8 @@ yarn deploy ### Interact with registry +The Registry used in this demo is a open test Registry (so no verification of Plugins or any other listing requirements). + - Register the Sample Plugin on the Registry ```bash yarn register-plugin diff --git a/contracts/contracts/Base.sol b/contracts/contracts/Base.sol new file mode 100644 index 0000000..753f6bf --- /dev/null +++ b/contracts/contracts/Base.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.18; + +import {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Integrations.sol"; + +enum MetadataProviderType { + IPFS, + URL, + Contract, + Event +} + +interface IMetadataProvider { + function retrieveMetadata(bytes32 metadataHash) external view returns (bytes memory metadata); +} + +struct PluginMetadata { + string name; + string version; + bool requiresRootAccess; + string iconUrl; + string appUrl; +} + +library PluginMetadataOps { + function encode(PluginMetadata memory data) internal pure returns (bytes memory) { + return + abi.encodePacked( + uint8(0x00), // Format + uint8(0x00), // Format version + abi.encode(data.name, data.version, data.requiresRootAccess, data.iconUrl, data.appUrl) // Plugin Metadata + ); + } + + function decode(bytes calldata data) internal pure returns (PluginMetadata memory) { + require(bytes16(data[0:2]) == bytes16(0x0000), "Unsupported format or format version"); + (string memory name, string memory version, bool requiresRootAccess, string memory iconUrl, string memory appUrl) = abi.decode( + data[2:], + (string, string, bool, string, string) + ); + return PluginMetadata(name, version, requiresRootAccess, iconUrl, appUrl); + } +} + +abstract contract BasePlugin is ISafeProtocolPlugin { + using PluginMetadataOps for PluginMetadata; + + string public name; + string public version; + bool public immutable requiresRootAccess; + bytes32 public immutable metadataHash; + + constructor(PluginMetadata memory metadata) { + name = metadata.name; + version = metadata.version; + requiresRootAccess = metadata.requiresRootAccess; + metadataHash = keccak256(metadata.encode()); + } +} + +abstract contract BasePluginWithStoredMetadata is BasePlugin, IMetadataProvider { + using PluginMetadataOps for PluginMetadata; + + bytes private encodedMetadata; + + constructor(PluginMetadata memory metadata) BasePlugin(metadata) { + encodedMetadata = metadata.encode(); + } + + function retrieveMetadata(bytes32 _metadataHash) external view override returns (bytes memory metadata) { + require(metadataHash == _metadataHash, "Cannot retrieve metadata"); + return encodedMetadata; + } + + function metadataProvider() public view override returns (uint256 providerType, bytes memory location) { + providerType = uint256(MetadataProviderType.Contract); + location = abi.encode(address(this)); + } +} + +abstract contract BasePluginWithEventMetadata is BasePlugin { + using PluginMetadataOps for PluginMetadata; + + event Metadata(bytes32 indexed metadataHash, bytes data); + + constructor(PluginMetadata memory metadata) BasePlugin(metadata) { + emit Metadata(metadataHash, metadata.encode()); + } + + function metadataProvider() public view override returns (uint256 providerType, bytes memory location) { + providerType = uint256(MetadataProviderType.Event); + location = abi.encode(address(this)); + } +} diff --git a/contracts/contracts/Plugins.sol b/contracts/contracts/Plugins.sol index 4d90566..5b98946 100644 --- a/contracts/contracts/Plugins.sol +++ b/contracts/contracts/Plugins.sol @@ -1,94 +1,92 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.18; +import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol"; import {ISafe} from "@safe-global/safe-core-protocol/contracts/interfaces/Accounts.sol"; -import {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Integrations.sol"; import {ISafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/interfaces/Manager.sol"; -import {SafeTransaction, SafeRootAccess} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; +import {SafeTransaction, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; +import {_getFeeCollectorRelayContext, _getFeeTokenRelayContext, _getFeeRelayContext} from "@gelatonetwork/relay-context/contracts/GelatoRelayContext.sol"; -enum MetadataProviderType { - IPFS, - URL, - Contract, - Event -} - -interface MetadataProvider { - function retrieveMetadata(bytes32 metadataHash) external view returns (bytes memory metadata); -} - -struct PluginMetadata { - string name; - string version; - bool requiresRootAccess; - string iconUrl; - string appUrl; -} - -library PluginMetadataOps { - function encode(PluginMetadata memory data) internal pure returns (bytes memory) { - return - abi.encodePacked( - uint8(0x00), // Format - uint8(0x00), // Format version - abi.encode(data.name, data.version, data.requiresRootAccess, data.iconUrl, data.appUrl) // Plugin Metadata - ); - } +address constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - function decode(bytes calldata data) internal pure returns (PluginMetadata memory) { - require(bytes16(data[0:2]) == bytes16(0x0000), "Unsupported format or format version"); - (string memory name, string memory version, bool requiresRootAccess, string memory iconUrl, string memory appUrl) = abi.decode( - data[2:], - (string, string, bool, string, string) - ); - return PluginMetadata(name, version, requiresRootAccess, iconUrl, appUrl); - } -} +contract RelayPlugin is BasePluginWithEventMetadata { + event MaxFeeUpdated(address indexed account, address indexed feeToken, uint256 maxFee); -abstract contract BasePlugin is ISafeProtocolPlugin, MetadataProvider { - using PluginMetadataOps for PluginMetadata; + error FeeTooHigh(address feeToken, uint256 fee); + error FeePaymentFailure(bytes data); + error UntrustedOrigin(address origin); + error RelayExecutionFailure(bytes data); + error InvalidRelayMethod(bytes4 data); - string public name; - string public version; - bool public immutable requiresRootAccess; - bytes32 public immutable metadataHash; - bytes private encodedMetadata; + address public immutable trustedOrigin; + bytes4 public immutable relayMethod; - constructor(PluginMetadata memory metadata) { - name = metadata.name; - version = metadata.version; - requiresRootAccess = metadata.requiresRootAccess; - // Metadata Format + Format Version + Encoded Metadata - encodedMetadata = metadata.encode(); - metadataHash = keccak256(encodedMetadata); - } + // Account => token => maxFee + mapping(address => mapping(address => uint256)) public maxFeePerToken; - // TODO: Legacy version that should be removed - function metaProvider() external view override returns (uint256 providerType, bytes memory location) { - return metadataProvider(); + constructor( + address _trustedOrigin, + bytes4 _relayMethod + ) + BasePluginWithEventMetadata( + PluginMetadata({ + name: "Relay Plugin", + version: "1.0.0", + requiresRootAccess: false, + iconUrl: "", + appUrl: "https://5afe.github.io/safe-core-protocol-demo/#/relay/${plugin}" + }) + ) + { + trustedOrigin = _trustedOrigin; + relayMethod = _relayMethod; } - function metadataProvider() public view returns (uint256 providerType, bytes memory location) { - providerType = uint256(MetadataProviderType.Contract); - location = abi.encode(address(this)); + function setMaxFeePerToken(address token, uint256 maxFee) external { + maxFeePerToken[msg.sender][token] = maxFee; + emit MaxFeeUpdated(msg.sender, token, maxFee); } - function retrieveMetadata(bytes32 _metadataHash) external view returns (bytes memory metadata) { - require(metadataHash == _metadataHash, "Cannot retrieve metadata"); - return encodedMetadata; + function payFee(ISafeProtocolManager manager, ISafe safe, uint256 nonce) internal { + address feeCollector = _getFeeCollectorRelayContext(); + address feeToken = _getFeeTokenRelayContext(); + uint256 fee = _getFeeRelayContext(); + SafeProtocolAction[] memory actions = new SafeProtocolAction[](1); + uint256 maxFee = maxFeePerToken[address(safe)][feeToken]; + if (fee > maxFee) revert FeeTooHigh(feeToken, fee); + if (feeToken == NATIVE_TOKEN || feeToken == address(0)) { + // If the native token is used for fee payment, then we directly send the fees to the fee collector + actions[0].to = payable(feeCollector); + actions[0].value = fee; + actions[0].data = ""; + } else { + // If a ERC20 token is used for fee payment, then we trigger a token transfer on the token for the fee to the fee collector + actions[0].to = payable(feeToken); + actions[0].value = 0; + actions[0].data = abi.encodeWithSignature("transfer(address,uint256)", feeCollector, fee); + } + // Note: Metadata format has not been proposed + SafeTransaction memory safeTx = SafeTransaction({actions: actions, nonce: nonce, metadataHash: bytes32(0)}); + try manager.executeTransaction(safe, safeTx) returns (bytes[] memory) {} catch (bytes memory reason) { + revert FeePaymentFailure(reason); + } } -} -contract SamplePlugin is BasePlugin { - ISafeProtocolManager public immutable manager; + function relayCall(address relayTarget, bytes calldata relayData) internal { + // Check relay data to avoid that module can be abused for arbitrary interactions + if (bytes4(relayData[:4]) != relayMethod) revert InvalidRelayMethod(bytes4(relayData[:4])); - constructor( - ISafeProtocolManager _manager - ) BasePlugin(PluginMetadata({name: "Sample Plugin", version: "1.0.0", requiresRootAccess: false, iconUrl: "", appUrl: ""})) { - manager = _manager; + // Perform relay call and require success to avoid that user paid for failed transaction + (bool success, bytes memory data) = relayTarget.call(relayData); + if (!success) revert RelayExecutionFailure(data); } - function executeFromPlugin(ISafe safe, SafeTransaction calldata safetx) external returns (bytes[] memory data) { - (data) = manager.executeTransaction(safe, safetx); + function executeFromPlugin(ISafeProtocolManager manager, ISafe safe, bytes calldata data) external { + if (trustedOrigin != address(0) && msg.sender != trustedOrigin) revert UntrustedOrigin(msg.sender); + + relayCall(address(safe), data); + // We use the hash of the tx to relay has a nonce as this is unique + uint256 nonce = uint256(keccak256(abi.encode(this, manager, safe, data, block.number))); + payFee(manager, safe, nonce); } } diff --git a/contracts/package.json b/contracts/package.json index 9ee83d9..fd7facc 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -24,6 +24,7 @@ "prepack": "yarn build" }, "devDependencies": { + "@gelatonetwork/relay-context": "^2.1.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", "@nomicfoundation/hardhat-ethers": "^3.0.0", "@nomicfoundation/hardhat-network-helpers": "^1.0.0", @@ -31,8 +32,8 @@ "@nomicfoundation/hardhat-verify": "^1.0.0", "@openzeppelin/contracts": "^4.9.1", "@safe-global/mock-contract": "^4.0.0", + "@safe-global/safe-core-protocol": "^0.1.0-alpha.4", "@safe-global/safe-singleton-factory": "^1.0.14", - "@safe-global/safe-core-protocol": "^0.1.0-alpha.3", "@typechain/ethers-v6": "^0.4.0", "@typechain/hardhat": "^8.0.0", "@types/chai": "^4.2.0", diff --git a/contracts/src/deploy/deploy_plugin.ts b/contracts/src/deploy/deploy_plugin.ts index 7c3b288..e54b55e 100644 --- a/contracts/src/deploy/deploy_plugin.ts +++ b/contracts/src/deploy/deploy_plugin.ts @@ -1,18 +1,22 @@ import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { getProtocolManagerAddress } from "../utils/protocol"; +import { getGelatoAddress } from "@gelatonetwork/relay-context"; +import { ZeroAddress } from "ethers"; const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; const { deployer } = await getNamedAccounts(); const { deploy } = deployments; - const manager = await getProtocolManagerAddress(hre) - console.log({manager}) - - await deploy("SamplePlugin", { + // execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes) + // https://www.4byte.directory/signatures/?bytes4_signature=0x6a761202 + const relayMethod = "0x6a761202" + // We don't use a trusted origin right now to make it easier to test. + // For production networks it is strongly recommended to set one to avoid potential fee extraction. + const trustedOrigin = ZeroAddress // hre.network.name === "hardhat" ? ZeroAddress : getGelatoAddress(hre.network.name) + await deploy("RelayPlugin", { from: deployer, - args: [manager], + args: [trustedOrigin, relayMethod], log: true, deterministicDeployment: true, }); diff --git a/contracts/src/tasks/test_registry.ts b/contracts/src/tasks/test_registry.ts index ad5ca27..5f58e12 100644 --- a/contracts/src/tasks/test_registry.ts +++ b/contracts/src/tasks/test_registry.ts @@ -1,14 +1,14 @@ import "hardhat-deploy"; import "@nomicfoundation/hardhat-ethers"; import { task } from "hardhat/config"; -import { getPlugin, getRegistry, getSamplePlugin } from "../utils/contracts"; +import { getPlugin, getRegistry, getRelayPlugin } from "../utils/contracts"; import { IntegrationType } from "../utils/constants"; import { loadPluginMetadata } from "../utils/metadata"; task("register-plugin", "Registers the sample Plugin in the Safe{Core} test register") .setAction(async (_, hre) => { const registry = await getRegistry(hre) - const plugin = await getSamplePlugin(hre) + const plugin = await getRelayPlugin(hre) await registry.addIntegration(await plugin.getAddress(), IntegrationType.Plugin) console.log("Registered Plugin registry") }); diff --git a/contracts/src/utils/contracts.ts b/contracts/src/utils/contracts.ts index 7684ec9..fef91ae 100644 --- a/contracts/src/utils/contracts.ts +++ b/contracts/src/utils/contracts.ts @@ -1,5 +1,5 @@ import { BaseContract } from "ethers"; -import { BasePlugin, SamplePlugin, TestSafeProtocolRegistryUnrestricted } from "../../typechain-types"; +import { BasePlugin, RelayPlugin, TestSafeProtocolRegistryUnrestricted } from "../../typechain-types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { getProtocolRegistryAddress } from "./protocol"; @@ -14,5 +14,5 @@ export const getSingleton = async (hre: HardhatRuntimeEn }; export const getPlugin = (hre: HardhatRuntimeEnvironment, address: string) => getInstance(hre, "BasePlugin", address); -export const getSamplePlugin = (hre: HardhatRuntimeEnvironment) => getSingleton(hre, "SamplePlugin"); +export const getRelayPlugin = (hre: HardhatRuntimeEnvironment) => getSingleton(hre, "RelayPlugin"); export const getRegistry = async (hre: HardhatRuntimeEnvironment) => getInstance(hre, "TestSafeProtocolRegistryUnrestricted", await getProtocolRegistryAddress(hre)); diff --git a/contracts/src/utils/metadata.ts b/contracts/src/utils/metadata.ts index a3570d1..2e1691a 100644 --- a/contracts/src/utils/metadata.ts +++ b/contracts/src/utils/metadata.ts @@ -1,5 +1,5 @@ -import { AbiCoder, isHexString, keccak256 } from "ethers"; -import { BasePlugin, MetadataProvider } from "../../typechain-types"; +import { AbiCoder, Interface, isHexString, keccak256 } from "ethers"; +import { BasePlugin, IMetadataProvider } from "../../typechain-types"; import { getInstance } from "../utils/contracts"; import { HardhatRuntimeEnvironment } from "hardhat/types"; @@ -14,20 +14,35 @@ interface PluginMetadata { // const ProviderType_IPFS = 0n; // const ProviderType_URL = 1n; const ProviderType_Contract = 2n; -// const ProviderType_Event = 3n; +const ProviderType_Event = BigInt(3); +const MetadataEvent: string[] = ["event Metadata(bytes32 indexed metadataHash, bytes data)"] const PluginMetadataType: string[] = ["string name", "string version", "bool requiresRootAccess", "string iconUrl", "string appUrl"]; const loadPluginMetadataFromContract = async (hre: HardhatRuntimeEnvironment, provider: string, metadataHash: string): Promise => { - const providerInstance = await getInstance(hre, "MetadataProvider", provider); + const providerInstance = await getInstance(hre, "IMetadataProvider", provider); return await providerInstance.retrieveMetadata(metadataHash); }; +const loadPluginMetadataFromEvent = async (hre: HardhatRuntimeEnvironment, provider: string, metadataHash: string): Promise => { + const eventInterface = new Interface(MetadataEvent) + const events = await hre.ethers.provider.getLogs({ + address: provider, + topics: eventInterface.encodeFilterTopics("Metadata", [metadataHash]) + }) + if (events.length == 0) throw Error("Metadata not found"); + const metadataEvent = events[events.length - 1]; + const decodedEvent = eventInterface.decodeEventLog("Metadata", metadataEvent.data, metadataEvent.topics) + return decodedEvent.data; +}; + const loadRawMetadata = async (hre: HardhatRuntimeEnvironment, plugin: BasePlugin, metadataHash: string): Promise => { const [type, source] = await plugin.metadataProvider(); switch (type) { case ProviderType_Contract: return loadPluginMetadataFromContract(hre, AbiCoder.defaultAbiCoder().decode(["address"], source)[0], metadataHash); + case ProviderType_Event: + return loadPluginMetadataFromEvent(hre, AbiCoder.defaultAbiCoder().decode(["address"], source)[0], metadataHash); default: throw Error("Unsupported MetadataProviderType"); } diff --git a/contracts/src/utils/protocol.ts b/contracts/src/utils/protocol.ts index 6cd388d..8eac372 100644 --- a/contracts/src/utils/protocol.ts +++ b/contracts/src/utils/protocol.ts @@ -21,8 +21,6 @@ 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 @@ -35,8 +33,6 @@ 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 diff --git a/contracts/test/SamplePlugin.spec.ts b/contracts/test/RelayPlugin.spec.ts similarity index 69% rename from contracts/test/SamplePlugin.spec.ts rename to contracts/test/RelayPlugin.spec.ts index 08172be..0d212ec 100644 --- a/contracts/test/SamplePlugin.spec.ts +++ b/contracts/test/RelayPlugin.spec.ts @@ -1,19 +1,20 @@ import hre, { deployments } from "hardhat"; import { expect } from "chai"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { getSamplePlugin } from "../src/utils/contracts"; +import { getRelayPlugin } from "../src/utils/contracts"; import { loadPluginMetadata } from "../src/utils/metadata"; describe("SamplePlugin", async () => { - let user1: SignerWithAddress; + let relayer: SignerWithAddress; before(async () => { - [user1] = await hre.ethers.getSigners(); + [relayer] = await hre.ethers.getSigners(); + console.log("Relayer: ", relayer.address); }); const setup = deployments.createFixture(async ({ deployments }) => { await deployments.fixture(); - const plugin = await getSamplePlugin(hre); + const plugin = await getRelayPlugin(hre); return { plugin, }; @@ -21,8 +22,7 @@ describe("SamplePlugin", async () => { it("should be inititalized correctly", async () => { const { plugin } = await setup(); - console.log(user1); - expect(await plugin.name()).to.be.eq("Sample Plugin"); + expect(await plugin.name()).to.be.eq("Relay Plugin"); expect(await plugin.version()).to.be.eq("1.0.0"); expect(await plugin.requiresRootAccess()).to.be.false; }); @@ -30,11 +30,11 @@ describe("SamplePlugin", async () => { it("can retrieve metadata for module", async () => { const { plugin } = await setup(); expect(await loadPluginMetadata(hre, plugin)).to.be.deep.eq({ - name: "Sample Plugin", + name: "Relay Plugin", version: "1.0.0", requiresRootAccess: false, iconUrl: "", - appUrl: "", + appUrl: "https://5afe.github.io/safe-core-protocol-demo/#/relay/${plugin}", }); }); }); diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 5cfc65a..352354e 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -450,6 +450,13 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@gelatonetwork/relay-context@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@gelatonetwork/relay-context/-/relay-context-2.1.0.tgz#46eef1162c7dca16031baeb528edceca686d9a3e" + integrity sha512-Pisn66Haq1OEBqrj9t8aPS2B7qX1j/AI0mDZNczeAws8JMn7PhZXLKcWYleAWUitlzFS59w4qS3Y/l7/8oWEiw== + dependencies: + "@openzeppelin/contracts" "4.8.0" + "@humanwhocodes/config-array@^0.11.10": version "0.11.10" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" @@ -778,6 +785,11 @@ "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.1.1" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.1" +"@openzeppelin/contracts@4.8.0": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" + integrity sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw== + "@openzeppelin/contracts@^4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.1.tgz#afa804d2c68398704b0175acc94d91a54f203645" @@ -788,10 +800,10 @@ resolved "https://registry.yarnpkg.com/@safe-global/mock-contract/-/mock-contract-4.0.0.tgz#8e1e17e93af5d4b343a6bb6cef8c1f513cb7a92e" integrity sha512-6ijStTgQI6JzYe8Nsc4j1VW4XQ89qCl7ZkRGxwwlxnaOMDYzVekwPACbg2kDDzhtJ4p8vSvE6ZroxSkvP7610A== -"@safe-global/safe-core-protocol@^0.1.0-alpha.3": - version "0.1.0-alpha.3" - resolved "https://registry.yarnpkg.com/@safe-global/safe-core-protocol/-/safe-core-protocol-0.1.0-alpha.3.tgz#355b81283b4ce611aa651af76186b1cd91ea0f45" - integrity sha512-iXIaqOrPuJVkTyLHlnzxa5+/oicK43snL6iH/m1f7nFqySvUlXcV29MeS9b3S5a6k8r3cMX9IlmK6GpWk0tx4w== +"@safe-global/safe-core-protocol@^0.1.0-alpha.4": + version "0.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@safe-global/safe-core-protocol/-/safe-core-protocol-0.1.0-alpha.4.tgz#a00622198458551c8cf4f339872939be5de0d233" + integrity sha512-d+2qnyqf3WdERlxwxZAxxTk0N0MYfr+ICqIem78rDYKlRYxbnM7AKlXXSiYJbPNADZ49cakPmib59Rs0IK93Kg== "@safe-global/safe-singleton-factory@^1.0.14": version "1.0.14" diff --git a/web/README.md b/web/README.md index b58e0af..c55e289 100644 --- a/web/README.md +++ b/web/README.md @@ -1,6 +1,18 @@ -# Getting Started with Create React App +# Safe{Core} Protocol Demo - Web App -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +This web app demonstrates how to interact with the Registry, the Manager and the [Sample Plugin](../contracts/). For this the following use cases are covered: + +- [Manage Plugins](../src/routes/plugins) + - The a list of all available plugins + - Enable/disable a plugin + - Open plugin app page +- [Sample Plugin App Page](../src/routes/samples) + - Configure Sample Plugin + - Use Sample Plugin + +### Sample Plugins + +- [Relay Plugin](../src/routes/samples/relay) - A plugin that allows to relay Safe transactions and pay a fee for it which is capped by the user. ## Available Scripts @@ -14,11 +26,6 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.\ You will also see any lint errors in the console. -### `yarn test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - ### `yarn build` Builds the app for production to the `build` folder.\ @@ -27,20 +34,6 @@ It correctly bundles React in production mode and optimizes the build for the be The build is minified and the filenames include the hashes.\ Your app is ready to be deployed! -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +### `yarn deploy` -To learn React, check out the [React documentation](https://reactjs.org/). +Deploys the current web app version as a production build to [GitHub pages](https://pages.github.com) \ No newline at end of file diff --git a/web/package.json b/web/package.json index e87ce73..898a5a9 100644 --- a/web/package.json +++ b/web/package.json @@ -6,11 +6,12 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@gelatonetwork/relay-sdk": "^3.1.0", "@mui/icons-material": "^5.14.0", "@mui/material": "^5.14.0", "@safe-global/safe-apps-provider": "^0.17.1", "@safe-global/safe-apps-sdk": "^8.0.0", - "@safe-global/safe-core-protocol": "^0.1.0-alpha.3", + "@safe-global/safe-core-protocol": "^0.1.0-alpha.4", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", @@ -18,6 +19,7 @@ "@types/node": "^16.7.13", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "axios": "^1.4.0", "blockies-ts": "^1.0.0", "ethers": "^6.6.3", "react": "^18.2.0", diff --git a/web/public/logo.png b/web/public/logo.png new file mode 100644 index 0000000..be11294 Binary files /dev/null and b/web/public/logo.png differ diff --git a/web/public/logo192.png b/web/public/logo192.png index fc44b0a..7fecc8b 100644 Binary files a/web/public/logo192.png and b/web/public/logo192.png differ diff --git a/web/public/logo512.png b/web/public/logo512.png index a4e47a6..4a4cafb 100644 Binary files a/web/public/logo512.png and b/web/public/logo512.png differ diff --git a/web/public/manifest.json b/web/public/manifest.json index 9df18f0..d99e2ff 100644 --- a/web/public/manifest.json +++ b/web/public/manifest.json @@ -1,7 +1,7 @@ { "short_name": "Safe{Core} Protocol Demo", "name": "Safe{Core} Protocol Demo", - "description": "Safe{Core} Protocol Demo", + "description": "App to test the interaction with Plugins from the Safe{Core} Protocol. Manage and configure Plugins for your Account.", "iconPath": "logo192.png", "icons": [ { diff --git a/web/src/index.tsx b/web/src/index.tsx index 9bd23fd..effe605 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -8,16 +8,23 @@ import { RouterProvider, } from "react-router-dom"; import PluginList from './routes/plugins/PluginList'; +import { RelayPlugin } from './routes/samples/relay/RelayPlugin'; const router = createHashRouter([ { path: "/", + index: true, element: , + errorElement: , }, { path: "/plugins", element: , }, + { + path: "/relay/:pluginAddress", + element: , + }, ]); const root = ReactDOM.createRoot( diff --git a/web/src/logic/metadata.ts b/web/src/logic/metadata.ts index 2ea50b9..8cd5679 100644 --- a/web/src/logic/metadata.ts +++ b/web/src/logic/metadata.ts @@ -1,5 +1,6 @@ -import { AbiCoder, Contract, isHexString, keccak256 } from "ethers"; +import { AbiCoder, Contract, Interface, isHexString, keccak256, getAddress } from "ethers"; import { getMetadataProvider } from "./protocol"; +import { getProvider } from "./web3"; export interface PluginMetadata { name: string; @@ -12,8 +13,9 @@ export interface PluginMetadata { // const ProviderType_IPFS = BigInt(0); // const ProviderType_URL = BigInt(1); const ProviderType_Contract = BigInt(2); -// const ProviderType_Event = BigInt(3); +const ProviderType_Event = BigInt(3); +const MetadataEvent: string[] = ["event Metadata(bytes32 indexed metadataHash, bytes data)"] const PluginMetadataType: string[] = ["string name", "string version", "bool requiresRootAccess", "string iconUrl", "string appUrl"]; const loadPluginMetadataFromContract = async (provider: string, metadataHash: string): Promise => { @@ -21,26 +23,46 @@ const loadPluginMetadataFromContract = async (provider: string, metadataHash: st return await providerInstance.retrieveMetadata(metadataHash); }; +const loadPluginMetadataFromEvent = async (provider: string, metadataHash: string): Promise => { + const web3Provider = await getProvider() + const eventInterface = new Interface(MetadataEvent) + const events = await web3Provider.getLogs({ + fromBlock: "earliest", + toBlock: "latest", + address: provider, + topics: eventInterface.encodeFilterTopics("Metadata", [metadataHash]) + }) + if (events.length == 0) throw Error("Metadata not found"); + const metadataEvent = events[events.length - 1]; + const decodedEvent = eventInterface.decodeEventLog("Metadata", metadataEvent.data, metadataEvent.topics) + return decodedEvent.data; +}; + + const loadRawMetadata = async (plugin: Contract, metadataHash: string): Promise => { const [type, source] = await plugin.metadataProvider(); - console.log(typeof type) switch (type) { case ProviderType_Contract: return loadPluginMetadataFromContract(AbiCoder.defaultAbiCoder().decode(["address"], source)[0], metadataHash); + case ProviderType_Event: + return loadPluginMetadataFromEvent(AbiCoder.defaultAbiCoder().decode(["address"], source)[0], metadataHash); default: throw Error("Unsupported MetadataProviderType"); } }; -export const loadPluginMetadata = async (plugin: Contract): Promise => { - console.log({plugin}) - const metadataHash = await plugin.metadataHash(); - const metadata = await loadRawMetadata(plugin, metadataHash); - if (metadataHash !== keccak256(metadata)) throw Error("Invalid metadata retrieved!"); - return decodePluginMetadata(metadata); -}; +const parseAppUrl = (rawUrl: string, pluginAddress: string | undefined) => { + // Check if URL contain template for plugin address + let parsedUrl = rawUrl; + if (rawUrl.indexOf("${plugin}") >= 0) { + // This will throw if no address is provided, but that is ok for now + const address = getAddress(pluginAddress!!) + parsedUrl = parsedUrl.replaceAll("${plugin}", address) + } + return parsedUrl +} -export const decodePluginMetadata = (data: string): PluginMetadata => { +export const decodePluginMetadata = (data: string, pluginAddress?: string): PluginMetadata => { if (!isHexString(data)) throw Error("Invalid data format"); const format = data.slice(2, 6); if (format !== "0000") throw Error("Unsupported format or format version"); @@ -51,6 +73,14 @@ export const decodePluginMetadata = (data: string): PluginMetadata => { version: decoded[1], requiresRootAccess: decoded[2], iconUrl: decoded[3], - appUrl: decoded[4], + appUrl: parseAppUrl(decoded[4], pluginAddress), }; }; + +export const loadPluginMetadata = async (plugin: Contract): Promise => { + console.log({plugin}) + const metadataHash = await plugin.metadataHash(); + const metadata = await loadRawMetadata(plugin, metadataHash); + if (metadataHash !== keccak256(metadata)) throw Error("Invalid metadata retrieved!"); + return decodePluginMetadata(metadata, await plugin.getAddress()); +}; diff --git a/web/src/logic/plugins.ts b/web/src/logic/plugins.ts index d2b353e..bf7d7ef 100644 --- a/web/src/logic/plugins.ts +++ b/web/src/logic/plugins.ts @@ -2,7 +2,7 @@ import { ZeroAddress, EventLog } from "ethers"; import { BaseTransaction } from '@safe-global/safe-apps-sdk'; import { PluginMetadata, loadPluginMetadata } from "./metadata"; import { getManager, getPlugin, getRegistry } from "./protocol"; -import { getSafeInfo, isConnectToSafe, submitTxs } from "./safeapp"; +import { getSafeInfo, isConnectedToSafe, submitTxs } from "./safeapp"; import { isModuleEnabled, buildEnableModule } from "./safe"; const SENTINEL_MODULES = "0x0000000000000000000000000000000000000001" @@ -15,7 +15,7 @@ export interface PluginDetails { export const loadPluginDetails = async(pluginAddress: string): Promise => { const plugin = await getPlugin(pluginAddress) const metadata = await loadPluginMetadata(plugin) - if (!await isConnectToSafe()) return { metadata } + if (!await isConnectedToSafe()) return { metadata } const enabled = await isPluginEnabled(pluginAddress) return { metadata, enabled } } @@ -31,7 +31,7 @@ export const loadPlugins = async(filterFlagged: boolean = true): Promise { - if (!await isConnectToSafe()) throw Error("Not connected to a Safe") + if (!await isConnectedToSafe()) throw Error("Not connected to a Safe") const manager = await getManager() const safeInfo = await getSafeInfo() const pluginInfo = await manager.enabledPlugins(safeInfo.safeAddress, plugin) @@ -39,7 +39,7 @@ export const isPluginEnabled = async(plugin: string) => { } export const loadEnabledPlugins = async(): Promise => { - if (!await isConnectToSafe()) throw Error("Not connected to a Safe") + if (!await isConnectedToSafe()) 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) @@ -56,7 +56,7 @@ const buildEnablePlugin = async(plugin: string, requiresRootAccess: boolean): Pr } export const enablePlugin = async(plugin: string, requiresRootAccess: boolean) => { - if (!await isConnectToSafe()) throw Error("Not connected to a Safe") + if (!await isConnectedToSafe()) throw Error("Not connected to a Safe") const manager = await getManager() const managerAddress = await manager.getAddress() const info = await getSafeInfo() @@ -81,7 +81,7 @@ const buildDisablePlugin = async(pointer: string, plugin: string): Promise { - if (!await isConnectToSafe()) throw Error("Not connected to a Safe") + if (!await isConnectedToSafe()) throw Error("Not connected to a Safe") const manager = await getManager() const txs: BaseTransaction[] = [] const enabledPlugins = await loadEnabledPlugins() diff --git a/web/src/logic/protocol.ts b/web/src/logic/protocol.ts index e2f6f69..43ccfe2 100644 --- a/web/src/logic/protocol.ts +++ b/web/src/logic/protocol.ts @@ -13,10 +13,7 @@ const PLUGIN_ABI = [ export const getManager = async() => { const provider = await getProvider() - const registryInfo = { - address: "0xb4Dc1B282706aB473cdD1b9899c57baD2BD3e2f3", - abi: protocolDeployments[5][0].contracts.SafeProtocolManager.abi - }; + const registryInfo = protocolDeployments[5][0].contracts.TestSafeProtocolManager; return new ethers.Contract( registryInfo.address, registryInfo.abi, @@ -28,7 +25,7 @@ export const getRegistry = async() => { const provider = await getProvider() const registryInfo = protocolDeployments[5][0].contracts.TestSafeProtocolRegistryUnrestricted; return new ethers.Contract( - "0x9EFbBcAD12034BC310581B9837D545A951761F5A", + registryInfo.address, registryInfo.abi, provider ) diff --git a/web/src/logic/safe.ts b/web/src/logic/safe.ts index 4b0a925..7e48990 100644 --- a/web/src/logic/safe.ts +++ b/web/src/logic/safe.ts @@ -1,13 +1,16 @@ -import { ethers } from "ethers" +import { ethers, BigNumberish, getAddress } from "ethers" import { getProvider } from "./web3"; import { BaseTransaction } from '@safe-global/safe-apps-sdk'; +import { SafeMultisigConfirmation, SafeMultisigTransaction } from "./services"; const SAFE_ABI = [ "function isModuleEnabled(address module) public view returns (bool)", - "function enableModule(address module) public" + "function nonce() public view returns (uint256)", + "function enableModule(address module) public", + "function execTransaction(address to,uint256 value,bytes calldata data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address payable refundReceiver,bytes memory signatures) public payable returns (bool success)" ] -// TODO: use safe-core-sdk here +// TODO: use safe-core-sdk here once Ethers v6 is supported const getSafe = async(address: string) => { const provider = await getProvider() return new ethers.Contract( @@ -22,6 +25,11 @@ export const isModuleEnabled = async(safeAddress: string, module: string): Promi return await safe.isModuleEnabled(module) } +export const getCurrentNonce = async(safeAddress: string): Promise => { + const safe = await getSafe(safeAddress) + return await safe.nonce() +} + export const buildEnableModule = async(safeAddress: string, module: string): Promise => { const safe = await getSafe(safeAddress) return { @@ -29,4 +37,65 @@ export const buildEnableModule = async(safeAddress: string, module: string): Pro value: "0", data: (await safe.enableModule.populateTransaction(module)).data } -} \ No newline at end of file +} + +export const buildSignatureBytes = (signatures: SafeMultisigConfirmation[]): string => { + const SIGNATURE_LENGTH_BYTES = 65; + signatures.sort((left, right) => left.owner.toLowerCase().localeCompare(right.owner.toLowerCase())); + + let signatureBytes = "0x"; + let dynamicBytes = ""; + for (const sig of signatures) { + if (sig.signatureType === "CONTRACT_SIGNATURE") { + /* + A contract signature has a static part of 65 bytes and the dynamic part that needs to be appended at the end of + end signature bytes. + The signature format is + Signature type == 0 + Constant part: 65 bytes + {32-bytes signature verifier}{32-bytes dynamic data position}{1-byte signature type} + Dynamic part (solidity bytes): 32 bytes + signature data length + {32-bytes signature length}{bytes signature data} + */ + const dynamicPartPosition = (signatures.length * SIGNATURE_LENGTH_BYTES + dynamicBytes.length / 2) + .toString(16) + .padStart(64, "0"); + const dynamicPartLength = (sig.signature.slice(2).length / 2).toString(16).padStart(64, "0"); + const staticSignature = `${sig.owner.slice(2).padStart(64, "0")}${dynamicPartPosition}00`; + const dynamicPartWithLength = `${dynamicPartLength}${sig.signature.slice(2)}`; + + signatureBytes += staticSignature; + dynamicBytes += dynamicPartWithLength; + } else { + signatureBytes += sig.signature.slice(2); + } + } + + return signatureBytes + dynamicBytes; +}; + +const getExecuteTxData = async ( + safeTx: SafeMultisigTransaction +): Promise => { + const safe = await getSafe(safeTx.safe) + console.log(safeTx) + return (await safe.execTransaction.populateTransaction( + safeTx.to, + safeTx.value, + safeTx.data, + safeTx.operation, + safeTx.safeTxGas, + safeTx.baseGas, + safeTx.gasPrice, + safeTx.gasToken, + safeTx.refundReceiver, + buildSignatureBytes(safeTx.confirmations!!) + )).data; +}; + +export const buildExecuteTx = async (tx: SafeMultisigTransaction): Promise<{to: string, data: string}> => { + return { + to: getAddress(tx.safe), + data: await getExecuteTxData(tx) + } +} \ No newline at end of file diff --git a/web/src/logic/safeapp.ts b/web/src/logic/safeapp.ts index f14c9a7..b549c3a 100644 --- a/web/src/logic/safeapp.ts +++ b/web/src/logic/safeapp.ts @@ -16,7 +16,7 @@ export const getSafeInfo = async() => { return cachedSafeInfo; } -export const isConnectToSafe = async() => { +export const isConnectedToSafe = async() => { try { const safeInfo = await Promise.race([ waitAndError(300), @@ -40,7 +40,7 @@ export const submitTxs = async(txs: BaseTransaction[]): Promise => { } export const openSafeApp = async(appUrl: string) => { - if (!isConnectToSafe()) return + if (!isConnectedToSafe()) return const safe = await getSafeInfo() const environmentInfo = await safeAppsSDK.safe.getEnvironmentInfo() const origin = environmentInfo.origin; @@ -48,7 +48,7 @@ export const openSafeApp = async(appUrl: string) => { const networkPrefix = chainInfo.shortName if (origin?.length) { window.open( - `${origin}/apps/open?safe=${networkPrefix}:${safe.safeAddress}&appUrl=${appUrl}`, + `${origin}/apps/open?safe=${networkPrefix}:${safe.safeAddress}&appUrl=${encodeURIComponent(appUrl)}`, '_blank', ) } diff --git a/web/src/logic/sample.ts b/web/src/logic/sample.ts new file mode 100644 index 0000000..c8e043c --- /dev/null +++ b/web/src/logic/sample.ts @@ -0,0 +1,126 @@ +import { ethers, getAddress, ZeroAddress } from "ethers" +import { getProvider } from "./web3"; +import { GelatoRelay } from "@gelatonetwork/relay-sdk" +import { submitTxs } from "./safeapp"; +import { getManager } from "./protocol"; +import { getCurrentNonce } from "./safe"; +import { getSafeMultisigTxs, SafeMultisigTransaction } from "./services"; + +const SAMPLE_PLUGIN_CHAIN_ID = 5 +const SAMPLE_PLUGIN_ADDRESS = getAddress("0xA68799b8f1F2535ba88530FeD2300cFC69D4ABd1") +export const NATIVE_TOKEN = getAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"); +const SAMPLE_PLUGIN_ABI = [ + "function maxFeePerToken(address account, address token) public view returns (uint256 maxFee)", + "function setMaxFeePerToken(address token, uint256 maxFee) external", + "function executeFromPlugin(address manager, address safe, bytes calldata data) external" +] +const ECR20_ABI = [ + "function decimals() public view returns (uint256 decimals)", + "function symbol() public view returns (string symbol)", +] + +const gelato = new GelatoRelay() + +export interface TokenInfo { + address: string, + symbol: string, + decimals: bigint +} + +export const isKnownSamplePlugin = (chainId: number, address: string): boolean => + ethers.toBigInt(chainId) == ethers.toBigInt(SAMPLE_PLUGIN_CHAIN_ID) && + getAddress(address) === SAMPLE_PLUGIN_ADDRESS + +const getRelayPlugin = async() => { + const provider = await getProvider() + return new ethers.Contract( + SAMPLE_PLUGIN_ADDRESS, + SAMPLE_PLUGIN_ABI, + provider + ) +} + +//0x6a7612020000000000000000000000007fae68e71edfd9af429f3c01e75bb905c79e10b70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000440d582f130000000000000000000000001083a997a822fed50aaaf785f95e2726440069e400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415c98c6a811a20f7b34a48b43cdad9d9901443de42e92f736abcc8a7f9e2ee7560550844cdf6e548a9a8b7ee7a0851d7cdc9356545ba345a3231ec5eb564cb7311b00000000000000000000000000000000000000000000000000000000000000 + +const getToken = async(address: string) => { + const provider = await getProvider() + return new ethers.Contract( + address, + ECR20_ABI, + provider + ) +} + +export const getNextTxs = async(safe: string): Promise => { + const currentNonce = await getCurrentNonce(safe) + const { results: txs } = await getSafeMultisigTxs(safe, { nonce: currentNonce }) + return txs +} + +export const getAvailableFeeToken = async(): Promise => { + return await gelato.getPaymentTokens(SAMPLE_PLUGIN_CHAIN_ID) +} + +export const getMaxFeePerToken = async(account: string, token: string): Promise => { + const plugin = await getRelayPlugin() + return await plugin.maxFeePerToken(account, token) +} + +export const updateMaxFeePerToken = async(token: string, maxFee: bigint) => { + try { + const plugin = await getRelayPlugin() + await submitTxs([ + { + to: await plugin.getAddress(), + value: "0", + data: (await plugin.setMaxFeePerToken.populateTransaction(token, maxFee)).data + } + ]) + } catch (e) { + console.error(e) + } +} + +export const getTokenInfo = async(address: string): Promise => { + if (address === NATIVE_TOKEN || address === ZeroAddress) return { + address, + symbol: "ETH", + decimals: BigInt(18) + } + const token = await getToken(address) + return { + address, + symbol: await token.symbol(), + decimals: await token.decimals() + } +} + +export const relayTx = async(account: string, data: string, feeToken: string) => { + try { + const plugin = await getRelayPlugin() + const manager = await getManager() + const request = { + chainId: SAMPLE_PLUGIN_CHAIN_ID, + target: await plugin.getAddress(), + data: (await plugin.executeFromPlugin.populateTransaction(await manager.getAddress(), account, data)).data, + feeToken, + isRelayContext: true + } + console.log({request}) + const response = await gelato.callWithSyncFee(request) + console.log(response) + return response.taskId + } catch (e) { + console.error(e) + return "" + } +} + +export const getStatus = async(taskId: string) => { + try { + return await gelato.getTaskStatus(taskId) + } catch (e) { + console.error(e) + return undefined + } +} \ No newline at end of file diff --git a/web/src/logic/services.ts b/web/src/logic/services.ts new file mode 100644 index 0000000..3ab174c --- /dev/null +++ b/web/src/logic/services.ts @@ -0,0 +1,63 @@ +// TODO: switch to api-kit once Ethers v6 is supported +import { getAddress, BigNumberish } from "ethers" +import axios from "axios"; + +export type Page = { + readonly count: number + readonly next?: string + readonly previous?: string + readonly results: T[] +} + +export type SafeMultisigConfirmation = { + readonly owner: string + readonly submissionDate: string + readonly transactionHash?: string + readonly confirmationType?: string + readonly signature: string + readonly signatureType?: string +} + +export type SafeMultisigTransaction = { + readonly safe: string + readonly to: string + readonly value: string + readonly data?: string + readonly operation: number + readonly gasToken: string + readonly safeTxGas: number + readonly baseGas: number + readonly gasPrice: string + readonly refundReceiver?: string + readonly nonce: number + readonly executionDate: string + readonly submissionDate: string + readonly modified: string + readonly blockNumber?: number + readonly transactionHash: string + readonly safeTxHash: string + readonly executor?: string + readonly isExecuted: boolean + readonly isSuccessful?: boolean + readonly ethGasPrice?: string + readonly gasUsed?: number + readonly fee?: string + readonly origin: string + readonly dataDecoded?: string + readonly confirmationsRequired: number + readonly confirmations?: SafeMultisigConfirmation[] + readonly trusted: boolean + readonly signatures?: string +} + + +const SAFE_TX_SERVISAFE_TX_SERVICE_BASECE_BASE = "https://safe-transaction-goerli.safe.global/api/" + +const multisigTxsEndpoint = (safe: string) => { + return SAFE_TX_SERVISAFE_TX_SERVICE_BASECE_BASE + `v1/safes/${getAddress(safe)}/multisig-transactions/` +} + +export const getSafeMultisigTxs = async (safe: string, params?: { nonce: BigNumberish }): Promise> => { + const response = await axios.get>(multisigTxsEndpoint(safe), { params }) + return response.data +} \ No newline at end of file diff --git a/web/src/logic/web3.ts b/web/src/logic/web3.ts index a6726b5..facce24 100644 --- a/web/src/logic/web3.ts +++ b/web/src/logic/web3.ts @@ -1,9 +1,9 @@ import { AbstractProvider, ethers } from "ethers" -import { getSafeAppsProvider, isConnectToSafe } from "./safeapp" +import { getSafeAppsProvider, isConnectedToSafe } from "./safeapp" import { PROTOCOL_PUBLIC_RPC } from "./constants" export const getProvider = async(): Promise => { - if (await isConnectToSafe()) { + if (await isConnectedToSafe()) { console.log("Use SafeAppsProvider") return await getSafeAppsProvider() } diff --git a/web/src/routes/samples/relay/NextTxs.tsx b/web/src/routes/samples/relay/NextTxs.tsx new file mode 100644 index 0000000..0588b77 --- /dev/null +++ b/web/src/routes/samples/relay/NextTxs.tsx @@ -0,0 +1,65 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./Relay.css"; +import { CircularProgress, Button, Typography } from '@mui/material'; +import { getNextTxs } from "../../../logic/sample"; +import { SafeInfo } from '@safe-global/safe-apps-sdk'; +import { SafeMultisigTransaction } from "../../../logic/services"; + +enum Status { + Loading, + Error, + Ready +} + +interface NextTx { + tx: SafeMultisigTransaction, + ready: boolean +} + +export const NextTxItem: FunctionComponent<{ next: NextTx, handleRelay: (tx: SafeMultisigTransaction) => void }> = ({ next, handleRelay }) => { + return (
+ {next.tx.safeTxHash} + {next.ready && } +
) +} + +export const NextTxsList: FunctionComponent<{ safeInfo: SafeInfo, handleRelay: (tx: SafeMultisigTransaction) => void }> = ({ safeInfo, handleRelay }) => { + const [ status, setStatus ] = useState(Status.Loading) + const [ nextTxs, setNextTxs ] = useState([]) + useEffect(() => { + setStatus(Status.Loading) + const fetchData = async() => { + try { + const txs = await getNextTxs(safeInfo.safeAddress) + setNextTxs(txs.map((tx) => { + return { + tx, + ready: (tx.confirmations?.length ?? 0) >= safeInfo.threshold + } + })) + setStatus(Status.Ready) + } catch (e) { + console.error(e) + setStatus(Status.Error) + } + } + fetchData(); + }, [setStatus, safeInfo]) + + switch(status) { + case Status.Loading: + return ( + + ) + case Status.Error: + return ( + + Error Loading Data + + ) + case Status.Ready: + return (<> + {nextTxs.map((nextTx) => )} + ) + } +}; diff --git a/web/src/routes/samples/relay/Relay.css b/web/src/routes/samples/relay/Relay.css new file mode 100644 index 0000000..669ce68 --- /dev/null +++ b/web/src/routes/samples/relay/Relay.css @@ -0,0 +1,24 @@ +.Sample { + height: 100vh; + width: 100vw; + text-align: center; + background-color: #282c34; + font-size: calc(10px + 2vmin); + color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; +} + +.NextTx { + width: 100%; + text-align: center; + font-size: calc(10px + 2vmin); + color: white; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + display: flex; +} \ No newline at end of file diff --git a/web/src/routes/samples/relay/RelayDialog.tsx b/web/src/routes/samples/relay/RelayDialog.tsx new file mode 100644 index 0000000..b10db19 --- /dev/null +++ b/web/src/routes/samples/relay/RelayDialog.tsx @@ -0,0 +1,102 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./Relay.css"; +import { CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Button, Typography } from '@mui/material'; +import { NATIVE_TOKEN, getStatus, relayTx } from "../../../logic/sample"; +import { SafeMultisigTransaction } from "../../../logic/services"; +import { buildExecuteTx } from "../../../logic/safe"; + +enum Status { + Loading, + Error, + Ready +} + +const dialogContent = (status: Status) => { + switch(status) { + case Status.Loading: + return ( + + ) + case Status.Error: + return ( + + Error Relaying Data + + ) + case Status.Ready: + return (<> + + Transaction has been relayed + + ) + } +} + +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +const isErrorTaskState = (state: string): boolean => { + return [ + "ExecReverted", + "Blacklisted", + "Cancelled", + "NotFound" + ].includes(state) +} + +export const RelayDialog: FunctionComponent<{ tx: SafeMultisigTransaction|undefined, feeToken: string|undefined, handleClose: () => void }> = ({ tx, feeToken, handleClose }) => { + const [ status, setStatus ] = useState(Status.Loading) + useEffect(() => { + if (tx == undefined) return + setStatus(Status.Loading) + const fetchData = async() => { + try { + const { to: account, data } = await buildExecuteTx(tx) + // TODO: remove fallback to native fee token and enforce that token is selected + const txId = await relayTx(account, data, feeToken || NATIVE_TOKEN) + let retries = 0; + while(retries < 10) { + const relayStatus = await getStatus(txId) + console.log({relayStatus}) + /* + CheckPending = "CheckPending", + ExecPending = "ExecPending", + ExecSuccess = "ExecSuccess", + ExecReverted = "ExecReverted", + WaitingForConfirmation = "WaitingForConfirmation", + Blacklisted = "Blacklisted", + Cancelled = "Cancelled", + NotFound = "NotFound" + */ + if (relayStatus == undefined || isErrorTaskState(relayStatus.taskState)) { + setStatus(Status.Error) + return + } else if (relayStatus.taskState === "ExecSuccess") { + setStatus(Status.Ready) + return + } else { + retries ++; + await sleep(2000) + } + } + setStatus(Status.Error) + } catch (e) { + console.error(e) + setStatus(Status.Error) + } + } + fetchData(); + }, [setStatus, tx, feeToken]) + + return + Relaying Transaction + + {dialogContent(status)} + + {status != Status.Loading && + + } + + +}; diff --git a/web/src/routes/samples/relay/RelayPlugin.tsx b/web/src/routes/samples/relay/RelayPlugin.tsx new file mode 100644 index 0000000..c7271b6 --- /dev/null +++ b/web/src/routes/samples/relay/RelayPlugin.tsx @@ -0,0 +1,115 @@ +import { FunctionComponent, useCallback, useEffect, useState } from "react"; +import { formatUnits, parseUnits } from "ethers" +import { useParams } from "react-router-dom"; +import "./Relay.css"; +import { CircularProgress, FormControl, InputLabel, Select, MenuItem, TextField, Button, Typography } from '@mui/material'; +import { TokenInfo, getAvailableFeeToken, getMaxFeePerToken, getTokenInfo, isKnownSamplePlugin, updateMaxFeePerToken } from "../../../logic/sample"; +import { getSafeInfo, isConnectedToSafe } from "../../../logic/safeapp"; +import { SafeInfo } from '@safe-global/safe-apps-sdk'; +import { NextTxsList } from "./NextTxs"; +import { SafeMultisigTransaction } from "../../../logic/services"; +import { RelayDialog } from "./RelayDialog"; + +export const RelayPlugin: FunctionComponent<{}> = () => { + const { pluginAddress } = useParams(); + const [ newMaxFee, setNewMaxFee ] = useState(""); + const [ txToRelay, setTxToRelay ] = useState(undefined); + const [ safeInfo, setSafeInfo ] = useState(undefined) + const [ feeTokens, setFeeTokens ] = useState([]) + const [ maxFee, setMaxFee ] = useState(undefined) + const [ selectedFeeToken, setSelectedFeeToken ] = useState(undefined) + const [ selectedFeeTokenInfo, setSelectedFeeTokenInfo ] = useState(undefined) + console.log({pluginAddress}) + useEffect(() => { + const fetchData = async() => { + try { + if (!await isConnectedToSafe()) throw Error("Not connected to Safe") + const info = await getSafeInfo() + if (!isKnownSamplePlugin(info.chainId, pluginAddress!!)) throw Error("Unknown Plugin") + setSafeInfo(info) + } catch (e) { + console.error(e) + } + } + fetchData(); + }, [pluginAddress]) + useEffect(() => { + const fetchData = async() => { + try { + const availableFeeTokens = await getAvailableFeeToken() + setFeeTokens(availableFeeTokens) + if (availableFeeTokens.length > 0) { + setSelectedFeeToken(availableFeeTokens[0]) + } + } catch (e) { + console.error(e) + } + } + fetchData(); + }, [pluginAddress]) + useEffect(() => { + if (selectedFeeToken === undefined) return + const fetchData = async() => { + try { + setSelectedFeeTokenInfo(undefined) + const tokenInfo = await getTokenInfo(selectedFeeToken) + console.log({tokenInfo}) + setSelectedFeeTokenInfo(tokenInfo) + } catch (e) { + console.error(e) + } + } + fetchData(); + }, [selectedFeeToken]) + useEffect(() => { + setMaxFee(undefined) + if (safeInfo === undefined || selectedFeeToken === undefined) return + const fetchData = async() => { + try { + const maxFee = await getMaxFeePerToken(safeInfo.safeAddress, selectedFeeToken) + setMaxFee(maxFee) + } catch (e) { + console.error(e) + } + } + fetchData(); + }, [selectedFeeToken, safeInfo]) + const updateMaxFee = useCallback(async (feeTokenInfo: TokenInfo, maxFeeInput: string) => { + console.log("UPDATE") + const targetMaxFee = parseUnits(maxFeeInput, feeTokenInfo.decimals) + await updateMaxFeePerToken(feeTokenInfo.address, targetMaxFee) + }, []) + + const isLoading = safeInfo === undefined || maxFee === undefined || selectedFeeTokenInfo === undefined + + return ( +
+ {isLoading && } + {feeTokens.length > 0 && selectedFeeToken !== undefined && <> + + Fee Token + + + } + {safeInfo !== undefined && maxFee !== undefined && selectedFeeTokenInfo !== undefined && <> +

Current max fee set: {formatUnits(maxFee, selectedFeeTokenInfo.decimals)} {selectedFeeTokenInfo.symbol}

+ + New max fee ({selectedFeeTokenInfo.symbol}):
+ setNewMaxFee(event.target.value)}/> +
+ + } + {safeInfo && setTxToRelay(tx)}/>} + setTxToRelay(undefined)} /> +
+ ); +}; diff --git a/web/tsconfig.json b/web/tsconfig.json index a273b0c..41675d9 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "target": "ES6", + "module": "CommonJS", "lib": [ "dom", "dom.iterable", @@ -13,7 +14,6 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, diff --git a/web/yarn.lock b/web/yarn.lock index ebea23c..2d4e728 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1429,6 +1429,356 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" + integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + +"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + +"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + +"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + +"@ethersproject/contracts@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" + integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + +"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" + integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" + integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + aes-js "3.0.0" + scrypt-js "3.0.1" + +"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + +"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + +"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" + integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + +"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/providers@5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bech32 "1.1.4" + ws "7.4.6" + +"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" + integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + +"@ethersproject/solidity@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" + integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + +"@ethersproject/units@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" + integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/wallet@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" + integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/json-wallets" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" + integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@gelatonetwork/relay-sdk@^3.1.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@gelatonetwork/relay-sdk/-/relay-sdk-3.5.0.tgz#f8592b17492a582421dbeea39ec03180f82d20f3" + integrity sha512-oj4rFH09yzFT4wnYCv7V3bZCIVhpuKbnK5jv0fgzclxTbbA6UEWZErn3QiD13yspiIkKafSgMWT0G8z41CINYg== + dependencies: + axios "0.24.0" + ethers "5.7.2" + "@humanwhocodes/config-array@^0.11.10": version "0.11.10" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" @@ -1986,10 +2336,10 @@ "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" viem "^1.0.0" -"@safe-global/safe-core-protocol@^0.1.0-alpha.3": - version "0.1.0-alpha.3" - resolved "https://registry.yarnpkg.com/@safe-global/safe-core-protocol/-/safe-core-protocol-0.1.0-alpha.3.tgz#355b81283b4ce611aa651af76186b1cd91ea0f45" - integrity sha512-iXIaqOrPuJVkTyLHlnzxa5+/oicK43snL6iH/m1f7nFqySvUlXcV29MeS9b3S5a6k8r3cMX9IlmK6GpWk0tx4w== +"@safe-global/safe-core-protocol@^0.1.0-alpha.4": + version "0.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@safe-global/safe-core-protocol/-/safe-core-protocol-0.1.0-alpha.4.tgz#a00622198458551c8cf4f339872939be5de0d233" + integrity sha512-d+2qnyqf3WdERlxwxZAxxTk0N0MYfr+ICqIem78rDYKlRYxbnM7AKlXXSiYJbPNADZ49cakPmib59Rs0IK93Kg== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.7.3" @@ -2867,6 +3217,11 @@ adjust-sourcemap-loader@^4.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" +aes-js@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== + aes-js@4.0.0-beta.5: version "4.0.0-beta.5" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" @@ -3138,6 +3493,22 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0" integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g== +axios@0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" + integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== + dependencies: + follow-redirects "^1.14.4" + +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -3291,6 +3662,11 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + bfj@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" @@ -3321,6 +3697,16 @@ bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -3376,6 +3762,11 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -4295,6 +4686,19 @@ electron-to-chromium@^1.4.431: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.456.tgz#aab5f3b421a07a73cf646d28e63c41238fc45849" integrity sha512-d+eSL4mT9m72cnDT/kfQj6Pv6Cid4pUVlLOl8esm2SZuXBgtXtUyvCfc9F++GHLWLoY4gMNqg+0IVAoQ3sosKA== +elliptic@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + email-addresses@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-5.0.0.tgz#7ae9e7f58eef7d5e3e2c2c2d3ea49b78dc854fa6" @@ -4745,6 +5149,42 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +ethers@5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" + integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== + dependencies: + "@ethersproject/abi" "5.7.0" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/address" "5.7.0" + "@ethersproject/base64" "5.7.0" + "@ethersproject/basex" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@ethersproject/bytes" "5.7.0" + "@ethersproject/constants" "5.7.0" + "@ethersproject/contracts" "5.7.0" + "@ethersproject/hash" "5.7.0" + "@ethersproject/hdnode" "5.7.0" + "@ethersproject/json-wallets" "5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/logger" "5.7.0" + "@ethersproject/networks" "5.7.1" + "@ethersproject/pbkdf2" "5.7.0" + "@ethersproject/properties" "5.7.0" + "@ethersproject/providers" "5.7.2" + "@ethersproject/random" "5.7.0" + "@ethersproject/rlp" "5.7.0" + "@ethersproject/sha2" "5.7.0" + "@ethersproject/signing-key" "5.7.0" + "@ethersproject/solidity" "5.7.0" + "@ethersproject/strings" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@ethersproject/units" "5.7.0" + "@ethersproject/wallet" "5.7.0" + "@ethersproject/web" "5.7.1" + "@ethersproject/wordlists" "5.7.0" + ethers@^6.6.3: version "6.6.3" resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.6.3.tgz#9bf11d1bd0f18c7c55087d1a52fdc8f3c33c8bab" @@ -5005,7 +5445,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -5045,6 +5485,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5360,11 +5809,28 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -5586,7 +6052,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6460,6 +6926,11 @@ jiti@^1.18.2: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6858,11 +7329,16 @@ mini-css-extract-plugin@^2.4.5: dependencies: schema-utils "^4.0.0" -minimalistic-assert@^1.0.0: +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -7970,6 +8446,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -8527,6 +9008,11 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +scrypt-js@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" + integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -9935,6 +10421,11 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + ws@8.12.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8"