Skip to content
Merged
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
85 changes: 76 additions & 9 deletions derive_client/clients/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
MakerTransferPositionModuleData,
MakerTransferPositionsModuleData,
RecipientTransferERC20ModuleData,
RFQQuoteDetails,
RFQQuoteModuleData,
SenderTransferERC20ModuleData,
TakerTransferPositionModuleData,
TakerTransferPositionsModuleData,
Expand Down Expand Up @@ -616,25 +618,90 @@ def send_quote(self, quote):
url = self.endpoints.private.send_quote
return self._send_request(url, quote)

def create_quote_object(
def create_quote(
self,
rfq_id,
legs,
direction,
):
"""Create a quote object."""
_, nonce, expiration = self.get_nonce_and_signature_expiry()
return {

rfq_legs: list[RFQQuoteDetails] = []
for leg in legs:
ticker = self.fetch_ticker(instrument_name=leg["instrument_name"])
rfq_quote_details = RFQQuoteDetails(
instrument_name=ticker["instrument_name"],
direction=leg["direction"],
asset_address=ticker["base_asset_address"],
sub_id=int(ticker["base_asset_sub_id"]),
price=leg["price"],
amount=Decimal(leg["amount"]),
)
rfq_legs.append(rfq_quote_details)

action = SignedAction(
subaccount_id=self.subaccount_id,
owner=self.wallet,
signer=self.signer.address,
signature_expiry_sec=MAX_INT_32,
nonce=nonce,
module_address=self.config.contracts.RFQ_MODULE,
module_data=RFQQuoteModuleData(
global_direction=direction,
max_fee=Decimal("123"),
legs=rfq_legs,
),
DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR,
ACTION_TYPEHASH=self.config.ACTION_TYPEHASH,
)

action.sign(self.signer.key)

payload = {
**action.to_json(),
"label": "",
"mmp": False,
"rfq_id": rfq_id,
}

return self.send_quote(quote=payload)

def cancel_rfq(self, rfq_id: str):
"""Cancel an RFQ."""
url = self.endpoints.private.cancel_rfq
payload = {
"subaccount_id": self.subaccount_id,
"rfq_id": rfq_id,
"legs": legs,
"direction": direction,
"max_fee": "10.0",
"nonce": nonce,
"signer": self.signer.address,
"signature_expiry_sec": expiration,
"signature": "filled_in_below",
}
return self._send_request(url, json=payload)

def cancel_batch_rfqs(self, rfq_id: str = None, label: str = None, nonce: int = None):
"""Cancel RFQs in batch."""
url = self.endpoints.private.cancel_batch_rfqs
payload = {
"subaccount_id": self.subaccount_id,
}
if rfq_id:
payload["rfq_id"] = rfq_id
if label:
payload["label"] = label
if nonce:
payload["nonce"] = nonce
return self._send_request(url, json=payload)

def poll_quotes(self, rfq_id: str = None, quote_id: str = None, status: RfqStatus = None):
url = self.endpoints.private.poll_quotes
payload = {
"subaccount_id": self.subaccount_id,
}
if rfq_id:
payload["rfq_id"] = rfq_id
if quote_id:
payload["quote_id"] = quote_id
if status:
payload["status"] = status.value
return self._send_request(url, json=payload)

def _send_request(self, url, json=None, params=None, headers=None):
headers = self._create_signature_headers() if not headers else headers
Expand Down
3 changes: 3 additions & 0 deletions derive_client/data_types/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ class RfqStatus(Enum):
"""RFQ statuses."""

OPEN = "open"
FILLED = "filled"
CANCELLED = "cancelled"
EXPIRED = "expired"


class EthereumJSONRPCErrorCode(IntEnum):
Expand Down
3 changes: 3 additions & 0 deletions derive_client/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def __init__(self, base_url: str):
set_mmp_config = Endpoint("private", "set_mmp_config")
send_rfq = Endpoint("private", "send_rfq")
poll_rfqs = Endpoint("private", "poll_rfqs")
poll_quotes = Endpoint("private", "poll_quotes")
cancel_rfq = Endpoint("private", "cancel_rfq")
cancel_batch_rfqs = Endpoint("private", "cancel_batch_rfqs")
send_quote = Endpoint("private", "send_quote")
deposit = Endpoint("private", "deposit")
withdraw = Endpoint("private", "withdraw")
Expand Down
56 changes: 0 additions & 56 deletions examples/poll_rfq.py

This file was deleted.

62 changes: 62 additions & 0 deletions examples/rfqs/create_rfq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Create an rfq using the REST API.
"""

import json
from datetime import datetime

from derive_client import DeriveClient
from derive_client.data_types import Environment
from derive_client.data_types.enums import InstrumentType, OrderSide, UnderlyingCurrency
from tests.conftest import TEST_PRIVATE_KEY, TEST_WALLET
from tests.test_rfq import Leg, Rfq

SLEEP_TIME = 1


def main():
"""
Sample of polling for RFQs and printing their status.
"""

client: DeriveClient = DeriveClient(
private_key=TEST_PRIVATE_KEY,
wallet=TEST_WALLET,
env=Environment.TEST,
)

# we get an option market
markets = client.fetch_instruments(
instrument_type=InstrumentType.OPTION,
currency=UnderlyingCurrency.ETH,
expired=False,
)

sorted_markets = sorted(markets, key=lambda m: (m['option_details']['expiry']))

zero_day_markets = list(
filter(lambda m: (m['option_details']['expiry'] - datetime.utcnow().timestamp()) < (3600 * 24), sorted_markets)
)
print("Zero day markets:")
selected_market = zero_day_markets[0]
print(json.dumps(selected_market, indent=2))
print(f"Found {len(zero_day_markets)} zero day markets")
expiry_time = selected_market['option_details']['expiry']
current_time = datetime.utcnow().timestamp()
print(
f"Expiry time: {expiry_time}, current time: {current_time}, time to expiry: {(expiry_time - current_time) / 3600:.2f} hours"
)

# we create an rfq for this market

leg = Leg(instrument_name=selected_market['instrument_name'], amount=1, direction=OrderSide.BUY)
request = Rfq(
legs=[leg],
subaccount_id=client.subaccount_id,
)
rfq = client.send_rfq(request.model_dump())
print("RFQ created with id:", rfq['rfq_id'])


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions examples/rfqs/poll_rfq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Example of how to poll RFQ (Request for Quote) status and handle transfers between subaccount and funding account.
"""

from time import sleep

from derive_client import DeriveClient
from derive_client.data_types import Environment
from tests.conftest import TEST_PRIVATE_KEY, TEST_WALLET

SLEEP_TIME = 1


def main():
"""
Sample of polling for RFQs and printing their status.
"""

client = DeriveClient(
private_key=TEST_PRIVATE_KEY,
wallet=TEST_WALLET,
env=Environment.TEST,
)

processed_rfqs = set()

while True:
quotes = client.poll_rfqs()
sleep(SLEEP_TIME) # Sleep for a while before polling again
raw_rfqs = quotes.get('rfqs', [])
rfqs = {rfq['rfq_id']: rfq for rfq in raw_rfqs}

if not rfqs:
print("No RFQs found, exiting.")
break
for rfq_id in rfqs:
if rfq_id in processed_rfqs:
continue
rfq = rfqs[rfq_id]
print(f"RFQ ID: {rfq_id} Status: {rfq['status']}, Legs: {len(rfq['legs'])}")
processed_rfqs.add(rfq_id)
for rfq in processed_rfqs.copy():
if rfq not in rfqs:
print(f"RFQ ID {rfq} no longer present in polled RFQs, removing from processed list.")
processed_rfqs.remove(rfq)


if __name__ == "__main__":
main()
14 changes: 13 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
Conftest for derive tests
"""

import time
from unittest.mock import MagicMock

import pytest

from derive_client.clients import AsyncClient
from derive_client.data_types import Environment
from derive_client.derive import DeriveClient
from derive_client.exceptions import DeriveJSONRPCException
from derive_client.utils import get_logger

TEST_WALLET = "0x8772185a1516f0d61fC1c2524926BfC69F95d698"
Expand All @@ -31,7 +33,17 @@ def derive_client():
wallet=TEST_WALLET, private_key=TEST_PRIVATE_KEY, env=Environment.TEST, logger=get_logger()
)
yield derive_client
derive_client.cancel_all()
while True:
try:
derive_client.cancel_all()
derive_client.cancel_batch_rfqs()
break
except DeriveJSONRPCException as e:
if "Retry after" in e.data:
wait_ms = int(e.data.split(" ")[2])
time.sleep(wait_ms / 1000)
continue
raise e


@pytest.fixture
Expand Down
Loading