diff --git a/api.md b/api.md index ab77b977f..f5f860fed 100644 --- a/api.md +++ b/api.md @@ -198,6 +198,76 @@ Returns a list of available wallets --- +### `GET /api/extended/wallet` + +Returns a list of available wallets with enriched information. It executes on-chain requests to populate the list of owners of each safe, and provides the attributes + +- `consistent_backup_owner`: This flag is `true` when all safes across the chains have exactly the same set of backup owner addresses. It ensures that ownership is identical across all safes, regardless of the number of owners. +- `consistent_backup_owner_count`: This flag is `true` when all safes have the same number of owners, and that number is either 0 (no backup owners) or 1 (exactly one backup owner). It checks for uniformity in the count of owners and restricts the count to these two cases. +- `consistent_safe_address`: This flag is `true` when all chains have the same safe address. It ensures there is a single safe address consistently used across all chains. + +
+ Response + +```json +[ + { + "address":"0xFafd5cb31a611C5e5aa65ea8c6226EB4328175E7", + "consistent_backup_owner": false, + "consistent_backup_owner_count": false, + "consistent_safe_address": true, + "ledger_type":"ethereum", + "safe_chains":[ + "gnosis", + "ethereum", + "base", + "optimistic" + ], + "safe_nonce":110558881674480320952254000342160989674913430251257716940579305238321962891821, + "safes":{ + "base":{ + "0xd56fb274ce2C66008D5c4C09980c4f36Ab81ff23":{ + "backup_owners": [], // Empty = no backup owners + "balances": {...} + } + }, + "ethereum":{ + "0xd56fb274ce2C66008D5c4C09980c4f36Ab81ff23":{ + "backup_owners":[ + "0x46eC2E77Fe3E367252f1A8a77470CE8eEd2A985b" + ], + "balances": {...} + } + }, + "gnosis":{ + "0xd56fb274ce2C66008D5c4C09980c4f36Ab81ff23":{ + "backup_owners":[ + "0x46eC2E77Fe3E367252f1A8a77470CE8eEd2A985b" + ], + "balances": { + "0x0000000000000000000000000000000000000000": 995899999999999999998, // xDAI + "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83": 0, // USDC + "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f": 960000000000000000000 // OLAS + } + }, + "optimistic":{ + "0xd56fb274ce2C66008D5c4C09980c4f36Ab81ff23":{ + "backup_owners":[ + "0x46eC2E77Fe3E367252f1A8a77470CE8eEd2A985b" + ], + "balances": {...} + } + } + }, + "single_backup_owner_per_safe":false + } +] +``` + +
+ +--- + ### `POST /api/wallet` Creates a master wallet for given chain type. If a wallet already exists for a given chain type, it returns the already existing wallet without creating an additional one. diff --git a/operate/cli.py b/operate/cli.py index ad7ae745a..6526e3462 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -447,6 +447,15 @@ 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.get("/api/extended/wallet") + @with_retries + async def _get_wallet_safe(request: Request) -> t.List[t.Dict]: + """Get wallets.""" + wallets = [] + for wallet in operate.wallet_manager: + wallets.append(wallet.extended_json) + return JSONResponse(content=wallets) + @app.get("/api/wallet/safe") @with_retries async def _get_safes(request: Request) -> t.List[t.Dict]: diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index 9d15b8663..1c66ef6c2 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -104,3 +104,11 @@ Chain.ETHEREUM: "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", Chain.MODE: "0xcfD1D50ce23C46D3Cf6407487B2F8934e96DC8f9", } + +USDC: t.Dict[Chain, str] = { + Chain.GNOSIS: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", + Chain.OPTIMISTIC: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + Chain.BASE: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Chain.ETHEREUM: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + Chain.MODE: "0xd988097fb8612cc24eeC14542bC03424c656005f", +} diff --git a/operate/wallet/master.py b/operate/wallet/master.py index 17664103e..882da6964 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -30,6 +30,7 @@ from aea.crypto.registries import make_ledger_api from aea.helpers.logging import setup_logger from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto +from autonomy.chain.base import registry_contracts from autonomy.chain.config import ChainType as ChainProfile from autonomy.chain.tx import TxSettler from web3 import Account @@ -40,9 +41,10 @@ ON_CHAIN_INTERACT_TIMEOUT, ) from operate.ledger import get_default_rpc +from operate.ledger.profiles import OLAS, USDC from operate.operate_types import Chain, LedgerType from operate.resource import LocalResource -from operate.utils.gnosis import add_owner +from operate.utils.gnosis import NULL_ADDRESS, add_owner from operate.utils.gnosis import create_safe as create_gnosis_safe from operate.utils.gnosis import get_owners, remove_owner, swap_owner from operate.utils.gnosis import transfer as transfer_from_safe @@ -145,6 +147,12 @@ def update_backup_owner( """Update backup owner.""" raise NotImplementedError() + # TODO move to resource.py if used in more resources similarly + @property + def extended_json(self) -> t.Dict: + """Get JSON representation with extended information (e.g., safe owners).""" + raise NotImplementedError + @classmethod def migrate_format(cls, path: Path) -> bool: """Migrate the JSON file format if needed.""" @@ -393,6 +401,51 @@ def update_backup_owner( return False + @property + def extended_json(self) -> t.Dict: + """Get JSON representation with extended information (e.g., safe owners).""" + rpc = None + tokens = (OLAS, USDC) + wallet_json = self.json + + if not self.safes: + return wallet_json + + owner_sets = set() + for chain, safe in self.safes.items(): + ledger_api = self.ledger_api(chain=chain, rpc=rpc) + owners = get_owners(ledger_api=ledger_api, safe=safe) + owners.remove(self.address) + + balances: t.Dict[str, int] = {} + balances[NULL_ADDRESS] = ledger_api.get_balance(safe) or 0 + for token in tokens: + balance = ( + registry_contracts.erc20.get_instance( + ledger_api=ledger_api, + contract_address=token[chain], + ) + .functions.balanceOf(safe) + .call() + ) + balances[token[chain]] = balance + + wallet_json["safes"][chain.value] = { + wallet_json["safes"][chain.value]: { + "backup_owners": owners, + "balances": balances, + } + } + owner_sets.add(frozenset(owners)) + + wallet_json["extended_json"] = True + wallet_json["consistent_safe_address"] = len(set(self.safes.values())) == 1 + wallet_json["consistent_backup_owner"] = len(owner_sets) == 1 + wallet_json["consistent_backup_owner_count"] = all( + len(owner) == 1 for owner in owner_sets + ) or all(len(owner) == 0 for owner in owner_sets) + return wallet_json + @classmethod def load(cls, path: Path) -> "EthereumMasterWallet": """Load master wallet."""