更新环境配置示例,添加 GRVT 相关的 API 凭证和选项,增强文档以支持新的交易所适配器,确保用户能够正确配置和使用 GRVT 交易功能。

This commit is contained in:
discountry
2025-09-27 16:12:20 +08:00
parent 64640f4494
commit dde7ab71a9
60 changed files with 16126 additions and 527 deletions
@@ -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()