Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,4 @@ dmypy.json
# env files
*.env
*.envrc
.idea/
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ test:
ENV=test poetry run python3 -m pytest -v --cov=huma_signals --color=yes --cov-report term-missing

run-local:
ENV=development poetry run python3 -m uvicorn huma_signals.api.main:app --reload
ENV=development poetry run python3 -m uvicorn huma_signals.api.main:app --reload --port 8001
30 changes: 30 additions & 0 deletions huma_signals/adapters/allowlist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Huma Lending Pool Signal Adapter

This is the repository for the Signal Adapter that fetch signal from Huma lending pool.

## Type of signals

- on-chain pool setting
- pool's EA (underwriter) settings
- pool liquidity
- pool utilization

## Local Development

See [here](../../../docs/getting_started.md) for the development guide.

## Required environment variable

The following environment variable is required to run the adapter

```bash
WEB3_PROVIDER_URL
```

You can get Alchemy keys [here](https://docs.alchemy.com/docs/alchemy-quickstart-guide).

## Tests

```
make test
```
34 changes: 34 additions & 0 deletions huma_signals/adapters/allowlist_contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Huma Lending Pool Signal Adapter

This determines whether a borrower is authorized to
borrow from a lending pool given a contract address that implements the following function:
```typescript
function getWhitelistedBorrowers() public view returns (address[] memory);
```

## Type of signals

- if borrower is in the set of authorized borrower addresses

## Local Development

See [here](../../../docs/getting_started.md) for the development guide.

## Required environment variable

The following environment variable is required to run the adapter

```bash
WEB3_PROVIDER_URL
ETHERSCAN_API_KEY
ETHERSCAN_BASE_URL
```
Keep in mind that the contract abi is pulled from Etherscan.

You can get Alchemy keys [here](https://docs.alchemy.com/docs/alchemy-quickstart-guide).

## Tests

```
make test
```
Empty file.
158 changes: 158 additions & 0 deletions huma_signals/adapters/allowlist_contract/abi/YourContract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
[
{
"inputs": [
{
"internalType": "address[]",
"name": "_owners",
"type": "address[]"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "greetingSetter",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "newGreeting",
"type": "string"
},
{
"indexed": false,
"internalType": "bool",
"name": "premium",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "GreetingChange",
"type": "event"
},
{
"inputs": [],
"name": "getWhitelistedBorrowers",
"outputs": [
{
"internalType": "address[]",
"name": "",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "greeting",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "owners",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "premium",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_newGreeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "totalCounter",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "userGreetingCounter",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]
56 changes: 56 additions & 0 deletions huma_signals/adapters/allowlist_contract/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pathlib
import aiofiles

import orjson
import pydantic
import web3
from typing import Any, ClassVar, List

from huma_signals import models
from huma_signals.adapters import models as adapter_models
from huma_signals.adapters.utils.etherscan_client import EtherscanClient
from huma_signals.commons import web3_utils
from huma_signals.commons.chains import Chain
from huma_signals.settings import settings


class AllowListSignal(models.HumaBaseModel):
"""
Signals emitted by the allowlist adapter.

Note this is a temporary adapter and will be removed once the allowlist is integrated into the EAs.
"""

on_allowlist: bool = False

class AllowListContractAdapter(adapter_models.SignalAdapterBase):
name: ClassVar[str] = "allowlist_contract"
required_inputs: ClassVar[List[str]] = ["contract_address", "chain", "borrower_wallet_address"]
signals: ClassVar[List[str]] = list(AllowListSignal.__fields__.keys())
etherscan_client: EtherscanClient = pydantic.Field(default=EtherscanClient())


async def fetch( # pylint: disable=arguments-differ
self, contract_address: str, chain: str, borrower_wallet_address: str, *args: Any, **kwargs: Any
) -> AllowListSignal:
chain_obj = Chain.from_chain_name(chain)
w3 = await web3_utils.get_w3(chain_obj, settings.web3_provider_url)

abi = await self._get_abi(chain_obj)
allow_list_contract = w3.eth.contract(
address=web3.Web3.to_checksum_address(contract_address),
abi=orjson.loads(abi),
)

whitelisted_borrowers = await allow_list_contract.functions.getWhitelistedBorrowers().call()
return AllowListSignal(
on_allowlist=(borrower_wallet_address in whitelisted_borrowers)
)

async def _get_abi(self, chain_obj):
if chain_obj == Chain.LOCAL:
async with aiofiles.open(pathlib.Path(__file__).parent.resolve() / "abi" / "YourContract.json",
encoding="utf-8") as f:
return await f.read()
else:
return await self.etherscan_client.get_contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413")
2 changes: 2 additions & 0 deletions huma_signals/adapters/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from huma_signals.adapters import models
from huma_signals.adapters.allowlist import adapter as allowlist_adapter
from huma_signals.adapters.allowlist_contract import adapter as allowlist_contract_adapter
from huma_signals.adapters.ethereum_wallet import adapter as ethereum_wallet_adapter
from huma_signals.adapters.lending_pools import adapter as lending_pools_adapter
from huma_signals.adapters.request_network import adapter as request_network_adapter
Expand All @@ -10,6 +11,7 @@
lending_pools_adapter.LendingPoolAdapter.name: lending_pools_adapter.LendingPoolAdapter,
request_network_adapter.RequestNetworkInvoiceAdapter.name: request_network_adapter.RequestNetworkInvoiceAdapter,
allowlist_adapter.AllowListAdapter.name: allowlist_adapter.AllowListAdapter,
allowlist_contract_adapter.AllowListContractAdapter.name: allowlist_contract_adapter.AllowListContractAdapter,
ethereum_wallet_adapter.EthereumWalletAdapter.name: ethereum_wallet_adapter.EthereumWalletAdapter,
}

Expand Down
Empty file.
27 changes: 27 additions & 0 deletions huma_signals/adapters/utils/etherscan_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pydantic
import httpx

from huma_signals import models
from huma_signals.settings import settings


class EtherscanClient(models.HumaBaseModel):
base_url: str = pydantic.Field(default=settings.etherscan_base_url)
api_key: str = pydantic.Field(default=settings.etherscan_api_key)

async def get_contract_abi(self, address: str) -> str:
try:
async with httpx.AsyncClient(base_url=self.base_url) as client:
resp = await client.get(
f"{self.base_url}/api?"
f"module=contract&"
f"action=getabi&"
f"address={address}&"
f"apikey={self.api_key}",
)
resp.raise_for_status()
payload = resp.json()
if payload["status"] == "1":
return payload["result"]
except httpx.HTTPStatusError as e:
raise e
3 changes: 3 additions & 0 deletions huma_signals/commons/chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Chain(enum.Enum):
ETHEREUM = "ETHEREUM"
GOERLI = "GOERLI"
POLYGON = "POLYGON"
LOCAL = "LOCAL"

@staticmethod
def from_chain_name(chain_name: str) -> Chain:
Expand All @@ -16,6 +17,8 @@ def from_chain_name(chain_name: str) -> Chain:
return Chain.GOERLI
if chain_name.lower() in ("polygon", "matic"):
return Chain.POLYGON
if chain_name.lower() in ("local"):
return Chain.LOCAL
raise ValueError(f"Unsupported chain: {chain_name}")

def chain_name(self) -> str:
Expand Down
1 change: 1 addition & 0 deletions huma_signals/commons/web3_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
chains.Chain.ETHEREUM: "1",
chains.Chain.GOERLI: "5",
chains.Chain.POLYGON: "998",
chains.Chain.LOCAL: "31337",
}

_MODULES: Dict[str, Union[Type[module.Module], Sequence[Any]]] = {
Expand Down