Skip to content

Commit

Permalink
Generic vault interface, second batch of work (#238)
Browse files Browse the repository at this point in the history
- Adding more vault interface structure
- Optimise `fetch_erc20_details` caching in some use cases
  • Loading branch information
miohtama authored Nov 24, 2024
1 parent b25c58b commit cba702a
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 13 deletions.
12 changes: 8 additions & 4 deletions eth_defi/balances.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass
from decimal import Decimal
from itertools import islice
from typing import Dict, Optional, Set
from typing import Dict, Optional, Set, Collection

import cachetools
import requests.exceptions
Expand Down Expand Up @@ -115,7 +115,7 @@ def fetch_erc20_balances_by_transfer_event(
def fetch_erc20_balances_by_token_list(
web3: Web3,
owner: HexAddress | str,
tokens: Set[HexAddress | str],
tokens: Collection[HexAddress | str],
block_identifier: BlockIdentifier = None,
decimalise=False,
) -> Dict[HexAddress | str, int | Decimal]:
Expand Down Expand Up @@ -164,12 +164,14 @@ def test_portfolio_token_list(web3: Web3, deployer: str, user_1: str, usdc: Cont
else:
last_block = None

chain_id = web3.eth.chain_id

logger.info(f"Reading the latest token balances for {len(tokens)} tokens at block identifier {block_identifier}, last block is {last_block}, address is {owner}")

balances = {}
for address in tokens:
# Uses cached token ABI
token = fetch_erc20_details(web3, address)
token = fetch_erc20_details(web3, address, chain_id=chain_id)
try:
if decimalise:
balances[address] = token.fetch_balance_of(owner, block_identifier)
Expand Down Expand Up @@ -322,6 +324,8 @@ def _handler(success, value):
return None
return value

chain_id = web3.eth.chain_id

logger.info(
"Looking up token balances for %d addresses, chunk size %d, gas limit %d",
len(tokens),
Expand Down Expand Up @@ -360,7 +364,7 @@ def _handler(success, value):
if decimalise:
result = {}
for token_address, raw_balance in all_calls.items():
token = fetch_erc20_details(web3, token_address, cache=token_cache)
token = fetch_erc20_details(web3, token_address, cache=token_cache, chain_id=chain_id)
result[token_address] = token.convert_to_decimals(raw_balance) if raw_balance is not None else None
else:
result = all_calls
Expand Down
13 changes: 13 additions & 0 deletions eth_defi/provider/anvil.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,5 +546,18 @@ def is_anvil(web3: Web3) -> bool:
return "anvil/" in web3.client_version


def is_mainnet_fork(web3: Web3) -> bool:
"""Have we forked mainnet for this test.
- Only relevant with :py:func:`is_anvil`
:return:
True if we think we are connected to a forked mainnet,
False if we think we are a standalone local dev chain.
"""
# Heurestics
return web3.eth.block_number > 500_000


# Backwards compatibility
fork_network_anvil = launch_anvil
24 changes: 24 additions & 0 deletions eth_defi/velvet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from eth_defi.balances import fetch_erc20_balances_by_token_list, fetch_erc20_balances_multicall
from eth_defi.vault.base import VaultBase, VaultInfo, VaultSpec, TradingUniverse, VaultPortfolio
from eth_defi.velvet.deposit import deposit_to_velvet
from eth_defi.velvet.enso import swap_with_velvet_and_enso

#: Signing API URL
Expand Down Expand Up @@ -97,6 +98,10 @@ def vault_address(self) -> HexAddress:
def owner_address(self) -> HexAddress:
return self.info["owner"]

@property
def portfolio_address(self) -> HexAddress:
return self.info["portfolio"]

@property
def name(self) -> str:
return self.info["name"]
Expand Down Expand Up @@ -165,6 +170,25 @@ def prepare_swap_with_enso(

return tx_data

def prepare_deposit_with_enso(
self,
from_: HexAddress | str,
deposit_token_address: HexAddress | str,
amount: int,
):
"""Prepare a deposit transaction with Enso intents.
- Velvet trades any incoming assets and distributes them on open positions
"""
tx_data = deposit_to_velvet(
portfolio=self.portfolio_address,
from_address=from_,
deposit_token_address=deposit_token_address,
amount=amount,
chain_id=self.web3.eth.chain_id,
)
return tx_data

def _make_api_request(
self,
endpoint: str,
Expand Down
3 changes: 3 additions & 0 deletions eth_defi/velvet/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@


VELVET_DEFAULT_API_URL = "https://eventsapi.velvetdao.xyz/api/v3"
75 changes: 75 additions & 0 deletions eth_defi/velvet/deposit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Velvet deposit handling.
- Need to call proprietary centralised API to make a deposit
"""
from pprint import pformat
import logging

import requests
from eth_typing import HexAddress
from requests import HTTPError
from web3 import Web3

from eth_defi.velvet.config import VELVET_DEFAULT_API_URL


logger = logging.getLogger(__name__)


class VelvetDepositError(Exception):
"""Error reply from velvet txn API"""


def deposit_to_velvet(
portfolio: HexAddress | str,
from_address: HexAddress | str,
deposit_token_address: HexAddress | str,
amount: int,
chain_id: int,
api_url=VELVET_DEFAULT_API_URL,
) -> dict:
"""Construct Velvet deposit payload.
- See https://github.com/Velvet-Capital/3rd-party-integration/issues/2#issuecomment-2490845963 for details
"""
assert portfolio.startswith("0x")
assert from_address.startswith("0x")
assert deposit_token_address.startswith("0x")
assert type(amount) == int
# payload = {
# "portfolio": "0x444ef5b66f3dc7f3d36fe607f84fcb2f3a666902",
# "depositAmount": 1,
# "depositToken": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
# "user": "0x3C96e2Fc58332746fbBAB5eC44f01572F99033ed",
# "depositType": "batch",
# "tokenType": "erc20"
# }

payload = {
"portfolio": portfolio,
"depositAmount": amount,
"depositToken": deposit_token_address,
"user": from_address,
"depositType": "batch",
"tokenType": "erc20"
}

url = f"{api_url}/portfolio/deposit"

logger.info("Velvet deposit to %s with params:\n%s", url, pformat(payload))

resp = requests.post(url, json=payload)

try:
resp.raise_for_status()
except HTTPError as e:
raise VelvetDepositError(f"Velvet API error on {api_url}, code {resp.status_code}: {resp.text}") from e

tx_data = resp.json()

if "error" in tx_data:
raise VelvetDepositError(str(tx_data))

tx_data["from"] = Web3.to_checksum_address(from_address)
tx_data["chainId"] = chain_id
return tx_data
9 changes: 4 additions & 5 deletions eth_defi/velvet/enso.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from eth_typing import HexAddress
from requests import HTTPError

from eth_defi.velvet.config import VELVET_DEFAULT_API_URL

logger = logging.getLogger(__name__)

Expand All @@ -17,9 +18,6 @@ class VelvetSwapError(Exception):
"""Error reply from velvet txn API"""


REBALANCER_API_URL = "https://eventsapi.velvetdao.xyz/api/v3/rebalance/txn"


def swap_with_velvet_and_enso(
chain_id: int,
rebalance_address: HexAddress,
Expand All @@ -29,7 +27,7 @@ def swap_with_velvet_and_enso(
swap_amount: int,
slippage: float,
remaining_tokens: set[HexAddress],
api_url: str = REBALANCER_API_URL,
api_url: str = VELVET_DEFAULT_API_URL,
) -> dict:
"""Set up a Enzo + Velvet swap tx.
Expand Down Expand Up @@ -65,7 +63,8 @@ def swap_with_velvet_and_enso(
# Log out everything, so we can post the data for others to debug
logger.info("Velvet + Enso swap:\n%s", pformat(payload))

resp = requests.post(api_url, json=payload)
url = f"{api_url}/rebalance/txn"
resp = requests.post(url, json=payload)

try:
resp.raise_for_status()
Expand Down
91 changes: 87 additions & 4 deletions tests/velvet/test_velvet_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
from eth_typing import HexAddress
from web3 import Web3

from eth_defi.hotwallet import HotWallet
from eth_defi.provider.anvil import AnvilLaunch, fork_network_anvil
from eth_defi.provider.broken_provider import get_almost_latest_block_number
from eth_defi.provider.multi_provider import create_multi_provider_web3
from eth_defi.token import TokenDetails, fetch_erc20_details
from eth_defi.trace import assert_transaction_success_with_explanation
from eth_defi.uniswap_v3.constants import UNISWAP_V3_DEPLOYMENTS
from eth_defi.uniswap_v3.deployment import UniswapV3Deployment, fetch_deployment
Expand All @@ -27,6 +29,8 @@

JSON_RPC_BASE = os.environ.get("JSON_RPC_BASE", "https://mainnet.base.org")

CI = os.environ.get("CI", None) is not None

pytestmark = pytest.mark.skipif(not JSON_RPC_BASE, reason="No JSON_RPC_BASE environment variable")


Expand All @@ -37,14 +41,21 @@ def vault_owner() -> HexAddress:


@pytest.fixture()
def anvil_base_fork(request, vault_owner) -> AnvilLaunch:
def usdc_holder() -> HexAddress:
# https://basescan.org/token/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913#balances
return "0x3304E22DDaa22bCdC5fCa2269b418046aE7b566A"



@pytest.fixture()
def anvil_base_fork(request, vault_owner, usdc_holder) -> AnvilLaunch:
"""Create a testable fork of live BNB chain.
:return: JSON-RPC URL for Web3
"""
launch = fork_network_anvil(
JSON_RPC_BASE,
unlocked_addresses=[vault_owner],
unlocked_addresses=[vault_owner, usdc_holder],
)
try:
yield launch
Expand All @@ -60,6 +71,40 @@ def web3(anvil_base_fork) -> Web3:
return web3


@pytest.fixture()
def usdc(web3) -> TokenDetails:
return fetch_erc20_details(
web3,
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
)


@pytest.fixture()
def hot_wallet_user(web3, usdc, usdc_holder) -> HotWallet:
"""A test account with USDC balance."""

hw = HotWallet.create_for_testing(
web3,
test_account_n=1,
eth_amount=10
)
hw.sync_nonce(web3)

# give hot wallet some native token
web3.eth.send_transaction(
{
"from": web3.eth.accounts[9],
"to": hw.address,
"value": 1 * 10**18,
}
)

# Top up with 999 USDC
tx_hash = usdc.contract.functions.transfer(hw.address, 999 * 10**6).transact({"from": usdc_holder, "gas": 100_000})
assert_transaction_success_with_explanation(web3, tx_hash)
return hw


@pytest.fixture()
def base_test_vault_spec() -> VaultSpec:
"""Vault https://dapp.velvet.capital/ManagerVaultDetails/0x205e80371f6d1b33dff7603ca8d3e92bebd7dc25"""
Expand Down Expand Up @@ -96,7 +141,7 @@ def test_fetch_vault_portfolio(vault: VelvetVault):
assert portfolio.spot_erc20["0x6921B130D297cc43754afba22e5EAc0FBf8Db75b"] > 0


@flaky.flaky
@pytest.mark.skipif(CI, reason="Enso is such unstable crap that there is no hope we could run any tests with in CI")
def test_vault_swap_partially(
vault: VelvetVault,
vault_owner: HexAddress,
Expand Down Expand Up @@ -145,7 +190,7 @@ def test_vault_swap_partially(
assert portfolio.spot_erc20["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"] < existing_usdc_balance


@flaky.flaky
@pytest.mark.skipif(CI, reason="Enso is such unstable crap that there is no hope we could run any tests with in CI")
def test_vault_swap_very_little(
vault: VelvetVault,
vault_owner: HexAddress,
Expand Down Expand Up @@ -215,3 +260,41 @@ def test_vault_swap_sell_to_usdc(
latest_block = web3.eth.block_number
portfolio = vault.fetch_portfolio(universe, latest_block)
assert portfolio.spot_erc20["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"] > existing_usdc_balance


@pytest.mark.skip(reason="Velvet API is broken")
def test_velvet_api_deposit(
vault: VelvetVault,
vault_owner: HexAddress,
hot_wallet_user: HotWallet,
usdc: TokenDetails,
):
"""Use Velvet API to perform deposit"""

web3 = vault.web3
universe = TradingUniverse(
spot_token_addresses={
"0x6921B130D297cc43754afba22e5EAc0FBf8Db75b", # DogInMe
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", # USDC on Base
}
)
latest_block = get_almost_latest_block_number(web3)
portfolio = vault.fetch_portfolio(universe, latest_block)
existing_usdc_balance = portfolio.spot_erc20["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"]
assert existing_usdc_balance > Decimal(1.0)

tx_data = vault.prepare_deposit_with_enso(
from_=hot_wallet_user.address,
deposit_token_address=usdc.address,
amount=500 * 10 ** 6,
)

hot_wallet_user.fill_in_gas_price(web3, tx_data)

signed_tx = hot_wallet_user.sign_transaction_with_new_nonce(tx_data)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
assert_transaction_success_with_explanation(web3, tx_hash)

# USDC balance has increased after the deposit
portfolio = vault.fetch_portfolio(universe, web3.eth.block_number)
assert portfolio.spot_erc20["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"] > existing_usdc_balance

0 comments on commit cba702a

Please sign in to comment.