Skip to content

Commit

Permalink
Merge pull request #496 from valory-xyz/feat/return_safe_owners
Browse files Browse the repository at this point in the history
Add enriched wallet info endpoint
  • Loading branch information
jmoreira-valory authored Nov 25, 2024
2 parents 12c331c + 2f36d4c commit cf68e3d
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 1 deletion.
70 changes: 70 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details>
<summary>Response</summary>

```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
}
]
```

</details>

---

### `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.
Expand Down
9 changes: 9 additions & 0 deletions operate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
8 changes: 8 additions & 0 deletions operate/ledger/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
55 changes: 54 additions & 1 deletion operate/wallet/master.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit cf68e3d

Please sign in to comment.