mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
更新环境配置示例,添加 GRVT 相关的 API 凭证和选项,增强文档以支持新的交易所适配器,确保用户能够正确配置和使用 GRVT 交易功能。
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pysdk.grvt_ccxt import GrvtCcxt
|
||||
from pysdk.grvt_ccxt_env import GrvtEnv
|
||||
from pysdk.grvt_ccxt_logging_selector import logger
|
||||
from pysdk.grvt_ccxt_test_utils import validate_return_values
|
||||
from pysdk.grvt_ccxt_types import DURATION_SECOND_IN_NSEC, GrvtOrderSide
|
||||
from pysdk.grvt_ccxt_utils import rand_uint32
|
||||
|
||||
|
||||
def get_open_orders(api: GrvtCcxt) -> list[dict]:
|
||||
open_orders: list[dict] = api.fetch_open_orders(
|
||||
symbol="BTC_USDT_Perp",
|
||||
params={"kind": "PERPETUAL"},
|
||||
)
|
||||
logger.info(f"open_orders: {open_orders=}")
|
||||
return open_orders
|
||||
|
||||
|
||||
def fetch_order_history(api: GrvtCcxt) -> dict:
|
||||
order_history: dict = api.fetch_order_history(
|
||||
params={"kind": "PERPETUAL", "limit": 3},
|
||||
)
|
||||
logger.info(f"order_history: {order_history=}")
|
||||
return order_history
|
||||
|
||||
def fetch_funding_history(api: GrvtCcxt) -> dict:
|
||||
start_date: datetime = datetime.strptime("2025-05-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
|
||||
funding_history: dict = api.fetch_funding_rate_history(
|
||||
symbol="BTC_USDT_Perp",
|
||||
since=int(start_date.timestamp() * DURATION_SECOND_IN_NSEC), # Convert to nanoseconds
|
||||
limit=500,
|
||||
)
|
||||
results: list = funding_history.get("result", [])
|
||||
if results:
|
||||
logger.info(f"funding_history: START={results[0]}")
|
||||
logger.info(f"funding_history: END={results[-1]}")
|
||||
else:
|
||||
logger.info(f"funding_history: No results found in {funding_history=}")
|
||||
return funding_history
|
||||
|
||||
|
||||
def cancel_orders(api: GrvtCcxt, open_orders: list) -> int:
|
||||
FN = "cancel_orders"
|
||||
order_count = 0
|
||||
for order_dict in open_orders:
|
||||
client_order_id = order_dict["metadata"].get("client_order_id")
|
||||
if client_order_id:
|
||||
# Cancel
|
||||
logger.info(f"{FN} cancel order by id:{order_dict['order_id']}")
|
||||
success = api.cancel_order(
|
||||
id=order_dict["order_id"], params={"time_to_live_ms": "1000"}
|
||||
)
|
||||
order_count += int(success)
|
||||
else:
|
||||
logger.warning(f"{FN} client_order_id not found in {order_dict=}")
|
||||
return order_count
|
||||
|
||||
def show_derisk_mm_ratios(api: GrvtCcxt, keyword: str) -> None:
|
||||
"""Show the current derisking market making ratios."""
|
||||
FN = "show_derisk_mm_ratios"
|
||||
acc_summary = api.get_account_summary(type="sub-account")
|
||||
maintenance_margin = acc_summary.get("maintenance_margin")
|
||||
derisk_margin = acc_summary.get("derisk_margin")
|
||||
derisk_ratio = acc_summary.get("derisk_to_maintenance_margin_ratio")
|
||||
logger.info(f"{FN} {keyword} {maintenance_margin=}")
|
||||
logger.info(f"{FN} {keyword} {derisk_margin=}")
|
||||
logger.info(f"{FN} {keyword} {derisk_ratio=}")
|
||||
logger.info(f"sub-account summary:\n{acc_summary}")
|
||||
|
||||
def set_derisk_mm_ratio(api: GrvtCcxt, ratio: str = "1.4") -> None:
|
||||
"""Set the derisking market making ratio."""
|
||||
FN = f"set_derisk_mm_ratio {ratio=}"
|
||||
logger.info(f"{FN} START")
|
||||
show_derisk_mm_ratios(api, "BEFORE")
|
||||
api.set_derisk_mm_ratio(ratio)
|
||||
show_derisk_mm_ratios(api, "AFTER")
|
||||
|
||||
|
||||
def cancel_all_orders(api: GrvtCcxt) -> bool:
|
||||
FN = "cancel_all_orders"
|
||||
logger.info(f"{FN} START")
|
||||
cancel_response = api.cancel_all_orders()
|
||||
logger.info(f"{FN} {cancel_response=}")
|
||||
return cancel_response
|
||||
|
||||
|
||||
def print_instruments(api: GrvtCcxt):
|
||||
logger.info("print_instruments: START")
|
||||
if not api.markets:
|
||||
return
|
||||
for market in list(api.markets.values())[:3]:
|
||||
logger.info(f"{market=}")
|
||||
instrument = market["instrument"]
|
||||
logger.info(f"fetch_market: {instrument=}, {api.fetch_market(instrument)}")
|
||||
logger.info(f"fetch_mini_ticker: {instrument=}, {api.fetch_mini_ticker(instrument)}")
|
||||
logger.info(f"fetch_ticker: {instrument=}, {api.fetch_ticker(instrument)}")
|
||||
logger.info(f"fetch_order_book {instrument=}, {api.fetch_order_book(instrument, limit=10)}")
|
||||
logger.info(
|
||||
f"fetch_recent_trades {instrument=}, {api.fetch_recent_trades(instrument, limit=5)}"
|
||||
)
|
||||
logger.info(f"fetch_trades {instrument=}, {api.fetch_trades(instrument, limit=5)}")
|
||||
logger.info(
|
||||
f"fetch_funding_rate_history {instrument=}, "
|
||||
f"{api.fetch_funding_rate_history(instrument, limit=5)}"
|
||||
)
|
||||
for type in ["TRADE", "MARK", "INDEX", "MID"]:
|
||||
ohlc = api.fetch_ohlcv(
|
||||
instrument, timeframe="5m", limit=5, params={"candle_type": type}
|
||||
)
|
||||
logger.info(f"fetch_ohlcv {type} {instrument=}, {ohlc}")
|
||||
|
||||
|
||||
def send_order(api: GrvtCcxt, side: GrvtOrderSide, client_order_id: int) -> dict:
|
||||
price = 94_000 if side == "buy" else 95_000
|
||||
send_order_response: dict = api.create_order(
|
||||
symbol="BTC_USDT_Perp",
|
||||
order_type="limit",
|
||||
side=side,
|
||||
amount=0.01,
|
||||
price=price,
|
||||
params={"client_order_id": client_order_id},
|
||||
)
|
||||
logger.info(f"send order: {send_order_response=} {client_order_id=}")
|
||||
return send_order_response
|
||||
|
||||
|
||||
def send_mkt_order(
|
||||
api: GrvtCcxt, symbol: str, side: GrvtOrderSide, amount: Decimal, client_order_id: int
|
||||
) -> dict:
|
||||
send_order_response: dict = api.create_order(
|
||||
symbol=symbol,
|
||||
order_type="market",
|
||||
side=side,
|
||||
amount=amount,
|
||||
params={"client_order_id": client_order_id},
|
||||
)
|
||||
logger.info(f"send mkt order: {send_order_response=} {client_order_id=}")
|
||||
return send_order_response
|
||||
|
||||
|
||||
# Test scenarios, called by the __main__ test routine
|
||||
def send_fetch_order(api: GrvtCcxt):
|
||||
client_order_id = rand_uint32()
|
||||
_ = send_order(api, side="buy", client_order_id=client_order_id)
|
||||
time.sleep(0.1)
|
||||
order_status = api.fetch_order(
|
||||
id=None,
|
||||
params={"client_order_id": client_order_id},
|
||||
)
|
||||
logger.info(f"result of fetch_order: {order_status=}")
|
||||
|
||||
|
||||
def check_cancel_check_orders(api: GrvtCcxt):
|
||||
logger.info("check_cancel_check_orders: START")
|
||||
open_orders = get_open_orders(api)
|
||||
if open_orders:
|
||||
cancel_orders(api, open_orders)
|
||||
get_open_orders(api)
|
||||
|
||||
|
||||
def fetch_my_trades(api: GrvtCcxt):
|
||||
logger.info("fetch_my_trades: START")
|
||||
my_trades = api.fetch_my_trades(
|
||||
symbol="BTC_USDT_Perp",
|
||||
limit=10,
|
||||
params={},
|
||||
)
|
||||
logger.info(f"my_trades: num trades:{len(my_trades)}")
|
||||
logger.info(f"my_trades: {my_trades=}")
|
||||
|
||||
|
||||
def cancel_send_order(api: GrvtCcxt):
|
||||
FN = "cancel_send_order"
|
||||
logger.info(f"{FN}: START")
|
||||
client_order_id: int = rand_uint32()
|
||||
logger.info(f"{FN} cancel order by {client_order_id=}")
|
||||
result = api.cancel_order(
|
||||
params={"client_order_id": client_order_id, "time_to_live_ms": "1000"}
|
||||
)
|
||||
logger.info(f"{FN} cancel_order: {result=}")
|
||||
order_response = send_mkt_order(
|
||||
api,
|
||||
symbol="BTC_USDT_Perp",
|
||||
side="sell",
|
||||
amount=Decimal("0.01"),
|
||||
client_order_id=client_order_id,
|
||||
)
|
||||
if order_response:
|
||||
time.sleep(0.1)
|
||||
# Get status
|
||||
logger.info(f"{FN} fetch_order by {client_order_id=}")
|
||||
order_status = api.fetch_order(params={"client_order_id": client_order_id})
|
||||
logger.info(f"{FN} {order_status=}")
|
||||
else:
|
||||
logger.warning(f"{FN}: order_response is None")
|
||||
|
||||
|
||||
def send_fetch_mkt_order(api: GrvtCcxt):
|
||||
FN = "send_fetch_mkt_order"
|
||||
logger.info(f"{FN}: START")
|
||||
client_order_id: int = rand_uint32()
|
||||
order_response = send_mkt_order(
|
||||
api,
|
||||
symbol="BTC_USDT_Perp",
|
||||
side="sell",
|
||||
amount=Decimal("0.01"),
|
||||
client_order_id=client_order_id,
|
||||
)
|
||||
if order_response:
|
||||
time.sleep(0.1)
|
||||
# Get status
|
||||
logger.info(f"{FN} fetch_order by {client_order_id=}")
|
||||
order_status = api.fetch_order(params={"client_order_id": client_order_id})
|
||||
logger.info(f"{FN} {order_status=}")
|
||||
else:
|
||||
logger.warning(f"{FN}: order_response is None")
|
||||
|
||||
|
||||
def print_markets(api: GrvtCcxt):
|
||||
logger.info("print_markets: START")
|
||||
if api.markets:
|
||||
logger.info(f"MARKETS:{len(api.markets)}")
|
||||
for market in api.markets.values():
|
||||
logger.info(f"MARKET:{market}")
|
||||
|
||||
|
||||
def fetch_all_markets(api: GrvtCcxt):
|
||||
logger.info("fetch_all_markets: START")
|
||||
instruments = api.fetch_all_markets()
|
||||
logger.info(f"fetch_all_markets: num instruments={len(instruments)}")
|
||||
|
||||
|
||||
def print_account_summary(api: GrvtCcxt):
|
||||
try:
|
||||
logger.info(f"sub-account summary:\n{api.get_account_summary(type='sub-account')}")
|
||||
logger.info(f"funding-account summary:\n{api.get_account_summary(type='funding')}")
|
||||
logger.info(f"aggregated-account summary:\n{api.get_account_summary(type='aggregated')}")
|
||||
logger.info(f"fetch_balance:\n{api.fetch_balance()}")
|
||||
except Exception as e:
|
||||
logger.error(f"account summary failed: {e}")
|
||||
|
||||
|
||||
def print_account_history(api: GrvtCcxt):
|
||||
try:
|
||||
hist = api.fetch_account_history(params={})
|
||||
logger.info(f"account history:\n{hist}")
|
||||
except Exception as e:
|
||||
logger.error(f"account history failed: {e}")
|
||||
|
||||
|
||||
def print_positions(api: GrvtCcxt):
|
||||
try:
|
||||
logger.info(f"positions:\n{api.fetch_positions(symbols=['BTC_USDT_Perp'])}")
|
||||
except Exception as e:
|
||||
logger.error(f"positions failed: {e}")
|
||||
|
||||
|
||||
def print_description(api: GrvtCcxt):
|
||||
try:
|
||||
logger.info(f"print_description: {api.describe()}")
|
||||
except Exception as e:
|
||||
logger.error(f"print_description failed: {e}")
|
||||
|
||||
|
||||
def test_grvt_ccxt():
|
||||
params = {
|
||||
"api_key": os.getenv("GRVT_API_KEY"),
|
||||
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
|
||||
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
|
||||
}
|
||||
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
|
||||
test_api = GrvtCcxt(env, logger, parameters=params, order_book_ccxt_format=True)
|
||||
function_list = [
|
||||
print_description,
|
||||
# -------- MARKET related
|
||||
fetch_all_markets,
|
||||
print_markets,
|
||||
print_instruments,
|
||||
print_account_summary,
|
||||
print_account_history,
|
||||
# print_positions,
|
||||
# -------- TRADE related
|
||||
# fetch_my_trades,
|
||||
fetch_order_history,
|
||||
fetch_funding_history,
|
||||
# # -------- order related
|
||||
send_fetch_order,
|
||||
fetch_my_trades,
|
||||
print_positions,
|
||||
check_cancel_check_orders,
|
||||
cancel_send_order,
|
||||
send_fetch_mkt_order,
|
||||
get_open_orders,
|
||||
send_fetch_order,
|
||||
get_open_orders,
|
||||
cancel_all_orders,
|
||||
get_open_orders,
|
||||
# Derisk MM ratio
|
||||
set_derisk_mm_ratio,
|
||||
]
|
||||
for f in function_list:
|
||||
try:
|
||||
f(test_api)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
|
||||
validate_return_values(test_api, "test_results_sync.csv")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_grvt_ccxt()
|
||||
@@ -0,0 +1,300 @@
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pysdk.grvt_ccxt_env import GrvtEnv
|
||||
from pysdk.grvt_ccxt_logging_selector import logger
|
||||
from pysdk.grvt_ccxt_pro import GrvtCcxtPro
|
||||
from pysdk.grvt_ccxt_test_utils import validate_return_values
|
||||
from pysdk.grvt_ccxt_types import DURATION_SECOND_IN_NSEC, GrvtOrderSide
|
||||
from pysdk.grvt_ccxt_utils import rand_uint32
|
||||
|
||||
|
||||
# Utility functions , not called directly by the __main__ test routine
|
||||
async def get_open_orders(api: GrvtCcxtPro) -> list[dict]:
|
||||
open_orders = await api.fetch_open_orders(
|
||||
|
||||
symbol="BTC_USDT_Perp",
|
||||
params={"kind": "PERPETUAL"},
|
||||
)
|
||||
logger.info(f"open_orders: {open_orders=}")
|
||||
return open_orders
|
||||
|
||||
|
||||
async def fetch_order_history(api: GrvtCcxtPro) -> dict:
|
||||
order_history: dict = await api.fetch_order_history(
|
||||
params={"kind": "PERPETUAL", "limit": 3},
|
||||
)
|
||||
logger.info(f"order_history: {order_history=}")
|
||||
return order_history
|
||||
|
||||
async def fetch_funding_history(api: GrvtCcxtPro) -> dict:
|
||||
start_date: datetime = datetime.strptime("2025-05-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
|
||||
funding_history: dict = await api.fetch_funding_rate_history(
|
||||
symbol="BTC_USDT_Perp",
|
||||
since=int(start_date.timestamp() * DURATION_SECOND_IN_NSEC), # Convert to nanoseconds
|
||||
limit=500,
|
||||
)
|
||||
results: list = funding_history.get("result", [])
|
||||
if results:
|
||||
logger.info(f"funding_history: START={results[0]}")
|
||||
logger.info(f"funding_history: END={results[-1]}")
|
||||
else:
|
||||
logger.info(f"funding_history: No results found in {funding_history=}")
|
||||
return funding_history
|
||||
|
||||
|
||||
async def cancel_orders(api: GrvtCcxtPro, open_orders: list) -> int:
|
||||
FN = "cancel_orders"
|
||||
logger.info(f"{FN} START")
|
||||
order_count: int = 0
|
||||
for order_dict in open_orders:
|
||||
client_order_id = order_dict["metadata"].get("client_order_id")
|
||||
if client_order_id:
|
||||
# Cancel
|
||||
logger.info(f"{FN} cancel order by id:{order_dict['order_id']}")
|
||||
await api.cancel_order(id=order_dict["order_id"])
|
||||
order_count += 1
|
||||
else:
|
||||
logger.warning(f"{FN} client_order_id not found in {order_dict=}")
|
||||
return order_count
|
||||
|
||||
async def show_derisk_mm_ratios(api: GrvtCcxtPro, keyword: str) -> None:
|
||||
"""Show the current derisking market making ratios."""
|
||||
FN = "show_derisk_mm_ratios"
|
||||
acc_summary = await api.get_account_summary(type="sub-account")
|
||||
maintenance_margin = acc_summary.get("maintenance_margin")
|
||||
derisk_margin = acc_summary.get("derisk_margin")
|
||||
derisk_ratio = acc_summary.get("derisk_to_maintenance_margin_ratio")
|
||||
logger.info(f"{FN} {keyword} {maintenance_margin=}")
|
||||
logger.info(f"{FN} {keyword} {derisk_margin=}")
|
||||
logger.info(f"{FN} {keyword} {derisk_ratio=}")
|
||||
logger.info(f"sub-account summary:\n{acc_summary}")
|
||||
|
||||
async def set_derisk_mm_ratio(api: GrvtCcxtPro, ratio: str = "1.5") -> None:
|
||||
"""Set the derisking market making ratio."""
|
||||
FN = f"set_derisk_mm_ratio {ratio=}"
|
||||
logger.info(f"{FN} START")
|
||||
await show_derisk_mm_ratios(api, "BEFORE")
|
||||
await api.set_derisk_mm_ratio(ratio)
|
||||
await show_derisk_mm_ratios(api, "AFTER")
|
||||
|
||||
async def cancel_all_orders(api: GrvtCcxtPro) -> bool:
|
||||
FN = "cancel_all_orders"
|
||||
logger.info(f"{FN} START")
|
||||
cancel_response = await api.cancel_all_orders()
|
||||
logger.info(f"{FN} {cancel_response=}")
|
||||
return cancel_response
|
||||
|
||||
|
||||
async def print_instruments(api: GrvtCcxtPro):
|
||||
logger.info("print_instruments: START")
|
||||
if not api.markets:
|
||||
return
|
||||
for market in list(api.markets.values())[:3]:
|
||||
logger.info(f"{market=}")
|
||||
instrument = market["instrument"]
|
||||
logger.info(f"fetch_mini_ticker: {instrument=}, {await api.fetch_mini_ticker(instrument)}")
|
||||
logger.info(f"fetch_ticker: {instrument=}, {await api.fetch_ticker(instrument)}")
|
||||
logger.info(
|
||||
f"fetch_order_book {instrument=}, {await api.fetch_order_book(instrument, limit=10)}"
|
||||
)
|
||||
logger.info(
|
||||
f"fetch_recent_trades {instrument=}, "
|
||||
f"{await api.fetch_recent_trades(instrument, limit=7)}"
|
||||
)
|
||||
logger.info(f"fetch_trades {instrument=}, {await api.fetch_trades(instrument, limit=5)}")
|
||||
logger.info(
|
||||
f"fetch_funding_rate_history {instrument=}, "
|
||||
f"{await api.fetch_funding_rate_history(instrument, limit=5)}"
|
||||
)
|
||||
for type in ["TRADE", "MARK", "INDEX", "MID"]:
|
||||
ohlc = await api.fetch_ohlcv(
|
||||
instrument, timeframe="1m", limit=5, params={"candle_type": type}
|
||||
)
|
||||
logger.info(f"fetch_ohlcv {type} {instrument=}, {ohlc}")
|
||||
|
||||
|
||||
async def send_order(api: GrvtCcxtPro, side: GrvtOrderSide, client_order_id: int) -> dict:
|
||||
price = 64_000 if side == "buy" else 65_000
|
||||
send_order_response = await api.create_order(
|
||||
symbol="BTC_USDT_Perp",
|
||||
order_type="limit",
|
||||
side=side,
|
||||
amount=0.01,
|
||||
price=price,
|
||||
params={"client_order_id": client_order_id},
|
||||
)
|
||||
logger.info(f"send order: {send_order_response=} {client_order_id=}")
|
||||
return send_order_response
|
||||
|
||||
|
||||
# Test scenarios, called by the __main__ test routine
|
||||
async def send_fetch_order(api: GrvtCcxtPro):
|
||||
client_order_id = rand_uint32()
|
||||
_ = await send_order(api, side="buy", client_order_id=client_order_id)
|
||||
order_status = await api.fetch_order(
|
||||
id=None,
|
||||
params={"client_order_id": client_order_id},
|
||||
)
|
||||
logger.info(f"result of fetch_order: {order_status=}")
|
||||
|
||||
|
||||
async def send_mkt_order(
|
||||
api: GrvtCcxtPro, symbol: str, side: GrvtOrderSide, amount: Decimal, client_order_id: int
|
||||
) -> dict:
|
||||
send_order_response = await api.create_order(
|
||||
symbol=symbol,
|
||||
order_type="market",
|
||||
side=side,
|
||||
amount=amount,
|
||||
params={"client_order_id": client_order_id},
|
||||
)
|
||||
logger.info(f"send mkt order: {send_order_response=} {client_order_id=}")
|
||||
return send_order_response
|
||||
|
||||
|
||||
async def check_cancel_check_orders(api: GrvtCcxtPro):
|
||||
logger.info("check_cancel_check_orders: START")
|
||||
open_orders = await get_open_orders(api)
|
||||
if open_orders:
|
||||
await cancel_orders(api, open_orders)
|
||||
await get_open_orders(api)
|
||||
|
||||
|
||||
async def fetch_my_trades(api: GrvtCcxtPro):
|
||||
logger.info("fetch_my_trades: START")
|
||||
my_trades = await api.fetch_my_trades(
|
||||
symbol="BTC_USDT_Perp",
|
||||
limit=10,
|
||||
params={},
|
||||
)
|
||||
logger.info(f"my_trades: num trades:{len(my_trades)}")
|
||||
logger.info(f"my_trades: {my_trades=}")
|
||||
|
||||
|
||||
async def cancel_send_order(api: GrvtCcxtPro):
|
||||
FN = "cancel_send_order"
|
||||
logger.info(f"{FN}: START")
|
||||
client_order_id: int = rand_uint32()
|
||||
logger.info(f"{FN} cancel order by {client_order_id=}")
|
||||
result = await api.cancel_order(
|
||||
params={"client_order_id": client_order_id, "time_to_live_ms": "1000"}
|
||||
)
|
||||
logger.info(f"{FN} cancel_order: {result=}")
|
||||
order_response = await send_mkt_order(
|
||||
api,
|
||||
symbol="BTC_USDT_Perp",
|
||||
side="sell",
|
||||
amount=Decimal("0.01"),
|
||||
client_order_id=client_order_id,
|
||||
)
|
||||
if order_response:
|
||||
# Get status
|
||||
logger.info(f"{FN} fetch_order by {client_order_id=}")
|
||||
order_status = await api.fetch_order(params={"client_order_id": client_order_id})
|
||||
logger.info(f"{FN} {order_status=}")
|
||||
|
||||
|
||||
async def print_markets(api: GrvtCcxtPro):
|
||||
logger.info("print_markets: START")
|
||||
if api.markets:
|
||||
logger.info(f"MARKETS:{len(api.markets)}")
|
||||
for market in api.markets.values():
|
||||
logger.info(f"MARKET:{market}")
|
||||
|
||||
|
||||
async def fetch_all_markets(api: GrvtCcxtPro):
|
||||
logger.info("fetch_all_markets: START")
|
||||
instruments = await api.fetch_all_markets()
|
||||
logger.info(f"fetch_all_markets: num instruments={len(instruments)}")
|
||||
|
||||
|
||||
async def print_account_summary(api: GrvtCcxtPro):
|
||||
try:
|
||||
logger.info("print_account_summary: START")
|
||||
logger.info(f"sub-account summary:\n{await api.get_account_summary(type='sub-account')}")
|
||||
logger.info(f"funding-account summary:\n{await api.get_account_summary(type='funding')}")
|
||||
logger.info(
|
||||
f"aggregated-account summary:\n{await api.get_account_summary(type='aggregated')}"
|
||||
)
|
||||
logger.info(f"fetch_balance:\n{await api.fetch_balance()}")
|
||||
except Exception as e:
|
||||
logger.error(f"account summary failed: {e}")
|
||||
|
||||
|
||||
async def print_account_history(api: GrvtCcxtPro):
|
||||
try:
|
||||
hist = await api.fetch_account_history(params={})
|
||||
logger.info(f"account history:\n{hist}")
|
||||
except Exception as e:
|
||||
logger.error(f"account history failed: {e}")
|
||||
|
||||
|
||||
async def print_positions(api: GrvtCcxtPro):
|
||||
try:
|
||||
logger.info(f"positions:\n{await api.fetch_positions(symbols=['BTC_USDT_Perp'])}")
|
||||
except Exception as e:
|
||||
logger.error(f"positions failed: {e}")
|
||||
|
||||
|
||||
async def print_description(api: GrvtCcxtPro):
|
||||
try:
|
||||
logger.info(f"print_description: {api.describe()}")
|
||||
except Exception as e:
|
||||
logger.error(f"print_description failed: {e}")
|
||||
|
||||
|
||||
async def grvt_ccxt_pro():
|
||||
params = {
|
||||
"api_key": os.getenv("GRVT_API_KEY"),
|
||||
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
|
||||
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
|
||||
}
|
||||
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
|
||||
test_api = GrvtCcxtPro(env, logger, parameters=params, order_book_ccxt_format=True)
|
||||
await test_api.load_markets()
|
||||
await asyncio.sleep(2)
|
||||
function_list = [
|
||||
print_description,
|
||||
# -------- MARKET related
|
||||
fetch_all_markets,
|
||||
print_markets,
|
||||
print_instruments,
|
||||
print_account_summary,
|
||||
print_account_history,
|
||||
print_positions,
|
||||
# Order / Trade history
|
||||
fetch_my_trades,
|
||||
fetch_order_history,
|
||||
fetch_funding_history,
|
||||
# Trade related
|
||||
send_fetch_order,
|
||||
check_cancel_check_orders,
|
||||
fetch_my_trades,
|
||||
fetch_order_history,
|
||||
print_positions,
|
||||
cancel_send_order,
|
||||
get_open_orders,
|
||||
send_fetch_order,
|
||||
get_open_orders,
|
||||
cancel_all_orders,
|
||||
get_open_orders,
|
||||
set_derisk_mm_ratio,
|
||||
]
|
||||
for f in function_list:
|
||||
try:
|
||||
await f(test_api)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
|
||||
validate_return_values(test_api, "test_results.csv")
|
||||
|
||||
|
||||
def test_grvt_ccxt_pro() -> None:
|
||||
asyncio.run(grvt_ccxt_pro())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_grvt_ccxt_pro()
|
||||
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from pysdk.grvt_ccxt import GrvtCcxt
|
||||
from pysdk.grvt_ccxt_env import GrvtEnv
|
||||
from pysdk.grvt_ccxt_logging_selector import logger
|
||||
|
||||
|
||||
def call_vault_manager_investor_history(api: GrvtCcxt):
|
||||
FN = "call_vault_manager_investor_history"
|
||||
logger.info(f"{FN}: START")
|
||||
try:
|
||||
history = api.fetch_vault_manager_investor_history()
|
||||
# Expect dict with result key and list of dicts with history items
|
||||
# [{'event_time': '1752057360756255849', 'off_chain_account_id': 'ACC:2s**fW',
|
||||
# 'vault_id': '2002239639', 'type': 'VAULT_REDEEM', 'price': '0.998912', 'size': '1900.0',
|
||||
# 'realized_pnl': '-0.077452', 'performance_fee': '0.0'}, ...]
|
||||
logger.info(f"{FN}: {history=}")
|
||||
except Exception as e:
|
||||
logger.error(f"{FN} failed: {e}")
|
||||
|
||||
|
||||
def call_vault_redemption_queue(api: GrvtCcxt):
|
||||
FN = "call_vault_redemption_queue"
|
||||
logger.info(f"{FN}: START")
|
||||
try:
|
||||
redemption_queue = api.fetch_vault_redemption_queue()
|
||||
logger.info(f"{FN}: {redemption_queue=}")
|
||||
except Exception as e:
|
||||
logger.error(f"{FN} failed: {e}")
|
||||
|
||||
|
||||
def test_grvt_ccxt_vault():
|
||||
params = {
|
||||
"api_key": os.getenv("GRVT_API_KEY"),
|
||||
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
|
||||
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
|
||||
}
|
||||
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
|
||||
test_api = GrvtCcxt(env, logger, parameters=params, order_book_ccxt_format=True)
|
||||
function_list = [
|
||||
call_vault_manager_investor_history,
|
||||
call_vault_redemption_queue,
|
||||
]
|
||||
for f in function_list:
|
||||
try:
|
||||
f(test_api)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_grvt_ccxt_vault()
|
||||
@@ -0,0 +1,60 @@
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from pysdk.grvt_ccxt_env import GrvtEnv
|
||||
from pysdk.grvt_ccxt_logging_selector import logger
|
||||
from pysdk.grvt_ccxt_pro import GrvtCcxtPro
|
||||
|
||||
|
||||
async def call_vault_manager_investor_history(api: GrvtCcxtPro):
|
||||
FN = "call_vault_manager_investor_history"
|
||||
logger.info(f"{FN}: START")
|
||||
try:
|
||||
history = await api.fetch_vault_manager_investor_history()
|
||||
# Expect dict with result key and list of dicts with history items
|
||||
# [{'event_time': '1752057360756255849', 'off_chain_account_id': 'ACC:2s**fW',
|
||||
# 'vault_id': '2002239639', 'type': 'VAULT_REDEEM', 'price': '0.998912', 'size': '1900.0',
|
||||
# 'realized_pnl': '-0.077452', 'performance_fee': '0.0'}, ...]
|
||||
logger.info(f"{FN}: {history=}")
|
||||
except Exception as e:
|
||||
logger.error(f"{FN} failed: {e}")
|
||||
|
||||
|
||||
async def call_vault_redemption_queue(api: GrvtCcxtPro):
|
||||
FN = "call_vault_redemption_queue"
|
||||
logger.info(f"{FN}: START")
|
||||
try:
|
||||
redemption_queue = await api.fetch_vault_redemption_queue()
|
||||
logger.info(f"{FN}: {redemption_queue=}")
|
||||
except Exception as e:
|
||||
logger.error(f"{FN} failed: {e}")
|
||||
|
||||
|
||||
async def grvt_ccxt_vault_pro():
|
||||
params = {
|
||||
"api_key": os.getenv("GRVT_API_KEY"),
|
||||
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
|
||||
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
|
||||
}
|
||||
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
|
||||
test_api = GrvtCcxtPro(env, logger, parameters=params, order_book_ccxt_format=True)
|
||||
await test_api.load_markets()
|
||||
await asyncio.sleep(2)
|
||||
function_list = [
|
||||
call_vault_manager_investor_history,
|
||||
call_vault_redemption_queue,
|
||||
]
|
||||
for f in function_list:
|
||||
try:
|
||||
await f(test_api)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
|
||||
|
||||
|
||||
def test_grvt_ccxt_vault_pro() -> None:
|
||||
asyncio.run(grvt_ccxt_vault_pro())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_grvt_ccxt_vault_pro()
|
||||
@@ -0,0 +1,297 @@
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from pysdk.grvt_ccxt_env import GrvtEnv, GrvtWSEndpointType
|
||||
from pysdk.grvt_ccxt_logging_selector import logger
|
||||
from pysdk.grvt_ccxt_types import GrvtOrderSide
|
||||
from pysdk.grvt_ccxt_utils import rand_uint32
|
||||
from pysdk.grvt_ccxt_ws import GrvtCcxtWS
|
||||
|
||||
|
||||
# Utility functions , not called directly by the __main__ test routine
|
||||
async def callback_general(message: dict) -> None:
|
||||
message.get("params", {}).get("channel")
|
||||
logger.info(f"callback_general(): message:{message}")
|
||||
|
||||
|
||||
async def grvt_ws_subscribe(api: GrvtCcxtWS, args_list: dict) -> None:
|
||||
"""Subscribes to Websocket channels/feeds in args list."""
|
||||
for stream, (callback, ws_endpoint_type, params) in args_list.items():
|
||||
logger.info(f"Subscribing to {stream} {params=}")
|
||||
await api.subscribe(
|
||||
stream=stream,
|
||||
callback=callback,
|
||||
ws_end_point_type=ws_endpoint_type,
|
||||
params=params,
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
async def subscribe(loop) -> GrvtCcxtWS:
|
||||
"""Subscribe to Websocket channels and feeds."""
|
||||
params = {
|
||||
"api_key": os.getenv("GRVT_API_KEY"),
|
||||
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
|
||||
"api_ws_version": os.getenv("GRVT_WS_STREAM_VERSION", "v1"),
|
||||
}
|
||||
if os.getenv("GRVT_PRIVATE_KEY"):
|
||||
params["private_key"] = os.getenv("GRVT_PRIVATE_KEY")
|
||||
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
|
||||
|
||||
test_api = GrvtCcxtWS(env, loop, logger, parameters=params)
|
||||
await test_api.initialize()
|
||||
pub_args_dict = {
|
||||
# ********* Market Data *********
|
||||
"mini.s": (
|
||||
callback_general,
|
||||
None, # use deafult endpoint
|
||||
{"instrument": "BTC_USDT_Perp"},
|
||||
),
|
||||
"mini.d": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
{"instrument": "BTC_USDT_Perp", "rate": 0},
|
||||
),
|
||||
"ticker.s": (
|
||||
callback_general,
|
||||
None, # use deafult endpoint
|
||||
{"instrument": "BTC_USDT_Perp"},
|
||||
),
|
||||
"ticker.d": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
{"instrument": "BTC_USDT_Perp"},
|
||||
),
|
||||
"book.s": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
{"instrument": "BTC_USDT_Perp"},
|
||||
),
|
||||
"book.d": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
{"instrument": "BTC_USDT_Perp"},
|
||||
),
|
||||
"trade": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
{"instrument": "BTC_USDT_Perp"},
|
||||
),
|
||||
"candle": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
{
|
||||
"instrument": "BTC_USDT_Perp",
|
||||
"interval": "CI_1_M",
|
||||
"type": "TRADE",
|
||||
},
|
||||
),
|
||||
}
|
||||
prv_args_dict = {
|
||||
# ********* Trade Data *********
|
||||
"position": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
{},
|
||||
),
|
||||
"order": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
{
|
||||
"instrument": "BTC_USDT_Perp",
|
||||
},
|
||||
),
|
||||
"cancel": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
{},
|
||||
),
|
||||
"state": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
{
|
||||
"instrument": "BTC_USDT_Perp",
|
||||
},
|
||||
),
|
||||
"fill": (
|
||||
callback_general,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
{
|
||||
"instrument": "BTC_USDT_Perp",
|
||||
},
|
||||
),
|
||||
"deposit": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}),
|
||||
"transfer": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}),
|
||||
"withdrawal": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}),
|
||||
}
|
||||
try:
|
||||
if "private_key" in params:
|
||||
await grvt_ws_subscribe(test_api, {**pub_args_dict, **prv_args_dict})
|
||||
else: # not private_key , subscribe to public feeds only
|
||||
await grvt_ws_subscribe(test_api, pub_args_dict)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in grvt_ws_subscribe: {e} {traceback.format_exc()}")
|
||||
return test_api
|
||||
|
||||
|
||||
async def rpc_create_order(
|
||||
test_api: GrvtCcxtWS, side: GrvtOrderSide, price: str, client_order_id: str = ""
|
||||
) -> str:
|
||||
if test_api and test_api._private_key:
|
||||
# Send order
|
||||
if not client_order_id:
|
||||
client_order_id = str(rand_uint32())
|
||||
payload = await test_api.rpc_create_order(
|
||||
symbol="BTC_USDT_Perp",
|
||||
order_type="limit",
|
||||
side=side,
|
||||
amount=0.001,
|
||||
price=price,
|
||||
params={
|
||||
"client_order_id": client_order_id,
|
||||
# "time_in_force": "IMMEDIATE_OR_CANCEL",
|
||||
"time_in_force": "GOOD_TILL_TIME",
|
||||
},
|
||||
)
|
||||
logger.info(f"rpc_create_order: {payload=}")
|
||||
return client_order_id
|
||||
return ""
|
||||
|
||||
|
||||
async def rpc_create_mkt_order(
|
||||
test_api: GrvtCcxtWS, symbol: str, side: GrvtOrderSide, client_order_id: str = ""
|
||||
) -> str:
|
||||
FN = "rpc_create_mkt_order"
|
||||
if test_api and test_api._private_key:
|
||||
# Send order
|
||||
if not client_order_id:
|
||||
client_order_id = str(rand_uint32())
|
||||
payload = await test_api.rpc_create_order(
|
||||
symbol=symbol,
|
||||
order_type="market",
|
||||
side=side,
|
||||
amount=0.001,
|
||||
params={
|
||||
"client_order_id": client_order_id,
|
||||
"time_in_force": "GOOD_TILL_TIME",
|
||||
},
|
||||
)
|
||||
logger.info(f"{FN}: {payload=}")
|
||||
return client_order_id
|
||||
return ""
|
||||
|
||||
|
||||
async def rpc_fetch_order(test_api: GrvtCcxtWS, client_order_id: str) -> None:
|
||||
if test_api and test_api._private_key:
|
||||
# Send order
|
||||
payload = await test_api.rpc_fetch_order(
|
||||
params={
|
||||
"client_order_id": client_order_id,
|
||||
},
|
||||
)
|
||||
logger.info(f"rpc_fetch_order: {payload=}")
|
||||
|
||||
|
||||
async def rpc_fetch_open_orders(test_api: GrvtCcxtWS) -> None:
|
||||
if test_api and test_api._private_key:
|
||||
# Send order
|
||||
payload: dict = await test_api.rpc_fetch_open_orders()
|
||||
logger.info(f"rpc_fetch_open_orders: {payload=}")
|
||||
|
||||
|
||||
async def rpc_cancel_order(
|
||||
test_api: GrvtCcxtWS, client_order_id: str, time_to_live_ms: str = ""
|
||||
) -> None:
|
||||
if test_api and test_api._private_key:
|
||||
# Send order
|
||||
params: dict = {"client_order_id": client_order_id}
|
||||
if time_to_live_ms:
|
||||
params["time_to_live_ms"] = time_to_live_ms
|
||||
payload = await test_api.rpc_cancel_order(params=params)
|
||||
logger.info(f"rpc_cancel_order: {payload=}")
|
||||
|
||||
|
||||
async def rpc_cancel_all_orders(test_api: GrvtCcxtWS) -> None:
|
||||
if test_api and test_api._private_key:
|
||||
# Send order
|
||||
payload: dict = await test_api.rpc_cancel_all_orders()
|
||||
logger.info(f"rpc_cancel_all_orders: {payload=}")
|
||||
|
||||
|
||||
async def cancel_send_rpc_order(test_api: GrvtCcxtWS) -> None:
|
||||
FN = "cancel_send_rpc_order"
|
||||
"""Cancels order then sends an order to be canceled."""
|
||||
if test_api and test_api._private_key:
|
||||
cloid = str(rand_uint32())
|
||||
logger.info(f"{FN} {cloid=}")
|
||||
await rpc_cancel_order(test_api, cloid, time_to_live_ms="0")
|
||||
await asyncio.sleep(1)
|
||||
await rpc_cancel_order(test_api, cloid, time_to_live_ms="5000")
|
||||
await asyncio.sleep(0.01)
|
||||
await rpc_create_mkt_order(
|
||||
test_api, symbol="BTC_USDT_Perp", side="buy", client_order_id=cloid
|
||||
)
|
||||
await asyncio.sleep(0.01)
|
||||
await rpc_fetch_order(test_api, cloid)
|
||||
|
||||
|
||||
async def send_check_cancel_rpc_order(test_api: GrvtCcxtWS) -> None:
|
||||
if test_api and test_api._private_key:
|
||||
# Send order
|
||||
cloid = await rpc_create_order(test_api, side="buy", price="60000")
|
||||
if cloid:
|
||||
await rpc_fetch_open_orders(test_api)
|
||||
await rpc_fetch_order(test_api, cloid)
|
||||
await asyncio.sleep(5)
|
||||
await rpc_cancel_order(test_api, cloid)
|
||||
cloid = await rpc_create_order(test_api, side="sell", price="70000")
|
||||
if cloid:
|
||||
await rpc_fetch_open_orders(test_api)
|
||||
await rpc_fetch_order(test_api, cloid)
|
||||
await asyncio.sleep(5)
|
||||
await rpc_cancel_all_orders(test_api)
|
||||
|
||||
|
||||
async def send_rpc_messages(test_api: GrvtCcxtWS) -> None:
|
||||
"""Sends test RPC messages for send/fetch/cancel orders."""
|
||||
await send_check_cancel_rpc_order(test_api)
|
||||
await cancel_send_rpc_order(test_api)
|
||||
|
||||
|
||||
async def shutdown(loop, test_api: GrvtCcxtWS) -> None:
|
||||
"""Clean up resources and stop the bot gracefully."""
|
||||
import time
|
||||
logger.info("Shutting down gracefully...")
|
||||
if test_api:
|
||||
for stream, message in test_api._last_message.items():
|
||||
logger.info(f"Last message: {stream=} {message=}")
|
||||
logger.info("Delete GrvtCcxtWS...")
|
||||
del test_api # Close the websocket connection and session
|
||||
time.sleep(3) # Allow time for cleanup
|
||||
await asyncio.sleep(5) # Allow time for cleanup
|
||||
logger.info("Cancelling all tasks...")
|
||||
tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task(loop)]
|
||||
_ = [task.cancel() for task in tasks]
|
||||
logger.info(f"Cancelling {len(tasks)=}")
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
logger.info("Shutdown complete.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
test_api = loop.run_until_complete(subscribe(loop))
|
||||
if not test_api:
|
||||
logger.error("Failed to subscribe to Websocket channels.")
|
||||
sys.exit(1)
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
loop.add_signal_handler(
|
||||
sig, lambda: asyncio.create_task(shutdown(loop, test_api))
|
||||
)
|
||||
loop.run_until_complete(asyncio.sleep(5))
|
||||
loop.run_until_complete(send_rpc_messages(test_api))
|
||||
loop.run_forever()
|
||||
loop.close()
|
||||
@@ -0,0 +1,137 @@
|
||||
import asyncio
|
||||
|
||||
from pysdk import grvt_raw_types
|
||||
from pysdk.grvt_raw_async import GrvtRawAsync
|
||||
from pysdk.grvt_raw_base import GrvtError
|
||||
|
||||
from .test_raw_utils import (
|
||||
get_config,
|
||||
get_test_order,
|
||||
get_test_transfer,
|
||||
get_test_withdrawal,
|
||||
)
|
||||
|
||||
|
||||
async def get_all_instruments() -> None:
|
||||
api = GrvtRawAsync(config=get_config())
|
||||
resp = await api.get_all_instruments_v1(
|
||||
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
|
||||
)
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected results to be non-null")
|
||||
if len(resp.result) == 0:
|
||||
raise ValueError("Expected results to be non-empty")
|
||||
|
||||
|
||||
async def open_orders() -> None:
|
||||
api = GrvtRawAsync(config=get_config())
|
||||
|
||||
# Skip test if trading account id is not set
|
||||
if api.config.trading_account_id is None or api.config.api_key is None:
|
||||
return None # Skip test if configs are not set
|
||||
|
||||
resp = await api.open_orders_v1(
|
||||
grvt_raw_types.ApiOpenOrdersRequest(
|
||||
sub_account_id=str(api.config.trading_account_id),
|
||||
kind=[grvt_raw_types.Kind.PERPETUAL],
|
||||
base=["BTC", "ETH"],
|
||||
quote=["USDT"],
|
||||
)
|
||||
)
|
||||
if isinstance(resp, GrvtError):
|
||||
api.logger.error(f"Received error: {resp}")
|
||||
return None
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected orders to be non-null")
|
||||
if len(resp.result) == 0:
|
||||
api.logger.info("Expected orders to be non-empty")
|
||||
|
||||
|
||||
async def create_order_with_signing() -> None:
|
||||
api = GrvtRawAsync(config=get_config())
|
||||
|
||||
inst_resp = await api.get_all_instruments_v1(
|
||||
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
|
||||
)
|
||||
if isinstance(inst_resp, GrvtError):
|
||||
raise ValueError(f"Received error: {inst_resp}")
|
||||
|
||||
order = get_test_order(api, {inst.instrument: inst for inst in inst_resp.result})
|
||||
if order is None:
|
||||
return None # Skip test if configs are not set
|
||||
resp = await api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order))
|
||||
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected order to be non-null")
|
||||
|
||||
|
||||
async def transfer_with_signing_async() -> None:
|
||||
api = GrvtRawAsync(config=get_config())
|
||||
transfer = get_test_transfer(api)
|
||||
|
||||
if transfer is None:
|
||||
return None # Skip test if configs are not set
|
||||
|
||||
resp = await api.transfer_v1(
|
||||
grvt_raw_types.ApiTransferRequest(
|
||||
transfer.from_account_id,
|
||||
transfer.from_sub_account_id,
|
||||
transfer.to_account_id,
|
||||
transfer.to_sub_account_id,
|
||||
transfer.currency,
|
||||
transfer.num_tokens,
|
||||
transfer.signature,
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected transfer response to be non-null")
|
||||
|
||||
|
||||
async def withdrawal_with_signing_async() -> None:
|
||||
api = GrvtRawAsync(config=get_config())
|
||||
withdrawal = get_test_withdrawal(api)
|
||||
|
||||
if withdrawal is None:
|
||||
return None # Skip test if configs are not set
|
||||
|
||||
resp = await api.withdrawal_v1(
|
||||
grvt_raw_types.ApiWithdrawalRequest(
|
||||
withdrawal.from_account_id,
|
||||
withdrawal.to_eth_address,
|
||||
withdrawal.currency,
|
||||
withdrawal.num_tokens,
|
||||
withdrawal.signature,
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected withdrawal response to be non-null")
|
||||
|
||||
|
||||
def test_get_all_instruments() -> None:
|
||||
asyncio.run(get_all_instruments())
|
||||
|
||||
|
||||
def test_open_orders() -> None:
|
||||
asyncio.run(open_orders())
|
||||
|
||||
|
||||
def test_create_order_with_signing() -> None:
|
||||
asyncio.run(create_order_with_signing())
|
||||
|
||||
|
||||
def test_transfer_with_signing_async() -> None:
|
||||
asyncio.run(transfer_with_signing_async())
|
||||
|
||||
|
||||
def test_withdrawal_with_signing_async() -> None:
|
||||
asyncio.run(withdrawal_with_signing_async())
|
||||
@@ -0,0 +1,400 @@
|
||||
import logging
|
||||
import traceback
|
||||
from pprint import pprint
|
||||
|
||||
from eth_account import Account
|
||||
|
||||
from pysdk.grvt_fixed_types import Transfer
|
||||
from pysdk.grvt_raw_base import GrvtApiConfig
|
||||
from pysdk.grvt_raw_env import GrvtEnv
|
||||
from pysdk.grvt_raw_signing import sign_order, sign_transfer
|
||||
from pysdk.grvt_raw_types import (
|
||||
Instrument,
|
||||
InstrumentSettlementPeriod,
|
||||
Kind,
|
||||
Order,
|
||||
OrderLeg,
|
||||
OrderMetadata,
|
||||
Signature,
|
||||
TimeInForce,
|
||||
TransferType,
|
||||
)
|
||||
|
||||
# Setup logger
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def test_sign_order_table():
|
||||
private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d"
|
||||
sub_account_id = "8289849667772468"
|
||||
expiry = 1730800479321350000
|
||||
nonce = 828700936
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"name": "test decimal precision 1, 3 decimals",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.013",
|
||||
limit_price="68900.5",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"want_r": "0xb00512d986a718b15136a8ba23de1c1ec84bbdb9958629cbbe4909bae620bb04",
|
||||
"want_s": "0x79f706de61c68cc14d7734594b5d8689df2b2a7b25951f9a3f61d799f4327ffc",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "test decimal precision 2, 9 decimals",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.123123123",
|
||||
limit_price="68900.777123479",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0",
|
||||
"want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "test decimal precision 3, round down",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.1231231234",
|
||||
limit_price="68900.7771234794",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0",
|
||||
"want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "test decimal precision 4, round down",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.1231231239",
|
||||
limit_price="68900.7771234799",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0",
|
||||
"want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
# {
|
||||
# "name": "no private key",
|
||||
# "order": Order(),
|
||||
# "want_error": ValueError("Private key is not set")
|
||||
# },
|
||||
# {
|
||||
# "name": "decimal precision test",
|
||||
# "order": Order(
|
||||
# sub_account_id="123",
|
||||
# time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
# legs=[
|
||||
# OrderLeg(
|
||||
# instrument="BTC_USDT_Perp",
|
||||
# size="1.013",
|
||||
# limit_price="64170.7",
|
||||
# is_buying_asset=True
|
||||
# )
|
||||
# ],
|
||||
# signature=Signature(
|
||||
# expiration=expiry,
|
||||
# nonce=nonce
|
||||
# )
|
||||
# ),
|
||||
# "want_error": None
|
||||
# }
|
||||
]
|
||||
|
||||
account = Account.from_key(private_key)
|
||||
|
||||
instruments = {
|
||||
"BTC_USDT_Perp": Instrument(
|
||||
instrument="BTC_USDT_Perp",
|
||||
instrument_hash="0x030501",
|
||||
base="BTC",
|
||||
quote="USDT",
|
||||
kind=Kind.PERPETUAL,
|
||||
venues=[],
|
||||
settlement_period=InstrumentSettlementPeriod.DAILY,
|
||||
tick_size="0.00000001",
|
||||
min_size="0.00000001",
|
||||
create_time="123",
|
||||
base_decimals=9,
|
||||
quote_decimals=9,
|
||||
max_position_size="1000000",
|
||||
)
|
||||
}
|
||||
|
||||
for tc in test_cases:
|
||||
config = GrvtApiConfig(
|
||||
env=GrvtEnv.TESTNET,
|
||||
private_key=private_key,
|
||||
trading_account_id=sub_account_id,
|
||||
api_key="not-needed",
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
signed_order = sign_order(tc["order"], config, account, instruments)
|
||||
pprint(signed_order)
|
||||
|
||||
# Verify signature fields are populated
|
||||
assert signed_order.signature.signer == str(account.address)
|
||||
|
||||
# Compare r, s, v values with expected values
|
||||
if "want_r" in tc:
|
||||
assert (
|
||||
signed_order.signature.r == tc["want_r"]
|
||||
), f"Test '{tc['name']}' failed: r value mismatch"
|
||||
if "want_s" in tc:
|
||||
assert (
|
||||
signed_order.signature.s == tc["want_s"]
|
||||
), f"Test '{tc['name']}' failed: s value mismatch"
|
||||
if "want_v" in tc:
|
||||
assert (
|
||||
signed_order.signature.v == tc["want_v"]
|
||||
), f"Test '{tc['name']}' failed: v value mismatch"
|
||||
|
||||
|
||||
def test_sign_transfer_table():
|
||||
chainId = 1
|
||||
private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d"
|
||||
main_account_id = "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1"
|
||||
sub_account_id = "8289849667772468"
|
||||
expiry = "1730800479321350000"
|
||||
nonce = 828700936
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"name": "Transfer $1 from main account to sub account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id="0",
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id=sub_account_id,
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"want_r": "0x21c7d7a8e225cb146c80dc79bbe818f915536817f1343e974cfdbe2bfc952cf1",
|
||||
"want_s": "0x6de83999555f6236e5a56c86876defe00b4776c428ab5a4d7f997d290baaea10",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1.5 from main account to sub account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id="0",
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id=sub_account_id,
|
||||
currency="USDT",
|
||||
num_tokens="1.5",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"want_r": "0xe0a9c66d8d11c3a9ae3624e150cbbdf85d542722cac5255cad4e50af5ac1ddcb",
|
||||
"want_s": "0x06769e5284352ead5735b8b11f9e9510d024bbb889a828889db4cb04132b52aa",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1 from sub account to main account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id=sub_account_id,
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"want_r": "0xc1214ee17dbc14f183297b9dd3f93120b16e633691817ee26045451bc629101c",
|
||||
"want_s": "0x2de27d226a3d3188742629ab222d430d7989d6ea3e6a86bc259606e371123df3",
|
||||
"want_v": 27,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1.5 from sub account to main account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id=sub_account_id,
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1.5",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"want_r": "0xb9b80dfd4b0d53e64b6dd1067d7d936c79a8c3966175bcefb2021cc71d08116f",
|
||||
"want_s": "0x3cbe955c9f56e41f70c658e02df07873e77d347aa5c422943b87fdfd94293ae6",
|
||||
"want_v": 27,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1 external",
|
||||
"transfer": Transfer(
|
||||
from_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
|
||||
from_sub_account_id="0",
|
||||
to_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"want_r": "0x185ad129ca0de3584fcd91f6df0c25d8065411041db117c50dabd057249a1a43",
|
||||
"want_s": "0x58b1979c1f8c65970578bc2756f17a0b5c7352c27007811800dcd8966351647d",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1.5 external revert",
|
||||
"transfer": Transfer(
|
||||
from_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
|
||||
from_sub_account_id="0",
|
||||
to_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1.5",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"want_r": "0xbbdd4726fef5cddb6eeb5fac07ed95702673293133d66481777a6a3ee82adc12",
|
||||
"want_s": "0x0ad3592ea3ebf428b260c56723386383253481bc188d9aeb87d9b21b85069821",
|
||||
"want_v": 28,
|
||||
"want_error": None,
|
||||
},
|
||||
]
|
||||
|
||||
account = Account.from_key(private_key)
|
||||
config = GrvtApiConfig(
|
||||
env=GrvtEnv.TESTNET,
|
||||
private_key=private_key,
|
||||
trading_account_id=sub_account_id,
|
||||
api_key="not-needed",
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
for tc in test_cases:
|
||||
signed = sign_transfer(tc["transfer"], config, account, chainId)
|
||||
pprint(signed)
|
||||
|
||||
# Verify signature fields are populated
|
||||
assert signed.signature.signer == str(account.address)
|
||||
|
||||
# Compare r, s, v values with expected values
|
||||
if "want_r" in tc:
|
||||
assert (
|
||||
signed.signature.r == tc["want_r"]
|
||||
), f"Test '{tc['name']}' failed: r value mismatch"
|
||||
if "want_s" in tc:
|
||||
assert (
|
||||
signed.signature.s == tc["want_s"]
|
||||
), f"Test '{tc['name']}' failed: s value mismatch"
|
||||
if "want_v" in tc:
|
||||
assert (
|
||||
signed.signature.v == tc["want_v"]
|
||||
), f"Test '{tc['name']}' failed: v value mismatch"
|
||||
|
||||
|
||||
def main():
|
||||
functions = [
|
||||
test_sign_order_table,
|
||||
test_sign_transfer_table,
|
||||
]
|
||||
for f in functions:
|
||||
try:
|
||||
f()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,837 @@
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from eth_account import Account
|
||||
from eth_account.messages import encode_typed_data
|
||||
|
||||
from pysdk.grvt_fixed_types import Transfer
|
||||
from pysdk.grvt_raw_base import GrvtApiConfig
|
||||
from pysdk.grvt_raw_env import GrvtEnv
|
||||
from pysdk.grvt_raw_signing import (
|
||||
EIP712_ORDER_MESSAGE_TYPE,
|
||||
EIP712_TRANSFER_MESSAGE_TYPE,
|
||||
build_EIP712_order_message_data,
|
||||
build_EIP712_transfer_message_data,
|
||||
get_EIP712_domain_data,
|
||||
sign_order,
|
||||
)
|
||||
from pysdk.grvt_raw_types import (
|
||||
Instrument,
|
||||
InstrumentSettlementPeriod,
|
||||
Kind,
|
||||
Order,
|
||||
OrderLeg,
|
||||
OrderMetadata,
|
||||
Signature,
|
||||
TimeInForce,
|
||||
TransferType,
|
||||
)
|
||||
|
||||
# Setup logger
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def test_sign_order_table():
|
||||
# Generated using https://key.tokenpocket.pro/#/?network=ETH
|
||||
# NOTE: `0x` hexadecimal prefix removed
|
||||
public_key = "ee2060eECaC18beC7F8F670D751801294911E445"
|
||||
private_key = "c0663ca94684aead40c41a1cb3a94b68a24296e87245be3a186e882a29a15ee0"
|
||||
|
||||
sub_account_id = "8289849667772468"
|
||||
expiry = 1730800479321350000
|
||||
nonce = 828700936
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"name": "test decimal precision 1, 3 decimals",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.013",
|
||||
limit_price="68900.5",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"subAccountID": "8289849667772468",
|
||||
"isMarket": false,
|
||||
"timeInForce": 1,
|
||||
"postOnly": false,
|
||||
"reduceOnly": false,
|
||||
"legs": [
|
||||
{
|
||||
"assetID": "0x030501",
|
||||
"contractSize": 1013000000,
|
||||
"limitPrice": 68900500000000,
|
||||
"isBuyingContract": false
|
||||
}
|
||||
],
|
||||
"nonce": 828700936,
|
||||
"expiration": 1730800479321350000
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 326
|
||||
}""",
|
||||
# Per EIP-191: https://eips.ethereum.org/EIPS/eip-191
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
|
||||
"body": "41650c08ab6e720f899307d7c2b4381a10c1301888375cec2dfefd6a583859eb"
|
||||
}""",
|
||||
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
|
||||
# Pre-image of the signed message's message hash
|
||||
# encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19"‖ version ‖ domainSeparator ‖ hashStruct(message)
|
||||
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f1641650c08ab6e720f899307d7c2b4381a10c1301888375cec2dfefd6a583859eb",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "03cb7ca7b353969ab2c00ff92fd472f81f59a84d28fa1aa39128176f21062982",
|
||||
"r": 14615867946748605809126568669694142791729730263440553623296112010401671344650,
|
||||
"s": 41758084966111141828834491248882149351523023670747687501271185591898818786045,
|
||||
"v": 28,
|
||||
"signature": "205049c0db6e38fd88e4e46be81db7bc354520d52ec6268d210fa35b86c4f20a5c523d0ff8e6f12542fdcf34a6e6e2c547986b7c8fa53f86a5e656d9ab44b2fd1c"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "test decimal precision 2, 9 decimals",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.123123123",
|
||||
limit_price="68900.777123479",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"subAccountID": "8289849667772468",
|
||||
"isMarket": false,
|
||||
"timeInForce": 1,
|
||||
"postOnly": false,
|
||||
"reduceOnly": false,
|
||||
"legs": [
|
||||
{
|
||||
"assetID": "0x030501",
|
||||
"contractSize": 1123123123,
|
||||
"limitPrice": 68900777123479,
|
||||
"isBuyingContract": false
|
||||
}
|
||||
],
|
||||
"nonce": 828700936,
|
||||
"expiration": 1730800479321350000
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 326
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
|
||||
"body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814"
|
||||
}""",
|
||||
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69",
|
||||
"r": 30067953785684815030293507105225786115862149795459199007947867478230986262090,
|
||||
"s": 23299979093064779986156565292425191814752388247725004533191995996670719399433,
|
||||
"v": 28,
|
||||
"signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "test decimal precision 3, round down",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.1231231234",
|
||||
limit_price="68900.7771234794",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"subAccountID": "8289849667772468",
|
||||
"isMarket": false,
|
||||
"timeInForce": 1,
|
||||
"postOnly": false,
|
||||
"reduceOnly": false,
|
||||
"legs": [
|
||||
{
|
||||
"assetID": "0x030501",
|
||||
"contractSize": 1123123123,
|
||||
"limitPrice": 68900777123479,
|
||||
"isBuyingContract": false
|
||||
}
|
||||
],
|
||||
"nonce": 828700936,
|
||||
"expiration": 1730800479321350000
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 326
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
|
||||
"body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814"
|
||||
}""",
|
||||
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69",
|
||||
"r": 30067953785684815030293507105225786115862149795459199007947867478230986262090,
|
||||
"s": 23299979093064779986156565292425191814752388247725004533191995996670719399433,
|
||||
"v": 28,
|
||||
"signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "test decimal precision 4, round down",
|
||||
"order": Order(
|
||||
metadata=OrderMetadata(
|
||||
client_order_id="1", create_time="1730800479321350000"
|
||||
),
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=TimeInForce.GOOD_TILL_TIME,
|
||||
post_only=False,
|
||||
is_market=False,
|
||||
reduce_only=False,
|
||||
legs=[
|
||||
OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.1231231239",
|
||||
limit_price="68900.7771234799",
|
||||
is_buying_asset=False,
|
||||
)
|
||||
],
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"subAccountID": "8289849667772468",
|
||||
"isMarket": false,
|
||||
"timeInForce": 1,
|
||||
"postOnly": false,
|
||||
"reduceOnly": false,
|
||||
"legs": [
|
||||
{
|
||||
"assetID": "0x030501",
|
||||
"contractSize": 1123123123,
|
||||
"limitPrice": 68900777123479,
|
||||
"isBuyingContract": false
|
||||
}
|
||||
],
|
||||
"nonce": 828700936,
|
||||
"expiration": 1730800479321350000
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 326
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
|
||||
"body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814"
|
||||
}""",
|
||||
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69",
|
||||
"r": 30067953785684815030293507105225786115862149795459199007947867478230986262090,
|
||||
"s": 23299979093064779986156565292425191814752388247725004533191995996670719399433,
|
||||
"v": 28,
|
||||
"signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c"
|
||||
}""",
|
||||
},
|
||||
]
|
||||
|
||||
account = Account.from_key(private_key)
|
||||
|
||||
instruments = {
|
||||
"BTC_USDT_Perp": Instrument(
|
||||
instrument="BTC_USDT_Perp",
|
||||
instrument_hash="0x030501",
|
||||
base="BTC",
|
||||
quote="USDT",
|
||||
kind=Kind.PERPETUAL,
|
||||
venues=[],
|
||||
settlement_period=InstrumentSettlementPeriod.DAILY,
|
||||
tick_size="0.00000001",
|
||||
min_size="0.00000001",
|
||||
create_time="123",
|
||||
base_decimals=9,
|
||||
quote_decimals=9,
|
||||
max_position_size="1000000",
|
||||
)
|
||||
}
|
||||
|
||||
for tc in test_cases:
|
||||
config = GrvtApiConfig(
|
||||
env=GrvtEnv.TESTNET,
|
||||
private_key=private_key,
|
||||
trading_account_id=sub_account_id,
|
||||
api_key="not-needed",
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
# Get intermediate values
|
||||
message_data = build_EIP712_order_message_data(tc["order"], instruments)
|
||||
domain_data = get_EIP712_domain_data(config.env, 326)
|
||||
signable_message = encode_typed_data(
|
||||
domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
signed_order = sign_order(tc["order"], config, account, instruments)
|
||||
|
||||
# Convert to comparable strings
|
||||
message_data_json = json.dumps(message_data, indent=2)
|
||||
domain_data_json = json.dumps(domain_data, indent=2)
|
||||
signable_message_json = json.dumps(
|
||||
{
|
||||
"version": signable_message.version.hex(),
|
||||
"header": signable_message.header.hex(),
|
||||
"body": signable_message.body.hex(),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
signed_message_json = json.dumps(
|
||||
{
|
||||
"message_hash": signed_message.message_hash.hex(),
|
||||
"r": signed_message.r,
|
||||
"s": signed_message.s,
|
||||
"v": signed_message.v,
|
||||
"signature": signed_message.signature.hex(),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
|
||||
signed_message_hash_preimage = (
|
||||
"0x19"
|
||||
+ signable_message.version.hex()
|
||||
+ signable_message.header.hex()
|
||||
+ signable_message.body.hex()
|
||||
)
|
||||
|
||||
# Strip whitespace for comparison
|
||||
assert (
|
||||
message_data_json.strip() == tc["expected_message_data_json"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: message_data mismatch.
|
||||
Wanted:
|
||||
{tc['expected_message_data_json'].strip()}
|
||||
Got:
|
||||
{message_data_json.strip()}
|
||||
"""
|
||||
assert (
|
||||
domain_data_json.strip() == tc["expected_domain_data_json"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: domain_data mismatch.
|
||||
Wanted:
|
||||
{tc['expected_domain_data_json'].strip()}
|
||||
Got:
|
||||
{domain_data_json.strip()}
|
||||
"""
|
||||
assert (
|
||||
signable_message_json.strip() == tc["expected_signable_message"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: signable_message mismatch.
|
||||
Wanted:
|
||||
{tc['expected_signable_message'].strip()}
|
||||
Got:
|
||||
{signable_message_json.strip()}
|
||||
"""
|
||||
assert (
|
||||
signed_message_hash_preimage.strip() == tc["digest_input"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: digest_input mismatch.
|
||||
Wanted:
|
||||
{tc["digest_input"].strip()
|
||||
}
|
||||
Got:
|
||||
{signed_message_hash_preimage.strip()}
|
||||
"""
|
||||
assert (
|
||||
signed_message_json.strip() == tc["expected_signed_message"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: signed_message mismatch.
|
||||
Wanted:
|
||||
{tc['expected_signed_message'].strip()}
|
||||
Got:
|
||||
{signed_message_json.strip()}
|
||||
"""
|
||||
assert (
|
||||
signed_order.signature.signer == str(account.address) == "0x" + public_key
|
||||
), f"""Test '{tc['name']}' failed: signer mismatch."""
|
||||
|
||||
|
||||
def test_sign_transfer_table():
|
||||
chainId = 1
|
||||
private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d"
|
||||
main_account_id = "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1"
|
||||
sub_account_id = "8289849667772468"
|
||||
expiry = "1730800479321350000"
|
||||
nonce = 828700936
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"name": "Transfer $1 from main account to sub account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id="0",
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id=sub_account_id,
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"fromSubAccount": "0",
|
||||
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"toSubAccount": "8289849667772468",
|
||||
"tokenCurrency": 3,
|
||||
"numTokens": 1000000,
|
||||
"nonce": 828700936,
|
||||
"expiration": "1730800479321350000"
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 1
|
||||
}""",
|
||||
# Per EIP-191: https://eips.ethereum.org/EIPS/eip-191
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
|
||||
"body": "8dfbfc161ca14b60b318aec118c0f77137ab6d5c0d6f4aa283a75995ec842a9f"
|
||||
}""",
|
||||
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
|
||||
# Pre-image of the signed message's message hash
|
||||
# encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19"‖ version ‖ domainSeparator ‖ hashStruct(message)
|
||||
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f68dfbfc161ca14b60b318aec118c0f77137ab6d5c0d6f4aa283a75995ec842a9f",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "f237c6e8ceaf8f75c35537c26a5810f5029fe3fa0179d636cfa75dba0eeaecda",
|
||||
"r": 15279414997690414521303216846501751476684522786442627331685751803272563600625,
|
||||
"s": 49712406548009011982925909577001640710775887931482920893646195243522061953552,
|
||||
"v": 28,
|
||||
"signature": "21c7d7a8e225cb146c80dc79bbe818f915536817f1343e974cfdbe2bfc952cf16de83999555f6236e5a56c86876defe00b4776c428ab5a4d7f997d290baaea101c"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1.5 from main account to sub account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id="0",
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id=sub_account_id,
|
||||
currency="USDT",
|
||||
num_tokens="1.5",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"fromSubAccount": "0",
|
||||
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"toSubAccount": "8289849667772468",
|
||||
"tokenCurrency": 3,
|
||||
"numTokens": 1500000,
|
||||
"nonce": 828700936,
|
||||
"expiration": "1730800479321350000"
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 1
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
|
||||
"body": "efc25031927cbee99a4b8c408d427168218312cb5fcbc2ef0644a25c411e9cd6"
|
||||
}""",
|
||||
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6efc25031927cbee99a4b8c408d427168218312cb5fcbc2ef0644a25c411e9cd6",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "e9a82979035693538d20cbd16a231379d68b2484f065a64b5ac962a0ef45ed2c",
|
||||
"r": 101618044735866410136029494650850506089543609286068143785089360195209387957707,
|
||||
"s": 2923457745704967739422721965786309607758766142290846761836065357300251710122,
|
||||
"v": 28,
|
||||
"signature": "e0a9c66d8d11c3a9ae3624e150cbbdf85d542722cac5255cad4e50af5ac1ddcb06769e5284352ead5735b8b11f9e9510d024bbb889a828889db4cb04132b52aa1c"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1 from sub account to main account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id=sub_account_id,
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"fromSubAccount": "8289849667772468",
|
||||
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"toSubAccount": "0",
|
||||
"tokenCurrency": 3,
|
||||
"numTokens": 1000000,
|
||||
"nonce": 828700936,
|
||||
"expiration": "1730800479321350000"
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 1
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
|
||||
"body": "325bbcdd0af37fd856ab19d5a34f04c98b4b9125fd924a41d149290d85cf5d1a"
|
||||
}""",
|
||||
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6325bbcdd0af37fd856ab19d5a34f04c98b4b9125fd924a41d149290d85cf5d1a",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "05a4682aaaffda9347f7f577623dc86be873d75680250d94f30e27fb450b0597",
|
||||
"r": 87355230145152558239319417798390737905701563801639010151017443441731256782876,
|
||||
"s": 20754249269006714296744776682911604802815023385414018875577784419437276970483,
|
||||
"v": 27,
|
||||
"signature": "c1214ee17dbc14f183297b9dd3f93120b16e633691817ee26045451bc629101c2de27d226a3d3188742629ab222d430d7989d6ea3e6a86bc259606e371123df31b"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1.5 from sub account to main account",
|
||||
"transfer": Transfer(
|
||||
from_account_id=main_account_id,
|
||||
from_sub_account_id=sub_account_id,
|
||||
to_account_id=main_account_id,
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1.5",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"fromSubAccount": "8289849667772468",
|
||||
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
|
||||
"toSubAccount": "0",
|
||||
"tokenCurrency": 3,
|
||||
"numTokens": 1500000,
|
||||
"nonce": 828700936,
|
||||
"expiration": "1730800479321350000"
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 1
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
|
||||
"body": "d20efa4f5ca3d7cd996d0a7be827a788fdea40ad9dc3f7d64f909ec3df2f0f1c"
|
||||
}""",
|
||||
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6d20efa4f5ca3d7cd996d0a7be827a788fdea40ad9dc3f7d64f909ec3df2f0f1c",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "57de810bfd4c46796b923242eba5fba199c69ec6159fd7f0bae13d20e6b2e32f",
|
||||
"r": 84003073399296424218543069615592899281416934630850361306323613156742252728687,
|
||||
"s": 27475502714605040799395938380083883707063662756512201821577577919846841662182,
|
||||
"v": 27,
|
||||
"signature": "b9b80dfd4b0d53e64b6dd1067d7d936c79a8c3966175bcefb2021cc71d08116f3cbe955c9f56e41f70c658e02df07873e77d347aa5c422943b87fdfd94293ae61b"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1 external",
|
||||
"transfer": Transfer(
|
||||
from_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
|
||||
from_sub_account_id="0",
|
||||
to_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"fromAccount": "0x922a4874196806460fc63b5bcbff45f94c87f76f",
|
||||
"fromSubAccount": "0",
|
||||
"toAccount": "0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
|
||||
"toSubAccount": "0",
|
||||
"tokenCurrency": 3,
|
||||
"numTokens": 1000000,
|
||||
"nonce": 828700936,
|
||||
"expiration": "1730800479321350000"
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 1
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
|
||||
"body": "c92edbd85a6756bbc1d472694218135800c5c34cecd62dfecf8b79696fb30a5a"
|
||||
}""",
|
||||
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6c92edbd85a6756bbc1d472694218135800c5c34cecd62dfecf8b79696fb30a5a",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "aa540bb37dc69583724239e2ee089f9863760b1f5dd3707f63254f5f9045a36b",
|
||||
"r": 11015968193451536627767691276906473050663313657726362297424610772811255519811,
|
||||
"s": 40117308978565698603770991327546519074883337154968913527997965101318785819773,
|
||||
"v": 28,
|
||||
"signature": "185ad129ca0de3584fcd91f6df0c25d8065411041db117c50dabd057249a1a4358b1979c1f8c65970578bc2756f17a0b5c7352c27007811800dcd8966351647d1c"
|
||||
}""",
|
||||
},
|
||||
{
|
||||
"name": "Transfer $1.5 external revert",
|
||||
"transfer": Transfer(
|
||||
from_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
|
||||
from_sub_account_id="0",
|
||||
to_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
|
||||
to_sub_account_id="0",
|
||||
currency="USDT",
|
||||
num_tokens="1.5",
|
||||
signature=Signature(
|
||||
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
|
||||
),
|
||||
transfer_type=TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
"expected_message_data_json": """
|
||||
{
|
||||
"fromAccount": "0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
|
||||
"fromSubAccount": "0",
|
||||
"toAccount": "0x922a4874196806460fc63b5bcbff45f94c87f76f",
|
||||
"toSubAccount": "0",
|
||||
"tokenCurrency": 3,
|
||||
"numTokens": 1500000,
|
||||
"nonce": 828700936,
|
||||
"expiration": "1730800479321350000"
|
||||
}""",
|
||||
"expected_domain_data_json": """
|
||||
{
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": 1
|
||||
}""",
|
||||
"expected_signable_message": """
|
||||
{
|
||||
"version": "01",
|
||||
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
|
||||
"body": "5660d6d82b43a7bd01c7bee9379478b27e2c7774d9e9aebd69ea163c523f8730"
|
||||
}""",
|
||||
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f65660d6d82b43a7bd01c7bee9379478b27e2c7774d9e9aebd69ea163c523f8730",
|
||||
"expected_signed_message": """
|
||||
{
|
||||
"message_hash": "2a02bf2a7772d74f176d75f987e984dd253837aba1faed4b0a65ba5a3038ce06",
|
||||
"r": 84973466961705873082056285677608514305288271637256010073726199940890275535890,
|
||||
"s": 4896548729346283309439956649162331116008267310844279523516289648065258100769,
|
||||
"v": 28,
|
||||
"signature": "bbdd4726fef5cddb6eeb5fac07ed95702673293133d66481777a6a3ee82adc120ad3592ea3ebf428b260c56723386383253481bc188d9aeb87d9b21b850698211c"
|
||||
}""",
|
||||
},
|
||||
]
|
||||
|
||||
account = Account.from_key(private_key)
|
||||
config = GrvtApiConfig(
|
||||
env=GrvtEnv.TESTNET,
|
||||
private_key=private_key,
|
||||
trading_account_id=sub_account_id,
|
||||
api_key="not-needed",
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
for tc in test_cases:
|
||||
message_data = build_EIP712_transfer_message_data(
|
||||
tc["transfer"], 3 # assume all test cases are USDT transfers
|
||||
)
|
||||
domain_data = get_EIP712_domain_data(config.env, chainId)
|
||||
signable_message = encode_typed_data(
|
||||
domain_data, EIP712_TRANSFER_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
|
||||
# Convert to comparable strings
|
||||
message_data_json = json.dumps(message_data, indent=2)
|
||||
domain_data_json = json.dumps(domain_data, indent=2)
|
||||
signable_message_json = json.dumps(
|
||||
{
|
||||
"version": signable_message.version.hex(),
|
||||
"header": signable_message.header.hex(),
|
||||
"body": signable_message.body.hex(),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
signed_message_json = json.dumps(
|
||||
{
|
||||
"message_hash": signed_message.message_hash.hex(),
|
||||
"r": signed_message.r,
|
||||
"s": signed_message.s,
|
||||
"v": signed_message.v,
|
||||
"signature": signed_message.signature.hex(),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
|
||||
signed_message_hash_preimage = (
|
||||
"0x19"
|
||||
+ signable_message.version.hex()
|
||||
+ signable_message.header.hex()
|
||||
+ signable_message.body.hex()
|
||||
)
|
||||
|
||||
# Strip whitespace for comparison
|
||||
assert (
|
||||
message_data_json.strip() == tc["expected_message_data_json"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: message_data mismatch.
|
||||
Wanted:
|
||||
{tc['expected_message_data_json'].strip()}
|
||||
Got:
|
||||
{message_data_json.strip()}
|
||||
"""
|
||||
assert (
|
||||
domain_data_json.strip() == tc["expected_domain_data_json"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: domain_data mismatch.
|
||||
Wanted:
|
||||
{tc['expected_domain_data_json'].strip()}
|
||||
Got:
|
||||
{domain_data_json.strip()}
|
||||
"""
|
||||
assert (
|
||||
signable_message_json.strip() == tc["expected_signable_message"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: signable_message mismatch.
|
||||
Wanted:
|
||||
{tc['expected_signable_message'].strip()}
|
||||
Got:
|
||||
{signable_message_json.strip()}
|
||||
"""
|
||||
assert (
|
||||
signed_message_hash_preimage.strip() == tc["digest_input"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: digest_input mismatch.
|
||||
Wanted:
|
||||
{tc["digest_input"].strip()
|
||||
}
|
||||
Got:
|
||||
{signed_message_hash_preimage.strip()}
|
||||
"""
|
||||
assert (
|
||||
signed_message_json.strip() == tc["expected_signed_message"].strip()
|
||||
), f"""
|
||||
Test '{tc['name']}' failed: signed_message mismatch.
|
||||
Wanted:
|
||||
{tc['expected_signed_message'].strip()}
|
||||
Got:
|
||||
{signed_message_json.strip()}
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
functions = [
|
||||
test_sign_transfer_table,
|
||||
]
|
||||
for f in functions:
|
||||
try:
|
||||
f()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,141 @@
|
||||
from pysdk import grvt_raw_types
|
||||
from pysdk.grvt_raw_base import GrvtError
|
||||
from pysdk.grvt_raw_sync import GrvtRawSync
|
||||
|
||||
from .test_raw_utils import (
|
||||
get_config,
|
||||
get_test_order,
|
||||
get_test_tpsl_order,
|
||||
get_test_transfer,
|
||||
get_test_withdrawal,
|
||||
)
|
||||
|
||||
|
||||
def test_get_all_instruments() -> None:
|
||||
api = GrvtRawSync(config=get_config())
|
||||
resp = api.get_all_instruments_v1(
|
||||
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
|
||||
)
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected results to be non-null")
|
||||
if len(resp.result) == 0:
|
||||
raise ValueError("Expected results to be non-empty")
|
||||
|
||||
|
||||
def test_open_orders() -> None:
|
||||
api = GrvtRawSync(config=get_config())
|
||||
|
||||
# Skip test if trading account id is not set
|
||||
if api.config.trading_account_id is None or api.config.api_key is None:
|
||||
return None # Skip test if configs are not set
|
||||
|
||||
resp = api.open_orders_v1(
|
||||
grvt_raw_types.ApiOpenOrdersRequest(
|
||||
# sub_account_id=233, Uncomment to test error path with invalid sub account id
|
||||
sub_account_id=str(api.config.trading_account_id),
|
||||
kind=[grvt_raw_types.Kind.PERPETUAL],
|
||||
base=["BTC", "ETH"],
|
||||
quote=["USDT"],
|
||||
)
|
||||
)
|
||||
if isinstance(resp, GrvtError):
|
||||
api.logger.error(f"Received error: {resp}")
|
||||
return None
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected orders to be non-null")
|
||||
if len(resp.result) == 0:
|
||||
api.logger.info("Expected orders to be non-empty")
|
||||
|
||||
|
||||
def test_create_order_with_signing() -> None:
|
||||
api = GrvtRawSync(config=get_config())
|
||||
|
||||
inst_resp = api.get_all_instruments_v1(
|
||||
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
|
||||
)
|
||||
if isinstance(inst_resp, GrvtError):
|
||||
raise ValueError(f"Received error: {inst_resp}")
|
||||
|
||||
order = get_test_order(api, {inst.instrument: inst for inst in inst_resp.result})
|
||||
if order is None:
|
||||
return None # Skip test if configs are not set
|
||||
resp = api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order))
|
||||
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected order to be non-null")
|
||||
|
||||
|
||||
def test_create_tpsl_order_with_signing() -> None:
|
||||
api = GrvtRawSync(config=get_config())
|
||||
|
||||
inst_resp = api.get_all_instruments_v1(
|
||||
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
|
||||
)
|
||||
if isinstance(inst_resp, GrvtError):
|
||||
raise ValueError(f"Received error: {inst_resp}")
|
||||
|
||||
order = get_test_tpsl_order(
|
||||
api, {inst.instrument: inst for inst in inst_resp.result}
|
||||
)
|
||||
if order is None:
|
||||
return None # Skip test if configs are not set
|
||||
resp = api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order))
|
||||
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected order to be non-null")
|
||||
|
||||
|
||||
def test_transfer_with_signing() -> None:
|
||||
api = GrvtRawSync(config=get_config())
|
||||
transfer = get_test_transfer(api)
|
||||
|
||||
if transfer is None:
|
||||
return None # Skip test if configs are not set
|
||||
|
||||
resp = api.transfer_v1(
|
||||
grvt_raw_types.ApiTransferRequest(
|
||||
transfer.from_account_id,
|
||||
transfer.from_sub_account_id,
|
||||
transfer.to_account_id,
|
||||
transfer.to_sub_account_id,
|
||||
transfer.currency,
|
||||
transfer.num_tokens,
|
||||
transfer.signature,
|
||||
grvt_raw_types.TransferType.STANDARD,
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected transfer response to be non-null")
|
||||
|
||||
|
||||
def test_withdrawal_with_signing() -> None:
|
||||
api = GrvtRawSync(config=get_config())
|
||||
withdrawal = get_test_withdrawal(api)
|
||||
|
||||
if withdrawal is None:
|
||||
return None # Skip test if configs are not set
|
||||
|
||||
resp = api.withdrawal_v1(
|
||||
grvt_raw_types.ApiWithdrawalRequest(
|
||||
withdrawal.from_account_id,
|
||||
withdrawal.to_eth_address,
|
||||
withdrawal.currency,
|
||||
withdrawal.num_tokens,
|
||||
withdrawal.signature,
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected withdrawal response to be non-null")
|
||||
@@ -0,0 +1,158 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
|
||||
from pysdk import grvt_fixed_types, grvt_raw_types
|
||||
from pysdk.grvt_raw_base import GrvtApiConfig, GrvtError
|
||||
from pysdk.grvt_raw_env import GrvtEnv
|
||||
from pysdk.grvt_raw_signing import sign_order, sign_transfer, sign_withdrawal
|
||||
from pysdk.grvt_raw_sync import GrvtRawSync
|
||||
|
||||
|
||||
def get_config() -> GrvtApiConfig:
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
conf = GrvtApiConfig(
|
||||
env=GrvtEnv(os.getenv("GRVT_ENV", "testnet")),
|
||||
trading_account_id=os.getenv("GRVT_SUB_ACCOUNT_ID"),
|
||||
private_key=os.getenv("GRVT_PRIVATE_KEY"),
|
||||
api_key=os.getenv("GRVT_API_KEY"),
|
||||
logger=logger,
|
||||
)
|
||||
logger.debug(conf)
|
||||
return conf
|
||||
|
||||
|
||||
def get_main_account_id(api: GrvtRawSync) -> str:
|
||||
resp = api.funding_account_summary_v1(grvt_raw_types.EmptyRequest())
|
||||
if isinstance(resp, GrvtError):
|
||||
raise ValueError(f"Received error: {resp}")
|
||||
if resp.result is None:
|
||||
raise ValueError("Expected funding_account_summary_v1 response to be non-null")
|
||||
return resp.result.main_account_id
|
||||
|
||||
|
||||
def get_test_order(
|
||||
api: GrvtRawSync, instruments: dict[str, grvt_raw_types.Instrument]
|
||||
) -> grvt_raw_types.Order | None:
|
||||
# Skip test if configs are not set
|
||||
if (
|
||||
api.config.trading_account_id is None
|
||||
or api.config.private_key is None
|
||||
or api.config.api_key is None
|
||||
):
|
||||
return None
|
||||
|
||||
order = grvt_raw_types.Order(
|
||||
sub_account_id=str(api.config.trading_account_id),
|
||||
time_in_force=grvt_raw_types.TimeInForce.GOOD_TILL_TIME,
|
||||
legs=[
|
||||
grvt_raw_types.OrderLeg(
|
||||
instrument="BTC_USDT_Perp",
|
||||
size="1.2", # 1.2 BTC
|
||||
limit_price="64170.7", # 80,000 USDT
|
||||
is_buying_asset=True,
|
||||
)
|
||||
],
|
||||
signature=grvt_raw_types.Signature(
|
||||
signer="", # Populated by sign_order
|
||||
r="", # Populated by sign_order
|
||||
s="", # Populated by sign_order
|
||||
v=0, # Populated by sign_order
|
||||
expiration=str(
|
||||
time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000
|
||||
), # 20 days
|
||||
nonce=random.randint(0, 2**32 - 1),
|
||||
),
|
||||
metadata=grvt_raw_types.OrderMetadata(
|
||||
client_order_id=str(random.randint(0, 2**32 - 1)),
|
||||
),
|
||||
)
|
||||
return sign_order(order, api.config, api.account, instruments)
|
||||
|
||||
|
||||
def get_test_tpsl_order(
|
||||
api: GrvtRawSync, instruments: dict[str, grvt_raw_types.Instrument]
|
||||
) -> grvt_raw_types.Order | None:
|
||||
order = get_test_order(api, instruments)
|
||||
if order:
|
||||
order.metadata.trigger = grvt_raw_types.TriggerOrderMetadata(
|
||||
trigger_type=grvt_raw_types.TriggerType.TAKE_PROFIT,
|
||||
tpsl=grvt_raw_types.TPSLOrderMetadata(
|
||||
trigger_by=grvt_raw_types.TriggerBy.LAST,
|
||||
trigger_price="64000",
|
||||
),
|
||||
)
|
||||
return order
|
||||
|
||||
|
||||
def get_test_transfer(api: GrvtRawSync) -> grvt_fixed_types.Transfer | None:
|
||||
# Skip test if configs are not set
|
||||
if (
|
||||
api.config.trading_account_id is None
|
||||
or api.config.private_key is None
|
||||
or api.config.api_key is None
|
||||
):
|
||||
return None
|
||||
|
||||
funding_account_address = get_main_account_id(api)
|
||||
|
||||
return sign_transfer(
|
||||
grvt_fixed_types.Transfer(
|
||||
from_account_id=funding_account_address,
|
||||
from_sub_account_id="0",
|
||||
to_account_id=funding_account_address,
|
||||
to_sub_account_id=str(api.config.trading_account_id),
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=grvt_raw_types.Signature(
|
||||
signer="",
|
||||
r="",
|
||||
s="",
|
||||
v=0,
|
||||
expiration=str(
|
||||
time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000
|
||||
), # 20 days
|
||||
nonce=random.randint(0, 2**32 - 1),
|
||||
),
|
||||
transfer_type=grvt_fixed_types.TransferType.STANDARD,
|
||||
transfer_metadata="",
|
||||
),
|
||||
api.config,
|
||||
api.account,
|
||||
)
|
||||
|
||||
|
||||
def get_test_withdrawal(api: GrvtRawSync) -> grvt_raw_types.Withdrawal | None:
|
||||
# Skip test if configs are not set
|
||||
if (
|
||||
api.config.trading_account_id is None
|
||||
or api.config.private_key is None
|
||||
or api.config.api_key is None
|
||||
):
|
||||
return None
|
||||
|
||||
funding_account_address = get_main_account_id(api)
|
||||
|
||||
return sign_withdrawal(
|
||||
grvt_raw_types.Withdrawal(
|
||||
from_account_id=funding_account_address,
|
||||
to_eth_address="0xed3FF6F4E84a64556e8F7d149dC3533f0c7D9c49", # Just a test address
|
||||
currency="USDT",
|
||||
num_tokens="1",
|
||||
signature=grvt_raw_types.Signature(
|
||||
signer="",
|
||||
r="",
|
||||
s="",
|
||||
v=0,
|
||||
expiration=str(
|
||||
time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000
|
||||
), # 20 days
|
||||
nonce=random.randint(0, 2**32 - 1),
|
||||
),
|
||||
),
|
||||
api.config,
|
||||
api.account,
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .test_grvt_raw_sync import test_transfer_with_signing
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_transfer_with_signing()
|
||||
@@ -0,0 +1,4 @@
|
||||
from .test_grvt_raw_sync import test_withdrawal_with_signing
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_withdrawal_with_signing()
|
||||
Reference in New Issue
Block a user