From 2db6b0c6fd56a09860af8cc250ecbb5dce82aa2f Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 16 May 2024 11:39:08 +0530 Subject: [PATCH 1/6] feat: add tools for managing owners on a safe --- operate/utils/gnosis.py | 111 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 10 deletions(-) diff --git a/operate/utils/gnosis.py b/operate/utils/gnosis.py index 9ef13c1ee..c258d2307 100644 --- a/operate/utils/gnosis.py +++ b/operate/utils/gnosis.py @@ -19,6 +19,7 @@ """Safe helpers.""" +import binascii import secrets import typing as t from enum import Enum @@ -160,14 +161,6 @@ def create_safe( ) -> t.Tuple[str, int]: """Create gnosis safe.""" salt_nonce = salt_nonce or _get_nonce() - tx = registry_contracts.gnosis_safe.get_deploy_transaction( - ledger_api=ledger_api, - deployer_address=crypto.address, - owners=[crypto.address], - threshold=1, - salt_nonce=salt_nonce, - ) - safe = tx.pop("contract_address") def _build( # pylint: disable=unused-argument *args: t.Any, **kwargs: t.Any @@ -195,10 +188,108 @@ def _build( # pylint: disable=unused-argument "build", _build, ) - tx_settler.transact( + receipt = tx_settler.transact( method=lambda: {}, contract="", kwargs={}, ) + instance = registry_contracts.gnosis_safe_proxy_factory.get_instance( + ledger_api=ledger_api, + contract_address="0xa6b71e26c5e0845f74c812102ca7114b6a896ab2", + ) + (event,) = instance.events.ProxyCreation().process_receipt(receipt) + return event["args"]["proxy"], salt_nonce + + +def get_owners(ledger_api: LedgerApi, safe: str) -> t.List[str]: + """Get list of owners.""" + return registry_contracts.gnosis_safe.get_owners( + ledger_api=ledger_api, + contract_address=safe, + ).get("owners", []) + - return safe, salt_nonce +def send_safe_txs( + txd: bytes, + safe: str, + ledger_api: LedgerApi, + crypto: Crypto, +) -> None: + """Send internal safe transaction.""" + owner = ledger_api.api.to_checksum_address( + crypto.address, + ) + safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash( + ledger_api=ledger_api, + contract_address=safe, + value=0, + safe_tx_gas=0, + to_address=safe, + data=txd, + operation=SafeOperation.CALL.value, + ).get("tx_hash") + safe_tx_bytes = binascii.unhexlify( + safe_tx_hash[2:], + ) + signatures = { + owner: crypto.sign_message( + message=safe_tx_bytes, + is_deprecated_mode=True, + )[2:] + } + transaction = registry_contracts.gnosis_safe.get_raw_safe_transaction( + ledger_api=ledger_api, + contract_address=safe, + sender_address=owner, + owners=(owner,), # type: ignore + to_address=safe, + value=0, + data=txd, + safe_tx_gas=0, + signatures_by_owner=signatures, + operation=SafeOperation.CALL.value, + nonce=ledger_api.api.eth.get_transaction_count(owner), + ) + ledger_api.get_transaction_receipt( + ledger_api.send_signed_transaction( + crypto.sign_transaction( + transaction, + ), + ) + ) + + +def add_owner( + ledger_api: LedgerApi, + crypto: Crypto, + safe: str, + owner: str, +) -> None: + """Add owner to a safe.""" + instance = registry_contracts.gnosis_safe.get_instance( + ledger_api=ledger_api, + contract_address=safe, + ) + txd = instance.encodeABI( + fn_name="addOwnerWithThreshold", + args=[ + owner, + 1, + ], + ) + send_safe_txs( + txd=bytes.fromhex(txd[2:]), + safe=safe, + ledger_api=ledger_api, + crypto=crypto, + ) + + +def swap_owner( # pylint: disable=unused-argument + ledger_api: LedgerApi, + crypto: Crypto, + safe: str, + old_owner: str, + new_owner: str, +) -> None: + """Swap owner on a safe.""" From c808e0659a0a4b10154996a7f10b3db26a2d9916 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 16 May 2024 11:40:42 +0530 Subject: [PATCH 2/6] feat: add endpoints for managing owners on a safe --- operate/cli.py | 83 +++++++++++++++++++++++++++++++++++- operate/wallet/master.py | 91 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) diff --git a/operate/cli.py b/operate/cli.py index f1d02edb6..b7dbb8487 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -309,6 +309,23 @@ async def _get_wallets(request: Request) -> t.List[t.Dict]: wallets.append(wallet.json) return JSONResponse(content=wallets) + @app.get("/api/wallet/{chain}") + @with_retries + async def _get_wallet_by_chain(request: Request) -> t.List[t.Dict]: + """Create wallet safe""" + ledger_type = get_ledger_type_from_chain_type( + chain=ChainType.from_string(request.path_params["chain"]) + ) + manager = operate.wallet_manager + if not manager.exists(ledger_type=ledger_type): + return JSONResponse( + content={"error": "Wallet does not exist"}, + status_code=404, + ) + return JSONResponse( + content=manager.load(ledger_type=ledger_type).json, + ) + @app.post("/api/wallet") @with_retries async def _create_wallet(request: Request) -> t.List[t.Dict]: @@ -339,7 +356,35 @@ async def _create_wallet(request: Request) -> t.List[t.Dict]: wallet, mnemonic = manager.create(ledger_type=ledger_type) return JSONResponse(content={"wallet": wallet.json, "mnemonic": mnemonic}) - @app.put("/api/wallet") + @app.get("/api/wallet/safe") + @with_retries + async def _get_safes(request: Request) -> t.List[t.Dict]: + """Create wallet safe""" + safes = [] + for wallet in operate.wallet_manager: + safes.append({wallet.ledger_type: wallet.safe}) + return JSONResponse(content=safes) + + @app.get("/api/wallet/safe/{chain}") + @with_retries + async def _get_safe(request: Request) -> t.List[t.Dict]: + """Create wallet safe""" + ledger_type = get_ledger_type_from_chain_type( + chain=ChainType.from_string(request.path_params["chain"]) + ) + manager = operate.wallet_manager + if not manager.exists(ledger_type=ledger_type): + return JSONResponse( + content={"error": "Wallet does not exist"}, + status_code=404, + ) + return JSONResponse( + content={ + "safe": manager.load(ledger_type=ledger_type).safe, + }, + ) + + @app.post("/api/wallet/safe") @with_retries async def _create_safe(request: Request) -> t.List[t.Dict]: """Create wallet safe""" @@ -363,10 +408,46 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: return JSONResponse(content={"error": "Wallet does not exist"}) wallet = manager.load(ledger_type=ledger_type) + if wallet.safe is not None: + return JSONResponse( + content={"safe": wallet.safe, "message": "Safe already exists!"} + ) + wallet.create_safe( # pylint: disable=no-member chain_type=chain_type, owner=data.get("owner"), ) + return JSONResponse(content={"safe": wallet.safe, "message": "Safe created!"}) + + @app.put("/api/wallet/safe") + @with_retries + async def _update_safe(request: Request) -> t.List[t.Dict]: + """Create wallet safe""" + # TODO: Extract login check as decorator + if operate.user_account is None: + return JSONResponse( + content={"error": "Cannot create safe; User account does not exist!"}, + status_code=400, + ) + + if operate.password is None: + return JSONResponse( + content={"error": "You need to login before creating a safe"}, + status_code=401, + ) + + data = await request.json() + chain_type = ChainType(data["chain_type"]) + ledger_type = get_ledger_type_from_chain_type(chain=chain_type) + manager = operate.wallet_manager + if not manager.exists(ledger_type=ledger_type): + return JSONResponse(content={"error": "Wallet does not exist"}) + + wallet = manager.load(ledger_type=ledger_type) + wallet.add_or_swap_owner( + chain_type=chain_type, + owner=data.get("owner"), + ) return JSONResponse(content=wallet.json) @app.get("/api/services") diff --git a/operate/wallet/master.py b/operate/wallet/master.py index 256f4ee88..c3e9622af 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -39,7 +39,9 @@ from operate.ledger import get_default_rpc from operate.resource import LocalResource from operate.types import ChainType, LedgerType +from operate.utils.gnosis import add_owner from operate.utils.gnosis import create_safe as create_gnosis_safe +from operate.utils.gnosis import get_owners, swap_owner class MasterWallet(LocalResource): @@ -108,6 +110,34 @@ def create_safe( """Create safe.""" raise NotImplementedError() + def add_backup_owner( + self, + chain_type: ChainType, + owner: str, + rpc: t.Optional[str] = None, + ) -> None: + """Create safe.""" + raise NotImplementedError() + + def swap_backup_owner( + self, + chain_type: ChainType, + old_owner: str, + new_owner: str, + rpc: t.Optional[str] = None, + ) -> None: + """Create safe.""" + raise NotImplementedError() + + def add_or_swap_owner( + self, + chain_type: ChainType, + owner: str, + rpc: t.Optional[str] = None, + ) -> None: + """Add or swap backup owner.""" + raise NotImplementedError() + @dataclass class EthereumMasterWallet(MasterWallet): @@ -181,6 +211,7 @@ def new( # Create wallet wallet = EthereumMasterWallet(path=path, address=crypto.address, safe_chains=[]) wallet.store() + wallet.password = password return wallet, mnemonic.split() def create_safe( @@ -201,6 +232,66 @@ def create_safe( self.safe_chains.append(chain_type) self.store() + def add_backup_owner( + self, + chain_type: ChainType, + owner: str, + rpc: t.Optional[str] = None, + ) -> None: + """Add a backup owner.""" + ledger_api = self.ledger_api(chain_type=chain_type, rpc=rpc) + if len(get_owners(ledger_api=ledger_api, safe=t.cast(str, self.safe))) == 2: + raise ValueError("Backup owner already exist!") + add_owner( + ledger_api=ledger_api, + safe=t.cast(str, self.safe), + owner=owner, + crypto=self.crypto, + ) + + def swap_backup_owner( + self, + chain_type: ChainType, + old_owner: str, + new_owner: str, + rpc: t.Optional[str] = None, + ) -> None: + """Swap backup owner.""" + ledger_api = self.ledger_api(chain_type=chain_type, rpc=rpc) + if len(get_owners(ledger_api=ledger_api, safe=t.cast(str, self.safe))) == 1: + raise ValueError("Backup owner does not exist, cannot swap!") + swap_owner( + ledger_api=ledger_api, + safe=t.cast(str, self.safe), + old_owner=old_owner, + new_owner=new_owner, + crypto=self.crypto, + ) + + def add_or_swap_owner( + self, + chain_type: ChainType, + owner: str, + rpc: t.Optional[str] = None, + ) -> None: + """Add or swap backup owner.""" + ledger_api = self.ledger_api(chain_type=chain_type, rpc=rpc) + owners = get_owners(ledger_api=ledger_api, safe=t.cast(str, self.safe)) + if len(owners) == 1: + return self.add_backup_owner(chain_type=chain_type, owner=owner, rpc=rpc) + + owners.remove(self.address) + (old_owner,) = owners + if old_owner == owner: + return None + + return self.swap_backup_owner( + chain_type=chain_type, + old_owner=old_owner, + new_owner=owner, + rpc=rpc, + ) + @classmethod def load(cls, path: Path) -> "EthereumMasterWallet": """Load master wallet.""" From 86dbbba91edf26b0af2eca6f3b25515991063690 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 16 May 2024 11:41:17 +0530 Subject: [PATCH 3/6] chore: update funding script --- scripts/fund.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/fund.py b/scripts/fund.py index 7357dec1a..a4724b508 100644 --- a/scripts/fund.py +++ b/scripts/fund.py @@ -14,7 +14,7 @@ OLAS_CONTRACT_ADDRESS_GNOSIS = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" -def fund(address: str) -> None: +def fund(address: str, amount: float = 10.0) -> None: """Fund an address.""" staking_keys_path = os.environ.get("STAKING_TEST_KEYS_PATH", None) ledger_api = EthereumApi(address="http://localhost:8545") @@ -22,7 +22,7 @@ def fund(address: str) -> None: tx = ledger_api.get_transfer_transaction( sender_address=crypto.address, destination_address=address, - amount=10000000000000000000, + amount=int(amount * 1e18), tx_fee=50000, tx_nonce="0x", chain_id=100, @@ -31,7 +31,7 @@ def fund(address: str) -> None: digest = ledger_api.send_signed_transaction(stx) ledger_api.get_transaction_receipt(tx_digest=digest, raise_on_try=True) - print(ledger_api.get_balance(address=address)) + print(f"Transferred: {ledger_api.get_balance(address=address)}") if staking_keys_path: staking_crypto = EthereumCrypto(staking_keys_path) with open( From 3d283a5baadae8aef6787a74f8fc6d47bc5cfa83 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 16 May 2024 11:41:30 +0530 Subject: [PATCH 4/6] feat: udpate setup wallet script to include a backup owner --- scripts/setup_wallet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/setup_wallet.py b/scripts/setup_wallet.py index 115ecc7c7..85728496b 100644 --- a/scripts/setup_wallet.py +++ b/scripts/setup_wallet.py @@ -58,10 +58,11 @@ fund(wallet["wallet"]["address"]) print( - requests.put( - "http://localhost:8000/api/wallet", + requests.post( + "http://localhost:8000/api/wallet/safe", json={ "chain_type": ChainType.GNOSIS, + "owner": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", # Backup owner }, ).json() ) From 6af7ffdbd3feef513d40696b91645321f2dabc8f Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 16 May 2024 11:41:52 +0530 Subject: [PATCH 5/6] feat: adjust frontend SDK --- frontend/service/Wallet.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/service/Wallet.ts b/frontend/service/Wallet.ts index b9ed9e0c5..43cf93b21 100644 --- a/frontend/service/Wallet.ts +++ b/frontend/service/Wallet.ts @@ -16,17 +16,27 @@ const createEoa = async (chain: Chain) => body: JSON.stringify({ chain_type: chain }), }).then((res) => res.json()); -const createSafe = async (chain: Chain) => - fetch(`${BACKEND_URL}/wallet`, { +const createSafe = async (chain: Chain, owner?: string) => + fetch(`${BACKEND_URL}/wallet/safe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ chain_type: chain, owner: owner }), + }).then((res) => res.json()); + +const addBackupOwner = async (chain: Chain, owner: string) => + fetch(`${BACKEND_URL}/wallet/safe`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ chain_type: chain }), + body: JSON.stringify({ chain_type: chain, owner: owner }), }).then((res) => res.json()); export const WalletService = { getWallets, createEoa, createSafe, + addBackupOwner }; From bc68b767ccac22d6c5bd1f665cf56144a3b87a81 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 16 May 2024 11:42:12 +0530 Subject: [PATCH 6/6] chore: linters --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 54b10e4d4..974242bb0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ ignore-patterns=contract.py ignore=operate/data/contracts/ [MESSAGES CONTROL] -disable=C0103,R0801,C0301,C0201,C0204,C0209,W1203,C0302,R1735,R1729,W0511,E0611,R0903 +disable=C0103,R0801,C0301,C0201,C0204,C0209,W1203,C0302,R1735,R1729,W0511,E0611,R0903,E1101 # See here for more options: https://www.codeac.io/documentation/pylint-configuration.html R1735: use-dict-literal