Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ release:
.PHONY: generate-models
generate-models:
python scripts/generate-models.py
poetry run ruff check --fix derive_client/data/generated/models.py
poetry run ruff format derive_client/data/generated/models.py
poetry run ruff check --fix derive_client/data_types/generated_models.py
poetry run ruff format derive_client/data_types/generated_models.py

.PHONY: generate-rest-api
generate-rest-api:
Expand All @@ -83,3 +83,9 @@ generate-rest-async-http:
python scripts/generate-rest-async-http.py
poetry run ruff check --fix tests/test_clients/test_rest/test_async_http
poetry run ruff format tests/test_clients/test_rest/test_async_http

.PHONY: generate-sync-bridge-client
generate-sync-bridge-client:
python scripts/generate-sync-bridge-client.py
poetry run ruff check --fix derive_client/_bridge/client.py
poetry run ruff format derive_client/_bridge/client.py
11 changes: 6 additions & 5 deletions derive_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
Init for the derive client
"""
"""Derive client package."""

from .derive import DeriveClient
from ._clients import AsyncHTTPClient, HTTPClient

DeriveClient
__all__ = [
"HTTPClient",
"AsyncHTTPClient",
]
186 changes: 113 additions & 73 deletions derive_client/_bridge/_derive_bridge.py

Large diffs are not rendered by default.

58 changes: 32 additions & 26 deletions derive_client/_bridge/_standard_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
import json
from decimal import Decimal
from logging import Logger
from typing import cast

from eth_account import Account
from eth_utils import keccak
from eth_account.signers.local import LocalAccount
from eth_utils.crypto import keccak
from returns.future import future_safe
from returns.io import IOResult
from web3 import AsyncWeb3
from web3.contract import AsyncContract
from web3.types import HexBytes, LogReceipt, TxReceipt
from web3.contract.async_contract import AsyncContract, AsyncContractEvent

from derive_client.constants import (
from derive_client.config import (
L1_CHUG_SPLASH_PROXY,
L1_CROSS_DOMAIN_MESSENGER_ABI_PATH,
L1_STANDARD_BRIDGE_ABI_PATH,
Expand All @@ -25,14 +24,17 @@
RESOLVED_DELEGATE_PROXY,
)
from derive_client.data_types import (
Address,
BridgeTxDetails,
BridgeTxResult,
BridgeType,
ChainID,
ChecksumAddress,
Currency,
PreparedBridgeTx,
TxHash,
TxResult,
TypedLogReceipt,
TypedTxReceipt,
)
from derive_client.exceptions import BridgeEventParseError, PartialBridgeResult, StandardBridgeRelayFailed
from derive_client.utils.w3 import to_base_units
Expand Down Expand Up @@ -81,7 +83,7 @@ def _load_l2_cross_domain_messenger_proxy(w3: AsyncWeb3) -> AsyncContract:
class StandardBridge:
"""Bridge tokens using Optimism's native standard bridge."""

def __init__(self, account: Account, logger: Logger):
def __init__(self, account: LocalAccount, logger: Logger):
"""
Initialize Standard bridge.

Expand All @@ -102,10 +104,10 @@ def __init__(self, account: Account, logger: Logger):
async def prepare_eth_tx(
self,
amount: Decimal,
to: Address,
to: ChecksumAddress,
source_chain: ChainID,
target_chain: ChainID,
) -> IOResult[PreparedBridgeTx, Exception]:
) -> PreparedBridgeTx:
currency = Currency.ETH

if source_chain is not ChainID.ETH or target_chain is not ChainID.DERIVE or to != self.account.address:
Expand All @@ -124,16 +126,16 @@ async def prepare_eth_tx(
@property
def private_key(self) -> str:
"""Private key of the owner (EOA)."""
return self.account._private_key
return self.account.key.to_0x_hex() # type: ignore[attr-defined]

@future_safe
async def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> IOResult[BridgeTxResult, Exception]:
async def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult:
tx_result = await self._send_bridge_tx(prepared_tx=prepared_tx)

return tx_result

@future_safe
async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> IOResult[BridgeTxResult, Exception]:
async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> BridgeTxResult:
try:
tx_result.source_tx.tx_receipt = await self._confirm_source_tx(tx_result=tx_result)
tx_result.target_tx = TxResult(tx_hash=await self._wait_for_target_event(tx_result=tx_result))
Expand All @@ -146,7 +148,7 @@ async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> IOResult[Brid
async def _prepare_eth_tx(
self,
value: int,
to: Address,
to: ChecksumAddress,
source_chain: ChainID,
target_chain: ChainID,
) -> PreparedBridgeTx:
Expand All @@ -169,9 +171,9 @@ async def _prepare_eth_tx(
signed_tx = sign_tx(w3=w3, tx=tx, private_key=self.private_key)

tx_details = BridgeTxDetails(
contract=func.address,
method=func.fn_name,
kwargs=func.kwargs,
contract=ChecksumAddress(func.address),
fn_name=func.fn_name,
fn_kwargs=func.kwargs,
tx=tx,
signed_tx=signed_tx,
)
Expand Down Expand Up @@ -209,7 +211,7 @@ async def _send_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult

return tx_result

async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TxReceipt:
async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TypedTxReceipt:
msg = "⏳ Checking source chain [%s] tx receipt for %s"
self.logger.info(msg, tx_result.source_chain.name, tx_result.source_tx.tx_hash)

Expand All @@ -222,14 +224,16 @@ async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TxReceipt:

return tx_receipt

async def _wait_for_target_event(self, tx_result: BridgeTxResult) -> HexBytes:
async def _wait_for_target_event(self, tx_result: BridgeTxResult) -> TxHash:
event_log = await self._fetch_standard_event_log(tx_result)
tx_hash = event_log["transactionHash"]
tx_hash = event_log.transactionHash
self.logger.info(f"Target event tx_hash found: {tx_hash.to_0x_hex()}")

return tx_hash
return TxHash(tx_hash)

async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TypedTxReceipt:
assert tx_result.target_tx is not None, "Expected tx_result.target_tx to exist"

async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TxReceipt:
msg = "⏳ Checking target chain [%s] tx receipt for %s"
self.logger.info(msg, tx_result.target_chain.name, tx_result.target_tx.tx_hash)

Expand All @@ -242,7 +246,9 @@ async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TxReceipt:

return tx_receipt

async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> LogReceipt:
async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> TypedLogReceipt:
assert tx_result.source_tx.tx_receipt, "Expected source_tx.receipt to exist"

source_event = self.l1_messenger_proxy.events.SentMessage()

target_w3 = self.w3s[tx_result.target_chain]
Expand All @@ -259,7 +265,7 @@ async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> LogRecei
sender = AsyncWeb3.to_checksum_address(args["sender"])
target = AsyncWeb3.to_checksum_address(args["target"])
message = args["message"]
value = tx_result.amount
value = tx_result.prepared_tx.amount

func = self.l1_messenger_proxy.functions.relayMessage(
_nonce=nonce,
Expand All @@ -274,8 +280,8 @@ async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> LogRecei
tx_result.event_id = msg_hash.hex()
self.logger.info(f"🗝️ Computed msgHash: {tx_result.event_id}")

target_event = self.l2_messenger_proxy.events.RelayedMessage()
failed_target_event = self.l2_messenger_proxy.events.FailedRelayedMessage()
target_event = cast(AsyncContractEvent, self.l2_messenger_proxy.events.RelayedMessage())
failed_target_event = cast(AsyncContractEvent, self.l2_messenger_proxy.events.FailedRelayedMessage())

filter_params = make_filter_params(
event=target_event,
Expand Down
41 changes: 21 additions & 20 deletions derive_client/_bridge/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
from decimal import Decimal
from logging import Logger

from eth_account import Account
from eth_account.signers.local import LocalAccount
from returns.io import IOResult

from derive_client._bridge._derive_bridge import DeriveBridge
from derive_client._bridge._standard_bridge import StandardBridge
from derive_client.data_types import (
Address,
BridgeTxResult,
BridgeType,
ChainID,
ChecksumAddress,
Currency,
Environment,
PreparedBridgeTx,
Expand All @@ -31,7 +31,7 @@ class AsyncBridgeClient:
- Multiple chains: BASE, ARBITRUM, OPTIMISM, ETH
"""

def __init__(self, env: Environment, account: Account, wallet: Address, logger: Logger):
def __init__(self, env: Environment, account: LocalAccount, wallet: ChecksumAddress, logger: Logger):
self._env = env
self._account = account
self._wallet = wallet
Expand All @@ -58,10 +58,11 @@ async def connect(self) -> None:
self._derive_bridge = derive_bridge
self._standard_bridge = StandardBridge(account=self._account, logger=self._logger)

def _ensure_bridge_available(self) -> None:
if self._derive_bridge and self._standard_bridge:
return
raise NotConnectedError("BridgeClient not connected. Call await .connect() first.")
def _require_bridges(self) -> tuple[DeriveBridge, StandardBridge]:
"""Return non-None bridges or raise. Keeps attributes private and typed."""
if self._derive_bridge is None or self._standard_bridge is None:
raise NotConnectedError("BridgeClient not connected. Call await .connect() first.")
return self._derive_bridge, self._standard_bridge

# === PUBLIC API (Simple - raises on error) ===
async def prepare_deposit_tx(
Expand Down Expand Up @@ -144,11 +145,11 @@ async def try_prepare_gas_deposit_tx(
) -> IOResult[PreparedBridgeTx, Exception]:
"""Prepare gas deposit with explicit error handling."""

self._ensure_bridge_available()
to = self._account.address
_, standard_bridge = self._require_bridges()
to = ChecksumAddress(self._account.address)
target_chain = ChainID.DERIVE

return await self._standard_bridge.prepare_eth_tx(
return await standard_bridge.prepare_eth_tx(
amount=amount,
to=to,
source_chain=chain_id,
Expand All @@ -164,8 +165,8 @@ async def try_prepare_deposit_tx(
) -> IOResult[PreparedBridgeTx, Exception]:
"""Prepare deposit with explicit error handling."""

self._ensure_bridge_available()
return await self._derive_bridge.prepare_deposit(
derive_bridge, _ = self._require_bridges()
return await derive_bridge.prepare_deposit(
amount=amount,
currency=currency,
chain_id=chain_id,
Expand All @@ -180,8 +181,8 @@ async def try_prepare_withdrawal_tx(
) -> IOResult[PreparedBridgeTx, Exception]:
"""Prepare withdrawal with explicit error handling."""

self._ensure_bridge_available()
return await self._derive_bridge.prepare_withdrawal(
derive_bridge, _ = self._require_bridges()
return await derive_bridge.prepare_withdrawal(
amount=amount,
currency=currency,
chain_id=chain_id,
Expand All @@ -190,17 +191,17 @@ async def try_prepare_withdrawal_tx(
async def try_submit_tx(self, *, prepared_tx: PreparedBridgeTx) -> IOResult[BridgeTxResult, Exception]:
"""Submit transaction with explicit error handling."""

self._ensure_bridge_available()
derive_bridge, standard_bridge = self._require_bridges()
if prepared_tx.bridge_type == BridgeType.STANDARD:
return await self._standard_bridge.submit_bridge_tx(prepared_tx=prepared_tx)
return await standard_bridge.submit_bridge_tx(prepared_tx=prepared_tx)

return await self._derive_bridge.submit_bridge_tx(prepared_tx=prepared_tx)
return await derive_bridge.submit_bridge_tx(prepared_tx=prepared_tx)

async def try_poll_tx_progress(self, *, tx_result: BridgeTxResult) -> IOResult[BridgeTxResult, Exception]:
"""Poll progress with explicit error handling."""

self._ensure_bridge_available()
derive_bridge, standard_bridge = self._require_bridges()
if tx_result.bridge_type == BridgeType.STANDARD:
return await self._standard_bridge.poll_bridge_progress(tx_result=tx_result)
return await standard_bridge.poll_bridge_progress(tx_result=tx_result)

return await self._derive_bridge.poll_bridge_progress(tx_result=tx_result)
return await derive_bridge.poll_bridge_progress(tx_result=tx_result)
Loading