mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7635b7d467 | ||
|
|
f565c399e3 | ||
|
|
b43eaa0c64 | ||
|
|
9a419c36ac | ||
|
|
0e5026aaa1 | ||
|
|
6273f3b94a | ||
|
|
e478fb4330 | ||
|
|
599b4ec3f3 | ||
|
|
42061017f6 | ||
|
|
b7cd6eed99 | ||
|
|
abb56b917a | ||
|
|
9124c06315 | ||
|
|
56bcaddb22 | ||
|
|
4c4abb3b09 | ||
|
|
c8b0ab1c8e | ||
|
|
e83bdfccf9 | ||
|
|
d6533733c9 | ||
|
|
5c1283fac2 | ||
|
|
0e4ae67573 | ||
|
|
6ab0759d3d | ||
|
|
25ab1ababf | ||
|
|
e60b32a7f8 | ||
|
|
46c0551133 | ||
|
|
7e2d312fe3 | ||
|
|
e8dd36f540 | ||
|
|
47d44c7cb5 | ||
|
|
048dfb1698 | ||
|
|
fc1a74174c | ||
|
|
b45539b7c2 | ||
|
|
a998b3d3a1 | ||
|
|
d392f54f21 |
+19
-5
@@ -10,11 +10,14 @@ TRADE_SYMBOL=BTCUSDT # Trading pair symbol
|
||||
TRADE_AMOUNT=0.001 # Base order quantity (base asset, e.g. BTC)
|
||||
|
||||
# Risk management (USD amounts unless noted)
|
||||
LOSS_LIMIT=0.03 # Max loss per trade in USDT before forced close
|
||||
LOSS_LIMIT=0.04 # Max loss per trade in USDT before forced close
|
||||
TRAILING_PROFIT=0.2 # Trailing stop activation profit (USDT)
|
||||
TRAILING_CALLBACK_RATE=0.2 # Trailing callback percent (e.g. 0.2 => 0.2%)
|
||||
PROFIT_LOCK_TRIGGER_USD=0.1 # Start moving base stop once unrealized PnL > this (USDT)
|
||||
PROFIT_LOCK_OFFSET_USD=0.05 # Base stop offset from entry after trigger (USDT)
|
||||
PROFIT_LOCK_TRIGGER_USD=0.08 # Start moving base stop once unrealized PnL > this (USDT)
|
||||
PROFIT_LOCK_OFFSET_USD=0.04 # Base stop offset from entry after trigger (USDT)
|
||||
BOLLINGER_LENGTH=20 # SMA window (minutes) used for Bollinger bandwidth
|
||||
BOLLINGER_STD_MULTIPLIER=2 # Standard deviation multiplier for Bollinger bands
|
||||
MIN_BOLLINGER_BANDWIDTH=0.001 # Require bandwidth >= this ratio before new entries
|
||||
|
||||
# Precision (per-symbol exchange filters)
|
||||
PRICE_TICK=0.1 # Price tick size (e.g. BTCUSDT uses 0.1)
|
||||
@@ -27,8 +30,7 @@ KLINE_INTERVAL=1m # Kline interval (e.g., 1m/3m/5m)
|
||||
MAX_CLOSE_SLIPPAGE_PCT=0.05 # Max allowed deviation vs mark when closing (0.05 => 5%)
|
||||
|
||||
# Maker-only settings
|
||||
MAKER_LOSS_LIMIT=0.03 # Maker loss cap (USDT). Defaults to LOSS_LIMIT if unset
|
||||
MAKER_PRICE_CHASE=0.3 # Price chase threshold (USDT)
|
||||
MAKER_LOSS_LIMIT=0.05 # Maker loss cap (USDT). Defaults to LOSS_LIMIT if unset
|
||||
MAKER_BID_OFFSET=0 # Bid quote offset from top bid (USDT)
|
||||
MAKER_ASK_OFFSET=0 # Ask quote offset from top ask (USDT)
|
||||
MAKER_REFRESH_INTERVAL_MS=1500 # Maker refresh cadence (ms)
|
||||
@@ -47,3 +49,15 @@ GRVT_ENV=prod
|
||||
# GRVT_COOKIE="gravity=..." # Pre-provisioned session cookie (auto-refresh uses API key when absent)
|
||||
# GRVT_ACCOUNT_ID= # Populated automatically after login
|
||||
# GRVT_SIGNER_PATH=./grvt-signer.cjs # Custom signature provider module
|
||||
|
||||
# Lighter authentication (set when EXCHANGE=lighter)
|
||||
LIGHTER_ACCOUNT_INDEX=
|
||||
LIGHTER_API_PRIVATE_KEY= # 40-byte hex private key (e.g., 0x...)
|
||||
LIGHTER_API_KEY_INDEX=0 # API key slot (default 0)
|
||||
LIGHTER_SYMBOL=BTCUSDT # Trading pair (defaults to TRADE_SYMBOL when omitted)
|
||||
LIGHTER_ENV=testnet # mainnet | testnet | staging | dev
|
||||
# LIGHTER_BASE_URL=https://testnet.zklighter.elliot.ai
|
||||
# LIGHTER_CHAIN_ID=300 # Override inferred chain id when needed
|
||||
# LIGHTER_MARKET_ID=1 # Prefer explicit market id when symbols differ
|
||||
# LIGHTER_PRICE_DECIMALS=3 # Manual override for price decimals (optional)
|
||||
# LIGHTER_SIZE_DECIMALS=3 # Manual override for size decimals (optional)
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
The Bun-based entry point (`index.ts`) lives at the root for quick smoke checks, alongside `tsconfig.json` and the Bun lockfile. The production-ready trading agents reside in `legacy/`, which is a pnpm-managed workspace. Inside `legacy/`, strategy scripts (`trendV2.ts`, `maker.ts`, `bot.ts`) and the CLI live at the top level, shared helpers are under `legacy/utils/`, and exchange adapters plus tests sit in `legacy/exchanges/`. Reference environment samples are provided in `legacy/env.example`, and longer-form docs are kept in `legacy/docs/`.
|
||||
The Bun entry point (`index.ts`) lives at the repo root for quick CLI smoke checks. All production code is under `src/`:
|
||||
|
||||
- `src/strategy/` gathers every live trading engine (`maker`, `offset-maker`, `trend`). Shared helpers for strategy wiring sit in `src/strategy/common/`.
|
||||
- `src/core/` keeps order coordination plus shared libs used by multiple strategies.
|
||||
- `src/exchanges/` exposes the adapters and REST/websocket clients.
|
||||
- `src/ui/` implements the Ink dashboards for each strategy (they remain independent dashboards).
|
||||
- `src/logging/` contains the trade log helper.
|
||||
- `src/utils/` and `src/config.ts` hold cross-cutting utilities and runtime config.
|
||||
- `docs/` stores reference material, while `tests/` contains Vitest suites.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
Run `bun install` at the root to satisfy the lightweight Bun demo. Change into `legacy/` for real work: `pnpm install` bootstraps dependencies, `pnpm start` launches the default trend strategy, `pnpm maker` starts the market-making loop, `pnpm cli:start` triggers the dual-exchange hedging flow, and `pnpm test` executes the full Vitest suite. Use `pnpm aster:test` when you only need the Aster adapter checks.
|
||||
- `bun install` – install dependencies.
|
||||
- `bun run index.ts` – launch the CLI menu.
|
||||
- `bun x vitest run` – execute the full test suite; `bun x vitest --watch` for incremental runs.
|
||||
|
||||
Strategy-specific scripts still execute through the CLI; there is no separate `legacy/` workspace.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
All code is modern TypeScript using native ES modules. Follow the existing two-space indentation, keep imports sorted from external to local, and prefer `camelCase` for variables/functions with `PascalCase` for classes and enums. Strategy files stay in the project root with descriptive verbs (for example `maker.ts`), while shared utilities belong under `legacy/utils/`. Comments should stay concise and explain non-obvious trading logic.
|
||||
Use modern TypeScript with ES modules, two-space indentation, and sorted imports (external → internal). Favor `camelCase` for variables/functions and `PascalCase` for classes/enums. Place new strategies under `src/strategy/`, shared utilities under `src/utils/` or `src/strategy/common/` when they only apply to strategies. Keep comments focused on non-obvious trading logic.
|
||||
|
||||
## Testing Guidelines
|
||||
Vitest powers unit and integration checks. Place new suites beside their subjects using the `<feature>.test.ts` pattern (e.g., `legacy/exchanges/aster.test.ts`). Ensure strategies ship with coverage for order flow, risk guardrails, and websocket edge cases before opening a pull request. Run `pnpm test --watch` during development to keep feedback tight.
|
||||
Vitest powers unit/integration tests. Co-locate new tests next to their subject using `<feature>.test.ts`. Strategies should have coverage for order lifecycle, risk guards, and websocket edge cases. Run `bun x vitest --watch` during development for fast feedback.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
The repository history is empty, so adopt a lightweight Conventional Commits style (for example `feat: add hedging status panel`) to keep future changelogs clear. Commits should stay scoped to one strategy or module. Pull requests need a concise summary, reproduction or validation notes (commands run, environments touched), and screenshots or log excerpts when behavior changes. Link tracking issues or tasks to help downstream coordination.
|
||||
Follow lightweight Conventional Commits (e.g. `feat: add hedging status panel`). Scope each commit to a single module or strategy. PRs should include:
|
||||
- Summary of changes.
|
||||
- Validation notes (commands run, environments touched).
|
||||
- Relevant logs or screenshots for behavior changes.
|
||||
Link issues/tasks when available.
|
||||
|
||||
## Environment & Secrets
|
||||
Copy `legacy/env.example` to `.env` and populate API keys for Bitget and AsterDex before running live strategies. Never commit secrets; rely on local dotenv files or your deployment platform's secret manager. Rotate keys immediately if logs or configs leave the sandbox.
|
||||
Duplicate `.env.example` to `.env` and populate API keys (Aster/GRVT) before running strategies. Do not commit secrets—use local `.env` or deployment secret managers. Rotate keys if they leak into logs or backups.
|
||||
|
||||
@@ -1,219 +1,155 @@
|
||||
# ritmex-bot
|
||||
|
||||
一个基于 Bun 的 Aster 永续合约终端机器人,内置趋势跟随(SMA30)与做市策略,使用 websocket 实时行情,命令行界面由 Ink 驱动,可在断线后自动恢复运行。
|
||||
基于 Bun 的 Aster 永续合约量化终端,内置趋势跟随(SMA30)与做市策略,支持快速恢复、实时行情订阅与日志追踪。
|
||||
|
||||
## 快速上手
|
||||
* [Aster 30% 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
|
||||
* [GRVT 手续费优惠注册链接](https://grvt.io/exchange/sign-up?ref=sea)
|
||||
|
||||
使用优惠码获取 30% 手续费折扣:[注册 Aster 获取手续费优惠](https://www.asterdex.com/zh-CN/referral/4665f3)
|
||||
## 文档索引
|
||||
- [English README](README_en.md)
|
||||
- [简明上手指南(零基础)](simple-readme.md)
|
||||
|
||||
如果你完全不懂代码,可以查看 **[小白教程](simple-readme.md) 了解使用方法。**
|
||||
## 项目亮点
|
||||
- **实时行情与风控**:Websocket + REST 自动同步账户、挂单与仓位。
|
||||
- **趋势策略**:SMA30 穿越入场,内置止损、移动止盈、布林带带宽过滤与步进锁盈。
|
||||
- **做市策略**:支持双边追价、风险阈值与订单自愈。
|
||||
- **模块化设计**:适配器、策略引擎与 CLI 解耦,方便扩展新交易所或策略。
|
||||
|
||||
遇到Bug,反馈问题,请到 [Telegram群组](https://t.me/+4fdo0quY87o4Mjhh)
|
||||
## 环境要求
|
||||
- Bun ≥ 1.2(含 `bun`、`bunx` 命令)
|
||||
- macOS、Linux 或 Windows (WSL 推荐)
|
||||
- Node.js 仅在某些安装路径需要,可选
|
||||
|
||||
### 一键脚本
|
||||
|
||||
一键安装并启动(macOS / Linux):
|
||||
## 快速启动脚本(macOS / Linux / WSL)
|
||||
```bash
|
||||
curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh | bash
|
||||
```
|
||||
脚本会自动安装 Bun、安装依赖、引导输入 API Key/Secret,生成 `.env` 并启动程序。
|
||||
脚本会安装 Bun、依赖,收集 Aster API Key/Secret,生成 `.env` 并启动 CLI。运行前请准备好 API 凭证。
|
||||
|
||||
Windows 使用 WSL(推荐):
|
||||
1. 先安装并启用 WSL,参考微软官方文档:[在 Windows 上安装 WSL](https://learn.microsoft.com/zh-cn/windows/wsl/install)
|
||||
2. 在命令行或者 PowerShell 输入 `wsl` 并按下回车打开 WSL。
|
||||
3. 在 WSL 中运行一键脚本:
|
||||
## 手动安装步骤
|
||||
1. **获取代码**
|
||||
```bash
|
||||
curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh | bash
|
||||
git clone https://github.com/discountry/ritmex-bot.git
|
||||
cd ritmex-bot
|
||||
```
|
||||
按提示输入 ASTER_API_KEY / ASTER_API_SECRET,脚本将自动安装 Bun、依赖并启动程序。
|
||||
|
||||
## 手动安装
|
||||
|
||||
1. **下载代码**
|
||||
- 如果会使用 Git:`git clone https://github.com/discountry/ritmex-bot.git`
|
||||
- 如果不会使用 Git:点击仓库页面的 `Code` → `Download ZIP`,将压缩包解压到如 `桌面/ritmex-bot` 的目录。
|
||||
2. **打开命令行并进入项目目录**
|
||||
- macOS:通过 Spotlight (`⌘ + 空格`) 搜索 “Terminal” 并打开。
|
||||
- Windows:在开始菜单搜索 “PowerShell” 或 “Windows Terminal” 并打开。
|
||||
- 使用 `cd` 切换到项目目录,例如:
|
||||
```bash
|
||||
# macOS / Linux
|
||||
cd ~/Desktop/ritmex-bot
|
||||
# Windows
|
||||
cd C:\Users\用户名\Desktop\ritmex-bot
|
||||
```
|
||||
3. **安装 [Bun](https://bun.com) ≥ 1.2**
|
||||
- macOS / Linux:
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
- Windows(PowerShell):
|
||||
```powershell
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```
|
||||
安装完成后关闭并重新打开终端,运行 `bun -v` 确认命令可用。
|
||||
|
||||
如果上述命令无法完成安装,请尝试 [bun官网](https://bun.com/get) 提供的各种安装方式。
|
||||
|
||||
Windows 用户如果无法正常安装,可以尝试先[安装 nodejs](https://nodejs.org/en/download)
|
||||
|
||||
然后使用 `npm` 安装 `bun`:
|
||||
```bash
|
||||
npm install -g bun
|
||||
```
|
||||
4. **安装依赖**
|
||||
不方便使用 Git 时,可在仓库页面下载 ZIP 并手动解压。
|
||||
2. **安装 Bun**
|
||||
- macOS / Linux:`curl -fsSL https://bun.sh/install | bash`
|
||||
- Windows PowerShell:`powershell -c "irm bun.sh/install.ps1 | iex"`
|
||||
安装后重新打开终端,确认 `bun -v` 正常输出版本号。
|
||||
3. **安装依赖**
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
5. **配置环境变量**
|
||||
复制 `.env.example` 为 `.env` 并填入你的 Aster API Key/Secret:
|
||||
4. **复制环境变量模板并填写**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
然后根据需要修改 `.env` 中的配置项:
|
||||
- API KEY 获取地址 [https://www.asterdex.com/zh-CN/api-management](https://www.asterdex.com/zh-CN/api-management)
|
||||
- `ASTER_API_KEY` / `ASTER_API_SECRET`:Aster 交易所提供的 API 凭证。
|
||||
- `TRADE_SYMBOL`:策略运行的交易对(默认 `BTCUSDT`),需与 API 权限范围一致。
|
||||
- `TRADE_AMOUNT`:单次下单数量(合约张数折算后单位为标的货币,例如 BTC)。
|
||||
- `LOSS_LIMIT`:单笔允许的最大亏损(USDT),触发即强制平仓。
|
||||
- `TRAILING_PROFIT` / `TRAILING_CALLBACK_RATE`:趋势策略的动态止盈触发值(单位 USDT)与回撤百分比(百分数,如 0.2 表示 0.2%)。
|
||||
- `PROFIT_LOCK_TRIGGER_USD` / `PROFIT_LOCK_OFFSET_USD`:达到一定浮盈后,将基础止损上调(做多)或下调(做空)到开仓价的偏移量(单位 USDT)。
|
||||
- `PRICE_TICK` / `QTY_STEP`:交易对的最小价格变动单位与最小下单数量步长(例如 BTCUSDT 分别为 0.1 与 0.001)。
|
||||
- `MAKER_*` 参数:做市策略追价阈值、报价偏移、刷新频率等,可按流动性需求调节。
|
||||
6. **运行机器人**
|
||||
按下文说明修改 `.env`,至少需要正确配置 Aster 或 GRVT 的 API。
|
||||
5. **运行 CLI**
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
在终端中按 ↑/↓ 选择 “趋势策略” 或 “做市策略”,回车启动。按 `Esc` 可返回选择菜单,`Ctrl+C` 退出。
|
||||
7. **风险提示**
|
||||
建议先在小额或仿真环境中测试策略;真实资金操作前请确认 API 仅开启必要权限,并逐步验证配置。
|
||||
方向键选择策略,回车启动;`Esc` 返回菜单,`Ctrl+C` 退出。
|
||||
|
||||
A Bun-powered trading workstation for Aster perpetual contracts. The project ships two production strategies—an SMA30 trend follower and a dual-sided maker—that share a modular gateway, UI, and runtime state derived entirely from the exchange. Everything runs in the terminal via Ink, with live websocket refresh and automatic recovery from restarts or network failures.
|
||||
## 环境变量配置指南
|
||||
核心变量在 `.env.example` 中给出默认值:
|
||||
|
||||
## Features
|
||||
- **Live data over websockets** with REST fallbacks and automatic re-sync after reconnects.
|
||||
- **Trend strategy**: SMA30 crossover entries, automated stop-loss / trailing-stop, and P&L tracking.
|
||||
- **Maker strategy**: adaptive bid/ask chasing, risk stops, and target order introspection.
|
||||
- **Extensibility**: exchange gateway, engines, and UI components are modular for new venues or strategies.
|
||||
| 变量 | 说明 |
|
||||
| --- | --- |
|
||||
| `ASTER_API_KEY` / `ASTER_API_SECRET` | Aster API 凭证,运行策略必填 |
|
||||
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT`) |
|
||||
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
|
||||
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
|
||||
| `TRAILING_PROFIT` / `TRAILING_CALLBACK_RATE` | 动态止盈触发值(USDT)与回撤百分比 |
|
||||
| `PROFIT_LOCK_TRIGGER_USD` / `PROFIT_LOCK_OFFSET_USD` | 浮盈超过阈值后上调止损的触发金额与偏移 |
|
||||
| `BOLLINGER_LENGTH` / `BOLLINGER_STD_MULTIPLIER` | 布林带宽度判定的窗口长度与标准差倍数 |
|
||||
| `MIN_BOLLINGER_BANDWIDTH` | 仅当带宽 ≥ 此比例时才触发入场信号 |
|
||||
| `PRICE_TICK` / `QTY_STEP` | 交易所要求的最小报价与数量精度 |
|
||||
| `POLL_INTERVAL_MS` | 趋势策略循环间隔(毫秒) |
|
||||
| `MAX_CLOSE_SLIPPAGE_PCT` | 平仓时相对标记价允许的最大偏差 |
|
||||
| `MAKER_*` 系列 | 做市策略独有参数(追价阈值、报价偏移、刷新频率等) |
|
||||
|
||||
## Requirements
|
||||
- [Bun](https://bun.com) ≥ 1.2
|
||||
- Node.js (optional, only if you prefer `npm` tooling)
|
||||
- Valid Aster API credentials with futures access
|
||||
切换到 GRVT 时,将 `EXCHANGE=grvt` 并补齐 `GRVT_API_KEY`、`GRVT_API_SECRET`、`GRVT_SUB_ACCOUNT_ID` 等变量;详情见 `.env.example`。
|
||||
|
||||
## Installation
|
||||
> 提示:你也可以通过命令行参数临时指定交易所(优先级高于环境变量):
|
||||
> ```bash
|
||||
> bun run index.ts --exchange grvt
|
||||
> bun run index.ts -e lighter
|
||||
> ```
|
||||
|
||||
## 常用命令
|
||||
```bash
|
||||
bun install
|
||||
bun run index.ts # 启动 CLI(默认)
|
||||
bun run start # 同上
|
||||
bun run dev # 调试模式,等价于运行 index.ts
|
||||
bun x vitest run # 执行单元测试
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Create an `.env` (or export environment variables) with at least:
|
||||
```bash
|
||||
ASTER_API_KEY=your_key
|
||||
ASTER_API_SECRET=your_secret
|
||||
TRADE_SYMBOL=BTCUSDT # optional, defaults to BTCUSDT
|
||||
TRADE_AMOUNT=0.001 # position size used by both strategies
|
||||
LOSS_LIMIT=0.03 # per-trade USD loss cap
|
||||
TRAILING_PROFIT=0.2 # trailing activation profit in USDT
|
||||
TRAILING_CALLBACK_RATE=0.2 # trailing callback in percent, e.g. 0.2 => 0.2%
|
||||
PROFIT_LOCK_TRIGGER_USD=0.1 # profit threshold to start moving base stop (USDT)
|
||||
PROFIT_LOCK_OFFSET_USD=0.05 # base stop offset from entry after trigger (USDT)
|
||||
PRICE_TICK=0.1 # price tick size; set per symbol
|
||||
QTY_STEP=0.001 # quantity step size; set per symbol
|
||||
```
|
||||
Additional maker-specific knobs (`MAKER_*`) live in `src/config.ts` and may be overridden via env vars:
|
||||
```bash
|
||||
# Maker-specific (units in USDT unless noted)
|
||||
MAKER_LOSS_LIMIT=0.03 # override maker risk stop; defaults to LOSS_LIMIT
|
||||
MAKER_PRICE_CHASE=0.3 # chase threshold
|
||||
MAKER_BID_OFFSET=0 # bid offset from top bid (USDT)
|
||||
MAKER_ASK_OFFSET=0 # ask offset from top ask (USDT)
|
||||
MAKER_REFRESH_INTERVAL_MS=1500 # maker refresh cadence (ms)
|
||||
MAKER_MAX_CLOSE_SLIPPAGE_PCT=0.05 # allowed deviation vs mark when closing
|
||||
MAKER_PRICE_TICK=0.1 # maker tick size; defaults to PRICE_TICK
|
||||
```
|
||||
|
||||
To switch the entire CLI to GRVT instead of Aster, set `EXCHANGE=grvt` and provide the programmable API credentials:
|
||||
## 静默启动与后台运行
|
||||
### 直接静默启动
|
||||
无需进入 Ink 菜单,可用命令行直接拉起指定策略:
|
||||
|
||||
```bash
|
||||
EXCHANGE=grvt
|
||||
GRVT_API_KEY=your_api_key
|
||||
GRVT_API_SECRET=0xabc123... # private key used for EIP-712 order signatures
|
||||
GRVT_SUB_ACCOUNT_ID=your_sub_account # sub account that actually trades
|
||||
GRVT_INSTRUMENT=BTC_USDT_Perp # instrument as defined by GRVT
|
||||
GRVT_SYMBOL=BTCUSDT # optional display symbol; defaults to instrument sans underscores
|
||||
GRVT_ENV=prod # optional environment switch (prod/testnet/staging/dev)
|
||||
# Optional overrides
|
||||
# GRVT_SIGNER_PATH=./grvt-signer.cjs # replace the built-in signer if you run an external service
|
||||
# GRVT_COOKIE="gravity=..." # pre-provisioned session cookie (auto-fetched via API key when absent)
|
||||
# GRVT_ACCOUNT_ID=... # populated automatically after login
|
||||
bun run index.ts --strategy trend --silent # 启动趋势策略
|
||||
bun run index.ts --strategy maker --silent # 启动做市策略
|
||||
bun run index.ts --strategy offset-maker --silent # 启动偏移做市策略
|
||||
```
|
||||
|
||||
The adapter logs in with `GRVT_API_KEY`, refreshes cookies automatically, and signs orders locally using `GRVT_API_SECRET` following GRVT's EIP‑712 schema. If you prefer to delegate signing to another process, set `GRVT_SIGNER_PATH`; the module should export a function (default export also works) that receives a context object (unsigned order, nonce/expiration, instrument metadata, chain id, etc.) and returns `{ signer, r, s, v, expiration, nonce }`. If you leave `GRVT_SIGNER_PATH` unset, the built-in signer uses `GRVT_API_SECRET` directly.
|
||||
|
||||
### 切换到 GRVT 交易所
|
||||
|
||||
将环境变量 `EXCHANGE` 设为 `grvt` 后,所有策略会改用 GRVT 适配器:
|
||||
如需同时指定交易所,可叠加 `--exchange/-e`(将覆盖 `.env` 中的 `EXCHANGE`/`TRADE_EXCHANGE`):
|
||||
|
||||
```bash
|
||||
EXCHANGE=grvt
|
||||
GRVT_API_KEY=你的APIKey
|
||||
GRVT_API_SECRET=0xabc123... # 用于订单签名的私钥
|
||||
GRVT_SUB_ACCOUNT_ID=your_sub_account_id # 具体交易子账号
|
||||
GRVT_INSTRUMENT=BTC_USDT_Perp # 交易品种,需与策略一致
|
||||
GRVT_SYMBOL=BTCUSDT # 可选,内部用于展示,默认由 instrument 推导
|
||||
GRVT_ENV=prod # 可选:prod / testnet / staging / dev,默认 prod
|
||||
# 可选覆盖项
|
||||
# GRVT_SIGNER_PATH=./grvt-signer.cjs # 如果你希望由外部服务签名
|
||||
# GRVT_COOKIE="gravity=..." # 预先获取到的 Cookie(若未提供,将使用 API Key 自动登录)
|
||||
# GRVT_ACCOUNT_ID=... # 登录后自动填充
|
||||
bun run index.ts --exchange grvt --strategy maker --silent
|
||||
bun run index.ts -e lighter -s offset-maker --silent
|
||||
```
|
||||
|
||||
适配器会基于 `GRVT_API_SECRET` 自动完成 EIP‑712 签名并提交订单。如果你需要自定义签名流程,可通过 `GRVT_SIGNER_PATH` 指定模块(CommonJS 或 ESM 均可)。模块需导出一个函数,接收签名上下文(未签名订单、nonce/expiration、合约元信息、链 ID 等)并返回包含 `signer/r/s/v/expiration/nonce` 字段的对象,例如:
|
||||
### 项目内置脚本
|
||||
`package.json` 提供了便捷脚本:
|
||||
|
||||
```js
|
||||
// grvt-signer.cjs
|
||||
module.exports = async function signOrder(context) {
|
||||
// 调用你自己的签名服务,或者使用本地私钥完成签名
|
||||
const signature = await mySigner(context);
|
||||
return {
|
||||
signer: signature.signer,
|
||||
r: signature.r,
|
||||
s: signature.s,
|
||||
v: signature.v,
|
||||
expiration: signature.expiration,
|
||||
nonce: signature.nonce,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
如未自定义 `GRVT_SIGNER_PATH`,适配器会直接使用 `GRVT_API_SECRET` 完成签名。
|
||||
|
||||
## Running the CLI
|
||||
```bash
|
||||
bun run index.ts # or: bun run dev / bun run start
|
||||
bun run start:trend:silent
|
||||
bun run start:maker:silent
|
||||
bun run start:offset:silent
|
||||
```
|
||||
Pick a strategy with the arrow keys. Press `Esc` to return to the menu. The dashboard shows live order books, holdings, pending orders, and recent events. 状态完全以交易所数据为准,重新启动时会自动同步账户和挂单。
|
||||
|
||||
## Testing
|
||||
### 使用 pm2 守护并自动重启
|
||||
将 `pm2` 安装到项目中(示例:`bun add -d pm2`),之后即可在不安装全局 pm2 的情况下运行:
|
||||
|
||||
```bash
|
||||
bun run test # bun x vitest run
|
||||
bun run test:watch # stay in watch mode
|
||||
bunx pm2 start bun --name ritmex-trend --cwd . --restart-delay 5000 -- run index.ts --strategy trend --silent
|
||||
```
|
||||
Current tests cover the order coordinator utilities and strategy helpers; add unit tests beside new modules as you extend the system.
|
||||
|
||||
## Project Layout
|
||||
- `src/config.ts` – shared runtime configuration
|
||||
- `src/core/` – trend & maker engines plus order coordination
|
||||
- `src/exchanges/` – Aster REST/WS gateway and adapters
|
||||
- `src/ui/` – Ink components and strategy dashboards
|
||||
- `src/utils/` – math helpers and strategy utilities
|
||||
- `tests/` – Vitest suites for critical modules
|
||||
亦可直接调用脚本:
|
||||
|
||||
## Troubleshooting
|
||||
- **Websocket reconnect loops**: ensure outbound access to `wss://fstream.asterdex.com/ws` and REST endpoints.
|
||||
- **429 or 5xx responses**: the gateway backs off automatically, but check your rate limits and credentials.
|
||||
- **CLI input errors**: run in a real TTY; non-interactive shells disable keyboard shortcuts but the UI still renders.
|
||||
```bash
|
||||
bun run pm2:start:trend
|
||||
bun run pm2:start:maker
|
||||
bun run pm2:start:offset
|
||||
```
|
||||
|
||||
## Contributing
|
||||
Issues and PRs are welcome. When adding strategies or exchanges, follow the modular patterns in `src/core` and add tests under `tests/`.
|
||||
根据需要调整 `--name`、`--cwd`、`--restart-delay` 等参数,完成后可执行 `pm2 save` 持久化进程列表。
|
||||
|
||||
## 测试
|
||||
项目使用 Vitest:
|
||||
```bash
|
||||
bun run test # 运行全部测试
|
||||
bun x vitest --watch
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
- 你需要至少 50-100 USDT 的资金才能运行策略
|
||||
- 请在交易所自行设置 50 倍左右的杠杆,本策略不包含杠杆设置
|
||||
- 请确保你电脑/服务器的时间是准确的真实世界时间
|
||||
- 持仓方式需要保持单向持仓
|
||||
- `.env` 未读取:确认文件位于项目根目录且变量名无误。
|
||||
- API 拒绝访问:检查交易所后台权限,确保开启合约读写。
|
||||
- 精度错误:同步交易对的最小价格与数量步长。
|
||||
更多排查步骤可参考 [简明上手指南](simple-readme.md)。
|
||||
|
||||
## 社区与支持
|
||||
- Telegram 交流群:[https://t.me/+4fdo0quY87o4Mjhh](https://t.me/+4fdo0quY87o4Mjhh)
|
||||
- 反馈或新特性建议请提交 Issue 或 PR
|
||||
|
||||
## 风险提示
|
||||
量化交易具备风险。建议在仿真或小额账户中验证策略表现,妥善保管 API 密钥,仅开启必要权限。
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
# ritmex-bot
|
||||
|
||||
A Bun-powered trading workstation for Aster perpetual contracts that ships two production-ready agents: an SMA30 trend follower and a dual-sided market maker. The CLI is built with Ink, synchronises risk state from the exchange, and automatically recovers from restarts or disconnects.
|
||||
|
||||
## Documentation Map
|
||||
- [中文 README](README.md)
|
||||
- [Beginner-friendly Quick Start](simple-readme.md)
|
||||
|
||||
## Highlights
|
||||
- **Live market data & risk sync** via websocket feeds with REST fallbacks, full reconciliation on restart.
|
||||
- **Trend engine** featuring SMA30 entries, fixed stop loss, trailing stop, Bollinger bandwidth gate, and profit-lock stepping.
|
||||
- **Market-making loop** with adaptive quote chasing, loss caps, and automatic order healing.
|
||||
- **Extensible architecture** decoupling exchange adapters, engines, and the Ink CLI for easy venue or strategy additions.
|
||||
|
||||
## Requirements
|
||||
- Bun ≥ 1.2 (`bun`, `bunx` available on PATH)
|
||||
- macOS, Linux, or Windows via WSL (native Windows works but WSL is recommended)
|
||||
- Node.js is optional unless your environment requires it for tooling
|
||||
|
||||
## One-Line Bootstrap (macOS / Linux / WSL)
|
||||
```bash
|
||||
curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh | bash
|
||||
```
|
||||
The script installs Bun, project dependencies, collects Aster API credentials, generates `.env`, and launches the CLI. Prepare your API Key/Secret before running.
|
||||
|
||||
## Manual Installation
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/discountry/ritmex-bot.git
|
||||
cd ritmex-bot
|
||||
```
|
||||
Alternatively download the ZIP from GitHub and extract it manually.
|
||||
2. **Install Bun**
|
||||
- macOS / Linux: `curl -fsSL https://bun.sh/install | bash`
|
||||
- Windows PowerShell: `powershell -c "irm bun.sh/install.ps1 | iex"`
|
||||
Re-open the terminal and confirm `bun -v` prints a version.
|
||||
3. **Install dependencies**
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
4. **Create your environment file**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Edit `.env` with your exchange credentials and overrides.
|
||||
5. **Launch the CLI**
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
Use the arrow keys to pick a strategy, `Enter` to start, `Esc` to return to the menu, and `Ctrl+C` to exit.
|
||||
|
||||
## Environment Variables
|
||||
The most important settings shipped in `.env.example` are summarised below:
|
||||
|
||||
| Variable | Purpose |
|
||||
| --- | --- |
|
||||
| `ASTER_API_KEY` / `ASTER_API_SECRET` | Required Aster exchange credentials |
|
||||
| `TRADE_SYMBOL` | Contract symbol, defaults to `BTCUSDT` |
|
||||
| `TRADE_AMOUNT` | Order size in base asset units |
|
||||
| `LOSS_LIMIT` | Max per-trade loss (USDT) before forced close |
|
||||
| `TRAILING_PROFIT` / `TRAILING_CALLBACK_RATE` | Trailing stop trigger amount (USDT) and pullback percentage |
|
||||
| `PROFIT_LOCK_TRIGGER_USD` / `PROFIT_LOCK_OFFSET_USD` | Move the base stop once unrealised PnL exceeds this trigger |
|
||||
| `BOLLINGER_LENGTH` / `BOLLINGER_STD_MULTIPLIER` | Window size and std-dev multiplier for bandwidth filtering |
|
||||
| `MIN_BOLLINGER_BANDWIDTH` | Minimum bandwidth ratio required before opening a new position |
|
||||
| `PRICE_TICK` / `QTY_STEP` | Exchange precision filters for price and quantity |
|
||||
| `POLL_INTERVAL_MS` | Trend engine polling cadence in milliseconds |
|
||||
| `MAX_CLOSE_SLIPPAGE_PCT` | Allowed deviation vs mark price when closing |
|
||||
| `MAKER_*` | Maker strategy knobs: chase threshold, quote offsets, refresh cadence, etc. |
|
||||
|
||||
To trade on GRVT, set `EXCHANGE=grvt` and populate `GRVT_API_KEY`, `GRVT_API_SECRET`, `GRVT_SUB_ACCOUNT_ID`, plus any optional overrides documented in `.env.example`.
|
||||
|
||||
> Tip: you can temporarily override the exchange via CLI flags (takes precedence over environment):
|
||||
> ```bash
|
||||
> bun run index.ts --exchange grvt
|
||||
> bun run index.ts -e lighter
|
||||
> ```
|
||||
|
||||
## Common Commands
|
||||
```bash
|
||||
bun run index.ts # Launch the CLI
|
||||
bun run start # Same as above
|
||||
bun run dev # Development entry point
|
||||
bun x vitest run # Execute the Vitest suite
|
||||
```
|
||||
|
||||
## Silent & Background Execution
|
||||
### Direct silent launch
|
||||
Skip the Ink menu and start a strategy straight from the CLI:
|
||||
|
||||
```bash
|
||||
bun run index.ts --strategy trend --silent # Trend engine
|
||||
bun run index.ts --strategy maker --silent # Maker engine
|
||||
bun run index.ts --strategy offset-maker --silent # Offset maker engine
|
||||
```
|
||||
|
||||
Combine with `--exchange/-e` to explicitly choose the venue (overrides `EXCHANGE`/`TRADE_EXCHANGE` from `.env`):
|
||||
|
||||
```bash
|
||||
bun run index.ts --exchange grvt --strategy maker --silent
|
||||
bun run index.ts -e lighter -s offset-maker --silent
|
||||
```
|
||||
|
||||
### Package scripts
|
||||
Convenience aliases are exposed in `package.json`:
|
||||
|
||||
```bash
|
||||
bun run start:trend:silent
|
||||
bun run start:maker:silent
|
||||
bun run start:offset:silent
|
||||
```
|
||||
|
||||
### Daemonising with pm2
|
||||
Install `pm2` locally (e.g. `bun add -d pm2`) and launch without a global install:
|
||||
|
||||
```bash
|
||||
bunx pm2 start bun --name ritmex-trend --cwd . --restart-delay 5000 -- run index.ts --strategy trend --silent
|
||||
```
|
||||
|
||||
You can also reuse the bundled scripts:
|
||||
|
||||
```bash
|
||||
bun run pm2:start:trend
|
||||
bun run pm2:start:maker
|
||||
bun run pm2:start:offset
|
||||
```
|
||||
|
||||
Adjust `--name`, `--cwd`, or `--restart-delay` to suit your environment and run `pm2 save` if you want the process to auto-start after reboot.
|
||||
|
||||
## Testing
|
||||
Vitest powers the unit tests:
|
||||
```bash
|
||||
bun run test
|
||||
bun x vitest --watch
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
- You need at least 50–100 USDT of capital before deploying a live strategy.
|
||||
- Set leverage on the exchange beforehand (around 50x is recommended); the bot does not change it for you.
|
||||
- Keep server/desktop time in sync with real-world time to avoid signature errors.
|
||||
- Make sure the exchange account is in one-way position mode.
|
||||
- **Env not loading**: ensure `.env` resides in the repository root and variable names are spelled correctly.
|
||||
- **Order rejected for precision**: align `PRICE_TICK`, `QTY_STEP`, and `TRADE_SYMBOL` with the exchange filters.
|
||||
- **Permission or auth errors**: double-check exchange API scopes.
|
||||
More step-by-step guidance is available in [simple-readme.md](simple-readme.md).
|
||||
|
||||
## Community & Support
|
||||
- Telegram: [https://t.me/+4fdo0quY87o4Mjhh](https://t.me/+4fdo0quY87o4Mjhh)
|
||||
- Issues and PRs are welcome for bug reports and feature ideas
|
||||
|
||||
## Disclaimer
|
||||
Algorithmic trading carries risk. Validate strategies with paper accounts or small capital first, safeguard your API keys, and only grant the minimum required permissions.
|
||||
@@ -8,6 +8,7 @@
|
||||
"axios": "^1.12.2",
|
||||
"ccxt": "^4.5.5",
|
||||
"dotenv": "^17.2.2",
|
||||
"ethereum-cryptography": "^2.1.3",
|
||||
"ink": "^6.3.1",
|
||||
"react": "^19.1.1",
|
||||
"ws": "^8.18.3",
|
||||
@@ -80,6 +81,10 @@
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ=="],
|
||||
@@ -124,6 +129,12 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
|
||||
|
||||
"@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
|
||||
|
||||
"@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="],
|
||||
|
||||
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
@@ -228,6 +239,8 @@
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="],
|
||||
|
||||
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
UV_VERSION: "0.4.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
- name: Set up uv
|
||||
run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
|
||||
- name: Restore uv cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.uv-cache
|
||||
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||
restore-keys: |
|
||||
uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||
uv-${{ runner.os }}
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
- name: Test with pytest
|
||||
run: uv run pytest tests --cov=src
|
||||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
|
||||
build-image:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -1,39 +0,0 @@
|
||||
# name: "codeql"
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
# pull_request:
|
||||
# branches: ["main"]
|
||||
|
||||
# jobs:
|
||||
# analyze:
|
||||
# name: Analyze
|
||||
# runs-on: ubuntu-24.04
|
||||
# permissions:
|
||||
# actions: read
|
||||
# contents: read
|
||||
# security-events: write
|
||||
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# language: [python]
|
||||
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Initialize CodeQL
|
||||
# uses: github/codeql-action/init@v3
|
||||
# with:
|
||||
# languages: ${{ matrix.language }}
|
||||
# queries: +security-and-quality
|
||||
|
||||
# - name: Autobuild
|
||||
# uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# - name: Perform CodeQL Analysis
|
||||
# uses: github/codeql-action/analyze@v3
|
||||
# with:
|
||||
# category: "/language:${{ matrix.language }}"
|
||||
@@ -1,61 +0,0 @@
|
||||
name: pr
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10.15", "3.10.x"]
|
||||
uv-version: ["0.3.5", "0.4.0"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: x64
|
||||
- name: Set up uv
|
||||
run: curl -LsSf https://astral.sh/uv/${{ matrix.uv-version }}/install.sh | sh
|
||||
- name: Restore uv cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.uv-cache
|
||||
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||
restore-keys: |
|
||||
uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||
uv-${{ runner.os }}
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
- name: Test with pytest
|
||||
run: uv run pytest tests --cov=src
|
||||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
|
||||
build-image:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -1,287 +0,0 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,visualstudiocode
|
||||
|
||||
### PyCharm+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### PyCharm+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/*
|
||||
|
||||
!.idea/codeStyles
|
||||
!.idea/runConfigurations
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# Tests
|
||||
test_results.csv
|
||||
test_results_sync.csv
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode
|
||||
@@ -1,79 +0,0 @@
|
||||
ci:
|
||||
skip: [pytest]
|
||||
|
||||
default_language_version:
|
||||
python: python3.10
|
||||
|
||||
repos:
|
||||
# general checks (see here: https://pre-commit.com/hooks.html)
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
# ruff - linting + formatting
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.6.3"
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff
|
||||
- id: ruff-format
|
||||
name: ruff-format
|
||||
|
||||
# mypy - lint-like type checking
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.11.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy
|
||||
|
||||
# docformatter - formats docstrings to follow PEP 257
|
||||
- repo: https://github.com/pycqa/docformatter
|
||||
rev: v1.7.5
|
||||
hooks:
|
||||
- id: docformatter
|
||||
name: docformatter
|
||||
args:
|
||||
[
|
||||
-r,
|
||||
-i,
|
||||
--pre-summary-newline,
|
||||
--make-summary-multi-line,
|
||||
--wrap-summaries,
|
||||
"90",
|
||||
--wrap-descriptions,
|
||||
"90",
|
||||
src,
|
||||
tests,
|
||||
]
|
||||
exclude: ^(artifacts/.*)$
|
||||
|
||||
# bandit - find common security issues
|
||||
- repo: https://github.com/pycqa/bandit
|
||||
rev: 1.7.9
|
||||
hooks:
|
||||
- id: bandit
|
||||
name: bandit
|
||||
exclude: ^tests/
|
||||
args:
|
||||
- -r
|
||||
- src
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pytest
|
||||
name: pytest
|
||||
entry: uv run pytest tests --cov=src
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
|
||||
# prettier - formatting JS, CSS, JSON, Markdown, ...
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: prettier
|
||||
exclude: ^(uv.lock)$
|
||||
@@ -1 +0,0 @@
|
||||
3.10
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
"schedule:daily",
|
||||
"group:all",
|
||||
":prConcurrentLimitNone",
|
||||
":prHourlyLimitNone",
|
||||
":prImmediately"
|
||||
],
|
||||
"labels": ["dependencies"]
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## Version [0.2.1] - 2025-07-24
|
||||
|
||||
### Changes in 0.2.1
|
||||
|
||||
- New methods in ccxt-compatible code for Strategy (Vault) managers to view investment history and current redemption queue of a Vault.
|
||||
- Classes `GrvtCcxt` and `GrvtCcxtPro`
|
||||
- Methods `fetch_vault_manager_investor_history` and `fetch_vault_redemption_queue`
|
||||
|
||||
## Version [0.2.0] - 2025-07-16
|
||||
|
||||
### Changes in 0.2.0
|
||||
|
||||
- additional fixes for removal of explicit enums in `grvt_raw_types.py`
|
||||
- Note: this update would `break existing integrations` for `non-create-order flows` (e.g., transfer/withdrawal history, transfer, get open orders, get-instrument filters) to handle currency strings directly instead of enums.
|
||||
- users will be losing the currency enum when they upgrade, so they would need to call the currencies endpoin (<https://api-docs.grvt.io/market_data_api/#get-currency> ), for which the support has been added in PySDK (`get_currency_v1()` in grvt_raw_async.py and grvt_raw_sync.py), for the metadata attached to the currency strings.
|
||||
|
||||
## Version [0.1.32] - 2025-07-15
|
||||
|
||||
### Changes in 0.1.32
|
||||
|
||||
- removed explicit enums in `grvt_raw_types.py` (no need to update SDK for when new coins are listed)
|
||||
- added vault-related functionality
|
||||
|
||||
## Version [0.1.31] - 2025-07-08
|
||||
|
||||
### Changes in 0.1.31
|
||||
|
||||
- added AVAX in the raw code (testing purposes)
|
||||
|
||||
## Version [0.1.30] - 2025-07-08
|
||||
|
||||
### Changes in 0.1.30
|
||||
|
||||
- added `H` in the **raw** code
|
||||
|
||||
## Version [0.1.29] - 2025-06-30
|
||||
|
||||
### Changes in 0.1.29
|
||||
|
||||
- Added new currencies: `HYPE, UNI, MOODENG, LAUNCHCOIN` in the **raw** code
|
||||
- Improvements and type fixes in `fetch_funding_rate_history()` methods of GrvtCcxt and GrvtCcxtPro.
|
||||
|
||||
## Version [0.1.28] - 2025-06-02
|
||||
|
||||
### Changes in 0.1.28
|
||||
|
||||
- Renamed currency name `AI_16_Z` into `AI16Z` in the **raw** code to match the exchange currency name.
|
||||
|
||||
## Version [0.1.27] - 2025-05-19
|
||||
|
||||
### Changes in 0.1.27
|
||||
|
||||
- `GrvtCcxt` and `GrvtCcxtPro` classes:
|
||||
|
||||
- renamed method `fetch_balances()` to `fetch_balance()` as defined in ccxt.
|
||||
|
||||
## Version [0.1.26] - 2025-05-15
|
||||
|
||||
### Added in 0.1.26
|
||||
|
||||
- `GrvtCcxt` and `GrvtCcxtPro` classes:
|
||||
|
||||
- new method `describe()` - returns a list of public method names
|
||||
- new method `fetch_balances()` - returns dict with balances in ccxt format.
|
||||
- constructor parameter `order_book_ccxt_format: bool = False` . If = True then order book snapshots from `fetch_order_book()` are in ccxt format.
|
||||
|
||||
### Fixed in 0.1.26
|
||||
|
||||
- Issues with typing and lynting errors
|
||||
|
||||
## Version [0.1.25] - 2025-04-25
|
||||
|
||||
### Fixed in 0.1.25
|
||||
|
||||
- Issues with typing and lynting errors
|
||||
- Fixed bug in test_grvt_ccxt.py
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,59 +0,0 @@
|
||||
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
.PHONY: help
|
||||
help: ## Show this help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sed -e 's/^Makefile://' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
|
||||
.PHONY: run
|
||||
run: ## Run the project
|
||||
uv run python -m pysdk.main
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the tests
|
||||
uv run pytest tests --cov=src
|
||||
|
||||
.PHONY: precommit
|
||||
precommit: ## Run the pre-commit hooks
|
||||
bash .git/hooks/pre-commit
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run the linter
|
||||
uv run ruff check .
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: ## Run the linter and fix the issues
|
||||
uv run ruff check --fix --unsafe-fixes .
|
||||
|
||||
.PHONY: format
|
||||
format: ## Run the formatter
|
||||
uv run ruff format .
|
||||
|
||||
.PHONY: typecheck
|
||||
typecheck: ## Run the type checker
|
||||
uv run mypy .
|
||||
|
||||
.PHONY: security
|
||||
security: ## Run the security checker
|
||||
uv run bandit .
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Clean the project
|
||||
uv run ruff clean .
|
||||
|
||||
.PHONY: install
|
||||
install: ## Install the project
|
||||
uv sync --all-extras --dev --frozen
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the project
|
||||
uv build
|
||||
|
||||
.PHONY: publish
|
||||
publish: ## Publish the project
|
||||
python3 build_readme.py
|
||||
make build
|
||||
uv run twine upload --skip-existing dist/*
|
||||
|
||||
# ==============================================================================
|
||||
# always make sure the following is the last line on this file
|
||||
.DEFAULT_GOAL := help
|
||||
@@ -1,300 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/astral-sh/uv" target="blank"><img src="https://github.com/astral-sh/uv/blob/8674968a17e5f2ee0dda01d17aaf609f162939ca/docs/assets/logo-letter.svg" height="100" alt="uv logo" /></a>
|
||||
<a href="https://pre-commit.com/" target="blank"><img src="https://pre-commit.com/logo.svg" height="100" alt="pre-commit logo" /></a>
|
||||
<a href="https://github.com/astral-sh/ruff" target="blank"><img src="https://raw.githubusercontent.com/astral-sh/ruff/8c20f14e62ddaf7b6d62674f300f5d19cbdc5acb/docs/assets/bolt.svg" height="100" alt="ruff logo" style="background-color: #ef5552" /></a>
|
||||
<a href="https://bandit.readthedocs.io/" target="blank"><img src="https://raw.githubusercontent.com/pycqa/bandit/main/logo/logo.svg" height="100" alt="bandit logo" /></a>
|
||||
<a href="https://docs.pytest.org/" target="blank"><img src="https://raw.githubusercontent.com/pytest-dev/pytest/main/doc/en/img/pytest_logo_curves.svg" height="100" alt="pytest logo" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.docker.com/" target="blank"><img src="https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png" height="60" alt="Docker logo" /></a>
|
||||
<a href="https://github.com/features/actions" target="blank"><img src="https://avatars.githubusercontent.com/u/44036562" height="60" alt="GitHub Actions logo" /></a>
|
||||
</p>
|
||||
|
||||
# GRVT Python SDK
|
||||
|
||||
[](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml)
|
||||
[](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml)
|
||||
[](https://github.com/smarlhens/python-boilerplate)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [GRVT Python SDK](#grvt-python-sdk)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What's in the library](#whats-in-the-library)
|
||||
- [Environments](#environments)
|
||||
- [Files](#files)
|
||||
- [Installation via pip](#installation-via-pip)
|
||||
- [Configuration](#configuration)
|
||||
- [Usage](#usage)
|
||||
- [Contributor's guide](#contributors-guide)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation of a source code](#installation-of-a-source-code)
|
||||
- [Manually run example files](#manually-run-example-files)
|
||||
- [What's in the box ?](#whats-in-the-box-)
|
||||
- [uv](#uv)
|
||||
- [pre-commit](#pre-commit)
|
||||
- [ruff](#ruff)
|
||||
- [mypy](#mypy)
|
||||
- [bandit](#bandit)
|
||||
- [docformatter](#docformatter)
|
||||
- [Testing](#testing)
|
||||
- [Makefile](#makefile)
|
||||
|
||||
---
|
||||
|
||||
## What's in the library
|
||||
|
||||
GRVT Python SDK library provides Python classes and utility methods for easy access to GRVT API endpoints across all environments.
|
||||
|
||||
### Environments
|
||||
|
||||
- `prod` - Production environment.
|
||||
- `testnet` - Testnet environment.
|
||||
- `staging` - Development Integration environment.
|
||||
- `dev` - Development environment.
|
||||
|
||||
SDK Library provides two types of classes:
|
||||
|
||||
1. **Raw** - thin wrapper classes around Rest API.
|
||||
2. **Ccxt-compatible** - classes with ccxt-like methods. Provide access to both Rest API and WebSockets.
|
||||
|
||||
### Files
|
||||
|
||||
Raw access:
|
||||
|
||||
- `grvt_raw_base.py` - base classes for Rest API access.
|
||||
- `grvt_raw_env.py` - definitions of environments for raw access.
|
||||
- `grvt_raw_signing.py` - utility methods for signing orders.
|
||||
- `grvt_raw_sync.py` - class for raw synchronous calls to Rest API.
|
||||
- `grvt_raw_async.py` - class for raw asynchronous calls to Rest API.
|
||||
|
||||
CCXT-compatible access:
|
||||
|
||||
- `grvt_ccxt_utils.py` - utility methods for signing orders.
|
||||
- `grvt_ccxt_env.py` - definitions of environments for ccxt-like access.
|
||||
- `grvt_ccxt_base.py` - base class for Rest API access.
|
||||
- `grvt_ccxt.py` - class for synchronous calls to Rest API.
|
||||
- `grvt_ccxt_pro.py` - class for asynchronous calls to Rest API.
|
||||
- `grvt_ccxt_ws.py` - class for WebSocket calls.
|
||||
|
||||
## Installation via pip
|
||||
|
||||
```bash
|
||||
pip install grvt-pysdk
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Setup these environment variables:
|
||||
|
||||
```bash
|
||||
export GRVT_PRIVATE_KEY="`Secret Private Key` in API setup"
|
||||
export GRVT_API_KEY="`API Key` in API setup"
|
||||
export GRVT_TRADING_ACCOUNT_ID=<`Trading account ID` in API>
|
||||
export GRVT_ENV="testnet"
|
||||
export GRVT_END_POINT_VERSION="v1"
|
||||
export GRVT_WS_STREAM_VERSION="v1"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**Examples of how to use various methods to connect to GRVT API:**
|
||||
|
||||
- [GRVT CCXT](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt.py) - Example of usage of CCXT-compatible Python class `GrvtCcxt` with `synchronous` Rest API calls.
|
||||
- [GRVT CCXT Pro](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_pro.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtPro` with `asynchronous` Rest API calls.
|
||||
- [GRVT CCXT WS](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_ws.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtWS` with `asynchronous` Rest API calls + Web Socket subscriptions + JSON RPC calls over Web Sockets.
|
||||
- [GRVT API Sync](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_sync.py) - Synchronous API client for GRVT
|
||||
- [GRVT API Async](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_async.py) - Asynchronous API client for GRVT
|
||||
|
||||
## Contributor's guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Python](https://www.python.org/downloads/) **>=3.10.0 < 3.13** (_tested with 3.10.15_)
|
||||
- [pre-commit](https://pre-commit.com/#install)
|
||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/) **>=0.3.3** (_tested with 0.4.0_)
|
||||
- [docker](https://docs.docker.com/get-docker/) (_optional_)
|
||||
|
||||
### Installation of a source code
|
||||
|
||||
1. Clone the git repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/grvt-technologies/grvt-pysdk.git
|
||||
```
|
||||
|
||||
2. Go into the project directory
|
||||
|
||||
```bash
|
||||
cd grvt-pysdk/
|
||||
```
|
||||
|
||||
3. Checkout working branch
|
||||
|
||||
```bash
|
||||
git checkout <branch>
|
||||
```
|
||||
|
||||
4. Install dependencies
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
5. Enable pre-commit hooks
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Manually run example files
|
||||
|
||||
Example files run extensive testing of the SDK API classes and log details about details of using GRVT API.
|
||||
|
||||
1. Change to tests folder
|
||||
|
||||
```bash
|
||||
cd tests/pysdk
|
||||
```
|
||||
|
||||
2. Run example of using synchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt.py`, class `GrvtCcxt`
|
||||
|
||||
```bash
|
||||
uv run python3 test_grvt_ccxt.py
|
||||
```
|
||||
|
||||
3. Run example of using asynchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt_pro.py`, class `GrvtCcxtPro`
|
||||
|
||||
```bash
|
||||
uv run python3 test_grvt_ccxt_pro.py
|
||||
```
|
||||
|
||||
4. Run example of using WebSockets subscriptions and JSON RPC calls via `grvt_ccxt_ws.py`, class `GrvtCcxtWS`
|
||||
|
||||
```bash
|
||||
uv run python3 test_grvt_ccxt_ws.py
|
||||
```
|
||||
|
||||
### What's in the box ?
|
||||
|
||||
#### uv
|
||||
|
||||
[uv](https://github.com/astral-sh/uv) is an extremely fast Python package and project manager, written in Rust.
|
||||
|
||||
**pyproject.toml file** ([`pyproject.toml`](pyproject.toml)): orchestrate your project and its dependencies
|
||||
**uv.lock file** ([`uv.lock`](uv.lock)): ensure that the package versions are consistent for everyone
|
||||
working on your project
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://docs.astral.sh/uv/).
|
||||
|
||||
#### pre-commit
|
||||
|
||||
[pre-commit](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks.
|
||||
|
||||
**.pre-commit-config.yaml file** ([`.pre-commit-config.yaml`](.pre-commit-config.yaml)): describes what repositories and
|
||||
hooks are installed
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://pre-commit.com/).
|
||||
|
||||
#### ruff
|
||||
|
||||
[ruff](https://github.com/astral-sh/ruff) is an extremely fast Python linter, written in Rust.
|
||||
|
||||
Rules are defined in the [`pyproject.toml`](pyproject.toml).
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://github.com/astral-sh/ruff#configuration).
|
||||
|
||||
#### mypy
|
||||
|
||||
[mypy](http://mypy-lang.org/) is an optional static type checker for Python that aims to combine the benefits of
|
||||
dynamic (or "duck") typing and static typing.
|
||||
|
||||
Rules are defined in the [`pyproject.toml`](pyproject.toml).
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://mypy.readthedocs.io/).
|
||||
|
||||
#### bandit
|
||||
|
||||
[bandit](https://bandit.readthedocs.io/) is a tool designed to find common security issues in Python code.
|
||||
|
||||
Rules are defined in the [`pyproject.toml`](pyproject.toml).
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://bandit.readthedocs.io/).
|
||||
|
||||
#### docformatter
|
||||
|
||||
[docformatter](https://github.com/PyCQA/docformatter) is a tool designed to format docstrings to
|
||||
follow [PEP 257](https://peps.python.org/pep-0257/).
|
||||
|
||||
Options are defined in the [`.pre-commit-config.yaml`](.pre-commit-config.yaml).
|
||||
|
||||
---
|
||||
|
||||
#### Testing
|
||||
|
||||
We are using [pytest](https://docs.pytest.org/) & [pytest-cov](https://github.com/pytest-dev/pytest-cov) to write tests.
|
||||
|
||||
To run tests with coverage:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
```text
|
||||
Name Stmts Miss Cover
|
||||
-------------------------------------------------------------
|
||||
src/pysdk/__init__.py 0 0 100%
|
||||
src/pysdk/grvt_ccxt.py 238 21 91%
|
||||
src/pysdk/grvt_ccxt_base.py 196 57 71%
|
||||
src/pysdk/grvt_ccxt_env.py 48 12 75%
|
||||
src/pysdk/grvt_ccxt_logging_selector.py 15 7 53%
|
||||
src/pysdk/grvt_ccxt_pro.py 245 69 72%
|
||||
src/pysdk/grvt_ccxt_test_utils.py 30 4 87%
|
||||
src/pysdk/grvt_ccxt_types.py 41 0 100%
|
||||
src/pysdk/grvt_ccxt_utils.py 237 89 62%
|
||||
src/pysdk/grvt_ccxt_ws.py 275 238 13%
|
||||
src/pysdk/grvt_raw_async.py 149 109 27%
|
||||
src/pysdk/grvt_raw_base.py 154 69 55%
|
||||
src/pysdk/grvt_raw_env.py 27 5 81%
|
||||
src/pysdk/grvt_raw_signing.py 33 17 48%
|
||||
src/pysdk/grvt_raw_sync.py 149 109 27%
|
||||
src/pysdk/grvt_raw_types.py 1031 0 100%
|
||||
-------------------------------------------------------------
|
||||
TOTAL 2868 806 72%
|
||||
|
||||
|
||||
=================================================================================================== 8 passed in 58.20s ====================================================================================================
|
||||
```
|
||||
|
||||
#### Makefile
|
||||
|
||||
We are using [Makefile](https://www.gnu.org/software/make/manual/make.html) to manage the project.
|
||||
|
||||
To see the list of commands:
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
```bash
|
||||
➜ grvt-pysdk git:(main) ✗ make
|
||||
help Show this help
|
||||
run Run the project
|
||||
test Run the tests
|
||||
precommit Run the pre-commit hooks
|
||||
lint Run the linter
|
||||
format Run the formatter
|
||||
typecheck Run the type checker
|
||||
security Run the security checker
|
||||
clean Clean the project
|
||||
install Install the project
|
||||
build Build the project
|
||||
publish Publish the project
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1,382 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/astral-sh/uv" target="blank"><img src="https://github.com/astral-sh/uv/blob/8674968a17e5f2ee0dda01d17aaf609f162939ca/docs/assets/logo-letter.svg" height="100" alt="uv logo" /></a>
|
||||
<a href="https://pre-commit.com/" target="blank"><img src="https://pre-commit.com/logo.svg" height="100" alt="pre-commit logo" /></a>
|
||||
<a href="https://github.com/astral-sh/ruff" target="blank"><img src="https://raw.githubusercontent.com/astral-sh/ruff/8c20f14e62ddaf7b6d62674f300f5d19cbdc5acb/docs/assets/bolt.svg" height="100" alt="ruff logo" style="background-color: #ef5552" /></a>
|
||||
<a href="https://bandit.readthedocs.io/" target="blank"><img src="https://raw.githubusercontent.com/pycqa/bandit/main/logo/logo.svg" height="100" alt="bandit logo" /></a>
|
||||
<a href="https://docs.pytest.org/" target="blank"><img src="https://raw.githubusercontent.com/pytest-dev/pytest/main/doc/en/img/pytest_logo_curves.svg" height="100" alt="pytest logo" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.docker.com/" target="blank"><img src="https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png" height="60" alt="Docker logo" /></a>
|
||||
<a href="https://github.com/features/actions" target="blank"><img src="https://avatars.githubusercontent.com/u/44036562" height="60" alt="GitHub Actions logo" /></a>
|
||||
</p>
|
||||
|
||||
# GRVT Python SDK
|
||||
|
||||
[](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml)
|
||||
[](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml)
|
||||
[](https://github.com/smarlhens/python-boilerplate)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [GRVT Python SDK](#grvt-python-sdk)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What's in the library](#whats-in-the-library)
|
||||
- [Environments](#environments)
|
||||
- [Files](#files)
|
||||
- [Installation via pip](#installation-via-pip)
|
||||
- [Configuration](#configuration)
|
||||
- [Usage](#usage)
|
||||
- [Contributor's guide](#contributors-guide)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation of a source code](#installation-of-a-source-code)
|
||||
- [Manually run example files](#manually-run-example-files)
|
||||
- [What's in the box ?](#whats-in-the-box-)
|
||||
- [uv](#uv)
|
||||
- [pre-commit](#pre-commit)
|
||||
- [ruff](#ruff)
|
||||
- [mypy](#mypy)
|
||||
- [bandit](#bandit)
|
||||
- [docformatter](#docformatter)
|
||||
- [Testing](#testing)
|
||||
- [Makefile](#makefile)
|
||||
|
||||
---
|
||||
|
||||
## What's in the library
|
||||
|
||||
GRVT Python SDK library provides Python classes and utility methods for easy access to GRVT API endpoints across all environments.
|
||||
|
||||
### Environments
|
||||
|
||||
- `prod` - Production environment.
|
||||
- `testnet` - Testnet environment.
|
||||
- `staging` - Development Integration environment.
|
||||
- `dev` - Development environment.
|
||||
|
||||
SDK Library provides two types of classes:
|
||||
|
||||
1. **Raw** - thin wrapper classes around Rest API.
|
||||
2. **Ccxt-compatible** - classes with ccxt-like methods. Provide access to both Rest API and WebSockets.
|
||||
|
||||
### Files
|
||||
|
||||
Raw access:
|
||||
|
||||
- `grvt_raw_base.py` - base classes for Rest API access.
|
||||
- `grvt_raw_env.py` - definitions of environments for raw access.
|
||||
- `grvt_raw_signing.py` - utility methods for signing orders.
|
||||
- `grvt_raw_sync.py` - class for raw synchronous calls to Rest API.
|
||||
- `grvt_raw_async.py` - class for raw asynchronous calls to Rest API.
|
||||
|
||||
CCXT-compatible access:
|
||||
|
||||
- `grvt_ccxt_utils.py` - utility methods for signing orders.
|
||||
- `grvt_ccxt_env.py` - definitions of environments for ccxt-like access.
|
||||
- `grvt_ccxt_base.py` - base class for Rest API access.
|
||||
- `grvt_ccxt.py` - class for synchronous calls to Rest API.
|
||||
- `grvt_ccxt_pro.py` - class for asynchronous calls to Rest API.
|
||||
- `grvt_ccxt_ws.py` - class for WebSocket calls.
|
||||
|
||||
## Installation via pip
|
||||
|
||||
```bash
|
||||
pip install grvt-pysdk
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Setup these environment variables:
|
||||
|
||||
```bash
|
||||
export GRVT_PRIVATE_KEY="`Secret Private Key` in API setup"
|
||||
export GRVT_API_KEY="`API Key` in API setup"
|
||||
export GRVT_TRADING_ACCOUNT_ID=<`Trading account ID` in API>
|
||||
export GRVT_ENV="testnet"
|
||||
export GRVT_END_POINT_VERSION="v1"
|
||||
export GRVT_WS_STREAM_VERSION="v1"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**Examples of how to use various methods to connect to GRVT API:**
|
||||
|
||||
- [GRVT CCXT](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt.py) - Example of usage of CCXT-compatible Python class `GrvtCcxt` with `synchronous` Rest API calls.
|
||||
- [GRVT CCXT Pro](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_pro.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtPro` with `asynchronous` Rest API calls.
|
||||
- [GRVT CCXT WS](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_ws.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtWS` with `asynchronous` Rest API calls + Web Socket subscriptions + JSON RPC calls over Web Sockets.
|
||||
- [GRVT API Sync](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_sync.py) - Synchronous API client for GRVT
|
||||
- [GRVT API Async](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_async.py) - Asynchronous API client for GRVT
|
||||
|
||||
## Contributor's guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Python](https://www.python.org/downloads/) **>=3.10.0 < 3.13** (_tested with 3.10.15_)
|
||||
- [pre-commit](https://pre-commit.com/#install)
|
||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/) **>=0.3.3** (_tested with 0.4.0_)
|
||||
- [docker](https://docs.docker.com/get-docker/) (_optional_)
|
||||
|
||||
### Installation of a source code
|
||||
|
||||
1. Clone the git repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/grvt-technologies/grvt-pysdk.git
|
||||
```
|
||||
|
||||
2. Go into the project directory
|
||||
|
||||
```bash
|
||||
cd grvt-pysdk/
|
||||
```
|
||||
|
||||
3. Checkout working branch
|
||||
|
||||
```bash
|
||||
git checkout <branch>
|
||||
```
|
||||
|
||||
4. Install dependencies
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
5. Enable pre-commit hooks
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Manually run example files
|
||||
|
||||
Example files run extensive testing of the SDK API classes and log details about details of using GRVT API.
|
||||
|
||||
1. Change to tests folder
|
||||
|
||||
```bash
|
||||
cd tests/pysdk
|
||||
```
|
||||
|
||||
2. Run example of using synchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt.py`, class `GrvtCcxt`
|
||||
|
||||
```bash
|
||||
uv run python3 test_grvt_ccxt.py
|
||||
```
|
||||
|
||||
3. Run example of using asynchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt_pro.py`, class `GrvtCcxtPro`
|
||||
|
||||
```bash
|
||||
uv run python3 test_grvt_ccxt_pro.py
|
||||
```
|
||||
|
||||
4. Run example of using WebSockets subscriptions and JSON RPC calls via `grvt_ccxt_ws.py`, class `GrvtCcxtWS`
|
||||
|
||||
```bash
|
||||
uv run python3 test_grvt_ccxt_ws.py
|
||||
```
|
||||
|
||||
### What's in the box ?
|
||||
|
||||
#### uv
|
||||
|
||||
[uv](https://github.com/astral-sh/uv) is an extremely fast Python package and project manager, written in Rust.
|
||||
|
||||
**pyproject.toml file** ([`pyproject.toml`](pyproject.toml)): orchestrate your project and its dependencies
|
||||
**uv.lock file** ([`uv.lock`](uv.lock)): ensure that the package versions are consistent for everyone
|
||||
working on your project
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://docs.astral.sh/uv/).
|
||||
|
||||
#### pre-commit
|
||||
|
||||
[pre-commit](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks.
|
||||
|
||||
**.pre-commit-config.yaml file** ([`.pre-commit-config.yaml`](.pre-commit-config.yaml)): describes what repositories and
|
||||
hooks are installed
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://pre-commit.com/).
|
||||
|
||||
#### ruff
|
||||
|
||||
[ruff](https://github.com/astral-sh/ruff) is an extremely fast Python linter, written in Rust.
|
||||
|
||||
Rules are defined in the [`pyproject.toml`](pyproject.toml).
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://github.com/astral-sh/ruff#configuration).
|
||||
|
||||
#### mypy
|
||||
|
||||
[mypy](http://mypy-lang.org/) is an optional static type checker for Python that aims to combine the benefits of
|
||||
dynamic (or "duck") typing and static typing.
|
||||
|
||||
Rules are defined in the [`pyproject.toml`](pyproject.toml).
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://mypy.readthedocs.io/).
|
||||
|
||||
#### bandit
|
||||
|
||||
[bandit](https://bandit.readthedocs.io/) is a tool designed to find common security issues in Python code.
|
||||
|
||||
Rules are defined in the [`pyproject.toml`](pyproject.toml).
|
||||
|
||||
For more configuration options and details, see the [configuration docs](https://bandit.readthedocs.io/).
|
||||
|
||||
#### docformatter
|
||||
|
||||
[docformatter](https://github.com/PyCQA/docformatter) is a tool designed to format docstrings to
|
||||
follow [PEP 257](https://peps.python.org/pep-0257/).
|
||||
|
||||
Options are defined in the [`.pre-commit-config.yaml`](.pre-commit-config.yaml).
|
||||
|
||||
---
|
||||
|
||||
#### Testing
|
||||
|
||||
We are using [pytest](https://docs.pytest.org/) & [pytest-cov](https://github.com/pytest-dev/pytest-cov) to write tests.
|
||||
|
||||
To run tests with coverage:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
```text
|
||||
Name Stmts Miss Cover
|
||||
-------------------------------------------------------------
|
||||
src/pysdk/__init__.py 0 0 100%
|
||||
src/pysdk/grvt_ccxt.py 238 21 91%
|
||||
src/pysdk/grvt_ccxt_base.py 196 57 71%
|
||||
src/pysdk/grvt_ccxt_env.py 48 12 75%
|
||||
src/pysdk/grvt_ccxt_logging_selector.py 15 7 53%
|
||||
src/pysdk/grvt_ccxt_pro.py 245 69 72%
|
||||
src/pysdk/grvt_ccxt_test_utils.py 30 4 87%
|
||||
src/pysdk/grvt_ccxt_types.py 41 0 100%
|
||||
src/pysdk/grvt_ccxt_utils.py 237 89 62%
|
||||
src/pysdk/grvt_ccxt_ws.py 275 238 13%
|
||||
src/pysdk/grvt_raw_async.py 149 109 27%
|
||||
src/pysdk/grvt_raw_base.py 154 69 55%
|
||||
src/pysdk/grvt_raw_env.py 27 5 81%
|
||||
src/pysdk/grvt_raw_signing.py 33 17 48%
|
||||
src/pysdk/grvt_raw_sync.py 149 109 27%
|
||||
src/pysdk/grvt_raw_types.py 1031 0 100%
|
||||
-------------------------------------------------------------
|
||||
TOTAL 2868 806 72%
|
||||
|
||||
|
||||
=================================================================================================== 8 passed in 58.20s ====================================================================================================
|
||||
```
|
||||
|
||||
#### Makefile
|
||||
|
||||
We are using [Makefile](https://www.gnu.org/software/make/manual/make.html) to manage the project.
|
||||
|
||||
To see the list of commands:
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
```bash
|
||||
➜ grvt-pysdk git:(main) ✗ make
|
||||
help Show this help
|
||||
run Run the project
|
||||
test Run the tests
|
||||
precommit Run the pre-commit hooks
|
||||
lint Run the linter
|
||||
format Run the formatter
|
||||
typecheck Run the type checker
|
||||
security Run the security checker
|
||||
clean Clean the project
|
||||
install Install the project
|
||||
build Build the project
|
||||
publish Publish the project
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Changelog
|
||||
|
||||
# Changelog
|
||||
|
||||
## Version [0.2.1] - 2025-07-24
|
||||
|
||||
### Changes in 0.2.1
|
||||
|
||||
- New methods in ccxt-compatible code for Strategy (Vault) managers to view investment history and current redemption queue of a Vault.
|
||||
- Classes `GrvtCcxt` and `GrvtCcxtPro`
|
||||
- Methods `fetch_vault_manager_investor_history` and `fetch_vault_redemption_queue`
|
||||
|
||||
## Version [0.2.0] - 2025-07-16
|
||||
|
||||
### Changes in 0.2.0
|
||||
|
||||
- additional fixes for removal of explicit enums in `grvt_raw_types.py`
|
||||
- Note: this update would `break existing integrations` for `non-create-order flows` (e.g., transfer/withdrawal history, transfer, get open orders, get-instrument filters) to handle currency strings directly instead of enums.
|
||||
- users will be losing the currency enum when they upgrade, so they would need to call the currencies endpoin (<https://api-docs.grvt.io/market_data_api/#get-currency> ), for which the support has been added in PySDK (`get_currency_v1()` in grvt_raw_async.py and grvt_raw_sync.py), for the metadata attached to the currency strings.
|
||||
|
||||
## Version [0.1.32] - 2025-07-15
|
||||
|
||||
### Changes in 0.1.32
|
||||
|
||||
- removed explicit enums in `grvt_raw_types.py` (no need to update SDK for when new coins are listed)
|
||||
- added vault-related functionality
|
||||
|
||||
## Version [0.1.31] - 2025-07-08
|
||||
|
||||
### Changes in 0.1.31
|
||||
|
||||
- added AVAX in the raw code (testing purposes)
|
||||
|
||||
## Version [0.1.30] - 2025-07-08
|
||||
|
||||
### Changes in 0.1.30
|
||||
|
||||
- added `H` in the **raw** code
|
||||
|
||||
## Version [0.1.29] - 2025-06-30
|
||||
|
||||
### Changes in 0.1.29
|
||||
|
||||
- Added new currencies: `HYPE, UNI, MOODENG, LAUNCHCOIN` in the **raw** code
|
||||
- Improvements and type fixes in `fetch_funding_rate_history()` methods of GrvtCcxt and GrvtCcxtPro.
|
||||
|
||||
## Version [0.1.28] - 2025-06-02
|
||||
|
||||
### Changes in 0.1.28
|
||||
|
||||
- Renamed currency name `AI_16_Z` into `AI16Z` in the **raw** code to match the exchange currency name.
|
||||
|
||||
## Version [0.1.27] - 2025-05-19
|
||||
|
||||
### Changes in 0.1.27
|
||||
|
||||
- `GrvtCcxt` and `GrvtCcxtPro` classes:
|
||||
|
||||
- renamed method `fetch_balances()` to `fetch_balance()` as defined in ccxt.
|
||||
|
||||
## Version [0.1.26] - 2025-05-15
|
||||
|
||||
### Added in 0.1.26
|
||||
|
||||
- `GrvtCcxt` and `GrvtCcxtPro` classes:
|
||||
|
||||
- new method `describe()` - returns a list of public method names
|
||||
- new method `fetch_balances()` - returns dict with balances in ccxt format.
|
||||
- constructor parameter `order_book_ccxt_format: bool = False` . If = True then order book snapshots from `fetch_order_book()` are in ccxt format.
|
||||
|
||||
### Fixed in 0.1.26
|
||||
|
||||
- Issues with typing and lynting errors
|
||||
|
||||
## Version [0.1.25] - 2025-04-25
|
||||
|
||||
### Fixed in 0.1.25
|
||||
|
||||
- Issues with typing and lynting errors
|
||||
- Fixed bug in test_grvt_ccxt.py
|
||||
@@ -1,9 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
readme = Path("README.md").read_text()
|
||||
changelog = Path("CHANGELOG.md").read_text()
|
||||
|
||||
combined = f"{readme}\n\n## Changelog\n\n{changelog}"
|
||||
|
||||
Path("README_PYPI.md").write_text(combined)
|
||||
print("✅ Combined README.md and CHANGELOG.md into README_PYPI.md")
|
||||
@@ -1,106 +0,0 @@
|
||||
[project]
|
||||
name = "grvt-pysdk"
|
||||
version = "0.2.1"
|
||||
|
||||
description = "GRVT Python SDK"
|
||||
requires-python = ">=3.10"
|
||||
license = { file = "LICENSE" }
|
||||
authors = [
|
||||
{ name = "GRVT", email = "contact@grvt.io" },
|
||||
]
|
||||
readme = { file = "README_PYPI.md", content-type = "text/markdown" }
|
||||
dependencies = [
|
||||
"aiohttp>=3.10.11",
|
||||
"backports-weakref>=1.0.post1",
|
||||
"dacite>=1.8.1",
|
||||
"dataclasses-json>=0.6.7",
|
||||
"eth-account>=0.13.4",
|
||||
"inflection>=0.5.1",
|
||||
"requests>=2.32.3",
|
||||
"websockets==13.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/gravity-technologies/grvt-pysdk"
|
||||
repository = "https://github.com/gravity-technologies/grvt-pysdk"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/pysdk"]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pytest>=8.3.2",
|
||||
"pytest-cov>=5.0.0",
|
||||
"mypy>=1.11.2",
|
||||
"bandit>=1.7.9",
|
||||
"docformatter>=1.7.5",
|
||||
"ruff>=0.6.2",
|
||||
"twine>=5.1.1",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-vvv"
|
||||
testpaths = "tests"
|
||||
|
||||
[tool.ruff]
|
||||
extend-exclude = [
|
||||
"__pycache__",
|
||||
"build",
|
||||
"dist",
|
||||
]
|
||||
target-version = "py312"
|
||||
line-length = 90
|
||||
src = ["src", "tests"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = [
|
||||
"C4",
|
||||
"D200",
|
||||
"D201",
|
||||
"D204",
|
||||
"D205",
|
||||
"D206",
|
||||
"D210",
|
||||
"D211",
|
||||
"D213",
|
||||
"D300",
|
||||
"D400",
|
||||
"D402",
|
||||
"D403",
|
||||
"D404",
|
||||
"D419",
|
||||
"E",
|
||||
"F",
|
||||
"G010",
|
||||
"I001",
|
||||
"INP001",
|
||||
"N805",
|
||||
"PERF101",
|
||||
"PERF102",
|
||||
"PERF401",
|
||||
"PERF402",
|
||||
"PGH004",
|
||||
"PGH005",
|
||||
"PIE794",
|
||||
"PIE796",
|
||||
"PIE807",
|
||||
"PIE810",
|
||||
"RET502",
|
||||
"RET503",
|
||||
"RET504",
|
||||
"RET505",
|
||||
"RUF015",
|
||||
"RUF100",
|
||||
"S101",
|
||||
"T20",
|
||||
"UP",
|
||||
"W",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
files = ["src", "tests"]
|
||||
strict = "true"
|
||||
@@ -1,812 +0,0 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Literal
|
||||
|
||||
import requests
|
||||
|
||||
from .grvt_ccxt_base import GrvtCcxtBase
|
||||
from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint
|
||||
from .grvt_ccxt_types import (
|
||||
Amount,
|
||||
GrvtInstrumentKind,
|
||||
GrvtInvalidOrder,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
from .grvt_ccxt_utils import (
|
||||
EnumEncoder,
|
||||
GrvtOrder,
|
||||
get_cookie_with_expiration,
|
||||
get_grvt_order,
|
||||
get_order_payload,
|
||||
)
|
||||
|
||||
|
||||
class GrvtCcxt(GrvtCcxtBase):
|
||||
"""
|
||||
GrvtCcxt class to interact with Grvt Rest API in synchronous mode.
|
||||
|
||||
Args:
|
||||
env: GrvtEnv (DEV, TESTNET, PROD)
|
||||
logger: logging.Logger
|
||||
parameters: dict with trading_account_id, private_key, api_key etc
|
||||
|
||||
Examples:
|
||||
>>> from grvt_api import GrvtCcxt
|
||||
>>> from grvt_env import GrvtEnv
|
||||
>>> grvt = GrvtCcxt(env=GrvtEnv.TESTNET)
|
||||
>>> grvt.fetch_markets()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: GrvtEnv,
|
||||
logger: logging.Logger | None = None,
|
||||
parameters: dict = {},
|
||||
order_book_ccxt_format: bool = False,
|
||||
):
|
||||
"""Initialize the GrvtCcxt instance."""
|
||||
super().__init__(env, logger, parameters, order_book_ccxt_format)
|
||||
self._clsname: str = type(self).__name__
|
||||
self._session: requests.Session = requests.Session()
|
||||
self._session.headers.update({"Content-Type": "application/json"})
|
||||
self.refresh_cookie()
|
||||
# Assign markets here
|
||||
self.markets: dict[str, dict] = self.load_markets()
|
||||
|
||||
def refresh_cookie(self) -> dict | None:
|
||||
"""Refresh the session cookie."""
|
||||
if not self.should_refresh_cookie():
|
||||
return self._cookie
|
||||
path = get_grvt_endpoint(self.env, "AUTH")
|
||||
self._cookie = get_cookie_with_expiration(path, self._api_key)
|
||||
self._path_return_value_map[path] = self._cookie
|
||||
if self._cookie:
|
||||
self._session.cookies.update({"gravity": self._cookie["gravity"]})
|
||||
if self._cookie["X-Grvt-Account-Id"]:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
|
||||
)
|
||||
self.logger.info(
|
||||
f"refresh_cookie {self._cookie=} {self._session.cookies=} {self._session.headers=}"
|
||||
)
|
||||
return self._cookie
|
||||
|
||||
# PRIVATE API CALLS
|
||||
def _auth_and_post(self, path: str, payload: dict) -> dict:
|
||||
FN = f"_auth_and_post {path=}"
|
||||
MAX_LEN_TO_LOG = 1280
|
||||
response: dict = {}
|
||||
if not path:
|
||||
self.logger.warning(f"{FN} Invalid path {path=} {payload=}")
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}")
|
||||
# Always see if need to referesh cookie before sending a request
|
||||
self.refresh_cookie()
|
||||
payload_json = json.dumps(payload, cls=EnumEncoder)
|
||||
self.logger.info(f"{FN} {payload=}\n{payload_json=}")
|
||||
return_value = self._session.post(path, data=payload_json, timeout=5)
|
||||
return_text: str = ""
|
||||
try:
|
||||
return_text = return_value.text
|
||||
response = return_value.json()
|
||||
except Exception as err:
|
||||
self.logger.warning(f"{FN} Unable to parse {return_value=} as json. {err=}")
|
||||
if not return_value.ok:
|
||||
self.logger.warning(f"{FN} ERROR {payload_json=}\n{return_value=}\n{response=}")
|
||||
else:
|
||||
if len(return_text) > MAX_LEN_TO_LOG:
|
||||
self.logger.debug(f"{FN} OK {return_value=} {response=}")
|
||||
self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**")
|
||||
else:
|
||||
self.logger.info(f"{FN} OK {return_value=} {response=}")
|
||||
self._path_return_value_map[path] = response
|
||||
return response
|
||||
|
||||
def _create_grvt_order(self, order: GrvtOrder) -> dict:
|
||||
"""
|
||||
Send a GrvtOrder object to the exchange.
|
||||
:param order: The GrvtOrder object.
|
||||
Return: dictionary representing the order response.
|
||||
"""
|
||||
FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}"
|
||||
order_payload = get_order_payload(
|
||||
order,
|
||||
private_key=self._private_key,
|
||||
env=self.env,
|
||||
instruments=self.markets,
|
||||
)
|
||||
path = get_grvt_endpoint(self.env, "CREATE_ORDER")
|
||||
self.logger.info(f"{FN} {path=} {order_payload=}")
|
||||
response: dict = self._auth_and_post(path, payload=order_payload)
|
||||
if response.get("result") is None:
|
||||
self.logger.error(f"{FN} Error: {response}")
|
||||
return {}
|
||||
self.logger.info(
|
||||
f"{FN} Order created:"
|
||||
f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}"
|
||||
)
|
||||
return response.get("result", {})
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""Ccxt compliant signature."""
|
||||
self._check_account_auth()
|
||||
self._check_valid_symbol(symbol)
|
||||
# Validate order fields
|
||||
self._check_order_arguments(order_type, side, amount, price)
|
||||
# create GrvtOrder object
|
||||
order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60)
|
||||
order = get_grvt_order(
|
||||
sub_account_id=self.get_trading_account_id(),
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side,
|
||||
amount=amount,
|
||||
limit_price=price,
|
||||
order_duration_secs=order_duration_secs,
|
||||
params=params,
|
||||
)
|
||||
return self._create_grvt_order(order)
|
||||
|
||||
def create_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
return self.create_order(symbol, "limit", side, amount, price, params)
|
||||
|
||||
def cancel_all_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature BUT lacks symbol
|
||||
Cancel all orders for a sub-account.
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
FN = f"{self._clsname} cancel_all_orders"
|
||||
payload: dict = self._get_payload_cancel_all_orders(params)
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel orders: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
def cancel_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Cancel specific order for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
id (str): exchange assigned order ID<br>
|
||||
symbol (str): trading symbol<br>
|
||||
params:
|
||||
* client_order_id (str): client assigned order ID<br>
|
||||
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
|
||||
Returns:
|
||||
True if cancel request was acked by exchange. False otherwise.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} cancel_order"
|
||||
self._check_account_auth()
|
||||
payload: dict = {
|
||||
"sub_account_id": self.get_trading_account_id(),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = str(id)
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
|
||||
|
||||
if "time_to_live_ms" in params:
|
||||
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
|
||||
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ORDER")
|
||||
self.logger.info(
|
||||
f"{FN} Send cancel {payload=} for trading_account_id={self.get_trading_account_id()}"
|
||||
)
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel order: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
def set_derisk_mm_ratio(self, ratio: str) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Set the Derisk to Maintenance marginb ratio for the account.
|
||||
Private call requires authorization.
|
||||
See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio)
|
||||
for details.
|
||||
|
||||
Args:
|
||||
ratio (Amount): The new derisking market making ratio.
|
||||
|
||||
Returns:
|
||||
True if the request was acknowledged by the exchange. False otherwise.
|
||||
"""
|
||||
FN = f"{self._clsname} set_derisk_mm_ratio"
|
||||
self._check_account_auth()
|
||||
payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio))
|
||||
path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO")
|
||||
self.logger.info(
|
||||
f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}"
|
||||
)
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
# set_ack = response.get("result", {}).get("ack")
|
||||
# if not set_ack:
|
||||
# self.logger.warning(f"{FN} failed to set derisk_mm_ratio: {response=}")
|
||||
# return False
|
||||
self.logger.info(f"{FN} Set derisk_mm_ratio {response=}")
|
||||
return True
|
||||
|
||||
def fetch_open_orders(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch open orders for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: (str) get orders for this symbol only.<br>
|
||||
since: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
limit: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
Returns:
|
||||
a list of dictionaries, each dict represent an order.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_open_orders(symbol, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
open_orders: list = response.get("result", [])
|
||||
if symbol:
|
||||
open_orders = [
|
||||
o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol
|
||||
]
|
||||
return open_orders
|
||||
|
||||
def fetch_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Get Order status by order_id or client_order_id
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>
|
||||
Args:
|
||||
id: (str) order_id to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`client_order_id` (int): client assigned order ID.<br>
|
||||
Return: dict with order's details or {} if order was NOT found.
|
||||
"""
|
||||
self._check_account_auth()
|
||||
payload = {
|
||||
"sub_account_id": self.get_trading_account_id(),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = id
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(
|
||||
f"{self._clsname} fetch_order() requires order_id or params['client_order_id']"
|
||||
)
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
def fetch_order_history(self, params: dict = {}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Get Order status by order_id or client_order_id
|
||||
Private call requires authorization.<br>
|
||||
See [Order History](https://api-docs.grvt.io/trading_api/#order-history)
|
||||
for details.<br>
|
||||
Args:
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
|
||||
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
|
||||
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
|
||||
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
|
||||
Return: a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent an order state.<br>.
|
||||
"""
|
||||
self._check_account_auth()
|
||||
payload = self._get_payload_fetch_order_history(params)
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
def get_account_summary(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Return: The account summary.
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
|
||||
for details.<br>
|
||||
Returns: dictionary with account data.<br>.
|
||||
"""
|
||||
FN = f"{self._clsname} get_account_summary {type=}"
|
||||
self._check_account_auth()
|
||||
payload = {}
|
||||
if type == "sub-account":
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY")
|
||||
payload = {"sub_account_id": self.get_trading_account_id()}
|
||||
elif type == "funding":
|
||||
path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY")
|
||||
elif type == "aggregated":
|
||||
path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY")
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}")
|
||||
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
sub_account: dict = response.get("result", {})
|
||||
if not sub_account:
|
||||
self.logger.info(f"{FN} No account summary for {path=} {payload=}")
|
||||
return sub_account
|
||||
|
||||
def fetch_balance(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch balances for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'.
|
||||
Valid values: 'sub-account', 'funding', 'aggregated'.
|
||||
|
||||
Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.<br>.
|
||||
"""
|
||||
account_summary: dict = self.get_account_summary(type)
|
||||
return self._get_balances_from_account_summary(account_summary)
|
||||
|
||||
def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict:
|
||||
"""
|
||||
HISTORICAL data.<br>
|
||||
Get account history.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account History](https://api-docs.grvt.io/trading_api/#account-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
limit: maximum number of account snapshots per page to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
|
||||
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
|
||||
`cursor` (str): cursor for the pagination. If cursor is present then we ignore
|
||||
`start_time` and `end_time`.<br>
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : list of account history snapshots.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_account_history(limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
def fetch_positions(self, symbols: list[str] = [], params={}):
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch positions for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Positions](https://api-docs.grvt.io/trading_api/#positions)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbols: list(str) get positions for these symbols only.<br>
|
||||
|
||||
Returns: list of dictionaries, each dict represent a position.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_positions(symbols, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_POSITIONS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
positions: list = response.get("result", [])
|
||||
if symbols:
|
||||
self.logger.info(f"fetch_positions filter positions by {symbols=}")
|
||||
positions = [p for p in positions if p.get("instrument") in symbols]
|
||||
return positions
|
||||
|
||||
def fetch_my_trades(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Fetch past trades for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent a trade.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_my_trades(symbol, since, limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
if symbol:
|
||||
# filter result by symbol
|
||||
trades: list = response.get("result", [])
|
||||
trades = [t for t in trades if t.get("instrument") == symbol]
|
||||
response["result"] = trades
|
||||
return response
|
||||
|
||||
# **************** PUBLIC API CALLS
|
||||
def load_markets(self) -> dict[str, dict]:
|
||||
self.logger.info("load_markets START")
|
||||
instruments = self.fetch_markets(
|
||||
params={
|
||||
"kind": GrvtInstrumentKind.PERPETUAL,
|
||||
# "base": "BTC",
|
||||
# "quote": "USDT",
|
||||
}
|
||||
)
|
||||
if instruments:
|
||||
self.markets = {
|
||||
str(i.get("instrument", "")): i for i in instruments if i.get("instrument")
|
||||
}
|
||||
self.logger.info(f"load_markets: loaded {len(self.markets)} markets.")
|
||||
else:
|
||||
self.logger.warning("load_markets: No markets found.")
|
||||
return self.markets
|
||||
|
||||
def fetch_markets(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Retrieve the list of all instruments of matching kind, base and quote
|
||||
supported by the exchange.
|
||||
|
||||
Params: dict with keys:<br>
|
||||
`is_active` (bool) - defaults to True.<br>
|
||||
`limit` (int) - defaiults to 20.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns: list of dictionaries per instrument with keys:<br>
|
||||
`instrument`: symbol e.g. 'BTC_USDT_Perp'.<br>
|
||||
`instrument_hash`: hashed symbol for order signing e.g. '0x030501'.<br>
|
||||
`base`: base currency e.g. 'BTC'.<br>
|
||||
`quote`: quote currency e.g. 'USDT'.<br>
|
||||
`kind`: kind of instrument 'PERPETUAL'/'FUTURE'.<br>
|
||||
'base_decimals': size multiplier for order signing.<br>
|
||||
`tick_size`: price tick size.<br>
|
||||
`min_size`: minimum order size.<br>
|
||||
"""
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_markets(params)
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_all_markets(
|
||||
self,
|
||||
is_active: bool | None = True,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Retrieve the list of all instruments supported by the exchange.<br>
|
||||
Params:<br>
|
||||
`is_active` (bool) - defaults to True.<br>.
|
||||
|
||||
Returns: list of dictionaries per instrument. See fetch_markets().<br>
|
||||
"""
|
||||
# Prepare request payload
|
||||
payload = {"is_active": is_active}
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_market(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the instrument object for a given symbol.
|
||||
:param symbol: The symbol of the instrument.
|
||||
"""
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENT")
|
||||
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_ticker(self, symbol: str, params: dict = {}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Retrieve the ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '592737000', 'best_ask_size': '21678', 'funding_rate_curr': 2544, 'funding_rate_avg': 0,
|
||||
# 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000',
|
||||
# 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500',
|
||||
# 'sell_volume_q': '68764000329900', 'high_price': '3435450000', 'low_price': '100000',
|
||||
# 'open_price': '32554000000000', 'open_interest': '8174350000000',
|
||||
# 'long_short_ratio': 1.0948905}
|
||||
path = get_grvt_endpoint(self.env, "GET_TICKER")
|
||||
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_mini_ticker(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the mini-ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The mini-ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '59273700000000', 'best_ask_size': '21678000000'}
|
||||
path = get_grvt_endpoint(self.env, "GET_MINI_TICKER")
|
||||
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Retrieve the order book of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The order book dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '0', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...]
|
||||
# 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...]
|
||||
payload = {"instrument": symbol, "aggregate": 1}
|
||||
if limit:
|
||||
payload["depth"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
if self.is_order_book_ccxt_format():
|
||||
# Convert to ccxt format
|
||||
return self.convert_grvt_ob_to_ccxt(response.get("result", {}))
|
||||
return response.get("result", {})
|
||||
|
||||
def fetch_recent_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
limit: int | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
Retrieve recent trades of a given instrument.
|
||||
:param symbol: The instrument name.
|
||||
:return: The list of trades for the instrument.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict[str, str | int] = {"instrument": symbol}
|
||||
if limit:
|
||||
payload["limit"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_TRADES")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int | None = None,
|
||||
limit: int = 10,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Retrieve trade history of a given instrument.
|
||||
:param symbol: The instrument name.
|
||||
:return: dict with field 'result' containing a list of trades.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict = self._get_payload_fetch_trades(
|
||||
symbol,
|
||||
since=since,
|
||||
limit=limit,
|
||||
params=params,
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
def fetch_funding_rate_history(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int = 0,
|
||||
limit: int = 1_000,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Retrieve the funding rates history of a given instrument.<br>
|
||||
Args:
|
||||
symbol (str): The instrument name.<br>
|
||||
since (int): fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: int - maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
Returns:
|
||||
dict with field 'result' containing list of dictionaries repesenting funding rate
|
||||
at a point in time with fields:<br>
|
||||
`instrument` (str): instrument name.<br>
|
||||
'funding_rate' (float): funding rate.<br>
|
||||
'funding_time' (int): funding time in nanoseconds.<br>
|
||||
'mark_price' (float): mark price.<br>.
|
||||
"""
|
||||
payload: dict[str, str | int] = {"instrument": symbol}
|
||||
if params.get("cursor"):
|
||||
payload["cursor"] = params["cursor"]
|
||||
else:
|
||||
if since:
|
||||
payload["start_time"] = str(since)
|
||||
if params.get("end_time"):
|
||||
payload["end_time"] = str(params["end_time"])
|
||||
if limit:
|
||||
payload["limit"] = int(limit)
|
||||
path = get_grvt_endpoint(self.env, "GET_FUNDING")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
def fetch_ohlcv(
|
||||
self,
|
||||
symbol: str,
|
||||
timeframe="1m",
|
||||
since: int = 0,
|
||||
limit: int = 10,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.
|
||||
|
||||
Retrieve the ohlc history of a given instrument.
|
||||
|
||||
Args:
|
||||
symbol: The instrument name.
|
||||
timeframe: The timeframe of the ohlc. See `ccxt_interval_to_grvt_candlestick_interval`.
|
||||
since: fetch ohlc since this timestamp in nanoseconds.
|
||||
limit: maximum number of ohlc to fetch.
|
||||
params: dictionary with parameters. Valid keys:
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.
|
||||
`end_time` (int): end time in nanoseconds.
|
||||
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.
|
||||
Returns:
|
||||
dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:<br>
|
||||
`instrument` - instrument name.<br>
|
||||
`open_time` - start of interval in nanoseconds.<br>
|
||||
`close_time` - end of interval in nanoseconds.<br>
|
||||
`open` - opening price.<br>
|
||||
`close` - closing price.<br>
|
||||
`high` - highest price.<br>
|
||||
`low` - lowest price.<br>
|
||||
`volume_u` - volume in units.<br>
|
||||
`volume_q` - volume in quote(USDT).<br>
|
||||
`trades` - number of trades.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} fetch_ohlcv"
|
||||
payload: dict[str, Any] = self._get_payload_fetch_ohlcv(
|
||||
symbol, timeframe, since, limit, params
|
||||
)
|
||||
self.logger.info(f"{FN} {payload=}")
|
||||
path = get_grvt_endpoint(self.env, "GET_CANDLESTICK")
|
||||
return self._auth_and_post(path, payload=payload)
|
||||
|
||||
# Vault Management APIs
|
||||
def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict:
|
||||
payload: dict = self._get_fetch_vault_manager_investor_history_payload(
|
||||
vault_id=self.get_trading_account_id(),
|
||||
only_own_investments=only_own_investments, # Default to False to fetch all investments
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY")
|
||||
# path = "https://trades.grvt.io/full/v1/vault_manager_investor_history"
|
||||
return self._auth_and_post(path, payload=payload)
|
||||
|
||||
def fetch_vault_redemption_queue(self):
|
||||
payload: dict = self._get_fetch_vault_redemption_queue_payload(
|
||||
vault_id=self.get_trading_account_id()
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE")
|
||||
# path = "https://trades.grvt.io/full/v1/vault_view_redemption_queue"
|
||||
return self._auth_and_post(path, payload=payload)
|
||||
@@ -1,586 +0,0 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, get_args
|
||||
|
||||
from .grvt_ccxt_env import GrvtEnv
|
||||
from .grvt_ccxt_types import (
|
||||
CandlestickInterval,
|
||||
CandlestickType,
|
||||
GrvtInvalidOrder,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
ccxt_interval_to_grvt_candlestick_interval,
|
||||
)
|
||||
from .grvt_ccxt_utils import get_kuq_from_symbol, sign_derisk_mm_ratio_request
|
||||
|
||||
# COOKIE_REFRESH_INTERVAL_SECS = 60 * 60 # 30 minutes
|
||||
|
||||
|
||||
class GrvtCcxtBase:
|
||||
"""
|
||||
GrvtCcxtBase is an abstract class for other Grvt Rest
|
||||
and WebSocket connectivity classes.
|
||||
|
||||
Args:
|
||||
env: GrvtCcxtBase (DEV, TESTNET, PROD)
|
||||
logger (logging.Logger, optional). Defaults to None.
|
||||
parameters: (dict, optional). Dict with trading_account_id, private_key, api_key etc
|
||||
defaults to empty.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: GrvtEnv,
|
||||
logger: logging.Logger | None = None,
|
||||
parameters: dict = {},
|
||||
order_book_ccxt_format: bool = False,
|
||||
):
|
||||
"""Initialize the GrvtCcxtBase part."""
|
||||
self.name: str = "GRVT"
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.env: GrvtEnv = env
|
||||
self._trading_account_id: str | None = parameters.get("trading_account_id")
|
||||
self._private_key: str = str(parameters.get("private_key", ""))
|
||||
self._api_key: str = str(parameters.get("api_key", ""))
|
||||
self._order_book_ccxt_format: bool = order_book_ccxt_format
|
||||
|
||||
self._path_return_value_map: dict = {}
|
||||
self._cookie: dict | None = None
|
||||
self.markets: dict = {}
|
||||
self._clsname: str = type(self).__name__
|
||||
self.logger.info(f"GrvtCcxtBase: {self.env=}, {self._trading_account_id=}")
|
||||
|
||||
def describe(self) -> list[str]:
|
||||
"""Returns the description of the class methods."""
|
||||
return [
|
||||
"create_order",
|
||||
"create_limit_order",
|
||||
"cancel_all_orders",
|
||||
"cancel_order",
|
||||
"fetch_balance",
|
||||
"fetch_open_orders",
|
||||
"fetch_order",
|
||||
"fetch_order_history",
|
||||
"get_account_summary",
|
||||
"fetch_account_history",
|
||||
"fetch_positions",
|
||||
"fetch_my_trades",
|
||||
"load_markets",
|
||||
"fetch_markets",
|
||||
"fetch_all_markets",
|
||||
"fetch_market",
|
||||
"fetch_ticker",
|
||||
"fetch_mini_ticker",
|
||||
"fetch_order_book",
|
||||
"fetch_recent_trades",
|
||||
"fetch_trades",
|
||||
"fetch_funding_rate_history",
|
||||
"fetch_ohlcv",
|
||||
]
|
||||
|
||||
def get_trading_account_id(self) -> str:
|
||||
"""Returns the trading account id."""
|
||||
return self._trading_account_id or ""
|
||||
|
||||
def is_order_book_ccxt_format(self) -> bool:
|
||||
"""Returns True if order book should be returned in CCXT format."""
|
||||
return self._order_book_ccxt_format
|
||||
|
||||
def should_refresh_cookie(self) -> bool:
|
||||
"""
|
||||
Retuns:
|
||||
True if this object has API key and the session cookie should be refreshed.
|
||||
False - otherwise.
|
||||
"""
|
||||
if not self._api_key:
|
||||
return False
|
||||
time_till_expiration = None
|
||||
if self._cookie and "expires" in self._cookie:
|
||||
time_till_expiration = self._cookie["expires"] - time.time()
|
||||
is_cookie_fresh = time_till_expiration is not None and time_till_expiration > 5
|
||||
if not is_cookie_fresh:
|
||||
self.logger.info(
|
||||
f"cookie should be refreshed {self._cookie=} now={time.time()}"
|
||||
f" {time_till_expiration=} secs"
|
||||
)
|
||||
return not is_cookie_fresh
|
||||
|
||||
def get_path_return_value_map(self) -> dict:
|
||||
"""Returns the path return value map."""
|
||||
return self._path_return_value_map
|
||||
|
||||
def get_endpoint_return_value(self, endpoint: str) -> dict:
|
||||
"""Returns the return value for the endpoint."""
|
||||
return self._path_return_value_map.get(endpoint, {})
|
||||
|
||||
def was_path_called(self, path: str) -> bool:
|
||||
"""Returns True if the path was called."""
|
||||
return path in self._path_return_value_map
|
||||
|
||||
# PRIVATE API CALLS
|
||||
|
||||
def _check_order_arguments(
|
||||
self, order_type: GrvtOrderType, side: GrvtOrderSide, amount: Num, price: Num
|
||||
) -> None:
|
||||
FN = f"{self._clsname} _check_order_arguments"
|
||||
if order_type not in get_args(GrvtOrderType):
|
||||
raise GrvtInvalidOrder(f"{FN}: order_type should be one of {get_args(GrvtOrderType)}")
|
||||
if side not in get_args(GrvtOrderSide):
|
||||
raise GrvtInvalidOrder(f"{FN}: side should be one of {get_args(GrvtOrderSide)}")
|
||||
if order_type == "limit":
|
||||
if price is None or Decimal(price) <= Decimal("0"):
|
||||
raise GrvtInvalidOrder(f"{FN}: requires a price argument for a limit order")
|
||||
elif order_type == "market":
|
||||
if price:
|
||||
raise GrvtInvalidOrder(
|
||||
f"{FN}: should not have a positive price argument for a market order"
|
||||
)
|
||||
if not amount or Decimal(amount) < Decimal("0"):
|
||||
raise GrvtInvalidOrder(f"{FN}: amount should be above 0")
|
||||
|
||||
def _check_account_auth(self) -> bool:
|
||||
if not self.get_trading_account_id():
|
||||
raise GrvtInvalidOrder(f"{self._clsname}: this action requires a trading_account_id")
|
||||
return True
|
||||
|
||||
def _check_valid_symbol(self, symbol: str) -> bool:
|
||||
if not self.markets:
|
||||
raise GrvtInvalidOrder(f"{self._clsname}: markets not loaded")
|
||||
market = self.markets.get(symbol)
|
||||
if not market:
|
||||
raise GrvtInvalidOrder(f"{self._clsname}: {symbol=} not found")
|
||||
return True
|
||||
|
||||
def _get_payload_cancel_all_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_order_history() method.<br>.
|
||||
|
||||
Args:
|
||||
params: (dict) with possible keys as:.<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
Returns: a dictionary with a payload for Rest API call to cancel all orders.<br>
|
||||
"""
|
||||
payload: dict[str, str | int | bool | list] = {
|
||||
"sub_account_id": str(self.get_trading_account_id())
|
||||
}
|
||||
|
||||
if "kind" in params:
|
||||
payload["kind"] = [params["kind"]]
|
||||
if "base" in params:
|
||||
payload["base"] = [params["base"]]
|
||||
if "quote" in params:
|
||||
payload["quote"] = [params["quote"]]
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_markets(self, params: dict) -> dict:
|
||||
payload: dict[str, str | int | bool | list] = {}
|
||||
if params.get("kind"):
|
||||
payload["kind"] = [params.get("kind")]
|
||||
if params.get("base"):
|
||||
payload["base"] = [params.get("base")]
|
||||
if params.get("quote"):
|
||||
payload["quote"] = [params.get("quote")]
|
||||
payload["limit"] = int(params.get("limit", 1_000))
|
||||
payload["is_active"] = bool(params.get("is_active", True))
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_my_trades(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_my_trades() method.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
`end_time` (int): fetch trades until this timestamp in nanoseconds.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with a payload for Rest API call to fetch trades.<br>
|
||||
"""
|
||||
payload: dict[str, str | int | list] = {
|
||||
"sub_account_id": str(self.get_trading_account_id())
|
||||
}
|
||||
if params.get("cursor"):
|
||||
payload["cursor"] = params["cursor"]
|
||||
else:
|
||||
if symbol:
|
||||
payload["instrument"] = symbol
|
||||
else:
|
||||
if "kind" in params:
|
||||
payload["kind"] = [params["kind"]]
|
||||
if "base" in params:
|
||||
payload["base"] = [params["base"]]
|
||||
if "quote" in params:
|
||||
payload["quote"] = [params["quote"]]
|
||||
if since:
|
||||
payload["start_time"] = str(since)
|
||||
if params.get("end_time"):
|
||||
payload["end_time"] = str(params["end_time"])
|
||||
if limit:
|
||||
payload["limit"] = int(limit)
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int | None = None,
|
||||
limit: int = 1_000,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_trades() method.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with a payload for Rest API call to fetch trades.<br>
|
||||
"""
|
||||
payload: dict[str, str | int] = {
|
||||
"sub_account_id": str(self.get_trading_account_id()),
|
||||
"instrument": symbol,
|
||||
}
|
||||
if params.get("cursor"):
|
||||
payload["cursor"] = params["cursor"]
|
||||
else:
|
||||
if since:
|
||||
payload["start_time"] = str(since)
|
||||
if params.get("end_time"):
|
||||
payload["end_time"] = str(params["end_time"])
|
||||
payload["limit"] = limit
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_account_history(
|
||||
self,
|
||||
# since: int | None = None,
|
||||
limit: int = 500,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_account_history() method.<br>.
|
||||
|
||||
Args:
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
|
||||
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
|
||||
`cursor` (int):cursor for the pagination. If cursor is present then we ignore
|
||||
`start_time` and `end_time`.<br>
|
||||
Returns:
|
||||
a dictionary with a payload for Rest API call to fetch account history.<br>
|
||||
"""
|
||||
payload: dict[str, str | int] = {"sub_account_id": str(self.get_trading_account_id())}
|
||||
|
||||
if params.get("cursor"):
|
||||
payload["cursor"] = params["cursor"]
|
||||
else:
|
||||
start_time = params.get("start_time")
|
||||
end_time = params.get("end_time")
|
||||
if start_time:
|
||||
payload["start_time"] = str(start_time)
|
||||
if end_time:
|
||||
payload["end_time"] = str(end_time)
|
||||
payload["limit"] = limit | 500
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_positions(self, symbols: list[str] = [], params={}) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_positions() method.<br>.
|
||||
|
||||
Args:
|
||||
symbols: list(str) get positions for these symbols only.<br>
|
||||
|
||||
Returns: a dictionary with a payload for Rest API call to fetch positions.<br>
|
||||
"""
|
||||
payload: dict[str, str | int | bool | list] = {
|
||||
"sub_account_id": str(self.get_trading_account_id())
|
||||
}
|
||||
if symbols:
|
||||
ks, us, qs = [], [], []
|
||||
for symbol in symbols:
|
||||
try:
|
||||
k, u, q = get_kuq_from_symbol(symbol)
|
||||
ks.append(k)
|
||||
us.append(u)
|
||||
qs.append(q)
|
||||
except Exception as e:
|
||||
raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_positions {e}")
|
||||
payload["kind"] = list(set(ks))
|
||||
payload["base"] = list(set(us))
|
||||
payload["quote"] = list(set(qs))
|
||||
else:
|
||||
if "kind" in params:
|
||||
payload["kind"] = [params["kind"]]
|
||||
if "base" in params:
|
||||
payload["base"] = [params["base"]]
|
||||
if "quote" in params:
|
||||
payload["quote"] = [params["quote"]]
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_order_history(
|
||||
self,
|
||||
params: dict,
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_order_history() method.<br>.
|
||||
|
||||
Args:
|
||||
params: (dict) with possible keys as:.<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
|
||||
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
|
||||
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
|
||||
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
|
||||
Returns: a dictionary with a payload for Rest API call to fetch order history.<br>
|
||||
"""
|
||||
payload: dict[str, str | int | bool | list] = {
|
||||
"sub_account_id": str(self.get_trading_account_id())
|
||||
}
|
||||
if "limit" in params:
|
||||
payload["limit"] = params["limit"]
|
||||
if params.get("cursor"):
|
||||
payload["cursor"] = params["cursor"]
|
||||
else:
|
||||
if "kind" in params:
|
||||
payload["kind"] = [params["kind"]]
|
||||
if "base" in params:
|
||||
payload["base"] = [params["base"]]
|
||||
if "quote" in params:
|
||||
payload["quote"] = [params["quote"]]
|
||||
if "expiration" in params:
|
||||
payload["expiration"] = [params["expiration"]]
|
||||
if "strike_price" in params:
|
||||
payload["strike_price"] = [params["strike_price"]]
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_open_orders(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_order_history() method.<br>.
|
||||
|
||||
Args:
|
||||
params: (dict) with possible keys as:.<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
Returns: a dictionary with a payload for Rest API call to fetch order history.<br>
|
||||
"""
|
||||
payload: dict[str, str | int | bool | list] = {
|
||||
"sub_account_id": str(self.get_trading_account_id())
|
||||
}
|
||||
if symbol:
|
||||
try:
|
||||
k, u, q = get_kuq_from_symbol(symbol)
|
||||
payload["kind"] = [k]
|
||||
payload["base"] = [u]
|
||||
payload["quote"] = [q]
|
||||
except Exception as e:
|
||||
raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_open_orders {e}")
|
||||
else:
|
||||
if "kind" in params:
|
||||
payload["kind"] = [params["kind"]]
|
||||
if "base" in params:
|
||||
payload["base"] = [params["base"]]
|
||||
if "quote" in params:
|
||||
payload["quote"] = [params["quote"]]
|
||||
return payload
|
||||
|
||||
def _get_payload_fetch_ohlcv(
|
||||
self,
|
||||
symbol: str,
|
||||
timeframe: str,
|
||||
since: int,
|
||||
limit: int,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_ohlcv() method.<br>.
|
||||
|
||||
Args:
|
||||
symbol: The instrument name.<br>
|
||||
timeframe: The timeframe of the ohlc.
|
||||
See `ccxt_interval_to_grvt_candlestick_interval`.<br>
|
||||
since: fetch ohlc since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of ohlc to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.<br>
|
||||
|
||||
Returns: a dictionary with a payload for Rest API call to fetch_ohlcv.<br>
|
||||
See [Candlestick] (https://api-docs.grvt.io/market_data_api/#candlestick_1)
|
||||
for more details.<br>
|
||||
"""
|
||||
if timeframe not in ccxt_interval_to_grvt_candlestick_interval:
|
||||
raise ValueError(f"Invalid timeframe {timeframe}")
|
||||
|
||||
interval: CandlestickInterval = ccxt_interval_to_grvt_candlestick_interval[timeframe]
|
||||
payload: dict[str, str | int | bool | list] = {"instrument": symbol}
|
||||
if params.get("cursor"):
|
||||
payload["cursor"] = params["cursor"]
|
||||
else:
|
||||
if interval:
|
||||
payload["interval"] = interval.value
|
||||
candle_type = CandlestickType.TRADE
|
||||
if "candle_type" in params:
|
||||
candle_type = CandlestickType[params["candle_type"]]
|
||||
payload["type"] = candle_type.value
|
||||
if since:
|
||||
payload["start_time"] = str(since)
|
||||
if "end_time" in params:
|
||||
payload["end_time"] = str(params["end_time"])
|
||||
if limit:
|
||||
payload["limit"] = int(limit)
|
||||
return payload
|
||||
|
||||
def _get_balances_from_account_summary(self, account_summary: dict) -> dict:
|
||||
balances: dict = {}
|
||||
balances["info"] = account_summary.get("spot_balances", [])
|
||||
balances["timestamp"] = int(int(account_summary.get("event_time", 0)) / 1_000_000)
|
||||
balances["datetime"] = (
|
||||
datetime.fromtimestamp(balances["timestamp"] / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[
|
||||
:-3
|
||||
]
|
||||
+ "Z"
|
||||
)
|
||||
balances["total"] = {}
|
||||
balances["free"] = {}
|
||||
balances["used"] = {}
|
||||
for currency_balance in account_summary.get("spot_balances", []):
|
||||
if not currency_balance or not isinstance(currency_balance, dict):
|
||||
continue
|
||||
currency: str = currency_balance.get("currency", "")
|
||||
if not currency:
|
||||
continue
|
||||
balances[currency] = {"total": currency_balance.get("balance", "0.0")}
|
||||
balances["total"][currency] = balances[currency]["total"]
|
||||
if currency == "USDT":
|
||||
balances[currency]["free"] = account_summary.get("available_balance", "0.0")
|
||||
balances[currency]["used"] = str(
|
||||
Decimal(balances[currency]["total"]) - Decimal(balances[currency]["free"])
|
||||
)
|
||||
else:
|
||||
balances[currency]["free"] = balances[currency]["total"]
|
||||
balances[currency]["used"] = "0.0"
|
||||
|
||||
balances["free"][currency] = balances[currency]["free"]
|
||||
balances["used"][currency] = balances[currency]["used"]
|
||||
return balances
|
||||
|
||||
def _get_set_derisk_mm_ratio_payload(
|
||||
self,
|
||||
ratio: str,
|
||||
) -> dict[str, str | dict]:
|
||||
"""
|
||||
Returns a payload for setting the derisking market making ratio.
|
||||
"""
|
||||
payload: dict[str, str | dict] = {
|
||||
"sub_account_id": self.get_trading_account_id(),
|
||||
"ratio": str(ratio),
|
||||
}
|
||||
signature: dict = sign_derisk_mm_ratio_request(
|
||||
self.env, int(self.get_trading_account_id()), str(ratio), self._private_key
|
||||
)
|
||||
payload["signature"] = signature
|
||||
return payload
|
||||
|
||||
def convert_grvt_ob_to_ccxt(self, order_book: dict) -> dict:
|
||||
"""
|
||||
Converts GRVT-specific order book format to CCXT format.
|
||||
"""
|
||||
ob_time_ms: int = int(order_book["event_time"]) // 1_000_000
|
||||
ccxt_ob = {
|
||||
"symbol": order_book["instrument"],
|
||||
"bids": [],
|
||||
"asks": [],
|
||||
"timestamp": ob_time_ms,
|
||||
"datetime": datetime.fromtimestamp(ob_time_ms / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[
|
||||
:-3
|
||||
]
|
||||
+ "Z",
|
||||
"nonce": int(order_book["event_time"]),
|
||||
}
|
||||
ccxt_ob["bids"] = [[bid["price"], bid["size"]] for bid in order_book["bids"]]
|
||||
ccxt_ob["asks"] = [[ask["price"], ask["size"]] for ask in order_book["asks"]]
|
||||
return ccxt_ob
|
||||
|
||||
# Vault Management APIs
|
||||
def _get_fetch_vault_manager_investor_history_payload(
|
||||
self,
|
||||
vault_id: str,
|
||||
only_own_investments: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_vault_manager_investor_history() method.<br>.
|
||||
|
||||
Args:
|
||||
vault_id: The vault id to fetch history for.<br>
|
||||
only_own_investments: If True, fetch only investments by the manager.<br>
|
||||
|
||||
Returns:
|
||||
A dictionary with a payload for Rest API call to fetch vault investor history.
|
||||
"""
|
||||
payload: dict[str, str | bool] = {
|
||||
"vault_id": vault_id,
|
||||
"only_own_investments": only_own_investments,
|
||||
}
|
||||
return payload
|
||||
|
||||
def _get_fetch_vault_redemption_queue_payload(
|
||||
self,
|
||||
vault_id: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Prepares payload for fetch_vault_redemption_queue() method.<br>.
|
||||
|
||||
Args:
|
||||
vault_id: The vault id to fetch redemption queue for.<br>
|
||||
|
||||
Returns:
|
||||
A dictionary with a payload for Rest API call to fetch vault redemption queue.
|
||||
"""
|
||||
return {"vault_id": vault_id}
|
||||
@@ -1,195 +0,0 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GrvtEnv(str, Enum):
|
||||
PROD = "prod"
|
||||
TESTNET = "testnet"
|
||||
STAGING = "staging"
|
||||
DEV = "dev"
|
||||
|
||||
# GrvtEndpointType defines the root path for a family of endpoints
|
||||
class GrvtEndpointType(str, Enum):
|
||||
EDGE = "edge"
|
||||
TRADE_DATA = "tdg"
|
||||
MARKET_DATA = "mdg"
|
||||
|
||||
|
||||
class GrvtWSEndpointType(str, Enum):
|
||||
TRADE_DATA = "tdg"
|
||||
MARKET_DATA = "mdg"
|
||||
TRADE_DATA_RPC_FULL = "tdg_rpc_full"
|
||||
MARKET_DATA_RPC_FULL = "mdg_rpc_full"
|
||||
|
||||
|
||||
END_POINT_VERSION = os.getenv("GRVT_END_POINT_VERSION", "v1")
|
||||
|
||||
|
||||
def get_grvt_endpoint_domains(env_name: str) -> dict[GrvtEndpointType, str]:
|
||||
if env_name == GrvtEnv.PROD.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: "https://edge.grvt.io",
|
||||
GrvtEndpointType.TRADE_DATA: "https://trades.grvt.io",
|
||||
GrvtEndpointType.MARKET_DATA: "https://market-data.grvt.io",
|
||||
}
|
||||
if env_name == GrvtEnv.TESTNET.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: f"https://edge.{env_name}.grvt.io",
|
||||
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.grvt.io",
|
||||
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.grvt.io",
|
||||
}
|
||||
if env_name == GrvtEnv.STAGING.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io",
|
||||
}
|
||||
if env_name == GrvtEnv.DEV.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io",
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def get_grvt_ws_endpoint(
|
||||
env: str,
|
||||
endpoint_type: GrvtWSEndpointType,
|
||||
) -> str:
|
||||
"""Returns string pointing to WS endpoint for given environment and endpoint type."""
|
||||
if env == GrvtEnv.PROD.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: "wss://trades.grvt.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: "wss://market-data.grvt.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: "wss://trades.grvt.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: "wss://market-data.grvt.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
if env == GrvtEnv.TESTNET.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.grvt.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.grvt.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.grvt.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.grvt.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
if env == GrvtEnv.STAGING.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
if env == GrvtEnv.DEV.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
return ""
|
||||
|
||||
# Mapping of WS stream names to DEFAULT endpoint types
|
||||
GRVT_WS_STREAMS = {
|
||||
# ******* Market Data ********
|
||||
"mini.s": GrvtWSEndpointType.MARKET_DATA,
|
||||
"mini.d": GrvtWSEndpointType.MARKET_DATA,
|
||||
"ticker.s": GrvtWSEndpointType.MARKET_DATA,
|
||||
"ticker.d": GrvtWSEndpointType.MARKET_DATA,
|
||||
"book.s": GrvtWSEndpointType.MARKET_DATA,
|
||||
"book.d": GrvtWSEndpointType.MARKET_DATA,
|
||||
"trade": GrvtWSEndpointType.MARKET_DATA,
|
||||
"candle": GrvtWSEndpointType.MARKET_DATA,
|
||||
# ******* Trade Data ********
|
||||
"order": GrvtWSEndpointType.TRADE_DATA,
|
||||
"state": GrvtWSEndpointType.TRADE_DATA,
|
||||
"cancel": GrvtWSEndpointType.TRADE_DATA,
|
||||
"position": GrvtWSEndpointType.TRADE_DATA,
|
||||
"fill": GrvtWSEndpointType.TRADE_DATA,
|
||||
"transfer": GrvtWSEndpointType.TRADE_DATA,
|
||||
"deposit": GrvtWSEndpointType.TRADE_DATA,
|
||||
"withdrawal": GrvtWSEndpointType.TRADE_DATA,
|
||||
}
|
||||
|
||||
|
||||
def is_trading_ws_endpoint(end_point_type: GrvtWSEndpointType) -> bool:
|
||||
return end_point_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]
|
||||
|
||||
|
||||
# "wss://market-data.testnet.grvt.io/ws"
|
||||
# wss://trades.testnet.grvt.io/ws
|
||||
# GRVT_ENDPOINTS defines the endpoint paths grouped by endpoint type
|
||||
GRVT_ENDPOINTS = {
|
||||
GrvtEndpointType.EDGE: {
|
||||
"GRAPHQL": "query",
|
||||
"AUTH": "auth/api_key/login",
|
||||
},
|
||||
GrvtEndpointType.TRADE_DATA: {
|
||||
"CREATE_ORDER": f"full/{END_POINT_VERSION}/create_order",
|
||||
"CANCEL_ALL_ORDERS": f"full/{END_POINT_VERSION}/cancel_all_orders",
|
||||
"CANCEL_ORDER": f"full/{END_POINT_VERSION}/cancel_order",
|
||||
"GET_OPEN_ORDERS": f"full/{END_POINT_VERSION}/open_orders",
|
||||
"GET_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/account_summary",
|
||||
"GET_FUNDING_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/funding_account_summary",
|
||||
"GET_AGGREGATED_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/aggregated_account_summary",
|
||||
"GET_ACCOUNT_HISTORY": f"full/{END_POINT_VERSION}/account_history",
|
||||
"GET_POSITIONS": f"full/{END_POINT_VERSION}/positions",
|
||||
"GET_ORDER": f"full/{END_POINT_VERSION}/order",
|
||||
"GET_ORDER_HISTORY": f"full/{END_POINT_VERSION}/order_history",
|
||||
"GET_FILL_HISTORY": f"full/{END_POINT_VERSION}/fill_history",
|
||||
"SET_DERISK_MM_RATIO": f"full/{END_POINT_VERSION}/set_derisk_mm_ratio",
|
||||
"GET_VAULT_MANAGER_INVESTOR_HISTORY": f"full/{END_POINT_VERSION}/vault_manager_investor_history",
|
||||
"GET_VAULT_REDEMPTION_QUEUE": f"full/{END_POINT_VERSION}/vault_view_redemption_queue",
|
||||
},
|
||||
GrvtEndpointType.MARKET_DATA: {
|
||||
"GET_ALL_INSTRUMENTS": f"full/{END_POINT_VERSION}/all_instruments",
|
||||
"GET_INSTRUMENTS": f"full/{END_POINT_VERSION}/instruments",
|
||||
"GET_INSTRUMENT": f"full/{END_POINT_VERSION}/instrument",
|
||||
"GET_TICKER": f"full/{END_POINT_VERSION}/ticker",
|
||||
"GET_MINI_TICKER": f"full/{END_POINT_VERSION}/mini",
|
||||
"GET_ORDER_BOOK": f"full/{END_POINT_VERSION}/book",
|
||||
"GET_TRADES": f"full/{END_POINT_VERSION}/trade",
|
||||
"GET_TRADE_HISTORY": f"full/{END_POINT_VERSION}/trade_history",
|
||||
"GET_FUNDING": f"full/{END_POINT_VERSION}/funding",
|
||||
"GET_CANDLESTICK": f"full/{END_POINT_VERSION}/kline",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_grvt_endpoint(environment: GrvtEnv, end_point: str) -> str:
|
||||
# if end_point == "GET_ALL_INSTRUMENTS":
|
||||
# return "https://market-data.testnet.grvt.io/full/v1/instruments"
|
||||
endpoint_domains = get_grvt_endpoint_domains(environment.value)
|
||||
for endpoints_type, endpoints in GRVT_ENDPOINTS.items():
|
||||
if end_point in endpoints:
|
||||
return f"{endpoint_domains[endpoints_type]}/{endpoints[end_point]}"
|
||||
return ""
|
||||
|
||||
|
||||
def get_all_grvt_endpoints(environment: GrvtEnv) -> dict[str, str]:
|
||||
endpoint_domains = get_grvt_endpoint_domains(environment.value)
|
||||
endpoints = {}
|
||||
for endpoints_type, endpoints_map in GRVT_ENDPOINTS.items():
|
||||
for endpoint, path in endpoints_map.items():
|
||||
endpoints[endpoint] = f"{endpoint_domains[endpoints_type]}/{path}"
|
||||
return endpoints
|
||||
|
||||
|
||||
CHAIN_IDS = {
|
||||
GrvtEnv.DEV.value: 327,
|
||||
GrvtEnv.STAGING.value: 327,
|
||||
GrvtEnv.TESTNET.value: 326,
|
||||
GrvtEnv.PROD.value: 325,
|
||||
}
|
||||
|
||||
########################################################
|
||||
@@ -1,32 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
LOG_FILE = os.getenv("LOG_FILE", "FALSE").upper()
|
||||
GRVT_ENV = os.getenv("GRVT_ENV")
|
||||
|
||||
if LOG_FILE == "TRUE":
|
||||
LOG_TIMESTAMP = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
|
||||
fn = sys.argv[0].split("/")[-1]
|
||||
fn_base = fn.split(".")[0]
|
||||
if GRVT_ENV:
|
||||
filename = f"logs/{fn_base}_{GRVT_ENV}_{LOG_TIMESTAMP}.log"
|
||||
else:
|
||||
filename = f"logs/{fn_base}_{LOG_TIMESTAMP}.log"
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
logging.basicConfig(
|
||||
filename=filename,
|
||||
level=os.getenv("LOGGING_LEVEL", "INFO"),
|
||||
format="%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Using FILE logger {LOG_FILE=}")
|
||||
else:
|
||||
logging.basicConfig(
|
||||
level=os.getenv("LOGGING_LEVEL", "INFO"),
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Using CONSOLE logger {LOG_FILE=}")
|
||||
@@ -1,841 +0,0 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .grvt_ccxt_base import GrvtCcxtBase
|
||||
|
||||
# import requests
|
||||
# from env import ENDPOINTS
|
||||
from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint
|
||||
from .grvt_ccxt_types import (
|
||||
Amount,
|
||||
GrvtInstrumentKind,
|
||||
GrvtInvalidOrder,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
from .grvt_ccxt_utils import (
|
||||
EnumEncoder,
|
||||
GrvtOrder,
|
||||
get_cookie_with_expiration,
|
||||
get_cookie_with_expiration_async,
|
||||
get_grvt_order,
|
||||
get_order_payload,
|
||||
)
|
||||
|
||||
|
||||
class GrvtCcxtPro(GrvtCcxtBase):
|
||||
"""
|
||||
GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode.
|
||||
|
||||
Args:
|
||||
env: GrvtCcxtPro (DEV, TESTNET, PROD)
|
||||
parameters: dict with trading_account_id, private_key, api_key etc
|
||||
|
||||
Examples:
|
||||
>>> from grvt_api_pro import GrvtCcxtPro
|
||||
>>> from grvt_env import GrvtEnv
|
||||
>>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET)
|
||||
>>> await grvt.fetch_markets()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: GrvtEnv,
|
||||
logger: logging.Logger | None = None,
|
||||
parameters: dict = {},
|
||||
order_book_ccxt_format: bool = False,
|
||||
):
|
||||
"""Initialize the GrvtCcxt instance."""
|
||||
super().__init__(env, logger, parameters, order_book_ccxt_format)
|
||||
self._clsname: str = type(self).__name__
|
||||
self._session = aiohttp.ClientSession(headers={"Content-Type": "application/json"})
|
||||
# Force sync call to get cookie here
|
||||
self._cookie = get_cookie_with_expiration(
|
||||
get_grvt_endpoint(self.env, "AUTH"), self._api_key
|
||||
)
|
||||
self.update_session_with_cookie()
|
||||
|
||||
def __del__(self):
|
||||
"""Close the aiohttp session when the instance is deleted."""
|
||||
self.logger.info(f"{self._clsname} __del__() called")
|
||||
if self._session:
|
||||
self.logger.info(f"{self._clsname} closing session")
|
||||
asyncio.get_running_loop().create_task(self._session.close())
|
||||
|
||||
def update_session_with_cookie(self) -> None:
|
||||
if self._cookie:
|
||||
self._session.cookie_jar.update_cookies({"gravity": self._cookie["gravity"]})
|
||||
if self._cookie["X-Grvt-Account-Id"]:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
|
||||
)
|
||||
self.logger.info(
|
||||
f"update_session_with_cookie {self._cookie=} {self._session.cookie_jar=}"
|
||||
f" {self._session.headers=}"
|
||||
)
|
||||
|
||||
async def refresh_cookie(self) -> dict | None:
|
||||
"""Refresh the session cookie."""
|
||||
if not self.should_refresh_cookie():
|
||||
return self._cookie
|
||||
path: str = get_grvt_endpoint(self.env, "AUTH")
|
||||
self._cookie = await get_cookie_with_expiration_async(path, self._api_key)
|
||||
self._path_return_value_map[path] = self._cookie
|
||||
self.update_session_with_cookie()
|
||||
return self._cookie
|
||||
|
||||
# PRIVATE API CALLS
|
||||
async def _auth_and_post(self, path: str, payload: dict) -> dict:
|
||||
FN = f"{self._clsname} _auth_and_post {path=}"
|
||||
MAX_LEN_TO_LOG = 1280
|
||||
response: dict = {}
|
||||
if not path:
|
||||
self.logger.warning(f"{FN} Invalid path {path=} {payload=}")
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}")
|
||||
# Always see if need to referesh cookie before sending a request
|
||||
await self.refresh_cookie()
|
||||
payload_json = json.dumps(payload, cls=EnumEncoder)
|
||||
self.logger.info(f"{FN} {payload=}\n{payload_json=}")
|
||||
return_text: str = ""
|
||||
async with self._session.post(
|
||||
url=path,
|
||||
data=payload_json,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
) as return_value:
|
||||
return_text: str = ""
|
||||
try:
|
||||
return_text = await return_value.text()
|
||||
response = await return_value.json(content_type="application/json")
|
||||
except Exception as err:
|
||||
self.logger.warning(
|
||||
f"{FN} Unable to parse {return_value=} as "
|
||||
f" json(content_type='application/json'). {err=}"
|
||||
)
|
||||
if not return_value.ok:
|
||||
self.logger.warning(f"{FN} {payload_json=}\n{return_value=}\n{response=}")
|
||||
else:
|
||||
if len(return_text) > MAX_LEN_TO_LOG:
|
||||
self.logger.debug(f"{FN} OK {return_value=} response={response}")
|
||||
self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**")
|
||||
else:
|
||||
self.logger.info(f"{FN} OK {return_value=} response={response}")
|
||||
self._path_return_value_map[path] = response
|
||||
return response or {}
|
||||
|
||||
async def _create_grvt_order(self, order: GrvtOrder) -> dict:
|
||||
"""
|
||||
Send a GrvtOrder object to the exchange.
|
||||
:param order: The GrvtOrder object.
|
||||
Return: dictionary representing the order response.
|
||||
"""
|
||||
FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}"
|
||||
order_payload = get_order_payload(
|
||||
order,
|
||||
private_key=self._private_key,
|
||||
env=self.env,
|
||||
instruments=self.markets,
|
||||
)
|
||||
path = get_grvt_endpoint(self.env, "CREATE_ORDER")
|
||||
self.logger.info(f"{FN} {path=} {order_payload=}")
|
||||
response: dict = await self._auth_and_post(path, payload=order_payload)
|
||||
if response.get("result") is None:
|
||||
self.logger.error(f"Error creating order, {response}")
|
||||
return {}
|
||||
self.logger.info(
|
||||
f"{FN} Order created:"
|
||||
f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}"
|
||||
)
|
||||
return response.get("result", {})
|
||||
|
||||
def _get_order_with_validations(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params: dict = {},
|
||||
) -> GrvtOrder:
|
||||
self._check_account_auth()
|
||||
self._check_valid_symbol(symbol)
|
||||
# Validate order fields
|
||||
self._check_order_arguments(order_type, side, amount, price)
|
||||
# create GrvtOrder object
|
||||
order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60)
|
||||
return get_grvt_order(
|
||||
sub_account_id=self.get_trading_account_id(),
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side,
|
||||
amount=amount,
|
||||
limit_price=price,
|
||||
order_duration_secs=order_duration_secs,
|
||||
params=params,
|
||||
)
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""Ccxt compliant signature."""
|
||||
order = self._get_order_with_validations(symbol, order_type, side, amount, price, params)
|
||||
return await self._create_grvt_order(order)
|
||||
|
||||
async def create_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
return await self.create_order(symbol, "limit", side, amount, price, params)
|
||||
|
||||
async def cancel_all_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature BUT lacks symbol
|
||||
Cancel all orders for a sub-account.
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
FN = f"{self._clsname} cancel_all_orders"
|
||||
payload: dict = self._get_payload_cancel_all_orders(params)
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel orders: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
async def cancel_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Cancel specific order for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
id (str): exchange assigned order ID<br>
|
||||
symbol (str): trading symbol<br>
|
||||
params:
|
||||
* client_order_id (str): client assigned order ID<br>
|
||||
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
|
||||
Returns:
|
||||
True if cancel request was acked by exchange. False otherwise.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} cancel_order"
|
||||
self._check_account_auth()
|
||||
# Prepare payload
|
||||
payload: dict = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = str(id)
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
|
||||
if "time_to_live_ms" in params:
|
||||
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
|
||||
|
||||
# Send cancel request
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ORDER")
|
||||
self.logger.info(
|
||||
f"{FN} Send cancel {payload=} for trading_account_id={self._trading_account_id}"
|
||||
)
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel order: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
async def set_derisk_mm_ratio(self, ratio: str) -> bool:
|
||||
"""
|
||||
|
||||
Set the Derisk to Maintenance marginb ratio for the account.
|
||||
Private call requires authorization.
|
||||
See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio)
|
||||
for details.
|
||||
|
||||
Args:
|
||||
ratio (Amount): The new derisking market making ratio.
|
||||
|
||||
Returns:
|
||||
True if the request was acknowledged by the exchange. False otherwise.
|
||||
"""
|
||||
FN = f"{self._clsname} set_derisk_mm_ratio"
|
||||
self._check_account_auth()
|
||||
payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio))
|
||||
path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO")
|
||||
self.logger.info(
|
||||
f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}"
|
||||
)
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
self.logger.info(f"{FN} Set derisk_mm_ratio {response=}")
|
||||
return True
|
||||
|
||||
async def fetch_open_orders(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch open orders for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get orders for this symbol only.<br>
|
||||
since: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
limit: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values are 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch orders
|
||||
for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
Returns:
|
||||
a list of dictionaries, each dict represent an order.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_open_orders(symbol, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
open_orders: list = response.get("result", [])
|
||||
if symbol:
|
||||
open_orders = [
|
||||
o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol
|
||||
]
|
||||
return open_orders
|
||||
|
||||
async def fetch_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Private call requires authorization.<br>
|
||||
See [Get Order](https://api-docs.grvt.io/trading_api/#get-order)
|
||||
for details.<br>.
|
||||
|
||||
Get Order status by either order_id or client_order_id
|
||||
Args:
|
||||
id: (str) order_id to fetch.<br>
|
||||
symbol: (str) NOT SUPPRTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`client_order_id` (int): client assigned order ID.<br>
|
||||
Returns:
|
||||
dict with order details or {} if order was NOT found.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} fetch_order"
|
||||
self._check_account_auth()
|
||||
payload = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = id
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or params['client_order_id']")
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
async def fetch_order_history(self, params: dict = {}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Get Order history of orders by kind/base/quote.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Order History](https://api-docs.grvt.io/trading_api/#order-history)
|
||||
for details.<br>
|
||||
Args:
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
|
||||
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
|
||||
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
|
||||
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
|
||||
Return: a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent an order state.<br>.
|
||||
"""
|
||||
self._check_account_auth()
|
||||
payload = self._get_payload_fetch_order_history(params)
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
async def get_account_summary(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Return: The account summary.
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#account_summary)
|
||||
for details.<br>
|
||||
Returns: dictionary with account data.<br>.
|
||||
"""
|
||||
FN = f"{self._clsname} get_account_summary {type=}"
|
||||
self._check_account_auth()
|
||||
payload = {}
|
||||
if type == "sub-account":
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY")
|
||||
payload = {"sub_account_id": str(self._trading_account_id)}
|
||||
elif type == "funding":
|
||||
path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY")
|
||||
elif type == "aggregated":
|
||||
path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY")
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}")
|
||||
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
sub_account: dict = response.get("result", {})
|
||||
if not sub_account:
|
||||
self.logger.info(f"{FN} No account summary for {path=} {payload=}")
|
||||
return sub_account
|
||||
|
||||
async def fetch_balance(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch balances for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'.
|
||||
Valid values: 'sub-account', 'funding', 'aggregated'.
|
||||
|
||||
Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.<br>.
|
||||
"""
|
||||
account_summary: dict = await self.get_account_summary(type)
|
||||
return self._get_balances_from_account_summary(account_summary)
|
||||
|
||||
async def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict:
|
||||
"""
|
||||
HISTORICAL data.<br>
|
||||
Get account history.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account History](https://api-docs.grvt.io/trading_api/#account-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
limit: maximum number of account snapshots per page to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
|
||||
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
|
||||
`cursor` (str): cursor for the pagination. If cursor is present then we ignore
|
||||
`start_time` and `end_time`.<br>
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : list of account history snapshots.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_account_history(limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
async def fetch_positions(self, symbols: list[str] = [], params={}) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch positions for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Positions](https://api-docs.grvt.io/trading_api/#positions)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbols: list(str) get positions for these symbols only.<br>
|
||||
|
||||
Returns: list of dictionaries, each dict represent a position.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_positions(symbols, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_POSITIONS")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
positions: list = response.get("result", [])
|
||||
if symbols:
|
||||
self.logger.info(f"fetch_positions filter positions by {symbols=}")
|
||||
positions = [p for p in positions if p.get("instrument") in symbols]
|
||||
return positions
|
||||
|
||||
async def fetch_my_trades(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Fetch past trades for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent a trade.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_my_trades(symbol, since, limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
if symbol:
|
||||
# filter result by symbol
|
||||
trades: list = response.get("result", [])
|
||||
trades = [t for t in trades if t.get("instrument") == symbol]
|
||||
response["result"] = trades
|
||||
return response
|
||||
|
||||
# **************** PUBLIC API CALLS
|
||||
async def load_markets(self) -> dict | None:
|
||||
self.logger.info("load_markets START")
|
||||
instruments = await self.fetch_markets(
|
||||
params={
|
||||
"kind": GrvtInstrumentKind.PERPETUAL,
|
||||
}
|
||||
)
|
||||
if instruments:
|
||||
self.markets = {i.get("instrument"): i for i in instruments}
|
||||
self.logger.info(f"load_markets: loaded {len(self.markets)} markets.")
|
||||
else:
|
||||
self.logger.warning("load_markets: No markets found.")
|
||||
return self.markets
|
||||
|
||||
async def fetch_markets(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
ccxt-compliant signature
|
||||
Retrieve the list of all instruments of matching kind, base and quote
|
||||
supported by the exchange.
|
||||
|
||||
Params: dict with keys:<br>
|
||||
`is_active` (bool) - defaults to True.<br>
|
||||
`limit` (int) - defaiults to 20.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns: list of dictionaries per instrument with keys:<br>
|
||||
`instrument`: symbol e.g. 'BTC_USDT_Perp'.<br>
|
||||
`instrument_hash`: hashed symbol for order signing e.g. '0x030501'.<br>
|
||||
`base`: base currency e.g. 'BTC'.<br>
|
||||
`quote`: quote currency e.g. 'USDT'.<br>
|
||||
`kind`: kind of instrument 'PERPETUAL'/'FUTURE'.<br>
|
||||
'base_decimals': size multiplier for order signing.<br>
|
||||
`tick_size`: price tick size.<br>
|
||||
`min_size`: minimum order size.<br>
|
||||
"""
|
||||
# Prepare payload
|
||||
payload = self._get_payload_fetch_markets(params)
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_all_markets(
|
||||
self,
|
||||
is_active: bool | None = True,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Retrieve the list of all instruments supported by the exchange.<br>
|
||||
Params:<br>
|
||||
`is_active` (bool) - defaults to True.<br>.
|
||||
|
||||
Returns: list of dictionaries per instrument. See fetch_markets().<br>
|
||||
"""
|
||||
# Prepare payload
|
||||
payload = {"is_active": is_active}
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
# Extract and return the list of instruments
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_market(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the instrument object for a given symbol.
|
||||
:param symbol: The symbol of the instrument.
|
||||
"""
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENT")
|
||||
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_ticker(self, symbol: str, params: dict = {}) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature
|
||||
Retrieve the ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '59273700', 'best_ask_size': '21670', 'funding_rate_curr': 2544, 'funding_rate_avg': 0,
|
||||
# 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000',
|
||||
# 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500',
|
||||
# 'sell_volume_q': '687640900', 'high_price': '343545000', 'low_price': '100000',
|
||||
# 'open_price': '32554000000000', 'open_interest': '8174350000000',
|
||||
# 'long_short_ratio': 1.0948905}
|
||||
path = get_grvt_endpoint(self.env, "GET_TICKER")
|
||||
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", {})
|
||||
|
||||
async def fetch_mini_ticker(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the mini-ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The mini-ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '59273700000000', 'best_ask_size': '21678000000'}
|
||||
path = get_grvt_endpoint(self.env, "GET_MINI_TICKER")
|
||||
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", {})
|
||||
|
||||
async def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature
|
||||
Retrieve the order book of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The order book dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '0', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...]
|
||||
# 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...]
|
||||
payload = {"instrument": symbol, "aggregate": 1}
|
||||
if limit:
|
||||
payload["depth"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
if self.is_order_book_ccxt_format():
|
||||
# Convert to ccxt format
|
||||
return self.convert_grvt_ob_to_ccxt(response.get("result", {}))
|
||||
return response.get("result", {})
|
||||
|
||||
async def fetch_recent_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
limit: int | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
Retrieve the recent trades a given instrument.<br>
|
||||
:param instrument: The instrument name.
|
||||
:return: The order book dictionary of the instrument.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict[str, str | int] = {"instrument": symbol}
|
||||
if limit:
|
||||
payload["limit"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_TRADES")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int | None = None,
|
||||
limit: int = 10,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt-compliant signature, HISTORICAL data.<br>
|
||||
Retrieve trade history of a given instrument.
|
||||
:param symbol: The instrument name.
|
||||
:return: dict with field 'result' containing a list of trades.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict = self._get_payload_fetch_trades(
|
||||
symbol,
|
||||
since=since,
|
||||
limit=limit,
|
||||
params=params,
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
async def fetch_funding_rate_history(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int = 0,
|
||||
limit: int = 1_000,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature, HISTORICAL data.<br>
|
||||
Retrieve the funding rates history of a given instrument.<br>
|
||||
Args:
|
||||
symbol (str): The instrument name.<br>
|
||||
since (int): fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: int - maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
Returns:
|
||||
dict with field 'result' containing list of dictionaries repesenting funding rate
|
||||
at a point in time with fields:<br>
|
||||
`instrument` (str): instrument name.<br>
|
||||
'funding_rate' (float): funding rate.<br>
|
||||
'funding_time' (int): funding time in nanoseconds.<br>
|
||||
'mark_price' (float): mark price.<br>.
|
||||
"""
|
||||
payload: dict[str, str | int] = {"instrument": symbol}
|
||||
if params.get("cursor"):
|
||||
payload["cursor"] = params["cursor"]
|
||||
else:
|
||||
if since:
|
||||
payload["start_time"] = str(since)
|
||||
if params.get("end_time"):
|
||||
payload["end_time"] = str(params["end_time"])
|
||||
if limit:
|
||||
payload["limit"] = int(limit)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_FUNDING")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
async def fetch_ohlcv(
|
||||
self,
|
||||
symbol: str,
|
||||
timeframe: str = "1m",
|
||||
since: int = 0,
|
||||
limit: int = 10,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature, HISTORICAL data.<br>
|
||||
Retrieve the ohlc history of a given instrument.<br>
|
||||
Args:
|
||||
symbol: The instrument name.<br>
|
||||
timeframe: The timeframe of the ohlc.
|
||||
See `ccxt_interval_to_grvt_candlestick_interval`.<br>
|
||||
since: fetch ohlc since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of ohlc to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.<br>
|
||||
Returns:
|
||||
dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:<br>
|
||||
`instrument` - instrument name.<br>
|
||||
`open_time` - start of interval in nanoseconds.<br>
|
||||
`close_time` - end of interval in nanoseconds.<br>
|
||||
`open` - opening price.<br>
|
||||
`close` - closing price.<br>
|
||||
`high` - highest price.<br>
|
||||
`low` - lowest price.<br>
|
||||
`volume_u` - volume in units.<br>
|
||||
`volume_q` - volume in quote(USDT).<br>
|
||||
`trades` - number of trades.<br>.
|
||||
"""
|
||||
FN: str = f"{self._clsname} fetch_ohlcv"
|
||||
payload: dict = self._get_payload_fetch_ohlcv(symbol, timeframe, since, limit, params)
|
||||
self.logger.info(f"{FN} {payload=}")
|
||||
path: str = get_grvt_endpoint(self.env, "GET_CANDLESTICK")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
# Vault Management APIs
|
||||
async def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict:
|
||||
payload: dict = self._get_fetch_vault_manager_investor_history_payload(
|
||||
vault_id=self.get_trading_account_id(),
|
||||
only_own_investments=only_own_investments, # Default to False to fetch all investments
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY")
|
||||
return await self._auth_and_post(path, payload=payload)
|
||||
|
||||
async def fetch_vault_redemption_queue(self):
|
||||
payload: dict = self._get_fetch_vault_redemption_queue_payload(
|
||||
vault_id=self.get_trading_account_id()
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE")
|
||||
return await self._auth_and_post(path, payload=payload)
|
||||
@@ -1,75 +0,0 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
|
||||
from .grvt_ccxt import GrvtCcxt
|
||||
from .grvt_ccxt_env import get_all_grvt_endpoints
|
||||
from .grvt_ccxt_pro import GrvtCcxtPro
|
||||
|
||||
|
||||
def default_check(return_value: dict) -> str:
|
||||
if not isinstance(return_value, list | dict):
|
||||
return "return_value is not a list or dict"
|
||||
if not return_value:
|
||||
return "return_value is empty"
|
||||
return "OK"
|
||||
|
||||
|
||||
def validate_return_values(api: GrvtCcxt | GrvtCcxtPro, result_filename: str) -> None:
|
||||
logging.info("validate_return_values: START")
|
||||
endpoint_check_map: dict[str, Callable] = {
|
||||
"GRAPHQL": default_check,
|
||||
"AUTH": default_check,
|
||||
"CREATE_ORDER": default_check,
|
||||
"CANCEL_ALL_ORDERS": default_check,
|
||||
"CANCEL_ORDER": default_check,
|
||||
"GET_OPEN_ORDERS": default_check,
|
||||
"GET_ACCOUNT_SUMMARY": default_check,
|
||||
"GET_FUNDING_ACCOUNT_SUMMARY": default_check,
|
||||
"GET_AGGREGATED_ACCOUNT_SUMMARY": default_check,
|
||||
"GET_ACCOUNT_HISTORY": default_check,
|
||||
"GET_POSITIONS": default_check,
|
||||
"GET_ORDER": default_check,
|
||||
"GET_ORDER_HISTORY": default_check,
|
||||
"GET_FILL_HISTORY": default_check,
|
||||
"GET_ALL_INSTRUMENTS": default_check,
|
||||
"GET_INSTRUMENTS": default_check,
|
||||
"GET_INSTRUMENT": default_check,
|
||||
"GET_TICKER": default_check,
|
||||
"GET_MINI_TICKER": default_check,
|
||||
"GET_ORDER_BOOK": default_check,
|
||||
"GET_TRADES": default_check,
|
||||
"GET_TRADE_HISTORY": default_check,
|
||||
"GET_FUNDING": default_check,
|
||||
"GET_CANDLESTICK": default_check,
|
||||
}
|
||||
all_endpoints = get_all_grvt_endpoints(api.env)
|
||||
end_point_status = {}
|
||||
for short_name, endpoint in all_endpoints.items():
|
||||
if not api.was_path_called(endpoint):
|
||||
logging.info(f"validate_return_values: {short_name=}, {endpoint=}, not called")
|
||||
end_point_status[short_name] = [endpoint, "not called"]
|
||||
else:
|
||||
return_value = api.get_endpoint_return_value(endpoint)
|
||||
if short_name in endpoint_check_map:
|
||||
check_function = endpoint_check_map.get(short_name)
|
||||
if not check_function or not callable(check_function):
|
||||
logging.error(
|
||||
f"validate_return_values: {short_name=} "
|
||||
f"not found in {endpoint_check_map.keys()=}"
|
||||
)
|
||||
continue
|
||||
check_result = check_function(return_value)
|
||||
logging.info(
|
||||
f"validate_return_values: {short_name=}, {endpoint=}, {check_result=}"
|
||||
)
|
||||
end_point_status[short_name] = [endpoint, check_result]
|
||||
else:
|
||||
logging.error(
|
||||
f"validate_return_values: NO {short_name=} in {endpoint_check_map.keys()=}"
|
||||
)
|
||||
end_point_status[short_name] = [endpoint, "no check"]
|
||||
with open(result_filename, "w") as file_handle:
|
||||
file_handle.write("NAME, URL, STATUS\n")
|
||||
for short_name, status in end_point_status.items():
|
||||
file_handle.write(f"{short_name}, {status[0]}, {status[1]}\n")
|
||||
logging.info("validate_return_values: END")
|
||||
@@ -1,81 +0,0 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
Num = None | str | float | int | Decimal
|
||||
Amount = Decimal | int | float | str
|
||||
GrvtOrderSide = Literal["buy", "sell"]
|
||||
GrvtOrderType = Literal["limit", "market"]
|
||||
|
||||
DURATION_SECOND_IN_NSEC = 1_000_000_000
|
||||
PRICE_MULTIPLIER = 1_000_000_000
|
||||
BTC_ETH_SIZE_MULTIPLIER = 1_000_000_000
|
||||
|
||||
|
||||
class GrvtInvalidOrder(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CandlestickInterval(Enum):
|
||||
CI_1_M = "CI_1_M"
|
||||
CI_3_M = "CI_3_M"
|
||||
CI_5_M = "CI_5_M"
|
||||
CI_15_M = "CI_15_M"
|
||||
CI_30_M = "CI_30_M"
|
||||
CI_1_H = "CI_1_H"
|
||||
CI_2_H = "CI_2_H"
|
||||
CI_4_H = "CI_4_H"
|
||||
CI_6_H = "CI_6_H"
|
||||
CI_8_H = "CI_8_H"
|
||||
CI_12_H = "CI_12_H"
|
||||
CI_1_D = "CI_1_D"
|
||||
CI_3_D = "CI_3_D"
|
||||
CI_5_D = "CI_5_D"
|
||||
CI_1_W = "CI_1_W"
|
||||
CI_2_W = "CI_2_W"
|
||||
CI_3_W = "CI_3_W"
|
||||
CI_4_W = "CI_4_W"
|
||||
|
||||
|
||||
ccxt_interval_to_grvt_candlestick_interval = {
|
||||
"1m": CandlestickInterval.CI_1_M,
|
||||
"3m": CandlestickInterval.CI_3_M,
|
||||
"5m": CandlestickInterval.CI_5_M,
|
||||
"15m": CandlestickInterval.CI_15_M,
|
||||
"30m": CandlestickInterval.CI_30_M,
|
||||
"1h": CandlestickInterval.CI_1_H,
|
||||
"2h": CandlestickInterval.CI_2_H,
|
||||
"4h": CandlestickInterval.CI_4_H,
|
||||
"6h": CandlestickInterval.CI_6_H,
|
||||
"8h": CandlestickInterval.CI_8_H,
|
||||
"12h": CandlestickInterval.CI_12_H,
|
||||
"1d": CandlestickInterval.CI_1_D,
|
||||
"3d": CandlestickInterval.CI_3_D,
|
||||
"5d": CandlestickInterval.CI_5_D,
|
||||
"1w": CandlestickInterval.CI_1_W,
|
||||
"2w": CandlestickInterval.CI_2_W,
|
||||
"3w": CandlestickInterval.CI_3_W,
|
||||
"4w": CandlestickInterval.CI_4_W,
|
||||
}
|
||||
|
||||
|
||||
class CandlestickType(Enum):
|
||||
TRADE = "TRADE"
|
||||
MARK = "MARK"
|
||||
INDEX = "INDEX"
|
||||
MID = "MID"
|
||||
|
||||
|
||||
class GrvtInstrumentKind(Enum):
|
||||
PERPETUAL = "PERPETUAL"
|
||||
FUTURE = "FUTURE"
|
||||
CALL = "CALL"
|
||||
PUT = "PUT"
|
||||
@@ -1,544 +0,0 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from http.cookies import SimpleCookie
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import requests
|
||||
from eth_account import Account
|
||||
from eth_account.messages import encode_typed_data, SignableMessage
|
||||
|
||||
from .grvt_ccxt_env import CHAIN_IDS, GrvtEnv
|
||||
from .grvt_ccxt_types import (
|
||||
BTC_ETH_SIZE_MULTIPLIER,
|
||||
DURATION_SECOND_IN_NSEC,
|
||||
Amount,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
|
||||
|
||||
def rand_uint32():
|
||||
return random.randint(0, 2**32 - 1)
|
||||
|
||||
|
||||
class TimeInForce(Enum):
|
||||
"""
|
||||
| | Must Fill All | Can Fill Partial |
|
||||
| - | - | - |
|
||||
| Must Fill Immediately | FOK | IOC |
|
||||
| Can Fill Till Time | AON | GTC |.
|
||||
|
||||
"""
|
||||
|
||||
# GTT - Remains open until it is cancelled, or expired
|
||||
GOOD_TILL_TIME = "GOOD_TILL_TIME"
|
||||
# AON - Either fill the whole order or none of it (Block Trades Only)
|
||||
ALL_OR_NONE = "ALL_OR_NONE"
|
||||
# IOC - Fill the order as much as possible, when hitting the orderbook. Then cancel it
|
||||
IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL"
|
||||
# FOK - Both AoN and IoC. Either fill the full order when hitting the orderbook, or cancel it
|
||||
FILL_OR_KILL = "FILL_OR_KILL"
|
||||
|
||||
|
||||
class SignTimeInForce(Enum):
|
||||
GOOD_TILL_TIME = 1
|
||||
ALL_OR_NONE = 2
|
||||
IMMEDIATE_OR_CANCEL = 3
|
||||
FILL_OR_KILL = 4
|
||||
|
||||
|
||||
TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = {
|
||||
TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME,
|
||||
TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE,
|
||||
TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL,
|
||||
TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL,
|
||||
}
|
||||
|
||||
|
||||
def get_EIP712_domain_data(env: GrvtEnv) -> dict[str, str | int]:
|
||||
# DO NOT MODIFY THESE VALUES ##############
|
||||
return {
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": CHAIN_IDS[env.value],
|
||||
}
|
||||
|
||||
|
||||
def get_cookie_with_expiration(
|
||||
path: str, api_key: str | None
|
||||
) -> dict[str, str | float | None] | None:
|
||||
"""
|
||||
Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token.
|
||||
:return: The session cookie.
|
||||
"""
|
||||
FN = f"get_cookie_with_expiration {path=}"
|
||||
if api_key:
|
||||
data = {}
|
||||
try:
|
||||
data = {"api_key": api_key}
|
||||
session = requests.Session()
|
||||
return_value = session.post(
|
||||
path,
|
||||
json=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
)
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(return_value.headers.get("Set-Cookie", ""))
|
||||
cookie_value: str = cookie["gravity"].value
|
||||
cookie_expiry: datetime = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "")
|
||||
logging.info(
|
||||
f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}"
|
||||
)
|
||||
return {
|
||||
"gravity": cookie_value,
|
||||
"expires": cookie_expiry.timestamp(),
|
||||
"X-Grvt-Account-Id": grvt_account_id,
|
||||
}
|
||||
logging.warning(f"{FN} Invalid return_value {data=} {path=} {return_value=}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
async def get_cookie_with_expiration_async(
|
||||
path: str, api_key: str | None
|
||||
) -> dict[str, str | float | None] | None:
|
||||
"""
|
||||
Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token.
|
||||
:return: The session cookie.
|
||||
"""
|
||||
FN = f"get_cookie_with_expiration_async {path=}"
|
||||
if api_key:
|
||||
data = {}
|
||||
try:
|
||||
data = {"api_key": api_key}
|
||||
logging.info(f"{FN} ask for cookie {path=} {data=}")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url=path, json=data, timeout=5) as return_value:
|
||||
logging.info(f"{FN} {return_value=}")
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(return_value.headers.get("Set-Cookie", ""))
|
||||
cookie_value: str = cookie["gravity"].value
|
||||
cookie_expiry: datetime = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "")
|
||||
logging.info(
|
||||
f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}"
|
||||
)
|
||||
return {
|
||||
"gravity": cookie_value,
|
||||
"expires": cookie_expiry.timestamp(),
|
||||
"X-Grvt-Account-Id": grvt_account_id,
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class GrvtKind(Enum):
|
||||
PERPETUAL = 1
|
||||
FUTURE = 2
|
||||
CALL = 3
|
||||
PUT = 4
|
||||
SPOT = 5
|
||||
|
||||
|
||||
class GrvtCurrency(Enum):
|
||||
USD = 1
|
||||
USDC = 2
|
||||
USDT = 3
|
||||
ETH = 4
|
||||
BTC = 5
|
||||
|
||||
|
||||
def hexlify(data: bytes) -> str:
|
||||
"""Convert a byte array to a hex string with a 0x prefix."""
|
||||
return f"0x{data.hex()}"
|
||||
|
||||
|
||||
class EnumEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
"""
|
||||
Custom JSON encoder for Enum types.
|
||||
:param obj: Object to serialize.
|
||||
:return: Serialized object.
|
||||
"""
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
|
||||
|
||||
def get_kuq_from_symbol(symbol: str) -> tuple[str, str, str]:
|
||||
parts = symbol.split("_")
|
||||
if len(parts) == 3:
|
||||
underlying, quote, kind = parts
|
||||
if kind == "Perp":
|
||||
kind = "PERPETUAL"
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=} {kind=}")
|
||||
elif len(parts) == 4:
|
||||
underlying, quote, kind, time_str = parts
|
||||
if kind == "Fut":
|
||||
kind = "FUTURE"
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=} {kind=}")
|
||||
elif len(parts) == 5:
|
||||
underlying, quote, kind, time_str, strike_price = parts
|
||||
if kind in {"Call", "Put"}:
|
||||
kind = kind.upper()
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=} {kind=}")
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=}")
|
||||
return kind, underlying, quote
|
||||
|
||||
|
||||
# Custom types
|
||||
EIP712_ORDER_MESSAGE_TYPE = {
|
||||
"Order": [
|
||||
{"name": "subAccountID", "type": "uint64"},
|
||||
{"name": "isMarket", "type": "bool"},
|
||||
{"name": "timeInForce", "type": "uint8"},
|
||||
{"name": "postOnly", "type": "bool"},
|
||||
{"name": "reduceOnly", "type": "bool"},
|
||||
{"name": "legs", "type": "OrderLeg[]"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
"OrderLeg": [
|
||||
{"name": "assetID", "type": "uint256"},
|
||||
{"name": "contractSize", "type": "uint64"},
|
||||
{"name": "limitPrice", "type": "uint64"},
|
||||
{"name": "isBuyingContract", "type": "bool"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtSignature:
|
||||
# The address (public key) of the wallet signing the payload
|
||||
signer: str
|
||||
r: str
|
||||
s: str
|
||||
v: int
|
||||
# Timestamp after which this signature expires, expressed in unix nanoseconds.
|
||||
# Must be capped at 30 days
|
||||
expiration: str
|
||||
"""
|
||||
Users can randomly generate this value, used as a signature deconflicting key.
|
||||
ie. You can send the same exact instruction twice with different nonces.
|
||||
When the same nonce is used, the same payload will generate the same signature.
|
||||
Our system will consider the payload a duplicate, and ignore it.
|
||||
"""
|
||||
nonce: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderMetadata:
|
||||
"""
|
||||
Metadata fields are used to support Backend only operations.
|
||||
Hence, fields in here are never signed, and is never transmitted to the smart contract.
|
||||
"""
|
||||
|
||||
"""
|
||||
`client_order_id`: A unique identifier of an active order, specified by the client
|
||||
This is used to identify the order in the client's system
|
||||
This value must be unique for all active orders in a subaccount,
|
||||
otehrwise amendment / cancellation will not work as expected
|
||||
Gravity UI will generate a random clientOrderID for each order in the range [0, 2^63 - 1]
|
||||
To prevent any conflicts, client machines should generate a random clientOrderID
|
||||
in the range [2^63, 2^64 - 1].
|
||||
When GRVT Backend receives an order with duplicate `client_order_id`, it will reject the order
|
||||
with rejectReason set to duplicate `client_order_id`.
|
||||
"""
|
||||
client_order_id: str
|
||||
# [Filled by GRVT Backend] Time at which the order was received by GRVT in unix nanoseconds
|
||||
create_time: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtOrderLeg:
|
||||
# The instrument to trade in this leg
|
||||
instrument: str
|
||||
# The total number of contracts to trade in this leg, expressed in base currency units.
|
||||
size: Decimal
|
||||
# Specifies if the order leg is a buy or sell
|
||||
is_buying_asset: bool
|
||||
"""
|
||||
The limit price of the order leg, expressed in `9` decimals.
|
||||
This is the number of quote currency units to pay/receive for this leg.
|
||||
This should be `null/0` if the order is a market order
|
||||
"""
|
||||
limit_price: Decimal
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtOrder:
|
||||
"""
|
||||
Order is a typed payload used throughout the GRVT platform to express all orders.
|
||||
GRVT orders are capable of expressing both single-legged, and multi-legged orders by default.
|
||||
All fields in the Order payload (except `id`, `metadata`, and `state`) are trustlessly enforced
|
||||
on our Hyperchain.
|
||||
This minimizes the amount of trust users have to offer to GRVT.
|
||||
"""
|
||||
|
||||
# The subaccount initiating the order
|
||||
sub_account_id: str
|
||||
# Supported time_in_force : GTT, IOC, FOK:<ul>
|
||||
time_in_force: TimeInForce
|
||||
legs: list[GrvtOrderLeg]
|
||||
# The signature approving this order
|
||||
signature: GrvtSignature
|
||||
# Order Metadata, ignored by the smart contract, and unsigned by the client
|
||||
metadata: OrderMetadata
|
||||
# is_market: If the order is a market order
|
||||
is_market: bool
|
||||
post_only: bool = False
|
||||
# If True, Order must reduce the position size, or be cancelled
|
||||
reduce_only: bool = False
|
||||
|
||||
|
||||
def get_signable_message(
|
||||
order: GrvtOrder, env: GrvtEnv, instruments: dict[str, dict]
|
||||
) -> bytes | None:
|
||||
FN = f"get_signable_message {order=}"
|
||||
size_multiplier = BTC_ETH_SIZE_MULTIPLIER
|
||||
PRICE_MULTIPLIER = 1_000_000_000
|
||||
legs = []
|
||||
for leg in order.legs:
|
||||
instrument = instruments.get(leg.instrument)
|
||||
if not instrument or not isinstance(instrument, dict):
|
||||
logging.error(f"{FN}: {leg.instrument=} not found in {instruments=}")
|
||||
return None
|
||||
if "base_decimals" not in instrument:
|
||||
logging.error(f"{FN}: no 'base_decimals' in {instrument=}")
|
||||
return None
|
||||
size_multiplier = 10 ** instrument["base_decimals"]
|
||||
if "instrument_hash" not in instrument:
|
||||
logging.error(f"{FN}: no 'instrument_hash' in {instrument=}")
|
||||
return None
|
||||
legs.append(
|
||||
{
|
||||
"assetID": instrument["instrument_hash"],
|
||||
"contractSize": int(Decimal(leg.size) * Decimal(size_multiplier)),
|
||||
"limitPrice": int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER)),
|
||||
"isBuyingContract": leg.is_buying_asset,
|
||||
}
|
||||
)
|
||||
message_data = {
|
||||
"subAccountID": order.sub_account_id,
|
||||
"isMarket": order.is_market or False,
|
||||
"timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value,
|
||||
"postOnly": order.post_only or False,
|
||||
"reduceOnly": order.reduce_only or False,
|
||||
"legs": legs,
|
||||
"nonce": order.signature.nonce,
|
||||
"expiration": order.signature.expiration,
|
||||
}
|
||||
domain_data: dict[str, str | int]= get_EIP712_domain_data(env)
|
||||
logging.info(f"{FN} {domain_data=}\n{EIP712_ORDER_MESSAGE_TYPE=}\n{message_data=}")
|
||||
return encode_typed_data(domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data)
|
||||
|
||||
|
||||
def get_order_payload(
|
||||
order: GrvtOrder, private_key: str, env: GrvtEnv, instruments: dict[str, dict]
|
||||
) -> dict:
|
||||
signable_message = get_signable_message(order, env, instruments)
|
||||
if signable_message is None:
|
||||
raise ValueError("Failed to create signable message")
|
||||
signed_message = Account.sign_message(signable_message, private_key)
|
||||
order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.v = signed_message.v
|
||||
order.signature.signer = Account.from_key(private_key).address
|
||||
|
||||
return {
|
||||
"order": {
|
||||
"sub_account_id": str(order.sub_account_id),
|
||||
"is_market": order.is_market,
|
||||
"time_in_force": order.time_in_force.name,
|
||||
"post_only": order.post_only,
|
||||
"reduce_only": order.reduce_only,
|
||||
"legs": [
|
||||
{
|
||||
"instrument": leg.instrument,
|
||||
"size": str(leg.size),
|
||||
"limit_price": str(leg.limit_price),
|
||||
"is_buying_asset": bool(leg.is_buying_asset),
|
||||
}
|
||||
for leg in order.legs
|
||||
],
|
||||
"signature": {
|
||||
"r": order.signature.r,
|
||||
"s": order.signature.s,
|
||||
"v": order.signature.v,
|
||||
"expiration": order.signature.expiration,
|
||||
"nonce": order.signature.nonce,
|
||||
"signer": order.signature.signer,
|
||||
},
|
||||
"metadata": {
|
||||
"client_order_id": order.metadata.client_order_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_order_rpc_payload(
|
||||
order: GrvtOrder,
|
||||
private_key: str,
|
||||
env: GrvtEnv,
|
||||
instruments: dict[str, dict],
|
||||
version: str = "v1",
|
||||
) -> dict:
|
||||
order_payload = get_order_payload(order, private_key, env, instruments)
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": f"{version}/create_order",
|
||||
"params": order_payload,
|
||||
}
|
||||
|
||||
|
||||
def get_grvt_order(
|
||||
sub_account_id: str,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
limit_price: Num,
|
||||
order_duration_secs: float = 5 * 60,
|
||||
params: dict = {},
|
||||
) -> GrvtOrder:
|
||||
"""
|
||||
Creates an order for a specified symbol with the given limit price and size.
|
||||
|
||||
Args:
|
||||
symbol .
|
||||
limit_price (int): The limit price for the order.
|
||||
size (float): The size of the order.
|
||||
is_buying_asset(bool) : Buy or Sell.
|
||||
|
||||
Returns:
|
||||
Order: The created perpetual order.
|
||||
"""
|
||||
limit_price = limit_price or 0
|
||||
is_buying_asset = side == "buy"
|
||||
is_market = order_type == "market"
|
||||
leg = GrvtOrderLeg(
|
||||
instrument=symbol,
|
||||
size=round(Decimal(amount), 9),
|
||||
is_buying_asset=is_buying_asset,
|
||||
limit_price=round(Decimal(limit_price), 9),
|
||||
)
|
||||
|
||||
# create an expiry time
|
||||
time_in_force = TimeInForce.GOOD_TILL_TIME
|
||||
if "time_in_force" in params:
|
||||
time_in_force = TimeInForce[params["time_in_force"]]
|
||||
post_only: bool = False
|
||||
if "post_only" in params:
|
||||
post_only = params["post_only"]
|
||||
reduce_only: bool = False
|
||||
if "reduce_only" in params:
|
||||
reduce_only = params["reduce_only"]
|
||||
expiry_ns: int = 0
|
||||
if order_duration_secs:
|
||||
expiry_ns = time.time_ns() + int(order_duration_secs * DURATION_SECOND_IN_NSEC)
|
||||
if "client_order_id" in params:
|
||||
client_order_id = int(params["client_order_id"])
|
||||
else:
|
||||
client_order_id = rand_uint32()
|
||||
signature = GrvtSignature(
|
||||
signer="",
|
||||
r="",
|
||||
s="",
|
||||
v=0,
|
||||
expiration=str(expiry_ns),
|
||||
nonce=rand_uint32(),
|
||||
)
|
||||
metadata = OrderMetadata(client_order_id=str(client_order_id))
|
||||
return GrvtOrder(
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=time_in_force,
|
||||
legs=[leg],
|
||||
signature=signature,
|
||||
metadata=metadata,
|
||||
is_market=is_market,
|
||||
post_only=post_only,
|
||||
reduce_only=reduce_only,
|
||||
)
|
||||
|
||||
def sign_derisk_mm_ratio_request(
|
||||
env: GrvtEnv, sub_account_id: int, ratio: str, private_key_hex: str
|
||||
):
|
||||
"""
|
||||
Generate a signature for setting the derisk to maintenance margin ratio.
|
||||
|
||||
:param sub_account_id: The sub-account ID to set the ratio for.
|
||||
:param ratio: The derisk to maintenance margin ratio as a string (e.g., "2.0").
|
||||
:param private_key_hex: The private key in hexadecimal format.
|
||||
:return: A dictionary containing the signature for the payload.
|
||||
"""
|
||||
derisk_ratio_int = int(Decimal(ratio) * 1_000_000)
|
||||
expiration_ns = int((time.time() + 86400) * 1_000_000_000)
|
||||
nonce = random.randint(1, 2**32 - 1)
|
||||
|
||||
domain_data = get_EIP712_domain_data(env)
|
||||
|
||||
types = {
|
||||
"SetDeriskToMaintenanceMarginRatio": [
|
||||
{"name": "subAccountID", "type": "uint64"},
|
||||
{"name": "deriskToMaintenanceMarginRatio", "type": "uint32"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
]
|
||||
}
|
||||
|
||||
signature_payload = {
|
||||
"subAccountID": sub_account_id,
|
||||
"deriskToMaintenanceMarginRatio": derisk_ratio_int,
|
||||
"nonce": nonce,
|
||||
"expiration": expiration_ns,
|
||||
}
|
||||
|
||||
message = encode_typed_data(domain_data, types, signature_payload)
|
||||
signed = Account.sign_message(message, private_key_hex)
|
||||
signer = Account.from_key(private_key_hex)
|
||||
|
||||
return {
|
||||
"signer": signer.address.lower(),
|
||||
"r": hex(signed.r),
|
||||
"s": hex(signed.s),
|
||||
"v": signed.v,
|
||||
"expiration": str(expiration_ns),
|
||||
"nonce": nonce,
|
||||
}
|
||||
@@ -1,785 +0,0 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
from asyncio.events import AbstractEventLoop
|
||||
from collections.abc import Callable
|
||||
from decimal import Decimal
|
||||
|
||||
import websockets
|
||||
|
||||
# import requests
|
||||
# from env import ENDPOINTS
|
||||
from .grvt_ccxt_env import (
|
||||
GRVT_WS_STREAMS,
|
||||
GrvtEnv,
|
||||
GrvtWSEndpointType,
|
||||
get_grvt_ws_endpoint,
|
||||
is_trading_ws_endpoint,
|
||||
)
|
||||
from .grvt_ccxt_pro import GrvtCcxtPro
|
||||
from .grvt_ccxt_types import (
|
||||
GrvtInvalidOrder,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
from .grvt_ccxt_utils import get_order_rpc_payload
|
||||
|
||||
WS_READ_TIMEOUT = 5
|
||||
|
||||
|
||||
class GrvtCcxtWS(GrvtCcxtPro):
|
||||
"""
|
||||
GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode.
|
||||
|
||||
Args:
|
||||
env: GrvtCcxtPro (DEV, TESTNET, PROD)
|
||||
parameters: dict with trading_account_id, private_key, api_key etc
|
||||
|
||||
Examples:
|
||||
>>> from grvt_api_pro import GrvtCcxtPro
|
||||
>>> from grvt_env import GrvtEnv
|
||||
>>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET)
|
||||
>>> await grvt.fetch_markets()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: GrvtEnv,
|
||||
loop: AbstractEventLoop,
|
||||
logger: logging.Logger | None = None,
|
||||
parameters: dict = {},
|
||||
):
|
||||
"""Initialize the GrvtCcxt instance."""
|
||||
super().__init__(env, logger, parameters)
|
||||
self._loop = loop
|
||||
self._clsname: str = type(self).__name__
|
||||
self.api_ws_version = parameters.get("api_ws_version", "v1")
|
||||
self.force_reconnect_flag: bool = False
|
||||
self.ws: dict[GrvtWSEndpointType, websockets.WebSocketClientProtocol | None] = {}
|
||||
self.callbacks: dict[GrvtWSEndpointType, dict[str, dict[str, Callable]]] = {}
|
||||
self.subscribed_streams: dict[GrvtWSEndpointType, dict] = {}
|
||||
self.api_url: dict[GrvtWSEndpointType, str] = {}
|
||||
self._last_message: dict[str, dict] = {}
|
||||
self._request_id = 0
|
||||
self.endpoint_types = [
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]
|
||||
# Initialize dictionaries for each endpoint type
|
||||
for grvt_endpoint_type in self.endpoint_types:
|
||||
self.api_url[grvt_endpoint_type] = get_grvt_ws_endpoint(
|
||||
self.env.value, grvt_endpoint_type
|
||||
)
|
||||
self.callbacks[grvt_endpoint_type] = {}
|
||||
self.subscribed_streams[grvt_endpoint_type] = {}
|
||||
self.ws[grvt_endpoint_type] = None
|
||||
self._loop.create_task(self._read_messages(grvt_endpoint_type))
|
||||
self.logger.info(f"{self._clsname} initialized {self.api_url=}")
|
||||
self.logger.info(f"{self._clsname} initialized {self.ws=}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self._clsname} {self.env=} {self.api_ws_version=}"
|
||||
|
||||
async def __aexit__(self):
|
||||
for grvt_endpoint_type in self.endpoint_types:
|
||||
await self._close_connection(grvt_endpoint_type)
|
||||
|
||||
def force_reconnect(self) -> None:
|
||||
self.force_reconnect_flag = True
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
Prepares the GrvtCcxtPro instance and connects to WS server.
|
||||
"""
|
||||
await self.load_markets()
|
||||
await self.refresh_cookie()
|
||||
self._loop.create_task(self.connect_all_channels())
|
||||
|
||||
def is_connection_open(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
|
||||
return (
|
||||
self.ws[grvt_endpoint_type] is not None and self.ws[grvt_endpoint_type].open
|
||||
)
|
||||
|
||||
def is_endpoint_connected(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
|
||||
"""
|
||||
For MARKET_DATA returns True if connection is open.
|
||||
for TRADE_DATA returns True if one of the following is true:
|
||||
1. No cookie - this means this is public connection and we can't connect to TRADE_DATA
|
||||
2. Connection to TRADE_DATA is open
|
||||
"""
|
||||
if grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
]:
|
||||
return self.is_connection_open(grvt_endpoint_type)
|
||||
if grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]:
|
||||
return bool(not self._cookie or self.is_connection_open(grvt_endpoint_type))
|
||||
raise ValueError(f"Unknown endpoint type {grvt_endpoint_type}")
|
||||
|
||||
def are_endpoints_connected(
|
||||
self, grvt_endpoint_types: list[GrvtWSEndpointType]
|
||||
) -> bool:
|
||||
return all(
|
||||
self.is_endpoint_connected(endpoint) for endpoint in grvt_endpoint_types
|
||||
)
|
||||
|
||||
async def connect_all_channels(self) -> None:
|
||||
"""
|
||||
Connects to all channels that are possible to connect.
|
||||
If cookie is NOT available, it will NOT connect to GrvtWSEndpointType.TRADE_DATA
|
||||
For trading connection: run this method after cookie is available.
|
||||
"""
|
||||
FN = "connect_all_channels"
|
||||
while True:
|
||||
try:
|
||||
for end_point_type in self.endpoint_types:
|
||||
if (
|
||||
not self.is_endpoint_connected(end_point_type)
|
||||
or self.force_reconnect_flag
|
||||
):
|
||||
await self._reconnect(end_point_type)
|
||||
all_are_connected = self.are_endpoints_connected(self.endpoint_types)
|
||||
self.logger.info(
|
||||
f"{FN} Connection status: {all_are_connected=} {self.force_reconnect_flag=}"
|
||||
)
|
||||
self.force_reconnect_flag = False
|
||||
except Exception as e:
|
||||
self.logger.exception(f"{FN} {e=}")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def connect_channel(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
|
||||
FN = f"{self._clsname} connect_channel {grvt_endpoint_type}"
|
||||
try:
|
||||
if self.is_endpoint_connected(grvt_endpoint_type):
|
||||
self.logger.info(f"{FN} Already connected")
|
||||
return True
|
||||
self.subscribed_streams[grvt_endpoint_type] = {}
|
||||
extra_headers = {}
|
||||
if self._cookie:
|
||||
extra_headers = {"Cookie": f"gravity={self._cookie['gravity']}"}
|
||||
if self._cookie["X-Grvt-Account-Id"]:
|
||||
extra_headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
|
||||
)
|
||||
if grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]:
|
||||
if self._cookie:
|
||||
self.ws[grvt_endpoint_type] = await websockets.connect(
|
||||
uri=self.api_url[grvt_endpoint_type],
|
||||
extra_headers=extra_headers,
|
||||
logger=self.logger,
|
||||
open_timeout=5,
|
||||
)
|
||||
self.logger.info(
|
||||
f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}"
|
||||
)
|
||||
else:
|
||||
self.logger.info(f"{FN} Waiting for cookie.")
|
||||
elif grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
]:
|
||||
self.ws[grvt_endpoint_type] = await websockets.connect(
|
||||
uri=self.api_url[grvt_endpoint_type],
|
||||
extra_headers=extra_headers,
|
||||
logger=self.logger,
|
||||
open_timeout=5,
|
||||
)
|
||||
self.logger.info(f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}")
|
||||
except (
|
||||
websockets.exceptions.ConnectionClosedOK,
|
||||
websockets.exceptions.ConnectionClosed,
|
||||
) as e:
|
||||
self.logger.info(f"{FN} connection already closed:{e}")
|
||||
self.ws[grvt_endpoint_type] = None
|
||||
except Exception as e:
|
||||
self.logger.warning(f"{FN} error:{e} traceback:{traceback.format_exc()}")
|
||||
self.ws[grvt_endpoint_type] = None
|
||||
# return True if connection successful
|
||||
return self.is_endpoint_connected(grvt_endpoint_type)
|
||||
|
||||
async def _close_connection(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
try:
|
||||
if self.ws[grvt_endpoint_type]:
|
||||
self.logger.info(f"{self._clsname} Closing connection...")
|
||||
await self.ws[grvt_endpoint_type].close()
|
||||
self.subscribed_streams[grvt_endpoint_type] = {}
|
||||
self.logger.info(f"{self._clsname} Connection closed")
|
||||
else:
|
||||
self.logger.info(f"{self._clsname} No connection to close")
|
||||
except Exception:
|
||||
self.logger.exception(
|
||||
f"{self._clsname} Error when closing connection {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
async def _reconnect(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
FN = f"{self._clsname} _reconnect {grvt_endpoint_type=}"
|
||||
try:
|
||||
self.logger.info(f"{FN} STARTS")
|
||||
await self._close_connection(grvt_endpoint_type)
|
||||
success: bool = await self.connect_channel(grvt_endpoint_type)
|
||||
if success:
|
||||
await self._resubscribe(grvt_endpoint_type)
|
||||
except Exception:
|
||||
self.logger.exception(f"{FN} failed {traceback.format_exc()}")
|
||||
|
||||
async def _resubscribe(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
if self.is_connection_open(grvt_endpoint_type):
|
||||
for versioned_stream in self.callbacks[grvt_endpoint_type]:
|
||||
for selector in self.callbacks[grvt_endpoint_type][versioned_stream]:
|
||||
self.logger.info(
|
||||
f"{self._clsname} _resubscribe {grvt_endpoint_type=}"
|
||||
f" {versioned_stream=}/{selector=}"
|
||||
)
|
||||
await self._subscribe_to_stream(
|
||||
grvt_endpoint_type, versioned_stream, selector
|
||||
)
|
||||
else:
|
||||
self.logger.warning(f"{self._clsname} _resubscribe - No connection.")
|
||||
|
||||
# **************** PUBLIC API CALLS
|
||||
def is_stream_subscribed(
|
||||
self, grvt_endpoint_type: GrvtWSEndpointType, stream: str
|
||||
) -> bool:
|
||||
versioned_stream = self.get_versioned_stream(stream)
|
||||
return self.subscribed_streams.get(grvt_endpoint_type, {}).get(
|
||||
versioned_stream, False
|
||||
)
|
||||
|
||||
def _check_susbcribed_stream(
|
||||
self, grvt_endpoint_type: GrvtWSEndpointType, message: dict
|
||||
) -> None:
|
||||
stream_subscribed: str = ""
|
||||
if "stream" in message:
|
||||
stream_subscribed = message["stream"]
|
||||
elif "result" in message and "stream" in message["result"]:
|
||||
stream_subscribed = message.get("result", {}).get("stream", "")
|
||||
if stream_subscribed:
|
||||
if not self.subscribed_streams[grvt_endpoint_type].get(stream_subscribed):
|
||||
self.logger.info(
|
||||
f"{self._clsname} subscribed to stream:{stream_subscribed}"
|
||||
)
|
||||
self.subscribed_streams[grvt_endpoint_type][stream_subscribed] = True
|
||||
|
||||
async def _read_messages(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
FN = f"{self._clsname} _read_messages {grvt_endpoint_type.value}"
|
||||
while True:
|
||||
if self.is_connection_open(grvt_endpoint_type):
|
||||
try:
|
||||
self.logger.debug(f"{FN} waiting for message")
|
||||
response = await asyncio.wait_for(
|
||||
self.ws[grvt_endpoint_type].recv(), timeout=WS_READ_TIMEOUT
|
||||
)
|
||||
message = json.loads(response)
|
||||
self.logger.debug(f"{FN} received {message=}")
|
||||
self._check_susbcribed_stream(grvt_endpoint_type, message)
|
||||
if "feed" in message:
|
||||
stream_subscribed: str | None = message.get("stream")
|
||||
selector: str = message.get("selector")
|
||||
if stream_subscribed is None:
|
||||
self.logger.warning(f"{FN} missing stream in {message=}")
|
||||
if selector is None:
|
||||
self.logger.warning(f"{FN} missing selector in {message=}")
|
||||
if stream_subscribed and selector:
|
||||
callback = (
|
||||
self.callbacks[grvt_endpoint_type]
|
||||
.get(stream_subscribed, {})
|
||||
.get(selector, None)
|
||||
)
|
||||
if callback:
|
||||
await callback(message)
|
||||
stream: str = self.get_non_versioned_stream(
|
||||
stream_subscribed
|
||||
)
|
||||
self._last_message[stream] = message
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"{FN} No callback for {stream_subscribed=}/{selector=}"
|
||||
)
|
||||
elif "jsonrpc" in message:
|
||||
"""
|
||||
{'jsonrpc': '', 'result': {'result':
|
||||
{'order_id': '0x00', 'sub_account_id': '8751933338735530',
|
||||
'is_market': False, 'time_in_force': 'GOOD_TILL_TIME', 'post_only': False,
|
||||
'reduce_only': False, 'legs': [{'instrument': 'BTC_USDT_Perp', 'size': '0.001',
|
||||
'limit_price': '50000.0', 'is_buying_asset': True}],
|
||||
'signature': {'signer': '0x2989e3783e2ae05f9a1538dd411a22a4cd9554ad',
|
||||
'r': '0xa566702c1e5557ab96e8d5197b6871456765a80556bba46c9d4928bd573ca66c',
|
||||
's': '0x6f6e0be6dca125643fce884ca28c0ae341b201efe49e10a9626859517b4a09af',
|
||||
'v': 28, 'expiration': '1729005262433997000', 'nonce': 3898454329},
|
||||
'metadata': {'client_order_id': '123', 'create_time': '1728918862633971628'},
|
||||
'state': {'status': 'OPEN', 'reject_reason': 'UNSPECIFIED',
|
||||
'book_size': ['0.001'], 'traded_size': ['0.0'], 'update_time': '1728918862633971628'}}},
|
||||
'id': 2}
|
||||
"""
|
||||
self.logger.debug(f"{FN} jsonrpc result:{message.get('result')}")
|
||||
else:
|
||||
self.logger.info(f"{FN} Non-actionable message:{message}")
|
||||
except (
|
||||
websockets.exceptions.ConnectionClosedError,
|
||||
websockets.exceptions.ConnectionClosedOK,
|
||||
):
|
||||
self.logger.exception(
|
||||
f"{FN} connection closed {traceback.format_exc()}"
|
||||
)
|
||||
await self._reconnect(grvt_endpoint_type)
|
||||
except asyncio.TimeoutError: # noqa: UP041
|
||||
self.logger.debug(f"{FN} Timeout {WS_READ_TIMEOUT} secs")
|
||||
pass
|
||||
except Exception:
|
||||
self.logger.exception(
|
||||
f"{FN} connection failed {traceback.format_exc()}"
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
self.logger.info(f"{FN} connection not open")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def _send(self, end_point_type: GrvtWSEndpointType, message: str):
|
||||
try:
|
||||
if self.ws[end_point_type] and self.ws[end_point_type].open:
|
||||
self.logger.info(
|
||||
f"{self._clsname} _send() {end_point_type=}"
|
||||
f" url:{self.api_url[end_point_type]} {message=}"
|
||||
)
|
||||
await self.ws[end_point_type].send(message)
|
||||
except websockets.exceptions.ConnectionClosedError as e:
|
||||
self.logger.info(f"{self._clsname} _send() Restarted connection {e}")
|
||||
await self._reconnect(end_point_type)
|
||||
if self.ws[end_point_type]:
|
||||
self.logger.info(
|
||||
f"{self._clsname} _send() RESEND on RECONNECT {end_point_type=}"
|
||||
f" url:{self.api_url[end_point_type]} {message=}"
|
||||
)
|
||||
await self.ws[end_point_type].send(message)
|
||||
except Exception:
|
||||
self.logger.exception(f"{self._clsname} send failed {traceback.format_exc()}")
|
||||
await self._reconnect(end_point_type)
|
||||
|
||||
def _construct_selector(self, stream: str, params: dict) -> str:
|
||||
feed: str = ""
|
||||
# ******** Market Data ********
|
||||
if stream.endswith(("mini.s", "mini.d", "ticker.s", "ticker.d")):
|
||||
feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}"
|
||||
if stream.endswith("book.s"):
|
||||
feed = (
|
||||
f"{params.get('instrument', '')}@{params.get('rate', '500')}-"
|
||||
f"{params.get('depth', '10')}"
|
||||
)
|
||||
if stream.endswith("book.d"):
|
||||
feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}"
|
||||
if stream.endswith("trade"):
|
||||
feed = f"{params.get('instrument', '')}@{params.get('limit', '50')}"
|
||||
if stream.endswith("candle"):
|
||||
feed = (
|
||||
f"{params.get('instrument', '')}@{params.get('interval', 'CI_1_M')}-"
|
||||
f"{params.get('type', 'TRADE')}"
|
||||
)
|
||||
# ******** Trade Data ********
|
||||
if stream.endswith(("order", "state", "position", "fill")):
|
||||
if not params:
|
||||
feed = f"{self._trading_account_id}"
|
||||
elif params.get("instrument"):
|
||||
feed = f"{self._trading_account_id}-{params.get('instrument', '')}"
|
||||
else:
|
||||
feed = (
|
||||
f"{self._trading_account_id}-{params.get('kind', '')}-"
|
||||
f"{params.get('base', '')}-{params.get('quote', '')}"
|
||||
)
|
||||
# Deposit, Transfer, Withdrawal
|
||||
if stream.endswith(("deposit", "transfer", "withdrawal")):
|
||||
feed = ""
|
||||
# f"{params.get('sub_account_id', '')}-{params.get('main_account_id', '')}"
|
||||
|
||||
return feed
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
stream: str,
|
||||
callback: Callable,
|
||||
ws_end_point_type: GrvtWSEndpointType | None = None,
|
||||
params: dict = {},
|
||||
) -> None:
|
||||
"""
|
||||
Subscribe to a stream with optional parameters.
|
||||
Call the callback function when a message is received.
|
||||
callback function should have the following signature:
|
||||
(dict) -> None.
|
||||
"""
|
||||
FN = f"{self._clsname} subscribe {stream=}"
|
||||
if not ws_end_point_type: # use default endpoint type
|
||||
ws_end_point_type = GRVT_WS_STREAMS.get(stream)
|
||||
if not ws_end_point_type:
|
||||
self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}")
|
||||
return
|
||||
is_trade_data = is_trading_ws_endpoint(ws_end_point_type)
|
||||
if is_trade_data and not self._trading_account_id:
|
||||
self.logger.error(
|
||||
f"{FN} {stream=} is a trading data connection. Requires trading_account_id."
|
||||
)
|
||||
return
|
||||
# create selector string and register callback
|
||||
selector: str = self._construct_selector(stream, params)
|
||||
versioned_stream: str = self.get_versioned_stream(stream)
|
||||
if versioned_stream not in self.callbacks[ws_end_point_type]:
|
||||
self.callbacks[ws_end_point_type][versioned_stream] = {}
|
||||
self.callbacks[ws_end_point_type][versioned_stream][selector] = callback
|
||||
self.logger.info(
|
||||
f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}"
|
||||
)
|
||||
# check if connection is open and subscribe
|
||||
if self.is_connection_open(ws_end_point_type):
|
||||
await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector)
|
||||
else:
|
||||
self.logger.info(f"{FN} Connection not open. Will subscribe on connect.")
|
||||
|
||||
async def re_subscribe_stream(
|
||||
self,
|
||||
stream: str,
|
||||
callback: Callable,
|
||||
ws_end_point_type: GrvtWSEndpointType | None = None,
|
||||
params: dict = {},
|
||||
) -> None:
|
||||
""" This method should be called in a separate task -
|
||||
otherwise it will block the event loop for 5 seconds.
|
||||
Unsubscribe from a specific stream and subscribe again with optional parameters.
|
||||
Call the callback function when a message is received.
|
||||
callback function should have the following signature:
|
||||
(dict) -> None.
|
||||
"""
|
||||
FN = f"{self._clsname} re_subscribe {stream=}"
|
||||
if not ws_end_point_type: # use default endpoint type
|
||||
ws_end_point_type = GRVT_WS_STREAMS.get(stream)
|
||||
if not ws_end_point_type:
|
||||
self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}")
|
||||
return
|
||||
if not self.is_connection_open(ws_end_point_type):
|
||||
self.logger.info(f"{FN} {ws_end_point_type=} not open. Try again after connect.")
|
||||
return
|
||||
is_trade_data = is_trading_ws_endpoint(ws_end_point_type)
|
||||
if is_trade_data and not self._trading_account_id:
|
||||
self.logger.error(
|
||||
f"{FN} {stream=} is a trading data connection. Requires trading_account_id."
|
||||
)
|
||||
return
|
||||
# create selector string and register callback
|
||||
selector: str = self._construct_selector(stream, params)
|
||||
versioned_stream: str = self.get_versioned_stream(stream)
|
||||
if versioned_stream not in self.callbacks[ws_end_point_type]:
|
||||
self.callbacks[ws_end_point_type][versioned_stream] = {}
|
||||
self.callbacks[ws_end_point_type][versioned_stream][selector] = callback
|
||||
# self.logger.info(
|
||||
# f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}"
|
||||
# )
|
||||
await self._unsubscribe_to_stream(ws_end_point_type, versioned_stream, selector)
|
||||
await asyncio.sleep(5) # wait for unsubscribe to complete
|
||||
await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector)
|
||||
|
||||
|
||||
def get_versioned_stream(self, stream: str) -> str:
|
||||
return (
|
||||
stream if self.api_ws_version == "v0" else f"{self.api_ws_version}.{stream}"
|
||||
)
|
||||
|
||||
def get_non_versioned_stream(self, versioned_stream: str) -> str:
|
||||
if self.api_ws_version == "v0":
|
||||
return versioned_stream
|
||||
return versioned_stream.split(".")[1]
|
||||
|
||||
async def _subscribe_to_stream(
|
||||
self,
|
||||
ws_end_point_type: GrvtWSEndpointType,
|
||||
versioned_stream: str,
|
||||
selector: str,
|
||||
) -> None:
|
||||
FN = (
|
||||
f"{self._clsname} _subscribe_to_stream {ws_end_point_type=}"
|
||||
f" {versioned_stream=} {selector=}"
|
||||
)
|
||||
self._request_id += 1
|
||||
if ws_end_point_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
]: # Legacy subscription
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"request_id": self._request_id,
|
||||
"stream": versioned_stream,
|
||||
"feed": [selector],
|
||||
"method": "subscribe",
|
||||
"is_full": True,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
else: # RPC WS format
|
||||
self._request_id += 1
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "subscribe",
|
||||
"params": {
|
||||
"stream": versioned_stream,
|
||||
"selectors": [selector],
|
||||
},
|
||||
"id": self._request_id,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
await self._send(ws_end_point_type, subscribe_json)
|
||||
stream: str = self.get_non_versioned_stream(versioned_stream)
|
||||
if stream not in self._last_message:
|
||||
self._last_message[stream] = {}
|
||||
|
||||
async def _unsubscribe_to_stream(
|
||||
self,
|
||||
ws_end_point_type: GrvtWSEndpointType,
|
||||
versioned_stream: str,
|
||||
selector: str,
|
||||
) -> None:
|
||||
FN = (
|
||||
f"{self._clsname} _unsubscribe_to_stream {ws_end_point_type=}"
|
||||
f" {versioned_stream=} {selector=}"
|
||||
)
|
||||
self._request_id += 1
|
||||
if ws_end_point_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
]: # Legacy subscription
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"request_id": self._request_id,
|
||||
"stream": versioned_stream,
|
||||
"feed": [selector],
|
||||
"method": "unsubscribe",
|
||||
"is_full": True,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
else: # RPC WS format
|
||||
self._request_id += 1
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "unsubscribe",
|
||||
"params": {
|
||||
"stream": versioned_stream,
|
||||
"selectors": [selector],
|
||||
},
|
||||
"id": self._request_id,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
await self._send(ws_end_point_type, subscribe_json)
|
||||
|
||||
def jsonrpc_wrap_payload(
|
||||
self, payload: dict, method: str, version: str = "v1"
|
||||
) -> dict:
|
||||
"""
|
||||
Wrap the payload in JSON-RPC format.
|
||||
"""
|
||||
self._request_id += 1
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": f"{version}/{method}",
|
||||
"params": payload,
|
||||
"id": self._request_id,
|
||||
}
|
||||
|
||||
async def send_rpc_message(
|
||||
self, end_point_type: GrvtWSEndpointType, message: dict
|
||||
) -> None:
|
||||
"""
|
||||
Send a message to the server.
|
||||
"""
|
||||
await self._send(end_point_type, json.dumps(message))
|
||||
self.logger.info(f"{self._clsname} send_rpc_message {end_point_type=} {message=}")
|
||||
|
||||
async def rpc_create_order(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: float | Decimal | str | int,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""
|
||||
Create an order.
|
||||
"""
|
||||
FN = f"{self._clsname} rpc_create_order"
|
||||
if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL):
|
||||
raise GrvtInvalidOrder("Trade data connection not available.")
|
||||
order = self._get_order_with_validations(
|
||||
symbol, order_type, side, amount, price, params
|
||||
)
|
||||
self.logger.info(f"{FN} {order=}")
|
||||
payload = get_order_rpc_payload(order, self._private_key, self.env, self.markets)
|
||||
self._request_id += 1
|
||||
payload["id"] = self._request_id
|
||||
self.logger.info(f"{FN} {payload=}")
|
||||
await self.send_rpc_message(GrvtWSEndpointType.TRADE_DATA_RPC_FULL, payload)
|
||||
return payload
|
||||
|
||||
async def rpc_create_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
side: GrvtOrderSide,
|
||||
amount: float | Decimal | str | int,
|
||||
price: Num,
|
||||
params={},
|
||||
) -> dict:
|
||||
return await self.rpc_create_order(symbol, "limit", side, amount, price, params)
|
||||
|
||||
async def rpc_cancel_all_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature BUT lacks symbol
|
||||
Cancel all orders for a sub-account.
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# FN = f"{self._clsname} rpc_cancel_all_orders"
|
||||
payload: dict = self._get_payload_cancel_all_orders(params)
|
||||
jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="cancel_all_orders")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
|
||||
async def rpc_cancel_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Cancel specific order for the account by sending JsonRpc call on WebSocket.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
id (str): exchange assigned order ID<br>
|
||||
symbol (str): trading symbol<br>
|
||||
params:
|
||||
* client_order_id (str): client assigned order ID<br>
|
||||
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
|
||||
Returns:
|
||||
payload used to cancel order.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} rpc_cancel_order"
|
||||
if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL):
|
||||
raise GrvtInvalidOrder("Trade data connection not available.")
|
||||
self._check_account_auth()
|
||||
# Prepare payload
|
||||
payload: dict = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = str(id)
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
|
||||
if "time_to_live_ms" in params:
|
||||
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
|
||||
# Send cancel requiest
|
||||
jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="cancel_order")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
|
||||
async def rpc_fetch_open_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Fetch open orders for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>.
|
||||
Fetches open orders for the account.<br>
|
||||
Sends JsonRpc call on WebSocket.<br>
|
||||
Args:
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values are 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch orders
|
||||
for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
Returns:
|
||||
payload used to fetch open orders.<br><br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload: dict = self._get_payload_fetch_open_orders(symbol=None, params=params)
|
||||
jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="open_orders")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
|
||||
async def rpc_fetch_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Get Order](https://api-docs.grvt.io/trading_api/#get-order)
|
||||
for details.<br>.
|
||||
Get Order status by either order_id or client_order_id.<br>
|
||||
Sends JsonRpc call on WebSocket.<br>
|
||||
Args:
|
||||
id: (str) order_id to fetch.<br>
|
||||
symbol: (str) NOT SUPPRTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`client_order_id` (int): client assigned order ID.<br>
|
||||
Returns:
|
||||
payload used to fetch order.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} rpc_fetch_order"
|
||||
self._check_account_auth()
|
||||
payload = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = id
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(
|
||||
f"{FN} requires either order_id or params['client_order_id']"
|
||||
)
|
||||
jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="order")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
@@ -1,25 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .grvt_raw_types import Signature, TransferType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transfer:
|
||||
# The account to transfer from
|
||||
from_account_id: str
|
||||
# The subaccount to transfer from (0 if transferring from main account)
|
||||
from_sub_account_id: str
|
||||
# The account to deposit into
|
||||
to_account_id: str
|
||||
# The subaccount to transfer to (0 if transferring to main account)
|
||||
to_sub_account_id: str
|
||||
# The token currency to transfer
|
||||
currency: str
|
||||
# The number of tokens to transfer
|
||||
num_tokens: str
|
||||
# The signature of the transfer
|
||||
signature: Signature
|
||||
# The type of transfer
|
||||
transfer_type: TransferType
|
||||
# The metadata of the transfer
|
||||
transfer_metadata: str
|
||||
@@ -1,365 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
from dacite import Config, from_dict
|
||||
|
||||
from . import grvt_raw_types as types
|
||||
from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawAsyncBase
|
||||
|
||||
# mypy: disable-error-code="no-any-return"
|
||||
|
||||
|
||||
class GrvtRawAsync(GrvtRawAsyncBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
self.md_rpc = self.env.market_data.rpc_endpoint
|
||||
self.td_rpc = self.env.trade_data.rpc_endpoint
|
||||
|
||||
async def get_instrument_v1(
|
||||
self, req: types.ApiGetInstrumentRequest
|
||||
) -> types.ApiGetInstrumentResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/instrument", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def get_all_instruments_v1(
|
||||
self, req: types.ApiGetAllInstrumentsRequest
|
||||
) -> types.ApiGetAllInstrumentsResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/all_instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def get_filtered_instruments_v1(
|
||||
self, req: types.ApiGetFilteredInstrumentsRequest
|
||||
) -> types.ApiGetFilteredInstrumentsResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def get_currency_v1(
|
||||
self, req: types.ApiGetCurrencyRequest
|
||||
) -> types.ApiGetCurrencyResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/currency", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def mini_ticker_v1(
|
||||
self, req: types.ApiMiniTickerRequest
|
||||
) -> types.ApiMiniTickerResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/mini", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def ticker_v1(
|
||||
self, req: types.ApiTickerRequest
|
||||
) -> types.ApiTickerResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/ticker", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def orderbook_levels_v1(
|
||||
self, req: types.ApiOrderbookLevelsRequest
|
||||
) -> types.ApiOrderbookLevelsResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/book", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def trade_v1(
|
||||
self, req: types.ApiTradeRequest
|
||||
) -> types.ApiTradeResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/trade", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def trade_history_v1(
|
||||
self, req: types.ApiTradeHistoryRequest
|
||||
) -> types.ApiTradeHistoryResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/trade_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def candlestick_v1(
|
||||
self, req: types.ApiCandlestickRequest
|
||||
) -> types.ApiCandlestickResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/kline", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def funding_rate_v1(
|
||||
self, req: types.ApiFundingRateRequest
|
||||
) -> types.ApiFundingRateResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/funding", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def create_order_v1(
|
||||
self, req: types.ApiCreateOrderRequest
|
||||
) -> types.ApiCreateOrderResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/create_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def cancel_order_v1(
|
||||
self, req: types.ApiCancelOrderRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def cancel_all_orders_v1(
|
||||
self, req: types.ApiCancelAllOrdersRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def get_order_v1(
|
||||
self, req: types.ApiGetOrderRequest
|
||||
) -> types.ApiGetOrderResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def open_orders_v1(
|
||||
self, req: types.ApiOpenOrdersRequest
|
||||
) -> types.ApiOpenOrdersResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/open_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def order_history_v1(
|
||||
self, req: types.ApiOrderHistoryRequest
|
||||
) -> types.ApiOrderHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/order_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def cancel_on_disconnect_v1(
|
||||
self, req: types.ApiCancelOnDisconnectRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def fill_history_v1(
|
||||
self, req: types.ApiFillHistoryRequest
|
||||
) -> types.ApiFillHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/fill_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def positions_v1(
|
||||
self, req: types.ApiPositionsRequest
|
||||
) -> types.ApiPositionsResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/positions", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def funding_payment_history_v1(
|
||||
self, req: types.ApiFundingPaymentHistoryRequest
|
||||
) -> types.ApiFundingPaymentHistoryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/funding_payment_history", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def deposit_history_v1(
|
||||
self, req: types.ApiDepositHistoryRequest
|
||||
) -> types.ApiDepositHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/deposit_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def transfer_v1(
|
||||
self, req: types.ApiTransferRequest
|
||||
) -> types.ApiTransferResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/transfer", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def transfer_history_v1(
|
||||
self, req: types.ApiTransferHistoryRequest
|
||||
) -> types.ApiTransferHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/transfer_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def withdrawal_v1(
|
||||
self, req: types.ApiWithdrawalRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def withdrawal_history_v1(
|
||||
self, req: types.ApiWithdrawalHistoryRequest
|
||||
) -> types.ApiWithdrawalHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def sub_account_summary_v1(
|
||||
self, req: types.ApiSubAccountSummaryRequest
|
||||
) -> types.ApiSubAccountSummaryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def sub_account_history_v1(
|
||||
self, req: types.ApiSubAccountHistoryRequest
|
||||
) -> types.ApiSubAccountHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/account_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def aggregated_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiAggregatedAccountSummaryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/aggregated_account_summary", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def funding_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiFundingAccountSummaryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/funding_account_summary", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def set_derisk_mm_ratio_v1(
|
||||
self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest
|
||||
) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def get_all_initial_leverage_v1(
|
||||
self, req: types.ApiGetAllInitialLeverageRequest
|
||||
) -> types.ApiGetAllInitialLeverageResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/get_all_initial_leverage", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def set_initial_leverage_v1(
|
||||
self, req: types.ApiSetInitialLeverageRequest
|
||||
) -> types.ApiSetInitialLeverageResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_burn_tokens_v1(
|
||||
self, req: types.ApiVaultBurnTokensRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_invest_v1(
|
||||
self, req: types.ApiVaultInvestRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_invest", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_investor_summary_v1(
|
||||
self, req: types.ApiVaultInvestorSummaryRequest
|
||||
) -> types.ApiVaultInvestorSummaryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_investor_summary", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_redeem_v1(
|
||||
self, req: types.ApiVaultRedeemRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_redeem_cancel_v1(
|
||||
self, req: types.ApiVaultRedeemCancelRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_redemption_queue_v1(
|
||||
self, req: types.ApiVaultViewRedemptionQueueRequest
|
||||
) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def query_vault_manager_investor_history_v1(
|
||||
self, req: types.ApiQueryVaultManagerInvestorHistoryRequest
|
||||
) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_manager_investor_history", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
@@ -1,267 +0,0 @@
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from http.cookies import SimpleCookie
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import requests # type: ignore
|
||||
from eth_account import Account
|
||||
|
||||
from .grvt_raw_env import GrvtEnv, GrvtEnvConfig, get_env_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtApiConfig:
|
||||
env: GrvtEnv
|
||||
trading_account_id: str | None
|
||||
private_key: str | None
|
||||
api_key: str | None
|
||||
logger: logging.Logger | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtError:
|
||||
code: int
|
||||
message: str
|
||||
status: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtCookie:
|
||||
gravity: str
|
||||
expires: datetime
|
||||
grvt_account_id: str | None = None
|
||||
|
||||
|
||||
class GrvtRawBase:
|
||||
"""
|
||||
GrvtRawBase is base class for Grvt Rest API classes.
|
||||
|
||||
This should not be used directly, but rather through a derivative API class.
|
||||
"""
|
||||
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
self.config = config
|
||||
self.env: GrvtEnvConfig = get_env_config(config.env)
|
||||
self.logger: logging.Logger = config.logger or logging.getLogger(__name__)
|
||||
self._cookie: GrvtCookie | None = None
|
||||
if self.config.private_key is not None:
|
||||
self.account: Account = Account.from_key(self.config.private_key)
|
||||
|
||||
"""
|
||||
Cookie handling
|
||||
"""
|
||||
|
||||
def _should_refresh_cookie(self) -> bool:
|
||||
if not self.config.api_key:
|
||||
raise ValueError("Attempting to use Authenticated API without API key set")
|
||||
time_till_expiration = None
|
||||
if self._cookie and self._cookie.expires:
|
||||
time_till_expiration = self._cookie.expires.timestamp() - time.time()
|
||||
is_cookie_fresh = time_till_expiration is not None and time_till_expiration > 5
|
||||
if not is_cookie_fresh:
|
||||
self.logger.info(
|
||||
f"cookie should be refreshed now={time.time()}"
|
||||
f" {time_till_expiration=} secs"
|
||||
)
|
||||
return not is_cookie_fresh
|
||||
|
||||
|
||||
class GrvtRawSyncBase(GrvtRawBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
# Sync API session
|
||||
self._session: requests.Session = requests.Session()
|
||||
self._session.headers.update({"Content-Type": "application/json"})
|
||||
|
||||
"""
|
||||
Cookie handling
|
||||
"""
|
||||
|
||||
def _refresh_cookie(self) -> None:
|
||||
if not self._should_refresh_cookie():
|
||||
return None
|
||||
# Get cookie
|
||||
self._cookie = self._get_cookie(
|
||||
self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key)
|
||||
)
|
||||
self.logger.info(f"refresh_cookie cookie={self._cookie}")
|
||||
# Update cookie in session
|
||||
if self._cookie:
|
||||
self._session.cookies.update({"gravity": self._cookie.gravity})
|
||||
if self._cookie.grvt_account_id:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie.grvt_account_id}
|
||||
)
|
||||
return None
|
||||
|
||||
def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None:
|
||||
FN = f"_get_cookie {path=}"
|
||||
try:
|
||||
return_value = self._session.post(
|
||||
path,
|
||||
json={"api_key": api_key},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
)
|
||||
self.logger.info(f"{FN} {return_value=}")
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie_header = return_value.headers.get("Set-Cookie")
|
||||
grvt_cookie = return_value.cookies.get("gravity")
|
||||
self.logger.info(
|
||||
f"{FN} OK {return_value.headers=} \n "
|
||||
f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}"
|
||||
)
|
||||
cookie.load(cookie_header)
|
||||
cookie_value = cookie["gravity"].value
|
||||
cookie_expiry = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str | None = return_value.headers.get(
|
||||
"X-Grvt-Account-Id"
|
||||
)
|
||||
return GrvtCookie(
|
||||
gravity=cookie_value,
|
||||
expires=cookie_expiry,
|
||||
grvt_account_id=grvt_account_id,
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
|
||||
"""
|
||||
Post handling
|
||||
"""
|
||||
|
||||
def _post(self, is_auth: bool, path: str, req: Any) -> Any:
|
||||
FN = f"_post {path=}"
|
||||
# Always see if need to referesh cookie before sending an authenticated request
|
||||
if is_auth:
|
||||
self._refresh_cookie()
|
||||
|
||||
req_json = json.dumps(req, cls=DataclassJSONEncoder)
|
||||
resp_json: Any = {}
|
||||
|
||||
self.logger.debug(f"{FN} {req_json=}")
|
||||
resp: requests.Response = self._session.post(path, data=req_json, timeout=5)
|
||||
try:
|
||||
resp_json = resp.json()
|
||||
if not resp.ok:
|
||||
self.logger.warning(f"{FN} Error {resp_json=}")
|
||||
else:
|
||||
self.logger.debug(f"{FN} OK {resp_json=}")
|
||||
except Exception as err:
|
||||
self.logger.error(f"{FN} Unable to parse {resp.text=} as json:{err=}")
|
||||
return resp_json
|
||||
|
||||
|
||||
class GrvtRawAsyncBase(GrvtRawBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
# Async API session
|
||||
self._session: aiohttp.ClientSession = aiohttp.ClientSession(
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
"""
|
||||
Cookie handling
|
||||
"""
|
||||
|
||||
async def _refresh_cookie(self) -> None:
|
||||
if not self._should_refresh_cookie():
|
||||
return None
|
||||
|
||||
# Get cookie
|
||||
self._cookie = await self._get_cookie(
|
||||
self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key)
|
||||
)
|
||||
self.logger.info(f"refresh_cookie cookie={self._cookie}")
|
||||
|
||||
# Update cookie in session
|
||||
if self._cookie:
|
||||
self._session.cookie_jar.update_cookies({"gravity": self._cookie.gravity})
|
||||
if self._cookie.grvt_account_id:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie.grvt_account_id}
|
||||
)
|
||||
return None
|
||||
|
||||
async def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None:
|
||||
FN = f"_get_cookie {path=}"
|
||||
try:
|
||||
data = {"api_key": api_key}
|
||||
self.logger.info(f"{FN} ask for cookie {path=} {data=}")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url=path, json=data, timeout=5) as return_value:
|
||||
self.logger.info(f"{FN} {return_value=}")
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie_header = return_value.headers.get("Set-Cookie")
|
||||
grvt_cookie = return_value.cookies.get("gravity")
|
||||
self.logger.info(
|
||||
f"{FN} OK {return_value.headers=} \n "
|
||||
f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}"
|
||||
)
|
||||
cookie.load(cookie_header)
|
||||
cookie_value = cookie["gravity"].value
|
||||
cookie_expiry = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str | None = return_value.headers.get(
|
||||
"X-Grvt-Account-Id"
|
||||
)
|
||||
return GrvtCookie(
|
||||
gravity=cookie_value,
|
||||
expires=cookie_expiry,
|
||||
grvt_account_id=grvt_account_id,
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
|
||||
"""
|
||||
Post handling
|
||||
"""
|
||||
|
||||
async def _post(self, is_auth: bool, path: str, req: Any) -> Any:
|
||||
FN = f"_post {path=}"
|
||||
# Always see if need to referesh cookie before sending an authenticated request
|
||||
if is_auth:
|
||||
await self._refresh_cookie()
|
||||
|
||||
req_json = json.dumps(req, cls=DataclassJSONEncoder)
|
||||
resp_json: Any = {}
|
||||
|
||||
self.logger.debug(f"{FN} {req_json=}")
|
||||
resp: aiohttp.ClientResponse = await self._session.post(
|
||||
path, data=req_json, timeout=5
|
||||
)
|
||||
try:
|
||||
resp_text = await resp.text()
|
||||
resp_json = json.loads(resp_text)
|
||||
if not resp.ok:
|
||||
self.logger.warning(f"{FN} Error {resp_text=}")
|
||||
else:
|
||||
self.logger.debug(f"{FN} OK {resp_text=}")
|
||||
except Exception as err:
|
||||
self.logger.error(f"{FN} Unable to parse {resp_text=} as json:{err=}")
|
||||
return resp_json
|
||||
|
||||
|
||||
class DataclassJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o: Any) -> Any:
|
||||
if dataclasses.is_dataclass(o):
|
||||
return dataclasses.asdict(o) # type: ignore
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
@@ -1,77 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GrvtEnv(Enum):
|
||||
DEV = "dev"
|
||||
STAGING = "staging"
|
||||
TESTNET = "testnet"
|
||||
PROD = "prod"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtEndpointConfig:
|
||||
rpc_endpoint: str
|
||||
ws_endpoint: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtEnvConfig:
|
||||
edge: GrvtEndpointConfig
|
||||
trade_data: GrvtEndpointConfig
|
||||
market_data: GrvtEndpointConfig
|
||||
chain_id: int
|
||||
|
||||
|
||||
def get_env_config(environment: GrvtEnv) -> GrvtEnvConfig:
|
||||
match environment:
|
||||
case GrvtEnv.PROD:
|
||||
return GrvtEnvConfig(
|
||||
edge=GrvtEndpointConfig(
|
||||
rpc_endpoint="https://edge.grvt.io",
|
||||
ws_endpoint=None,
|
||||
),
|
||||
trade_data=GrvtEndpointConfig(
|
||||
rpc_endpoint="https://trades.grvt.io",
|
||||
ws_endpoint="wss://trades.grvt.io/ws",
|
||||
),
|
||||
market_data=GrvtEndpointConfig(
|
||||
rpc_endpoint="https://market-data.grvt.io",
|
||||
ws_endpoint="wss://market-data.grvt.io/ws",
|
||||
),
|
||||
chain_id=325,
|
||||
)
|
||||
case GrvtEnv.TESTNET:
|
||||
return GrvtEnvConfig(
|
||||
edge=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://edge.{environment.value}.grvt.io",
|
||||
ws_endpoint=None,
|
||||
),
|
||||
trade_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://trades.{environment.value}.grvt.io",
|
||||
ws_endpoint=f"wss://trades.{environment.value}.grvt.io/ws",
|
||||
),
|
||||
market_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://market-data.{environment.value}.grvt.io",
|
||||
ws_endpoint=f"wss://market-data.{environment.value}.grvt.io/ws",
|
||||
),
|
||||
chain_id=326,
|
||||
)
|
||||
case GrvtEnv.DEV | GrvtEnv.STAGING:
|
||||
return GrvtEnvConfig(
|
||||
edge=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://edge.{environment.value}.gravitymarkets.io",
|
||||
ws_endpoint=None,
|
||||
),
|
||||
trade_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://trades.{environment.value}.gravitymarkets.io",
|
||||
ws_endpoint=f"wss://trades.{environment.value}.gravitymarkets.io/ws",
|
||||
),
|
||||
market_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://market-data.{environment.value}.gravitymarkets.io",
|
||||
ws_endpoint=f"wss://market-data.{environment.value}.gravitymarkets.io/ws",
|
||||
),
|
||||
chain_id=327 if environment == GrvtEnv.DEV else 328,
|
||||
)
|
||||
case _:
|
||||
raise ValueError(f"Unknown environment={environment}")
|
||||
@@ -1,248 +0,0 @@
|
||||
from enum import Enum
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
|
||||
from eth_account import Account
|
||||
from eth_account.messages import encode_typed_data
|
||||
|
||||
from .grvt_ccxt_utils import GrvtCurrency
|
||||
from .grvt_raw_base import GrvtApiConfig, GrvtEnv
|
||||
from .grvt_raw_types import Instrument, Order, Withdrawal, TimeInForce
|
||||
from .grvt_fixed_types import Transfer
|
||||
|
||||
#########################
|
||||
# INSTRUMENT CONVERSION #
|
||||
#########################
|
||||
|
||||
|
||||
PRICE_MULTIPLIER = 1_000_000_000
|
||||
|
||||
|
||||
class SignTimeInForce(Enum):
|
||||
GOOD_TILL_TIME = 1
|
||||
ALL_OR_NONE = 2
|
||||
IMMEDIATE_OR_CANCEL = 3
|
||||
FILL_OR_KILL = 4
|
||||
|
||||
|
||||
TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = {
|
||||
TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME,
|
||||
TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE,
|
||||
TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL,
|
||||
TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL,
|
||||
}
|
||||
|
||||
|
||||
#####################
|
||||
# EIP-712 chain IDs #
|
||||
#####################
|
||||
CHAIN_IDS = {
|
||||
GrvtEnv.DEV: 327,
|
||||
GrvtEnv.STAGING: 327,
|
||||
GrvtEnv.TESTNET: 326,
|
||||
GrvtEnv.PROD: 325,
|
||||
}
|
||||
|
||||
|
||||
def get_EIP712_domain_data(env: GrvtEnv, chainId: int | None) -> dict[str, str | int]:
|
||||
return {
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": chainId or CHAIN_IDS[env],
|
||||
}
|
||||
|
||||
|
||||
#####################
|
||||
# Sign Order #
|
||||
#####################
|
||||
|
||||
EIP712_ORDER_MESSAGE_TYPE = {
|
||||
"Order": [
|
||||
{"name": "subAccountID", "type": "uint64"},
|
||||
{"name": "isMarket", "type": "bool"},
|
||||
{"name": "timeInForce", "type": "uint8"},
|
||||
{"name": "postOnly", "type": "bool"},
|
||||
{"name": "reduceOnly", "type": "bool"},
|
||||
{"name": "legs", "type": "OrderLeg[]"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
"OrderLeg": [
|
||||
{"name": "assetID", "type": "uint256"},
|
||||
{"name": "contractSize", "type": "uint64"},
|
||||
{"name": "limitPrice", "type": "uint64"},
|
||||
{"name": "isBuyingContract", "type": "bool"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def sign_order(
|
||||
order: Order,
|
||||
config: GrvtApiConfig,
|
||||
account: Account,
|
||||
instruments: dict[str, Instrument],
|
||||
) -> Order:
|
||||
if config.private_key is None:
|
||||
raise ValueError("Private key is not set")
|
||||
|
||||
message_data = build_EIP712_order_message_data(order, instruments)
|
||||
|
||||
domain_data = get_EIP712_domain_data(config.env, CHAIN_IDS[config.env])
|
||||
signable_message = encode_typed_data(
|
||||
domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
|
||||
order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.v = signed_message.v
|
||||
order.signature.signer = str(account.address)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
def build_EIP712_order_message_data(
|
||||
order: Order, instruments: dict[str, Instrument]
|
||||
) -> dict[str, Any]:
|
||||
legs = []
|
||||
for leg in order.legs:
|
||||
instrument = instruments[leg.instrument]
|
||||
size_multiplier = 10**instrument.base_decimals
|
||||
|
||||
# use Decimal() instead of float() to avoid precision loss
|
||||
# int(float("1.013") * 1e9) = 1012999999
|
||||
# int(Decimal("1.013") * Decimal(1e9) = 1013000000
|
||||
size_int = int(Decimal(leg.size) * Decimal(size_multiplier))
|
||||
price_int = int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER))
|
||||
legs.append(
|
||||
{
|
||||
"assetID": instrument.instrument_hash,
|
||||
"contractSize": size_int,
|
||||
"limitPrice": price_int,
|
||||
"isBuyingContract": leg.is_buying_asset,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"subAccountID": order.sub_account_id,
|
||||
"isMarket": order.is_market or False,
|
||||
"timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value,
|
||||
"postOnly": order.post_only or False,
|
||||
"reduceOnly": order.reduce_only or False,
|
||||
"legs": legs,
|
||||
"nonce": order.signature.nonce,
|
||||
"expiration": order.signature.expiration,
|
||||
}
|
||||
|
||||
|
||||
#####################
|
||||
# Sign Transfer #
|
||||
#####################
|
||||
|
||||
EIP712_TRANSFER_MESSAGE_TYPE = {
|
||||
"Transfer": [
|
||||
{"name": "fromAccount", "type": "address"},
|
||||
{"name": "fromSubAccount", "type": "uint64"},
|
||||
{"name": "toAccount", "type": "address"},
|
||||
{"name": "toSubAccount", "type": "uint64"},
|
||||
{"name": "tokenCurrency", "type": "uint8"},
|
||||
{"name": "numTokens", "type": "uint64"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_EIP712_transfer_message_data(transfer: Transfer, currencyId: int):
|
||||
return {
|
||||
"fromAccount": transfer.from_account_id,
|
||||
"fromSubAccount": transfer.from_sub_account_id,
|
||||
"toAccount": transfer.to_account_id,
|
||||
"toSubAccount": transfer.to_sub_account_id,
|
||||
"tokenCurrency": currencyId,
|
||||
"numTokens": int(
|
||||
Decimal(transfer.num_tokens) * Decimal(1e6)
|
||||
), # USDT has 6 decimals
|
||||
"nonce": transfer.signature.nonce,
|
||||
"expiration": transfer.signature.expiration,
|
||||
}
|
||||
|
||||
|
||||
def sign_transfer(
|
||||
transfer: Transfer,
|
||||
config: GrvtApiConfig,
|
||||
account: Account,
|
||||
chainId: int | None = None,
|
||||
currencyId: int = 3, # currencyId of USDT; refer to Get Currency API
|
||||
) -> Transfer:
|
||||
if config.private_key is None:
|
||||
raise ValueError("Private key is not set")
|
||||
|
||||
domain = get_EIP712_domain_data(config.env, chainId)
|
||||
|
||||
message_data = build_EIP712_transfer_message_data(transfer, currencyId)
|
||||
signable_message = encode_typed_data(
|
||||
domain, EIP712_TRANSFER_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
|
||||
transfer.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
transfer.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
transfer.signature.v = signed_message.v
|
||||
transfer.signature.signer = str(account.address)
|
||||
|
||||
return transfer
|
||||
|
||||
|
||||
#####################
|
||||
# Sign Withdrawal #
|
||||
#####################
|
||||
|
||||
EIP712_WITHDRAWAL_MESSAGE_TYPE = {
|
||||
"Withdrawal": [
|
||||
{"name": "fromAccount", "type": "address"},
|
||||
{"name": "toEthAddress", "type": "address"},
|
||||
{"name": "tokenCurrency", "type": "uint8"},
|
||||
{"name": "numTokens", "type": "uint64"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_EIP712_withdrawal_message_data(withdrawal: Withdrawal, currencyId: int):
|
||||
return {
|
||||
"fromAccount": withdrawal.from_account_id,
|
||||
"toEthAddress": withdrawal.to_eth_address,
|
||||
"tokenCurrency": currencyId,
|
||||
"numTokens": int(
|
||||
Decimal(withdrawal.num_tokens) * Decimal(1e6)
|
||||
), # USDT has 6 decimals
|
||||
"nonce": withdrawal.signature.nonce,
|
||||
"expiration": withdrawal.signature.expiration,
|
||||
}
|
||||
|
||||
|
||||
def sign_withdrawal(
|
||||
withdrawal: Withdrawal,
|
||||
config: GrvtApiConfig,
|
||||
account: Account,
|
||||
chainId: int | None = None,
|
||||
currencyId: int = 3, # currencyId of USDT; refer to Get Currency API
|
||||
) -> Withdrawal:
|
||||
if config.private_key is None:
|
||||
raise ValueError("Private key is not set")
|
||||
|
||||
domain = get_EIP712_domain_data(config.env, chainId)
|
||||
|
||||
message_data = build_EIP712_withdrawal_message_data(withdrawal, currencyId)
|
||||
signable_message = encode_typed_data(
|
||||
domain, EIP712_WITHDRAWAL_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
|
||||
withdrawal.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
withdrawal.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
withdrawal.signature.v = signed_message.v
|
||||
withdrawal.signature.signer = str(account.address)
|
||||
|
||||
return withdrawal
|
||||
@@ -1,351 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
from dacite import Config, from_dict
|
||||
|
||||
from . import grvt_raw_types as types
|
||||
from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawSyncBase
|
||||
|
||||
# mypy: disable-error-code="no-any-return"
|
||||
|
||||
|
||||
class GrvtRawSync(GrvtRawSyncBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
self.md_rpc = self.env.market_data.rpc_endpoint
|
||||
self.td_rpc = self.env.trade_data.rpc_endpoint
|
||||
|
||||
def get_instrument_v1(
|
||||
self, req: types.ApiGetInstrumentRequest
|
||||
) -> types.ApiGetInstrumentResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/instrument", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def get_all_instruments_v1(
|
||||
self, req: types.ApiGetAllInstrumentsRequest
|
||||
) -> types.ApiGetAllInstrumentsResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/all_instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def get_filtered_instruments_v1(
|
||||
self, req: types.ApiGetFilteredInstrumentsRequest
|
||||
) -> types.ApiGetFilteredInstrumentsResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def get_currency_v1(
|
||||
self, req: types.ApiGetCurrencyRequest
|
||||
) -> types.ApiGetCurrencyResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/currency", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def mini_ticker_v1(
|
||||
self, req: types.ApiMiniTickerRequest
|
||||
) -> types.ApiMiniTickerResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/mini", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def ticker_v1(
|
||||
self, req: types.ApiTickerRequest
|
||||
) -> types.ApiTickerResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/ticker", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def orderbook_levels_v1(
|
||||
self, req: types.ApiOrderbookLevelsRequest
|
||||
) -> types.ApiOrderbookLevelsResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/book", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def trade_v1(self, req: types.ApiTradeRequest) -> types.ApiTradeResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/trade", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def trade_history_v1(
|
||||
self, req: types.ApiTradeHistoryRequest
|
||||
) -> types.ApiTradeHistoryResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/trade_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def candlestick_v1(
|
||||
self, req: types.ApiCandlestickRequest
|
||||
) -> types.ApiCandlestickResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/kline", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def funding_rate_v1(
|
||||
self, req: types.ApiFundingRateRequest
|
||||
) -> types.ApiFundingRateResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/funding", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def create_order_v1(
|
||||
self, req: types.ApiCreateOrderRequest
|
||||
) -> types.ApiCreateOrderResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/create_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def cancel_order_v1(
|
||||
self, req: types.ApiCancelOrderRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/cancel_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def cancel_all_orders_v1(
|
||||
self, req: types.ApiCancelAllOrdersRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def get_order_v1(
|
||||
self, req: types.ApiGetOrderRequest
|
||||
) -> types.ApiGetOrderResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def open_orders_v1(
|
||||
self, req: types.ApiOpenOrdersRequest
|
||||
) -> types.ApiOpenOrdersResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/open_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def order_history_v1(
|
||||
self, req: types.ApiOrderHistoryRequest
|
||||
) -> types.ApiOrderHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/order_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def cancel_on_disconnect_v1(
|
||||
self, req: types.ApiCancelOnDisconnectRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def fill_history_v1(
|
||||
self, req: types.ApiFillHistoryRequest
|
||||
) -> types.ApiFillHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/fill_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def positions_v1(
|
||||
self, req: types.ApiPositionsRequest
|
||||
) -> types.ApiPositionsResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/positions", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def funding_payment_history_v1(
|
||||
self, req: types.ApiFundingPaymentHistoryRequest
|
||||
) -> types.ApiFundingPaymentHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/funding_payment_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def deposit_history_v1(
|
||||
self, req: types.ApiDepositHistoryRequest
|
||||
) -> types.ApiDepositHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/deposit_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def transfer_v1(
|
||||
self, req: types.ApiTransferRequest
|
||||
) -> types.ApiTransferResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/transfer", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def transfer_history_v1(
|
||||
self, req: types.ApiTransferHistoryRequest
|
||||
) -> types.ApiTransferHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/transfer_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def withdrawal_v1(
|
||||
self, req: types.ApiWithdrawalRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/withdrawal", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def withdrawal_history_v1(
|
||||
self, req: types.ApiWithdrawalHistoryRequest
|
||||
) -> types.ApiWithdrawalHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def sub_account_summary_v1(
|
||||
self, req: types.ApiSubAccountSummaryRequest
|
||||
) -> types.ApiSubAccountSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def sub_account_history_v1(
|
||||
self, req: types.ApiSubAccountHistoryRequest
|
||||
) -> types.ApiSubAccountHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/account_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def aggregated_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiAggregatedAccountSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/aggregated_account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def funding_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiFundingAccountSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/funding_account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def set_derisk_mm_ratio_v1(
|
||||
self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest
|
||||
) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def get_all_initial_leverage_v1(
|
||||
self, req: types.ApiGetAllInitialLeverageRequest
|
||||
) -> types.ApiGetAllInitialLeverageResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/get_all_initial_leverage", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def set_initial_leverage_v1(
|
||||
self, req: types.ApiSetInitialLeverageRequest
|
||||
) -> types.ApiSetInitialLeverageResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_burn_tokens_v1(
|
||||
self, req: types.ApiVaultBurnTokensRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_invest_v1(
|
||||
self, req: types.ApiVaultInvestRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_invest", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_investor_summary_v1(
|
||||
self, req: types.ApiVaultInvestorSummaryRequest
|
||||
) -> types.ApiVaultInvestorSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_investor_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_redeem_v1(
|
||||
self, req: types.ApiVaultRedeemRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_redeem_cancel_v1(
|
||||
self, req: types.ApiVaultRedeemCancelRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_redemption_queue_v1(
|
||||
self, req: types.ApiVaultViewRedemptionQueueRequest
|
||||
) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def query_vault_manager_investor_history_v1(
|
||||
self, req: types.ApiQueryVaultManagerInvestorHistoryRequest
|
||||
) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError:
|
||||
resp = self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_manager_investor_history", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,316 +0,0 @@
|
||||
|
||||
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()
|
||||
@@ -1,300 +0,0 @@
|
||||
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()
|
||||
@@ -1,52 +0,0 @@
|
||||
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()
|
||||
@@ -1,60 +0,0 @@
|
||||
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()
|
||||
@@ -1,297 +0,0 @@
|
||||
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()
|
||||
@@ -1,137 +0,0 @@
|
||||
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())
|
||||
@@ -1,400 +0,0 @@
|
||||
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()
|
||||
@@ -1,837 +0,0 @@
|
||||
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()
|
||||
@@ -1,141 +0,0 @@
|
||||
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")
|
||||
@@ -1,158 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
from .test_grvt_raw_sync import test_transfer_with_signing
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_transfer_with_signing()
|
||||
@@ -1,4 +0,0 @@
|
||||
from .test_grvt_raw_sync import test_withdrawal_with_signing
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_withdrawal_with_signing()
|
||||
Generated
-1961
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
Welcome to the Lighter SDK and API Introduction. Here, we will go through everything from the system setup, to creating and cancelling all types of orders, to fetching exchange data.
|
||||
|
||||
Setting up an API KEY
|
||||
In order to get started using the Lighter API, you must first set up an API_KEY_PRIVATE_KEY, as you will need it to sign any transaction you want to make. You can find how to do it in the following example. The BASE_URL will reflect if your key is generated on testnet or mainnet (for mainnet, just change the BASE_URL in the example to https://mainnet.zklighter.elliot.ai). Note that you also need to provide your ETH_PRIVATE_KEY.
|
||||
|
||||
You can setup up to 253 API keys (2 <= API_KEY_INDEX <= 254). The 0 index is the one reserved for the desktop, while 1 is the one reserved for the mobile. Finally, the 255 index can be used as a value for the api_key_index parameter of the apikeys method of the AccountApi for getting the data about all the API keys.
|
||||
|
||||
In case you do not know your ACCOUNT_INDEX, you can find it by querying the AccountApi for the data about your account, as shown in this example.
|
||||
|
||||
Account types
|
||||
Lighter API users can operate under a Standard or Premium account. The Standard account is suitable for latency insensitive traders, providing 0 fees, while the premium one provides a latency suitable for HFTs at 0.2 bps maker and 2 bps taker fees. You can find more information about it in the Account Types section.
|
||||
|
||||
The Signer
|
||||
In order to create a transaction (create/cancel/modify order), you need to use the SignerClient. For initializing it, use the following lines of code:
|
||||
|
||||
Initialize SignerClient
|
||||
|
||||
client = lighter.SignerClient(
|
||||
url=BASE_URL,
|
||||
private_key=API_KEY_PRIVATE_KEY,
|
||||
account_index=ACCOUNT_INDEX,
|
||||
api_key_index=API_KEY_INDEX
|
||||
)
|
||||
The code for the signer can be found in the same repo, in the signer_client.py file. You may notice that it uses a binary for the signer: the code for it can be found in the lighter-go public repo, and you can compile it yourself using the justfile.
|
||||
|
||||
Nonces
|
||||
When signing a transaction, you may need to provide a nonce (number used once). A nonce needs to be incremented each time you sign something. You can get the next nonce that you need to use using the TransactionApi’s next_nonce method or take care of incrementing it yourself. Note that each nonce is handled per API_KEY.
|
||||
|
||||
Signing a transaction
|
||||
One can sign a transaction using the SignerClient’s sign_create_order, sign_modify_order, sign_cancel_order and its other similar methods. For actually pushing the transaction, you need to call send_tx or send_tx_batch using the TransactionApi. Here’s an example that includes such an operation.
|
||||
|
||||
Note that base_amount, price are to be passed as integers, and client_order_index is an unique (across all markets) identifier you provide for you to be able to reference this order later (e.g. if you want to cancel it).
|
||||
|
||||
The following values can be provided for the order_type parameter:
|
||||
|
||||
ORDER_TYPE_LIMIT
|
||||
ORDER_TYPE_MARKET
|
||||
ORDER_TYPE_STOP_LOSS
|
||||
ORDER_TYPE_STOP_LOSS_LIMIT
|
||||
ORDER_TYPE_TAKE_PROFIT
|
||||
ORDER_TYPE_TAKE_PROFIT_LIMIT
|
||||
ORDER_TYPE_TWAP
|
||||
The following values can be provided for the time_in_force parameter:
|
||||
|
||||
ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL
|
||||
ORDER_TIME_IN_FORCE_GOOD_TILL_TIME
|
||||
ORDER_TIME_IN_FORCE_POST_ONLY
|
||||
Signer Client Useful Wrapper Functions
|
||||
The SignerClient provides several functions that sign and push a type of transaction. Here’s a list of some of them:
|
||||
|
||||
create_order - signs and pushes a create order transaction;
|
||||
create_market_order - signs and pushes a create order transaction for a market order;
|
||||
create_cancel_order - signs and pushes a cancel transaction for a certain order. Note that the order_index needs to equal the client_order_index of the order to cancel;
|
||||
cancel_all_orders - signs and pushes a cancel all transactions. Note that, depending on the time_in_force provided, the transaction has different consequences:
|
||||
ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL - ImmediateCancelAll;
|
||||
ORDER_TIME_IN_FORCE_GOOD_TILL_TIME - ScheduledCancelAll;
|
||||
ORDER_TIME_IN_FORCE_POST_ONLY - AbortScheduledCancelAll.
|
||||
create_auth_token_with_expiry - creates an auth token (useful for getting data using the Api and Ws methods)
|
||||
API
|
||||
The SDK provides API classes that make calling the Lighter API easier. Here are some of them and the most important of their methods:
|
||||
|
||||
AccountApi - provides account data
|
||||
account - get account data either by l1_address or index
|
||||
accounts_by_l1_address - get data about all the accounts (master account and subaccounts)
|
||||
apikeys - get data about the api keys of an account (use api_key_index = 255 for getting data about all the api keys)
|
||||
TransactionApi - provides transaction related data
|
||||
next_nonce - get next nonce to be used for signing a transaction using a certain api key
|
||||
send_tx - push a transaction
|
||||
send_tx_batch - push several transactions at once
|
||||
OrderApi - provides data about orders, trades and the orderbook
|
||||
order_book_details - get data about a specific market’s orderbook
|
||||
order_books - get data about all markets’ orderbooks
|
||||
You can find the rest here. We also provide an example showing how to use some of these. For the methods that require an auth token, you can generate one using the create_auth_token_with_expiry method of the SignerClient (the same applies to the websockets auth).
|
||||
|
||||
WebSockets
|
||||
Lighter also provides access to essential info using websockets. A simple version of an WsClient for subscribing to account and orderbook updates is implemented here. You can also take it as an example implementation of such a client.
|
||||
|
||||
To get access to more data, you will need to connect to the websockets without the provided WsClient. You can find the streams you can connect to, how to connect, and the data they provide in the websockets section.
|
||||
@@ -0,0 +1,4 @@
|
||||
.idea
|
||||
vendor
|
||||
build/*
|
||||
!build/.keep
|
||||
@@ -0,0 +1,9 @@
|
||||
# lighter-go
|
||||
|
||||
In its current form, this repo serves as a starting point for anyone who wants to trade on Lighter using GO.
|
||||
It covers all the signing procedures in order to trade on Lighter with an API key.
|
||||
Minimal HTTP calls are implemented
|
||||
On chain support, like depositing on Ethereum or modifying an API key directly with an Ethereum Tx are not supported yet.
|
||||
|
||||
At the moment, its main purpose is to offer visibility on the code behind the precompiled libraries used by the Python SDK.
|
||||
If you'd like to compile your own binaries, the commands are in the `justfile`
|
||||
@@ -0,0 +1,659 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/elliottech/lighter-go/signer"
|
||||
"github.com/elliottech/lighter-go/types/txtypes"
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
gFp5 "github.com/elliottech/poseidon_crypto/field/goldilocks_quintic_extension"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
ethCommon "github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
type TransactOpts struct {
|
||||
FromAccountIndex *int64
|
||||
ApiKeyIndex *uint8
|
||||
ExpiredAt int64
|
||||
Nonce *int64
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
type PublicKey = gFp5.Element
|
||||
|
||||
type ChangePubKeyReq struct {
|
||||
PubKey [40]byte
|
||||
}
|
||||
|
||||
type TransferTxReq struct {
|
||||
ToAccountIndex int64
|
||||
USDCAmount int64
|
||||
Fee int64
|
||||
Memo [32]byte
|
||||
}
|
||||
|
||||
type WithdrawTxReq struct {
|
||||
USDCAmount uint64
|
||||
}
|
||||
|
||||
type CreateOrderTxReq struct {
|
||||
MarketIndex uint8
|
||||
ClientOrderIndex int64
|
||||
BaseAmount int64
|
||||
Price uint32
|
||||
IsAsk uint8
|
||||
Type uint8
|
||||
TimeInForce uint8
|
||||
ReduceOnly uint8
|
||||
TriggerPrice uint32
|
||||
OrderExpiry int64
|
||||
}
|
||||
|
||||
type CreateGroupedOrdersTxReq struct {
|
||||
GroupingType uint8
|
||||
Orders []*CreateOrderTxReq
|
||||
}
|
||||
|
||||
type ModifyOrderTxReq struct {
|
||||
MarketIndex uint8
|
||||
Index int64
|
||||
BaseAmount int64
|
||||
Price uint32
|
||||
TriggerPrice uint32
|
||||
}
|
||||
|
||||
type CancelOrderTxReq struct {
|
||||
MarketIndex uint8
|
||||
Index int64
|
||||
}
|
||||
|
||||
type CancelAllOrdersTxReq struct {
|
||||
TimeInForce uint8
|
||||
Time int64
|
||||
}
|
||||
|
||||
type CreatePublicPoolTxReq struct {
|
||||
OperatorFee int64
|
||||
InitialTotalShares int64
|
||||
MinOperatorShareRate int64
|
||||
}
|
||||
|
||||
type UpdatePublicPoolTxReq struct {
|
||||
PublicPoolIndex int64
|
||||
Status uint8
|
||||
OperatorFee int64
|
||||
MinOperatorShareRate int64
|
||||
}
|
||||
|
||||
type MintSharesTxReq struct {
|
||||
PublicPoolIndex int64
|
||||
ShareAmount int64
|
||||
}
|
||||
|
||||
type BurnSharesTxReq struct {
|
||||
PublicPoolIndex int64
|
||||
ShareAmount int64
|
||||
}
|
||||
|
||||
type UpdateLeverageTxReq struct {
|
||||
MarketIndex uint8
|
||||
InitialMarginFraction uint16
|
||||
MarginMode uint8
|
||||
}
|
||||
|
||||
type UpdateMarginTxReq struct {
|
||||
MarketIndex uint8
|
||||
USDCAmount int64
|
||||
Direction uint8
|
||||
}
|
||||
|
||||
func ConstructAuthToken(key signer.Signer, deadline time.Time, ops *TransactOpts) (string, error) {
|
||||
if ops.FromAccountIndex == nil {
|
||||
return "", fmt.Errorf("missing FromAccountIndex")
|
||||
}
|
||||
if ops.ApiKeyIndex == nil {
|
||||
return "", fmt.Errorf("missing ApiKeyIndex")
|
||||
}
|
||||
message := fmt.Sprintf("%v:%v:%v", deadline.Unix(), *ops.FromAccountIndex, *ops.ApiKeyIndex)
|
||||
|
||||
msgInField, err := g.ArrayFromCanonicalLittleEndianBytes([]byte(message))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to convert bytes to field element. message: %s, error: %w", message, err)
|
||||
}
|
||||
|
||||
msgHash := p2.HashToQuinticExtension(msgInField).ToLittleEndianBytes()
|
||||
|
||||
signatureBytes, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signature := ethCommon.Bytes2Hex(signatureBytes)
|
||||
|
||||
return fmt.Sprintf("%v:%v", message, signature), err
|
||||
}
|
||||
|
||||
func ConstructChangePubKeyTx(key signer.Signer, lighterChainId uint32, tx *ChangePubKeyReq, ops *TransactOpts) (*txtypes.L2ChangePubKeyTxInfo, error) {
|
||||
convertedTx := ConvertChangePubKeyTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructCreateSubAccountTx(key signer.Signer, lighterChainId uint32, ops *TransactOpts) (*txtypes.L2CreateSubAccountTxInfo, error) {
|
||||
convertedTx := ConvertCreateSubAccountTx(ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructCreatePublicPoolTx(key signer.Signer, lighterChainId uint32, tx *CreatePublicPoolTxReq, ops *TransactOpts) (*txtypes.L2CreatePublicPoolTxInfo, error) {
|
||||
convertedTx := ConvertCreatePublicPoolTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructUpdatePublicPoolTx(key signer.Signer, lighterChainId uint32, tx *UpdatePublicPoolTxReq, ops *TransactOpts) (*txtypes.L2UpdatePublicPoolTxInfo, error) {
|
||||
convertedTx := ConvertUpdatePublicPoolTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructWithdrawTx(key signer.Signer, lighterChainId uint32, tx *WithdrawTxReq, ops *TransactOpts) (*txtypes.L2WithdrawTxInfo, error) {
|
||||
convertedTx := ConvertWithdrawTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructCreateOrderTx(key signer.Signer, lighterChainId uint32, tx *CreateOrderTxReq, ops *TransactOpts) (*txtypes.L2CreateOrderTxInfo, error) {
|
||||
convertedTx := ConvertCreateOrderTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructL2CreateGroupedOrdersTx(key signer.Signer, lighterChainId uint32, tx *CreateGroupedOrdersTxReq, ops *TransactOpts) (*txtypes.L2CreateGroupedOrdersTxInfo, error) {
|
||||
convertedTx := ConvertCreateGroupedOrdersTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructL2CancelOrderTx(key signer.Signer, lighterChainId uint32, tx *CancelOrderTxReq, ops *TransactOpts) (*txtypes.L2CancelOrderTxInfo, error) {
|
||||
convertedTx := ConvertCancelOrderTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructL2ModifyOrderTx(key signer.Signer, lighterChainId uint32, tx *ModifyOrderTxReq, ops *TransactOpts) (*txtypes.L2ModifyOrderTxInfo, error) {
|
||||
convertedTx := ConvertModifyOrderTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructL2CancelAllOrdersTx(key signer.Signer, lighterChainId uint32, tx *CancelAllOrdersTxReq, ops *TransactOpts) (*txtypes.L2CancelAllOrdersTxInfo, error) {
|
||||
convertedTx := ConvertCancelAllOrdersTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructTransferTx(key signer.Signer, lighterChainId uint32, tx *TransferTxReq, ops *TransactOpts) (*txtypes.L2TransferTxInfo, error) {
|
||||
convertedTx := ConvertTransferTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructMintSharesTx(key signer.Signer, lighterChainId uint32, tx *MintSharesTxReq, ops *TransactOpts) (*txtypes.L2MintSharesTxInfo, error) {
|
||||
convertedTx := ConvertMintSharesTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructBurnSharesTx(key signer.Signer, lighterChainId uint32, tx *BurnSharesTxReq, ops *TransactOpts) (*txtypes.L2BurnSharesTxInfo, error) {
|
||||
convertedTx := ConvertBurnSharesTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructUpdateLeverageTx(key signer.Signer, lighterChainId uint32, tx *UpdateLeverageTxReq, ops *TransactOpts) (*txtypes.L2UpdateLeverageTxInfo, error) {
|
||||
convertedTx := ConvertUpdateLeverageTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConstructUpdateMarginTx(key signer.Signer, lighterChainId uint32, tx *UpdateMarginTxReq, ops *TransactOpts) (*txtypes.L2UpdateMarginTxInfo, error) {
|
||||
convertedTx := ConvertUpdateMarginTx(tx, ops)
|
||||
err := convertedTx.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgHash, err := convertedTx.Hash(lighterChainId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := key.Sign(msgHash, p2.NewPoseidon2())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
convertedTx.SignedHash = ethCommon.Bytes2Hex(msgHash)
|
||||
convertedTx.Sig = signature
|
||||
return convertedTx, nil
|
||||
}
|
||||
|
||||
func ConvertTransferTx(tx *TransferTxReq, ops *TransactOpts) *txtypes.L2TransferTxInfo {
|
||||
return &txtypes.L2TransferTxInfo{
|
||||
FromAccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
ToAccountIndex: tx.ToAccountIndex,
|
||||
USDCAmount: tx.USDCAmount,
|
||||
Fee: tx.Fee,
|
||||
Memo: tx.Memo,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertCreateOrderTx(tx *CreateOrderTxReq, ops *TransactOpts) *txtypes.L2CreateOrderTxInfo {
|
||||
return &txtypes.L2CreateOrderTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
OrderInfo: &txtypes.OrderInfo{MarketIndex: tx.MarketIndex,
|
||||
ClientOrderIndex: tx.ClientOrderIndex,
|
||||
BaseAmount: tx.BaseAmount,
|
||||
Price: tx.Price,
|
||||
IsAsk: tx.IsAsk,
|
||||
Type: tx.Type,
|
||||
TimeInForce: tx.TimeInForce,
|
||||
ReduceOnly: tx.ReduceOnly,
|
||||
TriggerPrice: tx.TriggerPrice,
|
||||
OrderExpiry: tx.OrderExpiry,
|
||||
},
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertCreateGroupedOrdersTx(tx *CreateGroupedOrdersTxReq, ops *TransactOpts) *txtypes.L2CreateGroupedOrdersTxInfo {
|
||||
ret := &txtypes.L2CreateGroupedOrdersTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
GroupingType: tx.GroupingType,
|
||||
Orders: []*txtypes.OrderInfo{},
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
|
||||
for _, order := range tx.Orders {
|
||||
ret.Orders = append(ret.Orders, &txtypes.OrderInfo{
|
||||
MarketIndex: order.MarketIndex,
|
||||
ClientOrderIndex: order.ClientOrderIndex,
|
||||
BaseAmount: order.BaseAmount,
|
||||
Price: order.Price,
|
||||
IsAsk: order.IsAsk,
|
||||
Type: order.Type,
|
||||
TimeInForce: order.TimeInForce,
|
||||
ReduceOnly: order.ReduceOnly,
|
||||
TriggerPrice: order.TriggerPrice,
|
||||
OrderExpiry: order.OrderExpiry,
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func ConvertCancelOrderTx(tx *CancelOrderTxReq, ops *TransactOpts) *txtypes.L2CancelOrderTxInfo {
|
||||
return &txtypes.L2CancelOrderTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
MarketIndex: tx.MarketIndex,
|
||||
Index: tx.Index,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertModifyOrderTx(tx *ModifyOrderTxReq, ops *TransactOpts) *txtypes.L2ModifyOrderTxInfo {
|
||||
return &txtypes.L2ModifyOrderTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
MarketIndex: tx.MarketIndex,
|
||||
Index: tx.Index,
|
||||
BaseAmount: tx.BaseAmount,
|
||||
Price: tx.Price,
|
||||
TriggerPrice: tx.TriggerPrice,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertCancelAllOrdersTx(tx *CancelAllOrdersTxReq, ops *TransactOpts) *txtypes.L2CancelAllOrdersTxInfo {
|
||||
return &txtypes.L2CancelAllOrdersTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
TimeInForce: tx.TimeInForce,
|
||||
Time: tx.Time,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertWithdrawTx(tx *WithdrawTxReq, ops *TransactOpts) *txtypes.L2WithdrawTxInfo {
|
||||
return &txtypes.L2WithdrawTxInfo{
|
||||
FromAccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
USDCAmount: tx.USDCAmount,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertChangePubKeyTx(tx *ChangePubKeyReq, ops *TransactOpts) *txtypes.L2ChangePubKeyTxInfo {
|
||||
return &txtypes.L2ChangePubKeyTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
PubKey: tx.PubKey[:],
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertCreateSubAccountTx(ops *TransactOpts) *txtypes.L2CreateSubAccountTxInfo {
|
||||
return &txtypes.L2CreateSubAccountTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertCreatePublicPoolTx(tx *CreatePublicPoolTxReq, ops *TransactOpts) *txtypes.L2CreatePublicPoolTxInfo {
|
||||
return &txtypes.L2CreatePublicPoolTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
OperatorFee: tx.OperatorFee,
|
||||
InitialTotalShares: tx.InitialTotalShares,
|
||||
MinOperatorShareRate: tx.MinOperatorShareRate,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertUpdatePublicPoolTx(tx *UpdatePublicPoolTxReq, ops *TransactOpts) *txtypes.L2UpdatePublicPoolTxInfo {
|
||||
return &txtypes.L2UpdatePublicPoolTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
PublicPoolIndex: tx.PublicPoolIndex,
|
||||
Status: tx.Status,
|
||||
OperatorFee: tx.OperatorFee,
|
||||
MinOperatorShareRate: tx.MinOperatorShareRate,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertMintSharesTx(tx *MintSharesTxReq, ops *TransactOpts) *txtypes.L2MintSharesTxInfo {
|
||||
return &txtypes.L2MintSharesTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
PublicPoolIndex: tx.PublicPoolIndex,
|
||||
ShareAmount: tx.ShareAmount,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertBurnSharesTx(tx *BurnSharesTxReq, ops *TransactOpts) *txtypes.L2BurnSharesTxInfo {
|
||||
return &txtypes.L2BurnSharesTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
PublicPoolIndex: tx.PublicPoolIndex,
|
||||
ShareAmount: tx.ShareAmount,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertUpdateLeverageTx(tx *UpdateLeverageTxReq, ops *TransactOpts) *txtypes.L2UpdateLeverageTxInfo {
|
||||
return &txtypes.L2UpdateLeverageTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
MarketIndex: tx.MarketIndex,
|
||||
InitialMarginFraction: tx.InitialMarginFraction,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertUpdateMarginTx(tx *UpdateMarginTxReq, ops *TransactOpts) *txtypes.L2UpdateMarginTxInfo {
|
||||
return &txtypes.L2UpdateMarginTxInfo{
|
||||
AccountIndex: *ops.FromAccountIndex,
|
||||
ApiKeyIndex: *ops.ApiKeyIndex,
|
||||
MarketIndex: tx.MarketIndex,
|
||||
USDCAmount: tx.USDCAmount,
|
||||
Direction: tx.Direction,
|
||||
ExpiredAt: ops.ExpiredAt,
|
||||
Nonce: *ops.Nonce,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2BurnSharesTxInfo)(nil)
|
||||
|
||||
type L2BurnSharesTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
PublicPoolIndex int64
|
||||
ShareAmount int64
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2BurnSharesTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2BurnShares
|
||||
}
|
||||
|
||||
func (txInfo *L2BurnSharesTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2BurnSharesTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2BurnSharesTxInfo) Validate() error {
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// PublicPoolIndex
|
||||
if txInfo.PublicPoolIndex < MinAccountIndex {
|
||||
return ErrPublicPoolIndexTooLow
|
||||
}
|
||||
if txInfo.PublicPoolIndex > MaxAccountIndex {
|
||||
return ErrPublicPoolIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.ShareAmount < MinPoolSharesToMintOrBurn {
|
||||
return ErrPoolBurnShareAmountTooLow
|
||||
}
|
||||
if txInfo.ShareAmount > MaxPoolSharesToMintOrBurn {
|
||||
return ErrPoolBurnShareAmountTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2BurnSharesTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 8)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2BurnShares))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.PublicPoolIndex))
|
||||
elems = append(elems, g.FromInt64(txInfo.ShareAmount))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2CancelAllOrdersTxInfo)(nil)
|
||||
|
||||
type L2CancelAllOrdersTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
TimeInForce uint8
|
||||
Time int64
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelAllOrdersTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2CancelAllOrders
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelAllOrdersTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelAllOrdersTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelAllOrdersTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrAccountIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex && txInfo.ApiKeyIndex != NilApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
// TimeInForce and Time
|
||||
switch txInfo.TimeInForce {
|
||||
case ImmediateCancelAll:
|
||||
if txInfo.Time != NilOrderExpiry {
|
||||
return ErrCancelAllTimeisNotNill
|
||||
}
|
||||
case ScheduledCancelAll:
|
||||
if txInfo.Time < MinOrderExpiry || txInfo.Time > MaxOrderExpiry {
|
||||
return ErrCancelAllTimeIsNotInRange
|
||||
}
|
||||
case AbortScheduledCancelAll:
|
||||
if txInfo.Time != 0 {
|
||||
return ErrCancelAllTimeIsNotInRange
|
||||
}
|
||||
default:
|
||||
return ErrInvalidCancelAllTimeInForce
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelAllOrdersTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 8)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2CancelAllOrders))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.TimeInForce)))
|
||||
elems = append(elems, g.FromInt64(txInfo.Time))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2CancelOrderTxInfo)(nil)
|
||||
|
||||
type L2CancelOrderTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
MarketIndex uint8
|
||||
Index int64 // Client Order Index or Order Index of the order to cancel
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelOrderTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2CancelOrder
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelOrderTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelOrderTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelOrderTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// MarketIndex
|
||||
if txInfo.MarketIndex < MinMarketIndex {
|
||||
return ErrMarketIndexTooLow
|
||||
}
|
||||
if txInfo.MarketIndex > MaxMarketIndex {
|
||||
return ErrMarketIndexTooHigh
|
||||
}
|
||||
|
||||
// Index
|
||||
if txInfo.Index < MinClientOrderIndex && txInfo.Index < MinOrderIndex {
|
||||
return ErrOrderIndexTooLow
|
||||
}
|
||||
if txInfo.Index > MaxClientOrderIndex && txInfo.Index > MaxOrderIndex {
|
||||
return ErrOrderIndexTooHigh
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CancelOrderTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 7)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2CancelOrder))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.MarketIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.Index))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
)
|
||||
|
||||
const (
|
||||
templateChangePubKey = "Register Lighter Account\n\npubkey: 0x%s\nnonce: %s\naccount index: %s\napi key index: %s\nOnly sign this message for a trusted client!"
|
||||
)
|
||||
|
||||
func getHex10FromUint64(value uint64) string {
|
||||
v := hexutil.EncodeUint64(value)
|
||||
v = strings.Replace(v, "0x", "", 1)
|
||||
|
||||
// Make sure result has fixed bytes
|
||||
vBytes := []byte(v)
|
||||
if len(vBytes) < 16 {
|
||||
toAppend := make([]byte, 16-len(vBytes))
|
||||
for i := range toAppend {
|
||||
toAppend[i] = 48
|
||||
}
|
||||
vBytes = append(toAppend, vBytes...)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("0x%s", string(vBytes))
|
||||
}
|
||||
|
||||
var _ TxInfo = (*L2ChangePubKeyTxInfo)(nil)
|
||||
|
||||
type L2ChangePubKeyTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
PubKey []byte
|
||||
L1Sig string
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2ChangePubKeyTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2ChangePubKey
|
||||
}
|
||||
|
||||
func (txInfo *L2ChangePubKeyTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2ChangePubKeyTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2ChangePubKeyTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
if !IsValidPubKey(txInfo.PubKey) {
|
||||
return ErrPubKeyInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2ChangePubKeyTxInfo) GetL1SignatureBody() string {
|
||||
signatureBody := fmt.Sprintf(templateChangePubKey,
|
||||
common.Bytes2Hex(txInfo.PubKey),
|
||||
getHex10FromUint64(uint64(txInfo.Nonce)),
|
||||
getHex10FromUint64(uint64(txInfo.AccountIndex)),
|
||||
getHex10FromUint64(uint64(txInfo.ApiKeyIndex)),
|
||||
)
|
||||
return signatureBody
|
||||
}
|
||||
|
||||
func (txInfo *L2ChangePubKeyTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 11)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2ChangePubKey))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
|
||||
pubKeyFieldElems, err := g.ArrayFromCanonicalLittleEndianBytes(txInfo.PubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert bytes to field element. bytes: %v, error: %w", txInfo.PubKey, err)
|
||||
}
|
||||
elems = append(elems, pubKeyFieldElems...)
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
curve "github.com/elliottech/poseidon_crypto/curve/ecgfp5"
|
||||
schnorr "github.com/elliottech/poseidon_crypto/signature/schnorr"
|
||||
)
|
||||
|
||||
type (
|
||||
Signature = schnorr.Signature
|
||||
PrivateKey = curve.ECgFp5Scalar
|
||||
)
|
||||
|
||||
const (
|
||||
NilApiKeyIndex = MaxApiKeyIndex + 1
|
||||
)
|
||||
|
||||
const (
|
||||
TxTypeL2ChangePubKey = 8
|
||||
TxTypeL2CreateSubAccount = 9
|
||||
TxTypeL2CreatePublicPool = 10
|
||||
TxTypeL2UpdatePublicPool = 11
|
||||
TxTypeL2Transfer = 12
|
||||
TxTypeL2Withdraw = 13
|
||||
TxTypeL2CreateOrder = 14
|
||||
TxTypeL2CancelOrder = 15
|
||||
TxTypeL2CancelAllOrders = 16
|
||||
TxTypeL2ModifyOrder = 17
|
||||
TxTypeL2MintShares = 18
|
||||
TxTypeL2BurnShares = 19
|
||||
TxTypeL2UpdateLeverage = 20
|
||||
|
||||
TxTypeInternalClaimOrder = 21
|
||||
TxTypeInternalCancelOrder = 22
|
||||
TxTypeInternalDeleverage = 23
|
||||
TxTypeInternalExitPosition = 24
|
||||
TxTypeInternalCancelAllOrders = 25
|
||||
TxTypeInternalLiquidatePosition = 26
|
||||
TxTypeInternalCreateOrder = 27
|
||||
|
||||
TxTypeL2CreateGroupedOrders = 28
|
||||
TxTypeL2UpdateMargin = 29
|
||||
)
|
||||
|
||||
// Order Type
|
||||
const (
|
||||
// User set order types
|
||||
LimitOrder = iota
|
||||
MarketOrder = 1
|
||||
StopLossOrder = 2
|
||||
StopLossLimitOrder = 3
|
||||
TakeProfitOrder = 4
|
||||
TakeProfitLimitOrder = 5
|
||||
TWAPOrder = 6
|
||||
|
||||
// Internal order types
|
||||
TWAPSubOrder = 7
|
||||
LiquidationOrder = 8
|
||||
|
||||
ApiMaxOrderType = TWAPOrder
|
||||
)
|
||||
|
||||
// Order Time-In-Force
|
||||
const (
|
||||
ImmediateOrCancel = iota
|
||||
GoodTillTime = 1
|
||||
PostOnly = 2
|
||||
)
|
||||
|
||||
// Grouping Type
|
||||
const (
|
||||
GroupingType = 0
|
||||
GroupingType_OneTriggersTheOther = 1
|
||||
GroupingType_OneCancelsTheOther = 2
|
||||
GroupingType_OneTriggersAOneCancelsTheOther = 3
|
||||
)
|
||||
|
||||
// Cancel All Orders Time-In-Force
|
||||
const (
|
||||
ImmediateCancelAll = iota
|
||||
ScheduledCancelAll = 1
|
||||
AbortScheduledCancelAll = 2
|
||||
)
|
||||
|
||||
const (
|
||||
HashLength int = 32
|
||||
|
||||
OneUSDC = 1000000
|
||||
|
||||
FeeTick int64 = 1_000_000
|
||||
MarginFractionTick int64 = 10_000
|
||||
ShareTick int64 = 10_000
|
||||
|
||||
MinAccountIndex int64 = 0
|
||||
MaxAccountIndex int64 = 281474976710654 // (1 << 48) - 2
|
||||
MinApiKeyIndex uint8 = 0
|
||||
MaxApiKeyIndex uint8 = 254 // (1 << 8) - 2
|
||||
MaxMasterAccountIndex int64 = 140737488355327 // (1 << 47) - 1
|
||||
|
||||
MinMarketIndex uint8 = 0
|
||||
MaxMarketIndex uint8 = 254 // (1 << 8) - 2
|
||||
|
||||
MaxInvestedPublicPoolCount int64 = 16
|
||||
InitialPoolShareValue int64 = 1_000 // 0.001 USDC
|
||||
MinInitialTotalShares int64 = 1_000 * (OneUSDC / InitialPoolShareValue) // 1,000 USDC worth of shares
|
||||
MaxInitialTotalShares int64 = 1_000_000_000 * (OneUSDC / InitialPoolShareValue) // 1,000,000,000 USDC worth of shares
|
||||
MaxPoolShares int64 = (1 << 60) - 1
|
||||
MaxBurntShareUSDCValue int64 = (1 << 60) - 1
|
||||
|
||||
MaxPoolEntryUSDC = (1 << 56) - 1 // 2^56 - 1 max USDC to invest in a pool
|
||||
MinPoolSharesToMintOrBurn int64 = 1
|
||||
MaxPoolSharesToMintOrBurn int64 = (1 << 60) - 1
|
||||
|
||||
MinNonce int64 = 0
|
||||
|
||||
MinOrderNonce int64 = 0
|
||||
MaxOrderNonce int64 = (1 << 48) - 1
|
||||
|
||||
NilClientOrderIndex int64 = 0
|
||||
NilOrderIndex int64 = 0
|
||||
|
||||
MinClientOrderIndex int64 = 1
|
||||
MaxClientOrderIndex int64 = (1 << 48) - 1
|
||||
|
||||
MinOrderIndex int64 = MaxClientOrderIndex + 1
|
||||
MaxOrderIndex int64 = (1 << 56) - 1
|
||||
|
||||
MinOrderBaseAmount int64 = 1
|
||||
MaxOrderBaseAmount int64 = (1 << 48) - 1
|
||||
NilOrderBaseAmount int64 = 0
|
||||
|
||||
NilOrderPrice uint32 = 0
|
||||
MinOrderPrice uint32 = 1
|
||||
MaxOrderPrice uint32 = (1 << 32) - 1
|
||||
|
||||
MinOrderCancelAllPeriod int64 = 1000 * 60 * 5 // 5 minutes
|
||||
MaxOrderCancelAllPeriod int64 = 1000 * 60 * 60 * 24 * 15 // 15 days
|
||||
|
||||
NilOrderExpiry int64 = 0
|
||||
MinOrderExpiry int64 = 1
|
||||
MaxOrderExpiry int64 = math.MaxInt64
|
||||
|
||||
MinOrderExpiryPeriod int64 = 1000 * 60 * 5 // 5 minutes
|
||||
MaxOrderExpiryPeriod int64 = 1000 * 60 * 60 * 24 * 30 // 30 days
|
||||
|
||||
NilOrderTriggerPrice uint32 = 0
|
||||
MinOrderTriggerPrice uint32 = 1
|
||||
MaxOrderTriggerPrice uint32 = (1 << 32) - 1
|
||||
|
||||
MaxGroupedOrderCount int64 = 3
|
||||
|
||||
MaxTimestamp = (1 << 48) - 1
|
||||
)
|
||||
|
||||
const (
|
||||
MaxExchangeUSDC = (1 << 60) - 1
|
||||
|
||||
MinTransferAmount int64 = 1
|
||||
MaxTransferAmount int64 = MaxExchangeUSDC
|
||||
|
||||
MinWithdrawalAmount uint64 = 1
|
||||
MaxWithdrawalAmount uint64 = MaxExchangeUSDC
|
||||
)
|
||||
|
||||
// Margin Modes
|
||||
const (
|
||||
CrossMargin = iota
|
||||
IsolatedMargin = 1
|
||||
)
|
||||
|
||||
const (
|
||||
RemoveFromIsolatedMargin = 0
|
||||
AddToIsolatedMargin = 1
|
||||
)
|
||||
@@ -0,0 +1,336 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2CreateGroupedOrdersTxInfo)(nil)
|
||||
|
||||
// !!! Ensure that if primary order is reduce only, all child orders are also reduce only
|
||||
// !!! Otherwise CancelPositionTiedAccountOrders flow breaks
|
||||
type L2CreateGroupedOrdersTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
GroupingType uint8
|
||||
|
||||
Orders []*OrderInfo
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2CreateGroupedOrders
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrAccountIndexTooHigh
|
||||
}
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
if len(txInfo.Orders) == 0 || len(txInfo.Orders) > int(MaxGroupedOrderCount) {
|
||||
return ErrOrderGroupSizeInvalid
|
||||
}
|
||||
|
||||
// MarketIndex for first order
|
||||
if txInfo.Orders[0].MarketIndex < MinMarketIndex {
|
||||
return ErrMarketIndexTooLow
|
||||
}
|
||||
if txInfo.Orders[0].MarketIndex > MaxMarketIndex {
|
||||
return ErrMarketIndexTooHigh
|
||||
}
|
||||
|
||||
// Perform range checks for all orders
|
||||
for _, order := range txInfo.Orders {
|
||||
// MarketIndex
|
||||
if order.MarketIndex != txInfo.Orders[0].MarketIndex {
|
||||
return ErrMarketIndexMismatch
|
||||
}
|
||||
|
||||
// ClientOrderIndex
|
||||
if order.ClientOrderIndex != NilClientOrderIndex {
|
||||
return ErrClientOrderIndexNotNil
|
||||
}
|
||||
|
||||
// BaseAmount
|
||||
if order.ReduceOnly != 1 && order.BaseAmount == NilOrderBaseAmount {
|
||||
return ErrBaseAmountTooLow
|
||||
}
|
||||
if order.BaseAmount != NilOrderBaseAmount && order.BaseAmount < MinOrderBaseAmount {
|
||||
return ErrBaseAmountTooLow
|
||||
}
|
||||
if order.BaseAmount > MaxOrderBaseAmount {
|
||||
return ErrBaseAmountTooHigh
|
||||
}
|
||||
|
||||
// Price
|
||||
if order.Price < MinOrderPrice {
|
||||
return ErrPriceTooLow
|
||||
}
|
||||
if order.Price > MaxOrderPrice {
|
||||
return ErrPriceTooHigh
|
||||
}
|
||||
|
||||
// IsAsk
|
||||
if order.IsAsk != 0 && order.IsAsk != 1 {
|
||||
return ErrIsAskInvalid
|
||||
}
|
||||
|
||||
// TimeInForce
|
||||
if order.TimeInForce != ImmediateOrCancel && order.TimeInForce != GoodTillTime && order.TimeInForce != PostOnly {
|
||||
return ErrOrderTimeInForceInvalid
|
||||
}
|
||||
|
||||
// ReduceOnly
|
||||
if order.ReduceOnly != 0 && order.ReduceOnly != 1 {
|
||||
return ErrOrderReduceOnlyInvalid
|
||||
}
|
||||
|
||||
// OrderExpiry
|
||||
if (order.OrderExpiry < MinOrderExpiry || order.OrderExpiry > MaxOrderExpiry) && order.OrderExpiry != NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
|
||||
// TriggerPrice
|
||||
if (order.TriggerPrice < MinOrderTriggerPrice || order.TriggerPrice > MaxOrderTriggerPrice) && order.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
}
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
switch txInfo.GroupingType {
|
||||
case GroupingType_OneCancelsTheOther:
|
||||
return txInfo.ValidateOCO()
|
||||
case GroupingType_OneTriggersTheOther:
|
||||
return txInfo.ValidateOTO()
|
||||
case GroupingType_OneTriggersAOneCancelsTheOther:
|
||||
return txInfo.ValidateOTOCO()
|
||||
default:
|
||||
return ErrGroupingTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) ValidateParentOrder(order *OrderInfo) error {
|
||||
switch order.Type {
|
||||
case MarketOrder:
|
||||
if order.TimeInForce != ImmediateOrCancel {
|
||||
return ErrOrderTimeInForceInvalid
|
||||
} else if order.OrderExpiry != NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
} else if order.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
}
|
||||
case LimitOrder:
|
||||
if order.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
} else if order.TimeInForce == ImmediateOrCancel && order.OrderExpiry != NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
} else if order.TimeInForce != ImmediateOrCancel && order.OrderExpiry == NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
default:
|
||||
return ErrOrderTypeInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) ValidateChildOrder(order *OrderInfo) error {
|
||||
switch order.Type {
|
||||
case StopLossOrder, TakeProfitOrder:
|
||||
if order.TimeInForce != ImmediateOrCancel {
|
||||
return ErrOrderTimeInForceInvalid
|
||||
} else if order.TriggerPrice == NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
} else if order.OrderExpiry == NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
case StopLossLimitOrder, TakeProfitLimitOrder:
|
||||
if order.TriggerPrice == NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
} else if order.OrderExpiry == NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
default:
|
||||
return ErrOrderTypeInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) ValidateSiblingOrders(orders []*OrderInfo) error {
|
||||
if len(orders) != 2 {
|
||||
return ErrOrderGroupSizeInvalid
|
||||
}
|
||||
slFlag := false
|
||||
tpFlag := false
|
||||
for _, order := range orders {
|
||||
err := txInfo.ValidateChildOrder(order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Type == StopLossOrder || order.Type == StopLossLimitOrder {
|
||||
slFlag = true
|
||||
} else if order.Type == TakeProfitOrder || order.Type == TakeProfitLimitOrder {
|
||||
tpFlag = true
|
||||
}
|
||||
}
|
||||
if !slFlag || !tpFlag {
|
||||
return ErrOrderTypeInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) ValidateOCO() error {
|
||||
if len(txInfo.Orders) != 2 {
|
||||
return ErrOrderGroupSizeInvalid
|
||||
}
|
||||
|
||||
// Ensure both orders base sizes are same
|
||||
if txInfo.Orders[0].BaseAmount != txInfo.Orders[1].BaseAmount {
|
||||
return ErrBaseAmountsNotEqual
|
||||
}
|
||||
|
||||
// Orders should be in the same direction
|
||||
if txInfo.Orders[0].IsAsk != txInfo.Orders[1].IsAsk {
|
||||
return ErrIsAskInvalid
|
||||
}
|
||||
|
||||
// Ensure both orders are reduce only
|
||||
if txInfo.Orders[0].ReduceOnly != 1 || txInfo.Orders[1].ReduceOnly != 1 {
|
||||
return ErrOrderReduceOnlyInvalid
|
||||
}
|
||||
|
||||
// Ensure both orders have the same non-nil expiry
|
||||
if txInfo.Orders[0].OrderExpiry != txInfo.Orders[1].OrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
|
||||
return txInfo.ValidateSiblingOrders(txInfo.Orders)
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) ValidateOTO() error {
|
||||
if len(txInfo.Orders) != 2 {
|
||||
return ErrOrderGroupSizeInvalid
|
||||
}
|
||||
|
||||
// Ensure child order base size is 0
|
||||
if txInfo.Orders[1].BaseAmount != NilOrderBaseAmount {
|
||||
return ErrBaseAmountNotNil
|
||||
}
|
||||
|
||||
// Orders should be in the opposite direction
|
||||
if txInfo.Orders[0].IsAsk == txInfo.Orders[1].IsAsk {
|
||||
return ErrIsAskInvalid
|
||||
}
|
||||
|
||||
// Ensure if expiries are not nil, they are the same
|
||||
if txInfo.Orders[0].OrderExpiry != NilOrderExpiry &&
|
||||
txInfo.Orders[0].OrderExpiry != txInfo.Orders[1].OrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
|
||||
err := txInfo.ValidateParentOrder(txInfo.Orders[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return txInfo.ValidateChildOrder(txInfo.Orders[1])
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) ValidateOTOCO() error {
|
||||
if len(txInfo.Orders) != 3 {
|
||||
return ErrOrderGroupSizeInvalid
|
||||
}
|
||||
|
||||
// Ensure child orders base size is 0
|
||||
if txInfo.Orders[1].BaseAmount != NilOrderBaseAmount || txInfo.Orders[2].BaseAmount != NilOrderBaseAmount {
|
||||
return ErrBaseAmountNotNil
|
||||
}
|
||||
|
||||
// Primary and child orders should be in the oppsite direction
|
||||
if txInfo.Orders[0].IsAsk == txInfo.Orders[1].IsAsk || txInfo.Orders[0].IsAsk == txInfo.Orders[2].IsAsk {
|
||||
return ErrIsAskInvalid
|
||||
}
|
||||
|
||||
// Ensure child orders has the same expiry
|
||||
if txInfo.Orders[1].OrderExpiry != txInfo.Orders[2].OrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
|
||||
// Ensure if expiries are not nil, they are the same
|
||||
if txInfo.Orders[0].OrderExpiry != NilOrderExpiry &&
|
||||
txInfo.Orders[0].OrderExpiry != txInfo.Orders[1].OrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
|
||||
err := txInfo.ValidateParentOrder(txInfo.Orders[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return txInfo.ValidateSiblingOrders(txInfo.Orders[1:])
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateGroupedOrdersTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 11)
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2CreateGroupedOrders))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.GroupingType)))
|
||||
|
||||
aggregatedOrderHash := p2.EmptyHashOut()
|
||||
for index, order := range txInfo.Orders {
|
||||
orderHash := p2.HashNoPad([]g.Element{
|
||||
g.FromUint32(uint32(order.MarketIndex)),
|
||||
g.FromInt64(order.ClientOrderIndex),
|
||||
g.FromInt64(order.BaseAmount),
|
||||
g.FromUint32(order.Price),
|
||||
g.FromUint32(uint32(order.IsAsk)),
|
||||
g.FromUint32(uint32(order.Type)),
|
||||
g.FromUint32(uint32(order.TimeInForce)),
|
||||
g.FromUint32(uint32(order.ReduceOnly)),
|
||||
g.FromUint32(order.TriggerPrice),
|
||||
g.FromInt64(order.OrderExpiry),
|
||||
})
|
||||
if index == 0 {
|
||||
aggregatedOrderHash = orderHash
|
||||
} else {
|
||||
aggregatedOrderHash = p2.HashNToOne([]p2.HashOut{aggregatedOrderHash, orderHash})
|
||||
}
|
||||
}
|
||||
elems = append(elems, aggregatedOrderHash[:]...)
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2CreateOrderTxInfo)(nil)
|
||||
|
||||
type L2CreateOrderTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
*OrderInfo
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateOrderTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2CreateOrder
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateOrderTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateOrderTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateOrderTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrAccountIndexTooHigh
|
||||
}
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// MarketIndex
|
||||
if txInfo.MarketIndex < MinMarketIndex {
|
||||
return ErrMarketIndexTooLow
|
||||
}
|
||||
if txInfo.MarketIndex > MaxMarketIndex {
|
||||
return ErrMarketIndexTooHigh
|
||||
}
|
||||
|
||||
// ClientOrderIndex
|
||||
if txInfo.ClientOrderIndex != NilClientOrderIndex {
|
||||
if txInfo.ClientOrderIndex < MinClientOrderIndex {
|
||||
return ErrClientOrderIndexTooLow
|
||||
}
|
||||
if txInfo.ClientOrderIndex > MaxClientOrderIndex {
|
||||
return ErrClientOrderIndexTooHigh
|
||||
}
|
||||
}
|
||||
|
||||
// BaseAmount
|
||||
if txInfo.ReduceOnly != 1 && txInfo.BaseAmount == NilOrderBaseAmount {
|
||||
return ErrBaseAmountTooLow
|
||||
}
|
||||
if txInfo.BaseAmount != NilOrderBaseAmount && txInfo.BaseAmount < MinOrderBaseAmount {
|
||||
return ErrBaseAmountTooLow
|
||||
}
|
||||
if txInfo.BaseAmount > MaxOrderBaseAmount {
|
||||
return ErrBaseAmountTooHigh
|
||||
}
|
||||
|
||||
// Price
|
||||
if txInfo.Price < MinOrderPrice {
|
||||
return ErrPriceTooLow
|
||||
}
|
||||
if txInfo.Price > MaxOrderPrice {
|
||||
return ErrPriceTooHigh
|
||||
}
|
||||
|
||||
// IsAsk
|
||||
if txInfo.IsAsk != 0 && txInfo.IsAsk != 1 {
|
||||
return ErrIsAskInvalid
|
||||
}
|
||||
|
||||
if txInfo.TimeInForce != ImmediateOrCancel && txInfo.TimeInForce != GoodTillTime && txInfo.TimeInForce != PostOnly {
|
||||
return ErrOrderTimeInForceInvalid
|
||||
}
|
||||
|
||||
if txInfo.ReduceOnly != 0 && txInfo.ReduceOnly != 1 {
|
||||
return ErrOrderReduceOnlyInvalid
|
||||
}
|
||||
|
||||
if (txInfo.OrderExpiry < MinOrderExpiry || txInfo.OrderExpiry > MaxOrderExpiry) && txInfo.OrderExpiry != NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
|
||||
switch txInfo.Type {
|
||||
case MarketOrder:
|
||||
if txInfo.TimeInForce != ImmediateOrCancel {
|
||||
return ErrOrderTimeInForceInvalid
|
||||
} else if txInfo.OrderExpiry != NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
} else if txInfo.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
}
|
||||
case LimitOrder:
|
||||
if txInfo.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
} else if txInfo.TimeInForce == ImmediateOrCancel && txInfo.OrderExpiry != NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
} else if txInfo.TimeInForce != ImmediateOrCancel && txInfo.OrderExpiry == NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
case StopLossOrder, TakeProfitOrder:
|
||||
if txInfo.TimeInForce != ImmediateOrCancel {
|
||||
return ErrOrderTimeInForceInvalid
|
||||
} else if txInfo.TriggerPrice == NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
} else if txInfo.OrderExpiry == NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
case StopLossLimitOrder, TakeProfitLimitOrder:
|
||||
if txInfo.TriggerPrice == NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
} else if txInfo.OrderExpiry == NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
case TWAPOrder:
|
||||
if txInfo.TimeInForce != GoodTillTime {
|
||||
return ErrOrderTimeInForceInvalid
|
||||
} else if txInfo.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
} else if txInfo.OrderExpiry == NilOrderExpiry {
|
||||
return ErrOrderExpiryInvalid
|
||||
}
|
||||
default:
|
||||
return ErrOrderTypeInvalid
|
||||
}
|
||||
|
||||
// TriggerPrice
|
||||
if (txInfo.TriggerPrice < MinOrderTriggerPrice || txInfo.TriggerPrice > MaxOrderTriggerPrice) && txInfo.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateOrderTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 16)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2CreateOrder))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.MarketIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.ClientOrderIndex))
|
||||
elems = append(elems, g.FromInt64(txInfo.BaseAmount))
|
||||
elems = append(elems, g.FromUint32(txInfo.Price))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.IsAsk)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.Type)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.TimeInForce)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ReduceOnly)))
|
||||
elems = append(elems, g.FromUint32(txInfo.TriggerPrice))
|
||||
elems = append(elems, g.FromInt64(txInfo.OrderExpiry))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2CreatePublicPoolTxInfo)(nil)
|
||||
|
||||
type L2CreatePublicPoolTxInfo struct {
|
||||
AccountIndex int64 // Master account index
|
||||
ApiKeyIndex uint8
|
||||
|
||||
OperatorFee int64
|
||||
InitialTotalShares int64
|
||||
MinOperatorShareRate int64
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2CreatePublicPoolTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2CreatePublicPool
|
||||
}
|
||||
|
||||
func (txInfo *L2CreatePublicPoolTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2CreatePublicPoolTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2CreatePublicPoolTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxMasterAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// OperatorFee
|
||||
if txInfo.OperatorFee < 0 || txInfo.OperatorFee > FeeTick {
|
||||
return ErrInvalidPoolOperatorFee
|
||||
}
|
||||
|
||||
// InitialTotalShares
|
||||
if txInfo.InitialTotalShares <= 0 {
|
||||
return ErrPoolInitialTotalSharesTooLow
|
||||
}
|
||||
if txInfo.InitialTotalShares > MaxInitialTotalShares {
|
||||
return ErrPoolInitialTotalSharesTooHigh
|
||||
}
|
||||
|
||||
// MinOperatorShareRate
|
||||
if txInfo.MinOperatorShareRate < 0 {
|
||||
return ErrPoolMinOperatorShareRateTooLow
|
||||
}
|
||||
if txInfo.MinOperatorShareRate > ShareTick {
|
||||
return ErrPoolMinOperatorShareRateTooHigh
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CreatePublicPoolTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 9)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2CreatePublicPool))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.OperatorFee))
|
||||
elems = append(elems, g.FromInt64(txInfo.InitialTotalShares))
|
||||
elems = append(elems, g.FromInt64(txInfo.MinOperatorShareRate))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2CreateSubAccountTxInfo)(nil)
|
||||
|
||||
type L2CreateSubAccountTxInfo struct {
|
||||
AccountIndex int64 // Master account index
|
||||
ApiKeyIndex uint8
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateSubAccountTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2CreateSubAccount
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateSubAccountTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateSubAccountTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateSubAccountTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2CreateSubAccountTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 6)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2CreateSubAccount))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package txtypes
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
ErrAccountIndexTooLow = fmt.Errorf("AccountIndex should not be less than %d", MinAccountIndex)
|
||||
ErrAccountIndexTooHigh = fmt.Errorf("AccountIndex should not be larger than %d", MaxAccountIndex)
|
||||
ErrNonceTooLow = fmt.Errorf("AccountNonce should not be less than %d", MinNonce)
|
||||
ErrInvalidCancelAllTimeInForce = fmt.Errorf("CancelAllTimeInForce is invalid")
|
||||
ErrOrderReduceOnlyInvalid = fmt.Errorf("ReduceOnly is invalid")
|
||||
ErrOrderTriggerPriceInvalid = fmt.Errorf("TriggerPrice is invalid")
|
||||
ErrOrderExpiryInvalid = fmt.Errorf("OrderExpiry is invalid")
|
||||
ErrExpiredAtInvalid = fmt.Errorf("ExpiredAt is invalid")
|
||||
ErrCancelAllTimeIsNotInRange = fmt.Errorf("CancelAllTime should be larger than 0 and not larger than %d", MaxOrderExpiry)
|
||||
ErrCancelAllTimeisNotNill = fmt.Errorf("CancelAllTime should be nil")
|
||||
ErrPubKeyInvalid = fmt.Errorf("PubKey is invalid")
|
||||
ErrToAccountIndexTooLow = fmt.Errorf("ToAccountIndex should not be less than %d", MinAccountIndex)
|
||||
ErrToAccountIndexTooHigh = fmt.Errorf("ToAccountIndex should not be larger than %d", MaxAccountIndex)
|
||||
ErrFromAccountIndexTooLow = fmt.Errorf("FromAccountIndex should not be less than %d", MinAccountIndex)
|
||||
ErrFromAccountIndexTooHigh = fmt.Errorf("FromAccountIndex should not be larger than %d", MaxAccountIndex)
|
||||
ErrApiKeyIndexTooLow = fmt.Errorf("ApiKeyIndex should not be less than %d", MinApiKeyIndex)
|
||||
ErrApiKeyIndexTooHigh = fmt.Errorf("ApiKeyIndex should not be larger than %d", MaxApiKeyIndex)
|
||||
ErrPublicPoolIndexTooLow = fmt.Errorf("PublicPoolIndex should not be less than %d", MinAccountIndex)
|
||||
ErrPublicPoolIndexTooHigh = fmt.Errorf("PublicPoolIndex should not be larger than %d", MaxAccountIndex)
|
||||
ErrInvalidPoolOperatorFee = fmt.Errorf("PoolOperatorFee should be larger than 0 and not larger than %d", FeeTick)
|
||||
ErrInvalidPoolStatus = fmt.Errorf("PoolStatus should be either 0 or 1")
|
||||
ErrPoolInitialTotalSharesTooLow = fmt.Errorf("PoolInitialTotalShares should be larger than %d", MinInitialTotalShares)
|
||||
ErrPoolInitialTotalSharesTooHigh = fmt.Errorf("PoolInitialTotalShares should not be larger than %d", MaxInitialTotalShares)
|
||||
ErrPoolMinOperatorShareRateTooLow = fmt.Errorf("PoolMinOperatorShareRate should be larger than 0")
|
||||
ErrPoolMinOperatorShareRateTooHigh = fmt.Errorf("PoolMinOperatorShareRate should not be larger than %d", ShareTick)
|
||||
ErrPoolMintShareAmountTooLow = fmt.Errorf("PoolMintShareAmount should be larger than %d", MinPoolSharesToMintOrBurn)
|
||||
ErrPoolMintShareAmountTooHigh = fmt.Errorf("PoolMintShareAmount should not be larger than %d", MaxPoolSharesToMintOrBurn)
|
||||
ErrPoolBurnShareAmountTooLow = fmt.Errorf("PoolBurnShareAmount should be larger than %d", MinPoolSharesToMintOrBurn)
|
||||
ErrPoolBurnShareAmountTooHigh = fmt.Errorf("PoolBurnShareAmount should not be larger than %d", MaxPoolSharesToMintOrBurn)
|
||||
ErrWithdrawalAmountTooLow = fmt.Errorf("WithdrawalAmount should be larger than %d", MinWithdrawalAmount)
|
||||
ErrWithdrawalAmountTooHigh = fmt.Errorf("WithdrawalAmount should not be larger than %d", MaxWithdrawalAmount)
|
||||
ErrTransferAmountTooLow = fmt.Errorf("TransferAmount should be larger than %d", MinTransferAmount)
|
||||
ErrTransferAmountTooHigh = fmt.Errorf("TransferAmount should not be larger than %d", MaxTransferAmount)
|
||||
ErrMarketIndexTooLow = fmt.Errorf("MarketIndex should not be less than %d", MinMarketIndex)
|
||||
ErrMarketIndexTooHigh = fmt.Errorf("MarketIndex should not be larger than %d", MaxMarketIndex)
|
||||
ErrMarketIndexMismatch = fmt.Errorf("MarketIndex should match the market index of the order")
|
||||
ErrInitialMarginFractionTooLow = fmt.Errorf("InitialMarginFraction should not be less than %d", 0)
|
||||
ErrInitialMarginFractionTooHigh = fmt.Errorf("InitialMarginFraction should not be larger than %d", MarginFractionTick)
|
||||
ErrClientOrderIndexTooLow = fmt.Errorf("ClientOrderIndex should not be less than %d", MinClientOrderIndex)
|
||||
ErrClientOrderIndexTooHigh = fmt.Errorf("ClientOrderIndex should not be larger than %d", MaxClientOrderIndex)
|
||||
ErrClientOrderIndexNotNil = fmt.Errorf("ClientOrderIndex should be nil")
|
||||
ErrOrderIndexTooLow = fmt.Errorf("OrderIndex should not be less than %d", MinOrderIndex)
|
||||
ErrOrderIndexTooHigh = fmt.Errorf("OrderIndex should not be larger than %d", MaxOrderIndex)
|
||||
ErrBaseAmountTooLow = fmt.Errorf("BaseAmount should not be less than %d", MinOrderBaseAmount)
|
||||
ErrBaseAmountTooHigh = fmt.Errorf("BaseAmount should not be larger than %d", MaxOrderBaseAmount)
|
||||
ErrBaseAmountsNotEqual = fmt.Errorf("BaseAmounts should be equal")
|
||||
ErrBaseAmountNotNil = fmt.Errorf("BaseAmount should be nil")
|
||||
ErrPriceTooLow = fmt.Errorf("OrderPrice should not be less than %d", MinOrderPrice)
|
||||
ErrPriceTooHigh = fmt.Errorf("OrderPrice should not be larger than %d", MaxOrderPrice)
|
||||
ErrIsAskInvalid = fmt.Errorf("IsAsk should be 0 or 1")
|
||||
ErrOrderTypeInvalid = fmt.Errorf("OrderType is not valid")
|
||||
ErrOrderTimeInForceInvalid = fmt.Errorf("OrderTimeInForce is not valid")
|
||||
ErrGroupingTypeInvalid = fmt.Errorf("GroupingType is not valid")
|
||||
ErrOrderGroupSizeInvalid = fmt.Errorf("OrderGroupSize is not valid")
|
||||
ErrInvalidSignature = fmt.Errorf("TxSignature is invalid")
|
||||
ErrInvalidMarginMode = fmt.Errorf("MarginMode is not valid")
|
||||
ErrCancelModeInvalid = fmt.Errorf("CancelMode is not valid")
|
||||
ErrInvalidUpdateMarginDirection = fmt.Errorf("Margin movement direction is not valid")
|
||||
ErrTransferFeeNegative = fmt.Errorf("Transfer fee is negative")
|
||||
ErrTransferFeeTooHigh = fmt.Errorf("Transfer fee is higher than %d", MaxTransferAmount)
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
package txtypes
|
||||
|
||||
import g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
|
||||
type TxInfo interface {
|
||||
GetTxType() uint8
|
||||
|
||||
GetTxInfo() (string, error)
|
||||
|
||||
// GetTxHash returns the hash that was signed when creating this transaction.
|
||||
// The hash coincides with the TxHash received from Lighter after submitting this Tx.
|
||||
// It can be used to get the TxHash in advance, or to double-check the correctness of the SDK.
|
||||
// As this hash is signed by the ApiKey, if the value differs than the one computed by the server,
|
||||
// it'll result in an invalid signature.
|
||||
// Returns empty string if the Tx is not signed.
|
||||
GetTxHash() string
|
||||
|
||||
Validate() error
|
||||
|
||||
Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error)
|
||||
}
|
||||
|
||||
type OrderInfo struct {
|
||||
MarketIndex uint8
|
||||
|
||||
ClientOrderIndex int64
|
||||
|
||||
BaseAmount int64
|
||||
Price uint32
|
||||
IsAsk uint8
|
||||
|
||||
Type uint8
|
||||
TimeInForce uint8
|
||||
ReduceOnly uint8
|
||||
TriggerPrice uint32
|
||||
OrderExpiry int64
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2MintSharesTxInfo)(nil)
|
||||
|
||||
type L2MintSharesTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
PublicPoolIndex int64
|
||||
ShareAmount int64
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2MintSharesTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2MintShares
|
||||
}
|
||||
|
||||
func (txInfo *L2MintSharesTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2MintSharesTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2MintSharesTxInfo) Validate() error {
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// PublicPoolIndex
|
||||
if txInfo.PublicPoolIndex < MinAccountIndex {
|
||||
return ErrPublicPoolIndexTooLow
|
||||
}
|
||||
if txInfo.PublicPoolIndex > MaxAccountIndex {
|
||||
return ErrPublicPoolIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.ShareAmount < MinPoolSharesToMintOrBurn {
|
||||
return ErrPoolMintShareAmountTooLow
|
||||
}
|
||||
if txInfo.ShareAmount > MaxPoolSharesToMintOrBurn {
|
||||
return ErrPoolMintShareAmountTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2MintSharesTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 8)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2MintShares))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.PublicPoolIndex))
|
||||
elems = append(elems, g.FromInt64(txInfo.ShareAmount))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2ModifyOrderTxInfo)(nil)
|
||||
|
||||
type L2ModifyOrderTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
MarketIndex uint8
|
||||
Index int64 // Client Order Index or Order Index of the order to modify
|
||||
BaseAmount int64
|
||||
Price uint32
|
||||
TriggerPrice uint32
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2ModifyOrderTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2ModifyOrder
|
||||
}
|
||||
|
||||
func (txInfo *L2ModifyOrderTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2ModifyOrderTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2ModifyOrderTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrAccountIndexTooHigh
|
||||
}
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// MarketIndex
|
||||
if txInfo.MarketIndex < MinMarketIndex {
|
||||
return ErrMarketIndexTooLow
|
||||
}
|
||||
if txInfo.MarketIndex > MaxMarketIndex {
|
||||
return ErrMarketIndexTooHigh
|
||||
}
|
||||
|
||||
// Index
|
||||
if txInfo.Index < MinClientOrderIndex && txInfo.Index < MinOrderIndex {
|
||||
return ErrClientOrderIndexTooLow
|
||||
}
|
||||
if txInfo.Index > MaxClientOrderIndex && txInfo.Index > MaxOrderIndex {
|
||||
return ErrClientOrderIndexTooHigh
|
||||
}
|
||||
|
||||
// BaseAmount
|
||||
if txInfo.BaseAmount != NilOrderBaseAmount && txInfo.BaseAmount < MinOrderBaseAmount {
|
||||
return ErrBaseAmountTooLow
|
||||
}
|
||||
if txInfo.BaseAmount > MaxOrderBaseAmount {
|
||||
return ErrBaseAmountTooHigh
|
||||
}
|
||||
|
||||
// Price
|
||||
if txInfo.Price < MinOrderPrice {
|
||||
return ErrPriceTooLow
|
||||
}
|
||||
if txInfo.Price > MaxOrderPrice {
|
||||
return ErrPriceTooHigh
|
||||
}
|
||||
|
||||
// TriggerPrice
|
||||
if (txInfo.TriggerPrice < MinOrderTriggerPrice || txInfo.TriggerPrice > MaxOrderTriggerPrice) && txInfo.TriggerPrice != NilOrderTriggerPrice {
|
||||
return ErrOrderTriggerPriceInvalid
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2ModifyOrderTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 11)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2ModifyOrder))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.MarketIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.Index))
|
||||
elems = append(elems, g.FromInt64(txInfo.BaseAmount))
|
||||
elems = append(elems, g.FromUint32(txInfo.Price))
|
||||
elems = append(elems, g.FromUint32(txInfo.TriggerPrice))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
const templateTransfer = "Transfer\n\nnonce: %s\nfrom: %s\napi key: %s\nto: %s\namount: %s\nfee: %s\nmemo: %s\nOnly sign this message for a trusted client!"
|
||||
|
||||
var _ TxInfo = (*L2TransferTxInfo)(nil)
|
||||
|
||||
type L2TransferTxInfo struct {
|
||||
FromAccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
ToAccountIndex int64
|
||||
USDCAmount int64 // USDCAmount is given with 6 decimals
|
||||
Fee int64
|
||||
Memo [32]byte
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2TransferTxInfo) Validate() error {
|
||||
// plus one for treasury account
|
||||
if txInfo.FromAccountIndex < MinAccountIndex+1 {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.FromAccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.ToAccountIndex < MinAccountIndex+1 {
|
||||
return ErrToAccountIndexTooLow
|
||||
}
|
||||
if txInfo.ToAccountIndex > MaxAccountIndex {
|
||||
return ErrToAccountIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.USDCAmount <= 0 {
|
||||
return ErrTransferAmountTooLow
|
||||
}
|
||||
if txInfo.USDCAmount > MaxTransferAmount {
|
||||
return ErrTransferAmountTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Fee < 0 {
|
||||
return ErrTransferFeeNegative
|
||||
}
|
||||
if txInfo.Fee > MaxTransferAmount {
|
||||
return ErrTransferFeeTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2TransferTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2Transfer
|
||||
}
|
||||
|
||||
func (txInfo *L2TransferTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2TransferTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2TransferTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 11)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2Transfer))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.FromAccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.ToAccountIndex))
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)&0xFFFFFFFF)) //nolint:gosec
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)>>32)) //nolint:gosec
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.Fee)&0xFFFFFFFF)) //nolint:gosec
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.Fee)>>32)) //nolint:gosec
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
|
||||
func (txInfo *L2TransferTxInfo) GetL1SignatureBody() string {
|
||||
hexMemo := hex.EncodeToString(txInfo.Memo[:])
|
||||
hexMemo = strings.Replace(hexMemo, "0x", "", 1)
|
||||
|
||||
signatureBody := fmt.Sprintf(
|
||||
templateTransfer,
|
||||
|
||||
getHex10FromUint64(uint64(txInfo.Nonce)),
|
||||
getHex10FromUint64(uint64(txInfo.FromAccountIndex)),
|
||||
getHex10FromUint64(uint64(txInfo.ApiKeyIndex)),
|
||||
getHex10FromUint64(uint64(txInfo.ToAccountIndex)),
|
||||
getHex10FromUint64(uint64(txInfo.USDCAmount)),
|
||||
getHex10FromUint64(uint64(txInfo.Fee)),
|
||||
hexMemo,
|
||||
)
|
||||
return signatureBody
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2UpdateLeverageTxInfo)(nil)
|
||||
|
||||
type L2UpdateLeverageTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
MarketIndex uint8
|
||||
InitialMarginFraction uint16
|
||||
MarginMode uint8
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateLeverageTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2UpdateLeverage
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateLeverageTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateLeverageTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateLeverageTxInfo) Validate() error {
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// MarketIndex
|
||||
if txInfo.MarketIndex < MinMarketIndex {
|
||||
return ErrMarketIndexTooLow
|
||||
}
|
||||
if txInfo.MarketIndex > MaxMarketIndex {
|
||||
return ErrMarketIndexTooHigh
|
||||
}
|
||||
|
||||
// InitialMarginFraction
|
||||
if txInfo.InitialMarginFraction <= 0 {
|
||||
return ErrInitialMarginFractionTooLow
|
||||
}
|
||||
if txInfo.InitialMarginFraction > uint16(MarginFractionTick) { //nolint:gosec
|
||||
return ErrInitialMarginFractionTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
if txInfo.MarginMode != CrossMargin && txInfo.MarginMode != IsolatedMargin {
|
||||
return ErrInvalidMarginMode
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateLeverageTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 9)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2UpdateLeverage))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromInt64(int64(txInfo.MarketIndex)))
|
||||
elems = append(elems, g.FromInt64(int64(txInfo.InitialMarginFraction)))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.MarginMode)))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2UpdateMarginTxInfo)(nil)
|
||||
|
||||
type L2UpdateMarginTxInfo struct {
|
||||
AccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
MarketIndex uint8
|
||||
USDCAmount int64
|
||||
Direction uint8
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateMarginTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2UpdateMargin
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateMarginTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateMarginTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateMarginTxInfo) Validate() error {
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// MarketIndex
|
||||
if txInfo.MarketIndex < MinMarketIndex {
|
||||
return ErrMarketIndexTooLow
|
||||
}
|
||||
if txInfo.MarketIndex > MaxMarketIndex {
|
||||
return ErrMarketIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.USDCAmount <= 0 {
|
||||
return ErrTransferAmountTooLow
|
||||
}
|
||||
if txInfo.USDCAmount > MaxTransferAmount {
|
||||
return ErrTransferAmountTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Direction != RemoveFromIsolatedMargin && txInfo.Direction != AddToIsolatedMargin {
|
||||
return ErrInvalidUpdateMarginDirection
|
||||
}
|
||||
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdateMarginTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 10)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2UpdateMargin))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromInt64(int64(txInfo.MarketIndex)))
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)&0xFFFFFFFF)) //nolint:gosec
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)>>32)) //nolint:gosec
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.Direction)))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2UpdatePublicPoolTxInfo)(nil)
|
||||
|
||||
type L2UpdatePublicPoolTxInfo struct {
|
||||
AccountIndex int64 // Master account index
|
||||
ApiKeyIndex uint8
|
||||
|
||||
PublicPoolIndex int64
|
||||
|
||||
Status uint8
|
||||
OperatorFee int64
|
||||
MinOperatorShareRate int64
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdatePublicPoolTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2UpdatePublicPool
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdatePublicPoolTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdatePublicPoolTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdatePublicPoolTxInfo) Validate() error {
|
||||
// AccountIndex
|
||||
if txInfo.AccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.AccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
// PublicPoolIndex
|
||||
if txInfo.PublicPoolIndex < MinAccountIndex {
|
||||
return ErrPublicPoolIndexTooLow
|
||||
}
|
||||
if txInfo.PublicPoolIndex > MaxAccountIndex {
|
||||
return ErrPublicPoolIndexTooHigh
|
||||
}
|
||||
|
||||
// Status
|
||||
if txInfo.Status != 0 && txInfo.Status != 1 {
|
||||
return ErrInvalidPoolStatus
|
||||
}
|
||||
|
||||
// OperatorFee
|
||||
if txInfo.OperatorFee < 0 || txInfo.OperatorFee > FeeTick {
|
||||
return ErrInvalidPoolOperatorFee
|
||||
}
|
||||
|
||||
// MinOperatorShareRate
|
||||
if txInfo.MinOperatorShareRate < 0 {
|
||||
return ErrPoolMinOperatorShareRateTooLow
|
||||
}
|
||||
if txInfo.MinOperatorShareRate > ShareTick {
|
||||
return ErrPoolMinOperatorShareRateTooHigh
|
||||
}
|
||||
|
||||
// Nonce
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2UpdatePublicPoolTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 10)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2UpdatePublicPool))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.AccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromInt64(txInfo.PublicPoolIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.Status)))
|
||||
elems = append(elems, g.FromInt64(txInfo.OperatorFee))
|
||||
elems = append(elems, g.FromInt64(txInfo.MinOperatorShareRate))
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package txtypes
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func IsValidPubKey(bytes []byte) bool {
|
||||
if len(bytes) != 40 {
|
||||
return false
|
||||
}
|
||||
|
||||
return !isZeroByteSlice(bytes)
|
||||
}
|
||||
|
||||
func isZeroByteSlice(bytes []byte) bool {
|
||||
for _, s := range bytes {
|
||||
if s != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getTxInfo(tx interface{}) (string, error) {
|
||||
txInfoBytes, err := json.Marshal(tx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(txInfoBytes), nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package txtypes
|
||||
|
||||
import (
|
||||
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
|
||||
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
|
||||
)
|
||||
|
||||
var _ TxInfo = (*L2WithdrawTxInfo)(nil)
|
||||
|
||||
type L2WithdrawTxInfo struct {
|
||||
FromAccountIndex int64
|
||||
ApiKeyIndex uint8
|
||||
|
||||
USDCAmount uint64 // USDCAmount is given with 6 decimals
|
||||
|
||||
ExpiredAt int64
|
||||
Nonce int64
|
||||
Sig []byte
|
||||
SignedHash string `json:"-"`
|
||||
}
|
||||
|
||||
func (txInfo *L2WithdrawTxInfo) Validate() error {
|
||||
if txInfo.FromAccountIndex < MinAccountIndex {
|
||||
return ErrFromAccountIndexTooLow
|
||||
}
|
||||
if txInfo.FromAccountIndex > MaxAccountIndex {
|
||||
return ErrFromAccountIndexTooHigh
|
||||
}
|
||||
|
||||
// ApiKeyIndex
|
||||
if txInfo.ApiKeyIndex < MinApiKeyIndex {
|
||||
return ErrApiKeyIndexTooLow
|
||||
}
|
||||
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
|
||||
return ErrApiKeyIndexTooHigh
|
||||
}
|
||||
|
||||
if txInfo.USDCAmount == 0 {
|
||||
return ErrWithdrawalAmountTooLow
|
||||
}
|
||||
if txInfo.USDCAmount > MaxWithdrawalAmount {
|
||||
return ErrWithdrawalAmountTooHigh
|
||||
}
|
||||
|
||||
if txInfo.Nonce < MinNonce {
|
||||
return ErrNonceTooLow
|
||||
}
|
||||
|
||||
if txInfo.ExpiredAt < 0 || txInfo.ExpiredAt > MaxTimestamp {
|
||||
return ErrExpiredAtInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txInfo *L2WithdrawTxInfo) GetTxType() uint8 {
|
||||
return TxTypeL2Withdraw
|
||||
}
|
||||
|
||||
func (txInfo *L2WithdrawTxInfo) GetTxInfo() (string, error) {
|
||||
return getTxInfo(txInfo)
|
||||
}
|
||||
|
||||
func (txInfo *L2WithdrawTxInfo) GetTxHash() string {
|
||||
return txInfo.SignedHash
|
||||
}
|
||||
|
||||
func (txInfo *L2WithdrawTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
|
||||
elems := make([]g.Element, 0, 8)
|
||||
|
||||
elems = append(elems, g.FromUint32(lighterChainId))
|
||||
elems = append(elems, g.FromUint32(TxTypeL2Withdraw))
|
||||
elems = append(elems, g.FromInt64(txInfo.Nonce))
|
||||
elems = append(elems, g.FromInt64(txInfo.ExpiredAt))
|
||||
|
||||
elems = append(elems, g.FromInt64(txInfo.FromAccountIndex))
|
||||
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)&0xFFFFFFFF)) //nolint:gosec
|
||||
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)>>32)) //nolint:gosec
|
||||
|
||||
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# NOTE: This file is auto generated by OpenAPI Generator.
|
||||
# URL: https://openapi-generator.tech
|
||||
#
|
||||
# ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||
|
||||
name: lighter Python package
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest --ignore-glob="*api.py"
|
||||
@@ -0,0 +1,71 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
dev/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
venv/
|
||||
.venv/
|
||||
.python-version
|
||||
.pytest_cache
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
#Ipython Notebook
|
||||
.ipynb_checkpoints
|
||||
openapi-generator-cli.jar
|
||||
|
||||
|
||||
.idea
|
||||
|
||||
examples/secrets.py
|
||||
@@ -0,0 +1,28 @@
|
||||
# NOTE: This file is auto generated by OpenAPI Generator.
|
||||
# URL: https://openapi-generator.tech
|
||||
#
|
||||
# ref: https://docs.gitlab.com/ee/ci/README.html
|
||||
# ref: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
|
||||
|
||||
stages:
|
||||
- test
|
||||
|
||||
.pytest:
|
||||
stage: test
|
||||
script:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r test-requirements.txt
|
||||
- pytest --cov=lighter --ignore-glob *api.py
|
||||
|
||||
pytest-3.8:
|
||||
extends: .pytest
|
||||
image: python:3.8-alpine
|
||||
pytest-3.9:
|
||||
extends: .pytest
|
||||
image: python:3.9-alpine
|
||||
pytest-3.10:
|
||||
extends: .pytest
|
||||
image: python:3.10-alpine
|
||||
pytest-3.11:
|
||||
extends: .pytest
|
||||
image: python:3.11-alpine
|
||||
@@ -0,0 +1,23 @@
|
||||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
||||
@@ -0,0 +1,294 @@
|
||||
docs/Account.md
|
||||
docs/AccountApi.md
|
||||
docs/AccountApiKeys.md
|
||||
docs/AccountLimits.md
|
||||
docs/AccountMarginStats.md
|
||||
docs/AccountMarketStats.md
|
||||
docs/AccountMetadata.md
|
||||
docs/AccountMetadatas.md
|
||||
docs/AccountPnL.md
|
||||
docs/AccountPosition.md
|
||||
docs/AccountStats.md
|
||||
docs/AccountTradeStats.md
|
||||
docs/Announcement.md
|
||||
docs/AnnouncementApi.md
|
||||
docs/Announcements.md
|
||||
docs/ApiKey.md
|
||||
docs/Block.md
|
||||
docs/BlockApi.md
|
||||
docs/Blocks.md
|
||||
docs/BridgeApi.md
|
||||
docs/BridgeSupportedNetwork.md
|
||||
docs/Candlestick.md
|
||||
docs/CandlestickApi.md
|
||||
docs/Candlesticks.md
|
||||
docs/ContractAddress.md
|
||||
docs/CurrentHeight.md
|
||||
docs/Cursor.md
|
||||
docs/DailyReturn.md
|
||||
docs/DepositHistory.md
|
||||
docs/DepositHistoryItem.md
|
||||
docs/DetailedAccount.md
|
||||
docs/DetailedAccounts.md
|
||||
docs/DetailedCandlestick.md
|
||||
docs/EnrichedTx.md
|
||||
docs/ExchangeStats.md
|
||||
docs/ExportData.md
|
||||
docs/Funding.md
|
||||
docs/FundingApi.md
|
||||
docs/FundingRate.md
|
||||
docs/FundingRates.md
|
||||
docs/Fundings.md
|
||||
docs/InfoApi.md
|
||||
docs/L1Metadata.md
|
||||
docs/L1ProviderInfo.md
|
||||
docs/LiqTrade.md
|
||||
docs/Liquidation.md
|
||||
docs/LiquidationInfo.md
|
||||
docs/LiquidationInfos.md
|
||||
docs/MarketInfo.md
|
||||
docs/NextNonce.md
|
||||
docs/NotificationApi.md
|
||||
docs/Order.md
|
||||
docs/OrderApi.md
|
||||
docs/OrderBook.md
|
||||
docs/OrderBookDepth.md
|
||||
docs/OrderBookDetail.md
|
||||
docs/OrderBookDetails.md
|
||||
docs/OrderBookOrders.md
|
||||
docs/OrderBookStats.md
|
||||
docs/OrderBooks.md
|
||||
docs/Orders.md
|
||||
docs/PnLEntry.md
|
||||
docs/PositionFunding.md
|
||||
docs/PositionFundings.md
|
||||
docs/PriceLevel.md
|
||||
docs/PublicPool.md
|
||||
docs/PublicPoolInfo.md
|
||||
docs/PublicPoolMetadata.md
|
||||
docs/PublicPoolShare.md
|
||||
docs/PublicPools.md
|
||||
docs/ReferralApi.md
|
||||
docs/ReferralPointEntry.md
|
||||
docs/ReferralPoints.md
|
||||
docs/ReqExportData.md
|
||||
docs/ReqGetAccount.md
|
||||
docs/ReqGetAccountActiveOrders.md
|
||||
docs/ReqGetAccountApiKeys.md
|
||||
docs/ReqGetAccountByL1Address.md
|
||||
docs/ReqGetAccountInactiveOrders.md
|
||||
docs/ReqGetAccountLimits.md
|
||||
docs/ReqGetAccountMetadata.md
|
||||
docs/ReqGetAccountPnL.md
|
||||
docs/ReqGetAccountTxs.md
|
||||
docs/ReqGetBlock.md
|
||||
docs/ReqGetBlockTxs.md
|
||||
docs/ReqGetByAccount.md
|
||||
docs/ReqGetCandlesticks.md
|
||||
docs/ReqGetDepositHistory.md
|
||||
docs/ReqGetFastWithdrawInfo.md
|
||||
docs/ReqGetFundings.md
|
||||
docs/ReqGetL1Metadata.md
|
||||
docs/ReqGetL1Tx.md
|
||||
docs/ReqGetLatestDeposit.md
|
||||
docs/ReqGetLiquidationInfos.md
|
||||
docs/ReqGetNextNonce.md
|
||||
docs/ReqGetOrderBookDetails.md
|
||||
docs/ReqGetOrderBookOrders.md
|
||||
docs/ReqGetOrderBooks.md
|
||||
docs/ReqGetPositionFunding.md
|
||||
docs/ReqGetPublicPools.md
|
||||
docs/ReqGetPublicPoolsMetadata.md
|
||||
docs/ReqGetRangeWithCursor.md
|
||||
docs/ReqGetRangeWithIndex.md
|
||||
docs/ReqGetRangeWithIndexSortable.md
|
||||
docs/ReqGetRecentTrades.md
|
||||
docs/ReqGetReferralPoints.md
|
||||
docs/ReqGetTrades.md
|
||||
docs/ReqGetTransferFeeInfo.md
|
||||
docs/ReqGetTransferHistory.md
|
||||
docs/ReqGetTx.md
|
||||
docs/ReqGetWithdrawHistory.md
|
||||
docs/RespChangeAccountTier.md
|
||||
docs/RespGetFastBridgeInfo.md
|
||||
docs/RespPublicPoolsMetadata.md
|
||||
docs/RespSendTx.md
|
||||
docs/RespSendTxBatch.md
|
||||
docs/RespWithdrawalDelay.md
|
||||
docs/ResultCode.md
|
||||
docs/RiskInfo.md
|
||||
docs/RiskParameters.md
|
||||
docs/RootApi.md
|
||||
docs/SharePrice.md
|
||||
docs/SimpleOrder.md
|
||||
docs/Status.md
|
||||
docs/SubAccounts.md
|
||||
docs/Ticker.md
|
||||
docs/Trade.md
|
||||
docs/Trades.md
|
||||
docs/TransactionApi.md
|
||||
docs/TransferFeeInfo.md
|
||||
docs/TransferHistory.md
|
||||
docs/TransferHistoryItem.md
|
||||
docs/Tx.md
|
||||
docs/TxHash.md
|
||||
docs/TxHashes.md
|
||||
docs/Txs.md
|
||||
docs/ValidatorInfo.md
|
||||
docs/WithdrawHistory.md
|
||||
docs/WithdrawHistoryItem.md
|
||||
docs/ZkLighterInfo.md
|
||||
git_push.sh
|
||||
lighter/__init__.py
|
||||
lighter/api/__init__.py
|
||||
lighter/api/account_api.py
|
||||
lighter/api/announcement_api.py
|
||||
lighter/api/block_api.py
|
||||
lighter/api/bridge_api.py
|
||||
lighter/api/candlestick_api.py
|
||||
lighter/api/funding_api.py
|
||||
lighter/api/info_api.py
|
||||
lighter/api/notification_api.py
|
||||
lighter/api/order_api.py
|
||||
lighter/api/referral_api.py
|
||||
lighter/api/root_api.py
|
||||
lighter/api/transaction_api.py
|
||||
lighter/api_client.py
|
||||
lighter/api_response.py
|
||||
lighter/configuration.py
|
||||
lighter/exceptions.py
|
||||
lighter/models/__init__.py
|
||||
lighter/models/account.py
|
||||
lighter/models/account_api_keys.py
|
||||
lighter/models/account_limits.py
|
||||
lighter/models/account_margin_stats.py
|
||||
lighter/models/account_market_stats.py
|
||||
lighter/models/account_metadata.py
|
||||
lighter/models/account_metadatas.py
|
||||
lighter/models/account_pn_l.py
|
||||
lighter/models/account_position.py
|
||||
lighter/models/account_stats.py
|
||||
lighter/models/account_trade_stats.py
|
||||
lighter/models/announcement.py
|
||||
lighter/models/announcements.py
|
||||
lighter/models/api_key.py
|
||||
lighter/models/block.py
|
||||
lighter/models/blocks.py
|
||||
lighter/models/bridge_supported_network.py
|
||||
lighter/models/candlestick.py
|
||||
lighter/models/candlesticks.py
|
||||
lighter/models/contract_address.py
|
||||
lighter/models/current_height.py
|
||||
lighter/models/cursor.py
|
||||
lighter/models/daily_return.py
|
||||
lighter/models/deposit_history.py
|
||||
lighter/models/deposit_history_item.py
|
||||
lighter/models/detailed_account.py
|
||||
lighter/models/detailed_accounts.py
|
||||
lighter/models/detailed_candlestick.py
|
||||
lighter/models/enriched_tx.py
|
||||
lighter/models/exchange_stats.py
|
||||
lighter/models/export_data.py
|
||||
lighter/models/funding.py
|
||||
lighter/models/funding_rate.py
|
||||
lighter/models/funding_rates.py
|
||||
lighter/models/fundings.py
|
||||
lighter/models/l1_metadata.py
|
||||
lighter/models/l1_provider_info.py
|
||||
lighter/models/liq_trade.py
|
||||
lighter/models/liquidation.py
|
||||
lighter/models/liquidation_info.py
|
||||
lighter/models/liquidation_infos.py
|
||||
lighter/models/market_info.py
|
||||
lighter/models/next_nonce.py
|
||||
lighter/models/order.py
|
||||
lighter/models/order_book.py
|
||||
lighter/models/order_book_depth.py
|
||||
lighter/models/order_book_detail.py
|
||||
lighter/models/order_book_details.py
|
||||
lighter/models/order_book_orders.py
|
||||
lighter/models/order_book_stats.py
|
||||
lighter/models/order_books.py
|
||||
lighter/models/orders.py
|
||||
lighter/models/pn_l_entry.py
|
||||
lighter/models/position_funding.py
|
||||
lighter/models/position_fundings.py
|
||||
lighter/models/price_level.py
|
||||
lighter/models/public_pool.py
|
||||
lighter/models/public_pool_info.py
|
||||
lighter/models/public_pool_metadata.py
|
||||
lighter/models/public_pool_share.py
|
||||
lighter/models/public_pools.py
|
||||
lighter/models/referral_point_entry.py
|
||||
lighter/models/referral_points.py
|
||||
lighter/models/req_export_data.py
|
||||
lighter/models/req_get_account.py
|
||||
lighter/models/req_get_account_active_orders.py
|
||||
lighter/models/req_get_account_api_keys.py
|
||||
lighter/models/req_get_account_by_l1_address.py
|
||||
lighter/models/req_get_account_inactive_orders.py
|
||||
lighter/models/req_get_account_limits.py
|
||||
lighter/models/req_get_account_metadata.py
|
||||
lighter/models/req_get_account_pn_l.py
|
||||
lighter/models/req_get_account_txs.py
|
||||
lighter/models/req_get_block.py
|
||||
lighter/models/req_get_block_txs.py
|
||||
lighter/models/req_get_by_account.py
|
||||
lighter/models/req_get_candlesticks.py
|
||||
lighter/models/req_get_deposit_history.py
|
||||
lighter/models/req_get_fast_withdraw_info.py
|
||||
lighter/models/req_get_fundings.py
|
||||
lighter/models/req_get_l1_metadata.py
|
||||
lighter/models/req_get_l1_tx.py
|
||||
lighter/models/req_get_latest_deposit.py
|
||||
lighter/models/req_get_liquidation_infos.py
|
||||
lighter/models/req_get_next_nonce.py
|
||||
lighter/models/req_get_order_book_details.py
|
||||
lighter/models/req_get_order_book_orders.py
|
||||
lighter/models/req_get_order_books.py
|
||||
lighter/models/req_get_position_funding.py
|
||||
lighter/models/req_get_public_pools.py
|
||||
lighter/models/req_get_public_pools_metadata.py
|
||||
lighter/models/req_get_range_with_cursor.py
|
||||
lighter/models/req_get_range_with_index.py
|
||||
lighter/models/req_get_range_with_index_sortable.py
|
||||
lighter/models/req_get_recent_trades.py
|
||||
lighter/models/req_get_referral_points.py
|
||||
lighter/models/req_get_trades.py
|
||||
lighter/models/req_get_transfer_fee_info.py
|
||||
lighter/models/req_get_transfer_history.py
|
||||
lighter/models/req_get_tx.py
|
||||
lighter/models/req_get_withdraw_history.py
|
||||
lighter/models/resp_change_account_tier.py
|
||||
lighter/models/resp_get_fast_bridge_info.py
|
||||
lighter/models/resp_public_pools_metadata.py
|
||||
lighter/models/resp_send_tx.py
|
||||
lighter/models/resp_send_tx_batch.py
|
||||
lighter/models/resp_withdrawal_delay.py
|
||||
lighter/models/result_code.py
|
||||
lighter/models/risk_info.py
|
||||
lighter/models/risk_parameters.py
|
||||
lighter/models/share_price.py
|
||||
lighter/models/simple_order.py
|
||||
lighter/models/status.py
|
||||
lighter/models/sub_accounts.py
|
||||
lighter/models/ticker.py
|
||||
lighter/models/trade.py
|
||||
lighter/models/trades.py
|
||||
lighter/models/transfer_fee_info.py
|
||||
lighter/models/transfer_history.py
|
||||
lighter/models/transfer_history_item.py
|
||||
lighter/models/tx.py
|
||||
lighter/models/tx_hash.py
|
||||
lighter/models/tx_hashes.py
|
||||
lighter/models/txs.py
|
||||
lighter/models/validator_info.py
|
||||
lighter/models/withdraw_history.py
|
||||
lighter/models/withdraw_history_item.py
|
||||
lighter/models/zk_lighter_info.py
|
||||
lighter/py.typed
|
||||
lighter/rest.py
|
||||
setup.cfg
|
||||
test-requirements.txt
|
||||
test/__init__.py
|
||||
tox.ini
|
||||
@@ -0,0 +1 @@
|
||||
7.7.0
|
||||
@@ -0,0 +1,16 @@
|
||||
# ref: https://docs.travis-ci.com/user/languages/python
|
||||
language: python
|
||||
python:
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
# uncomment the following if needed
|
||||
#- "3.11-dev" # 3.11 development branch
|
||||
#- "nightly" # nightly build
|
||||
# command to install dependencies
|
||||
install:
|
||||
- "pip install -r requirements.txt"
|
||||
- "pip install -r test-requirements.txt"
|
||||
# command to run tests
|
||||
script: pytest --cov=lighter
|
||||
@@ -0,0 +1,198 @@
|
||||
# Lighter Python
|
||||
|
||||
Python SDK for Lighter
|
||||
|
||||
## Requirements.
|
||||
|
||||
Python 3.8+
|
||||
|
||||
## Installation & Usage
|
||||
### pip install
|
||||
|
||||
If the python package is hosted on a repository, you can install directly using:
|
||||
|
||||
```sh
|
||||
pip install git+https://github.com/elliottech/lighter-python.git
|
||||
```
|
||||
|
||||
|
||||
Then import the package:
|
||||
```python
|
||||
import lighter
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
Execute `pytest` to run the tests.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Please follow the [installation procedure](#installation--usage) and then run the following:
|
||||
|
||||
```python
|
||||
|
||||
import lighter
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
client = lighter.ApiClient()
|
||||
account_api = lighter.AccountApi(client)
|
||||
account = await account_api.get_account(by="index", value="1")
|
||||
print(account)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
```
|
||||
|
||||
# Examples
|
||||
## [Read API Functions](examples/get_info.py)
|
||||
```sh
|
||||
python examples/get_info.py
|
||||
```
|
||||
|
||||
## [Websocket Sync Order Books & Accounts](examples/ws.py)
|
||||
```sh
|
||||
python examples/ws.py
|
||||
```
|
||||
|
||||
## [Create & Cancel Orders](examples/create_cancel_order.py)
|
||||
```sh
|
||||
python examples/create_cancel_order.py
|
||||
```
|
||||
|
||||
## Documentation for API Endpoints
|
||||
|
||||
All URIs are relative to *https://mainnet.zklighter.elliot.ai*
|
||||
|
||||
Class | Method | HTTP request | Description
|
||||
------------ | ------------- | ------------- | -------------
|
||||
*AccountApi* | [**account**](docs/AccountApi.md#account) | **GET** /api/v1/account | account
|
||||
*AccountApi* | [**accounts_by_l1_address**](docs/AccountApi.md#accounts_by_l1_address) | **GET** /api/v1/accountsByL1Address | accountsByL1Address
|
||||
*AccountApi* | [**apikeys**](docs/AccountApi.md#apikeys) | **GET** /api/v1/apikeys | apikeys
|
||||
*AccountApi* | [**pnl**](docs/AccountApi.md#pnl) | **GET** /api/v1/pnl | pnl
|
||||
*AccountApi* | [**public_pools**](docs/AccountApi.md#public_pools) | **GET** /api/v1/publicPools | publicPools
|
||||
*BlockApi* | [**block**](docs/BlockApi.md#block) | **GET** /api/v1/block | block
|
||||
*BlockApi* | [**blocks**](docs/BlockApi.md#blocks) | **GET** /api/v1/blocks | blocks
|
||||
*BlockApi* | [**current_height**](docs/BlockApi.md#current_height) | **GET** /api/v1/currentHeight | currentHeight
|
||||
*CandlestickApi* | [**candlesticks**](docs/CandlestickApi.md#candlesticks) | **GET** /api/v1/candlesticks | candlesticks
|
||||
*CandlestickApi* | [**fundings**](docs/CandlestickApi.md#fundings) | **GET** /api/v1/fundings | fundings
|
||||
*OrderApi* | [**account_inactive_orders**](docs/OrderApi.md#account_inactive_orders) | **GET** /api/v1/accountInactiveOrders | accountInactiveOrders
|
||||
*OrderApi* | [**exchange_stats**](docs/OrderApi.md#exchange_stats) | **GET** /api/v1/exchangeStats | exchangeStats
|
||||
*OrderApi* | [**order_book_details**](docs/OrderApi.md#order_book_details) | **GET** /api/v1/orderBookDetails | orderBookDetails
|
||||
*OrderApi* | [**order_book_orders**](docs/OrderApi.md#order_book_orders) | **GET** /api/v1/orderBookOrders | orderBookOrders
|
||||
*OrderApi* | [**order_books**](docs/OrderApi.md#order_books) | **GET** /api/v1/orderBooks | orderBooks
|
||||
*OrderApi* | [**recent_trades**](docs/OrderApi.md#recent_trades) | **GET** /api/v1/recentTrades | recentTrades
|
||||
*OrderApi* | [**trades**](docs/OrderApi.md#trades) | **GET** /api/v1/trades | trades
|
||||
*RootApi* | [**info**](docs/RootApi.md#info) | **GET** /info | info
|
||||
*RootApi* | [**status**](docs/RootApi.md#status) | **GET** / | status
|
||||
*TransactionApi* | [**account_txs**](docs/TransactionApi.md#account_txs) | **GET** /api/v1/accountTxs | accountTxs
|
||||
*TransactionApi* | [**block_txs**](docs/TransactionApi.md#block_txs) | **GET** /api/v1/blockTxs | blockTxs
|
||||
*TransactionApi* | [**deposit_history**](docs/TransactionApi.md#deposit_history) | **GET** /api/v1/deposit/history | deposit_history
|
||||
*TransactionApi* | [**next_nonce**](docs/TransactionApi.md#next_nonce) | **GET** /api/v1/nextNonce | nextNonce
|
||||
*TransactionApi* | [**send_tx**](docs/TransactionApi.md#send_tx) | **POST** /api/v1/sendTx | sendTx
|
||||
*TransactionApi* | [**send_tx_batch**](docs/TransactionApi.md#send_tx_batch) | **POST** /api/v1/sendTxBatch | sendTxBatch
|
||||
*TransactionApi* | [**tx**](docs/TransactionApi.md#tx) | **GET** /api/v1/tx | tx
|
||||
*TransactionApi* | [**tx_from_l1_tx_hash**](docs/TransactionApi.md#tx_from_l1_tx_hash) | **GET** /api/v1/txFromL1TxHash | txFromL1TxHash
|
||||
*TransactionApi* | [**txs**](docs/TransactionApi.md#txs) | **GET** /api/v1/txs | txs
|
||||
*TransactionApi* | [**withdraw_history**](docs/TransactionApi.md#withdraw_history) | **GET** /api/v1/withdraw/history | withdraw_history
|
||||
|
||||
|
||||
## Documentation For Models
|
||||
|
||||
- [Account](docs/Account.md)
|
||||
- [AccountApiKeys](docs/AccountApiKeys.md)
|
||||
- [AccountMarketStats](docs/AccountMarketStats.md)
|
||||
- [AccountMetadata](docs/AccountMetadata.md)
|
||||
- [AccountPnL](docs/AccountPnL.md)
|
||||
- [AccountPosition](docs/AccountPosition.md)
|
||||
- [AccountStats](docs/AccountStats.md)
|
||||
- [ApiKey](docs/ApiKey.md)
|
||||
- [Block](docs/Block.md)
|
||||
- [Blocks](docs/Blocks.md)
|
||||
- [BridgeSupportedNetwork](docs/BridgeSupportedNetwork.md)
|
||||
- [Candlestick](docs/Candlestick.md)
|
||||
- [Candlesticks](docs/Candlesticks.md)
|
||||
- [ContractAddress](docs/ContractAddress.md)
|
||||
- [CurrentHeight](docs/CurrentHeight.md)
|
||||
- [Cursor](docs/Cursor.md)
|
||||
- [DepositHistory](docs/DepositHistory.md)
|
||||
- [DepositHistoryItem](docs/DepositHistoryItem.md)
|
||||
- [DetailedAccount](docs/DetailedAccount.md)
|
||||
- [DetailedAccounts](docs/DetailedAccounts.md)
|
||||
- [DetailedCandlestick](docs/DetailedCandlestick.md)
|
||||
- [EnrichedTx](docs/EnrichedTx.md)
|
||||
- [ExchangeStats](docs/ExchangeStats.md)
|
||||
- [Funding](docs/Funding.md)
|
||||
- [Fundings](docs/Fundings.md)
|
||||
- [L1ProviderInfo](docs/L1ProviderInfo.md)
|
||||
- [Liquidation](docs/Liquidation.md)
|
||||
- [MarketInfo](docs/MarketInfo.md)
|
||||
- [NextNonce](docs/NextNonce.md)
|
||||
- [Order](docs/Order.md)
|
||||
- [OrderBook](docs/OrderBook.md)
|
||||
- [OrderBookDepth](docs/OrderBookDepth.md)
|
||||
- [OrderBookDetail](docs/OrderBookDetail.md)
|
||||
- [OrderBookDetails](docs/OrderBookDetails.md)
|
||||
- [OrderBookOrders](docs/OrderBookOrders.md)
|
||||
- [OrderBookStats](docs/OrderBookStats.md)
|
||||
- [OrderBooks](docs/OrderBooks.md)
|
||||
- [Orders](docs/Orders.md)
|
||||
- [PnLEntry](docs/PnLEntry.md)
|
||||
- [PositionFunding](docs/PositionFunding.md)
|
||||
- [PriceLevel](docs/PriceLevel.md)
|
||||
- [PublicPool](docs/PublicPool.md)
|
||||
- [PublicPoolInfo](docs/PublicPoolInfo.md)
|
||||
- [PublicPoolShare](docs/PublicPoolShare.md)
|
||||
- [PublicPools](docs/PublicPools.md)
|
||||
- [ReqGetAccount](docs/ReqGetAccount.md)
|
||||
- [ReqGetAccountApiKeys](docs/ReqGetAccountApiKeys.md)
|
||||
- [ReqGetAccountByL1Address](docs/ReqGetAccountByL1Address.md)
|
||||
- [ReqGetAccountInactiveOrders](docs/ReqGetAccountInactiveOrders.md)
|
||||
- [ReqGetAccountPnL](docs/ReqGetAccountPnL.md)
|
||||
- [ReqGetAccountTxs](docs/ReqGetAccountTxs.md)
|
||||
- [ReqGetBlock](docs/ReqGetBlock.md)
|
||||
- [ReqGetBlockTxs](docs/ReqGetBlockTxs.md)
|
||||
- [ReqGetByAccount](docs/ReqGetByAccount.md)
|
||||
- [ReqGetCandlesticks](docs/ReqGetCandlesticks.md)
|
||||
- [ReqGetDepositHistory](docs/ReqGetDepositHistory.md)
|
||||
- [ReqGetFundings](docs/ReqGetFundings.md)
|
||||
- [ReqGetL1Tx](docs/ReqGetL1Tx.md)
|
||||
- [ReqGetLatestDeposit](docs/ReqGetLatestDeposit.md)
|
||||
- [ReqGetNextNonce](docs/ReqGetNextNonce.md)
|
||||
- [ReqGetOrderBookDetails](docs/ReqGetOrderBookDetails.md)
|
||||
- [ReqGetOrderBookOrders](docs/ReqGetOrderBookOrders.md)
|
||||
- [ReqGetOrderBooks](docs/ReqGetOrderBooks.md)
|
||||
- [ReqGetPublicPools](docs/ReqGetPublicPools.md)
|
||||
- [ReqGetRangeWithCursor](docs/ReqGetRangeWithCursor.md)
|
||||
- [ReqGetRangeWithIndex](docs/ReqGetRangeWithIndex.md)
|
||||
- [ReqGetRangeWithIndexSortable](docs/ReqGetRangeWithIndexSortable.md)
|
||||
- [ReqGetRecentTrades](docs/ReqGetRecentTrades.md)
|
||||
- [ReqGetTrades](docs/ReqGetTrades.md)
|
||||
- [ReqGetTx](docs/ReqGetTx.md)
|
||||
- [ReqGetWithdrawHistory](docs/ReqGetWithdrawHistory.md)
|
||||
- [ResultCode](docs/ResultCode.md)
|
||||
- [SimpleOrder](docs/SimpleOrder.md)
|
||||
- [Status](docs/Status.md)
|
||||
- [SubAccounts](docs/SubAccounts.md)
|
||||
- [Ticker](docs/Ticker.md)
|
||||
- [Trade](docs/Trade.md)
|
||||
- [Trades](docs/Trades.md)
|
||||
- [Tx](docs/Tx.md)
|
||||
- [TxHash](docs/TxHash.md)
|
||||
- [TxHashes](docs/TxHashes.md)
|
||||
- [Txs](docs/Txs.md)
|
||||
- [ValidatorInfo](docs/ValidatorInfo.md)
|
||||
- [WithdrawHistory](docs/WithdrawHistory.md)
|
||||
- [WithdrawHistoryItem](docs/WithdrawHistoryItem.md)
|
||||
- [ZkLighterInfo](docs/ZkLighterInfo.md)
|
||||
|
||||
|
||||
[//]: # (<a id="documentation-for-authorization"></a>)
|
||||
|
||||
[//]: # (## Documentation For Authorization)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (Endpoints do not require authorization.)
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Account
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**code** | **int** | |
|
||||
**message** | **str** | | [optional]
|
||||
**account_type** | **int** | |
|
||||
**index** | **int** | |
|
||||
**l1_address** | **str** | |
|
||||
**cancel_all_time** | **int** | |
|
||||
**total_order_count** | **int** | |
|
||||
**total_isolated_order_count** | **int** | |
|
||||
**pending_order_count** | **int** | |
|
||||
**available_balance** | **str** | |
|
||||
**status** | **int** | |
|
||||
**collateral** | **str** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account import Account
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of Account from a JSON string
|
||||
account_instance = Account.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(Account.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_dict = account_instance.to_dict()
|
||||
# create an instance of Account from a dict
|
||||
account_from_dict = Account.from_dict(account_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,930 @@
|
||||
# lighter.AccountApi
|
||||
|
||||
All URIs are relative to *https://mainnet.zklighter.elliot.ai*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**account**](AccountApi.md#account) | **GET** /api/v1/account | account
|
||||
[**account_limits**](AccountApi.md#account_limits) | **GET** /api/v1/accountLimits | accountLimits
|
||||
[**account_metadata**](AccountApi.md#account_metadata) | **GET** /api/v1/accountMetadata | accountMetadata
|
||||
[**accounts_by_l1_address**](AccountApi.md#accounts_by_l1_address) | **GET** /api/v1/accountsByL1Address | accountsByL1Address
|
||||
[**apikeys**](AccountApi.md#apikeys) | **GET** /api/v1/apikeys | apikeys
|
||||
[**change_account_tier**](AccountApi.md#change_account_tier) | **POST** /api/v1/changeAccountTier | changeAccountTier
|
||||
[**l1_metadata**](AccountApi.md#l1_metadata) | **GET** /api/v1/l1Metadata | l1Metadata
|
||||
[**liquidations**](AccountApi.md#liquidations) | **GET** /api/v1/liquidations | liquidations
|
||||
[**pnl**](AccountApi.md#pnl) | **GET** /api/v1/pnl | pnl
|
||||
[**position_funding**](AccountApi.md#position_funding) | **GET** /api/v1/positionFunding | positionFunding
|
||||
[**public_pools**](AccountApi.md#public_pools) | **GET** /api/v1/publicPools | publicPools
|
||||
[**public_pools_metadata**](AccountApi.md#public_pools_metadata) | **GET** /api/v1/publicPoolsMetadata | publicPoolsMetadata
|
||||
|
||||
|
||||
# **account**
|
||||
> DetailedAccounts account(by, value)
|
||||
|
||||
account
|
||||
|
||||
Get account by account's index. <br>More details about account index: [Account Index](https://apidocs.lighter.xyz/docs/account-index)<hr>**Response Description:**<br><br>1) **Status:** 1 is active 0 is inactive.<br>2) **Collateral:** The amount of collateral in the account.<hr>**Position Details Description:**<br>1) **OOC:** Open order count in that market.<br>2) **Sign:** 1 for Long, -1 for Short.<br>3) **Position:** The amount of position in that market.<br>4) **Avg Entry Price:** The average entry price of the position.<br>5) **Position Value:** The value of the position.<br>6) **Unrealized PnL:** The unrealized profit and loss of the position.<br>7) **Realized PnL:** The realized profit and loss of the position.
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.detailed_accounts import DetailedAccounts
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
by = 'by_example' # str |
|
||||
value = 'value_example' # str |
|
||||
|
||||
try:
|
||||
# account
|
||||
api_response = await api_instance.account(by, value)
|
||||
print("The response of AccountApi->account:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->account: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**by** | **str**| |
|
||||
**value** | **str**| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**DetailedAccounts**](DetailedAccounts.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **account_limits**
|
||||
> AccountLimits account_limits(account_index, authorization=authorization, auth=auth)
|
||||
|
||||
accountLimits
|
||||
|
||||
Get account limits
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.account_limits import AccountLimits
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
account_index = 56 # int |
|
||||
authorization = 'authorization_example' # str | make required after integ is done (optional)
|
||||
auth = 'auth_example' # str | made optional to support header auth clients (optional)
|
||||
|
||||
try:
|
||||
# accountLimits
|
||||
api_response = await api_instance.account_limits(account_index, authorization=authorization, auth=auth)
|
||||
print("The response of AccountApi->account_limits:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->account_limits: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**account_index** | **int**| |
|
||||
**authorization** | **str**| make required after integ is done | [optional]
|
||||
**auth** | **str**| made optional to support header auth clients | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**AccountLimits**](AccountLimits.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **account_metadata**
|
||||
> AccountMetadatas account_metadata(by, value, authorization=authorization, auth=auth)
|
||||
|
||||
accountMetadata
|
||||
|
||||
Get account metadatas
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.account_metadatas import AccountMetadatas
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
by = 'by_example' # str |
|
||||
value = 'value_example' # str |
|
||||
authorization = 'authorization_example' # str | (optional)
|
||||
auth = 'auth_example' # str | (optional)
|
||||
|
||||
try:
|
||||
# accountMetadata
|
||||
api_response = await api_instance.account_metadata(by, value, authorization=authorization, auth=auth)
|
||||
print("The response of AccountApi->account_metadata:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->account_metadata: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**by** | **str**| |
|
||||
**value** | **str**| |
|
||||
**authorization** | **str**| | [optional]
|
||||
**auth** | **str**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**AccountMetadatas**](AccountMetadatas.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **accounts_by_l1_address**
|
||||
> SubAccounts accounts_by_l1_address(l1_address)
|
||||
|
||||
accountsByL1Address
|
||||
|
||||
Get accounts by l1_address returns all accounts associated with the given L1 address
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.sub_accounts import SubAccounts
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
l1_address = 'l1_address_example' # str |
|
||||
|
||||
try:
|
||||
# accountsByL1Address
|
||||
api_response = await api_instance.accounts_by_l1_address(l1_address)
|
||||
print("The response of AccountApi->accounts_by_l1_address:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->accounts_by_l1_address: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**l1_address** | **str**| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**SubAccounts**](SubAccounts.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **apikeys**
|
||||
> AccountApiKeys apikeys(account_index, api_key_index=api_key_index)
|
||||
|
||||
apikeys
|
||||
|
||||
Get account api key. Set `api_key_index` to 255 to retrieve all api keys associated with the account.
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.account_api_keys import AccountApiKeys
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
account_index = 56 # int |
|
||||
api_key_index = 255 # int | (optional) (default to 255)
|
||||
|
||||
try:
|
||||
# apikeys
|
||||
api_response = await api_instance.apikeys(account_index, api_key_index=api_key_index)
|
||||
print("The response of AccountApi->apikeys:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->apikeys: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**account_index** | **int**| |
|
||||
**api_key_index** | **int**| | [optional] [default to 255]
|
||||
|
||||
### Return type
|
||||
|
||||
[**AccountApiKeys**](AccountApiKeys.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **change_account_tier**
|
||||
> RespChangeAccountTier change_account_tier(account_index, new_tier, authorization=authorization, auth=auth)
|
||||
|
||||
changeAccountTier
|
||||
|
||||
Change account tier
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.resp_change_account_tier import RespChangeAccountTier
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
account_index = 56 # int |
|
||||
new_tier = 'new_tier_example' # str |
|
||||
authorization = 'authorization_example' # str | make required after integ is done (optional)
|
||||
auth = 'auth_example' # str | made optional to support header auth clients (optional)
|
||||
|
||||
try:
|
||||
# changeAccountTier
|
||||
api_response = await api_instance.change_account_tier(account_index, new_tier, authorization=authorization, auth=auth)
|
||||
print("The response of AccountApi->change_account_tier:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->change_account_tier: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**account_index** | **int**| |
|
||||
**new_tier** | **str**| |
|
||||
**authorization** | **str**| make required after integ is done | [optional]
|
||||
**auth** | **str**| made optional to support header auth clients | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**RespChangeAccountTier**](RespChangeAccountTier.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: multipart/form-data
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **l1_metadata**
|
||||
> L1Metadata l1_metadata(l1_address, authorization=authorization, auth=auth)
|
||||
|
||||
l1Metadata
|
||||
|
||||
Get L1 metadata
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.l1_metadata import L1Metadata
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
l1_address = 'l1_address_example' # str |
|
||||
authorization = 'authorization_example' # str | make required after integ is done (optional)
|
||||
auth = 'auth_example' # str | made optional to support header auth clients (optional)
|
||||
|
||||
try:
|
||||
# l1Metadata
|
||||
api_response = await api_instance.l1_metadata(l1_address, authorization=authorization, auth=auth)
|
||||
print("The response of AccountApi->l1_metadata:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->l1_metadata: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**l1_address** | **str**| |
|
||||
**authorization** | **str**| make required after integ is done | [optional]
|
||||
**auth** | **str**| made optional to support header auth clients | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**L1Metadata**](L1Metadata.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **liquidations**
|
||||
> LiquidationInfos liquidations(account_index, limit, authorization=authorization, auth=auth, market_id=market_id, cursor=cursor)
|
||||
|
||||
liquidations
|
||||
|
||||
Get liquidation infos
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.liquidation_infos import LiquidationInfos
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
account_index = 56 # int |
|
||||
limit = 56 # int |
|
||||
authorization = 'authorization_example' # str | make required after integ is done (optional)
|
||||
auth = 'auth_example' # str | made optional to support header auth clients (optional)
|
||||
market_id = 255 # int | (optional) (default to 255)
|
||||
cursor = 'cursor_example' # str | (optional)
|
||||
|
||||
try:
|
||||
# liquidations
|
||||
api_response = await api_instance.liquidations(account_index, limit, authorization=authorization, auth=auth, market_id=market_id, cursor=cursor)
|
||||
print("The response of AccountApi->liquidations:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->liquidations: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**account_index** | **int**| |
|
||||
**limit** | **int**| |
|
||||
**authorization** | **str**| make required after integ is done | [optional]
|
||||
**auth** | **str**| made optional to support header auth clients | [optional]
|
||||
**market_id** | **int**| | [optional] [default to 255]
|
||||
**cursor** | **str**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**LiquidationInfos**](LiquidationInfos.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **pnl**
|
||||
> AccountPnL pnl(by, value, resolution, start_timestamp, end_timestamp, count_back, authorization=authorization, auth=auth, ignore_transfers=ignore_transfers)
|
||||
|
||||
pnl
|
||||
|
||||
Get account PnL chart
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.account_pn_l import AccountPnL
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
by = 'by_example' # str |
|
||||
value = 'value_example' # str |
|
||||
resolution = 'resolution_example' # str |
|
||||
start_timestamp = 56 # int |
|
||||
end_timestamp = 56 # int |
|
||||
count_back = 56 # int |
|
||||
authorization = 'authorization_example' # str | (optional)
|
||||
auth = 'auth_example' # str | (optional)
|
||||
ignore_transfers = False # bool | (optional) (default to False)
|
||||
|
||||
try:
|
||||
# pnl
|
||||
api_response = await api_instance.pnl(by, value, resolution, start_timestamp, end_timestamp, count_back, authorization=authorization, auth=auth, ignore_transfers=ignore_transfers)
|
||||
print("The response of AccountApi->pnl:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->pnl: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**by** | **str**| |
|
||||
**value** | **str**| |
|
||||
**resolution** | **str**| |
|
||||
**start_timestamp** | **int**| |
|
||||
**end_timestamp** | **int**| |
|
||||
**count_back** | **int**| |
|
||||
**authorization** | **str**| | [optional]
|
||||
**auth** | **str**| | [optional]
|
||||
**ignore_transfers** | **bool**| | [optional] [default to False]
|
||||
|
||||
### Return type
|
||||
|
||||
[**AccountPnL**](AccountPnL.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **position_funding**
|
||||
> PositionFundings position_funding(account_index, limit, authorization=authorization, auth=auth, market_id=market_id, cursor=cursor, side=side)
|
||||
|
||||
positionFunding
|
||||
|
||||
Get accounts position fundings
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.position_fundings import PositionFundings
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
account_index = 56 # int |
|
||||
limit = 56 # int |
|
||||
authorization = 'authorization_example' # str | (optional)
|
||||
auth = 'auth_example' # str | (optional)
|
||||
market_id = 255 # int | (optional) (default to 255)
|
||||
cursor = 'cursor_example' # str | (optional)
|
||||
side = all # str | (optional) (default to all)
|
||||
|
||||
try:
|
||||
# positionFunding
|
||||
api_response = await api_instance.position_funding(account_index, limit, authorization=authorization, auth=auth, market_id=market_id, cursor=cursor, side=side)
|
||||
print("The response of AccountApi->position_funding:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->position_funding: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**account_index** | **int**| |
|
||||
**limit** | **int**| |
|
||||
**authorization** | **str**| | [optional]
|
||||
**auth** | **str**| | [optional]
|
||||
**market_id** | **int**| | [optional] [default to 255]
|
||||
**cursor** | **str**| | [optional]
|
||||
**side** | **str**| | [optional] [default to all]
|
||||
|
||||
### Return type
|
||||
|
||||
[**PositionFundings**](PositionFundings.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **public_pools**
|
||||
> PublicPools public_pools(index, limit, authorization=authorization, auth=auth, filter=filter, account_index=account_index)
|
||||
|
||||
publicPools
|
||||
|
||||
Get public pools
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.public_pools import PublicPools
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
index = 56 # int |
|
||||
limit = 56 # int |
|
||||
authorization = 'authorization_example' # str | (optional)
|
||||
auth = 'auth_example' # str | (optional)
|
||||
filter = 'filter_example' # str | (optional)
|
||||
account_index = 56 # int | (optional)
|
||||
|
||||
try:
|
||||
# publicPools
|
||||
api_response = await api_instance.public_pools(index, limit, authorization=authorization, auth=auth, filter=filter, account_index=account_index)
|
||||
print("The response of AccountApi->public_pools:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->public_pools: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**index** | **int**| |
|
||||
**limit** | **int**| |
|
||||
**authorization** | **str**| | [optional]
|
||||
**auth** | **str**| | [optional]
|
||||
**filter** | **str**| | [optional]
|
||||
**account_index** | **int**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**PublicPools**](PublicPools.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **public_pools_metadata**
|
||||
> RespPublicPoolsMetadata public_pools_metadata(index, limit, authorization=authorization, auth=auth, filter=filter, account_index=account_index)
|
||||
|
||||
publicPoolsMetadata
|
||||
|
||||
Get public pools metadata
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.resp_public_pools_metadata import RespPublicPoolsMetadata
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AccountApi(api_client)
|
||||
index = 56 # int |
|
||||
limit = 56 # int |
|
||||
authorization = 'authorization_example' # str | (optional)
|
||||
auth = 'auth_example' # str | (optional)
|
||||
filter = 'filter_example' # str | (optional)
|
||||
account_index = 56 # int | (optional)
|
||||
|
||||
try:
|
||||
# publicPoolsMetadata
|
||||
api_response = await api_instance.public_pools_metadata(index, limit, authorization=authorization, auth=auth, filter=filter, account_index=account_index)
|
||||
print("The response of AccountApi->public_pools_metadata:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AccountApi->public_pools_metadata: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**index** | **int**| |
|
||||
**limit** | **int**| |
|
||||
**authorization** | **str**| | [optional]
|
||||
**auth** | **str**| | [optional]
|
||||
**filter** | **str**| | [optional]
|
||||
**account_index** | **int**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**RespPublicPoolsMetadata**](RespPublicPoolsMetadata.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# AccountApiKeys
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**code** | **int** | |
|
||||
**message** | **str** | | [optional]
|
||||
**api_keys** | [**List[ApiKey]**](ApiKey.md) | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_api_keys import AccountApiKeys
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountApiKeys from a JSON string
|
||||
account_api_keys_instance = AccountApiKeys.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountApiKeys.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_api_keys_dict = account_api_keys_instance.to_dict()
|
||||
# create an instance of AccountApiKeys from a dict
|
||||
account_api_keys_from_dict = AccountApiKeys.from_dict(account_api_keys_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# AccountLimits
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**code** | **int** | |
|
||||
**message** | **str** | | [optional]
|
||||
**max_llp_percentage** | **int** | |
|
||||
**user_tier** | **str** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_limits import AccountLimits
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountLimits from a JSON string
|
||||
account_limits_instance = AccountLimits.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountLimits.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_limits_dict = account_limits_instance.to_dict()
|
||||
# create an instance of AccountLimits from a dict
|
||||
account_limits_from_dict = AccountLimits.from_dict(account_limits_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# AccountMarginStats
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**collateral** | **str** | |
|
||||
**portfolio_value** | **str** | |
|
||||
**leverage** | **str** | |
|
||||
**available_balance** | **str** | |
|
||||
**margin_usage** | **str** | |
|
||||
**buying_power** | **str** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_margin_stats import AccountMarginStats
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountMarginStats from a JSON string
|
||||
account_margin_stats_instance = AccountMarginStats.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountMarginStats.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_margin_stats_dict = account_margin_stats_instance.to_dict()
|
||||
# create an instance of AccountMarginStats from a dict
|
||||
account_margin_stats_from_dict = AccountMarginStats.from_dict(account_margin_stats_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# AccountMarketStats
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**market_id** | **int** | |
|
||||
**daily_trades_count** | **int** | |
|
||||
**daily_base_token_volume** | **float** | |
|
||||
**daily_quote_token_volume** | **float** | |
|
||||
**weekly_trades_count** | **int** | |
|
||||
**weekly_base_token_volume** | **float** | |
|
||||
**weekly_quote_token_volume** | **float** | |
|
||||
**monthly_trades_count** | **int** | |
|
||||
**monthly_base_token_volume** | **float** | |
|
||||
**monthly_quote_token_volume** | **float** | |
|
||||
**total_trades_count** | **int** | |
|
||||
**total_base_token_volume** | **float** | |
|
||||
**total_quote_token_volume** | **float** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_market_stats import AccountMarketStats
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountMarketStats from a JSON string
|
||||
account_market_stats_instance = AccountMarketStats.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountMarketStats.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_market_stats_dict = account_market_stats_instance.to_dict()
|
||||
# create an instance of AccountMarketStats from a dict
|
||||
account_market_stats_from_dict = AccountMarketStats.from_dict(account_market_stats_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# AccountMetadata
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**account_index** | **int** | |
|
||||
**name** | **str** | |
|
||||
**description** | **str** | |
|
||||
**can_invite** | **bool** | Remove After FE uses L1 meta endpoint |
|
||||
**referral_points_percentage** | **str** | Remove After FE uses L1 meta endpoint |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_metadata import AccountMetadata
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountMetadata from a JSON string
|
||||
account_metadata_instance = AccountMetadata.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountMetadata.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_metadata_dict = account_metadata_instance.to_dict()
|
||||
# create an instance of AccountMetadata from a dict
|
||||
account_metadata_from_dict = AccountMetadata.from_dict(account_metadata_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# AccountMetadatas
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**code** | **int** | |
|
||||
**message** | **str** | | [optional]
|
||||
**account_metadatas** | [**List[AccountMetadata]**](AccountMetadata.md) | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_metadatas import AccountMetadatas
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountMetadatas from a JSON string
|
||||
account_metadatas_instance = AccountMetadatas.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountMetadatas.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_metadatas_dict = account_metadatas_instance.to_dict()
|
||||
# create an instance of AccountMetadatas from a dict
|
||||
account_metadatas_from_dict = AccountMetadatas.from_dict(account_metadatas_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# AccountPnL
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**code** | **int** | |
|
||||
**message** | **str** | | [optional]
|
||||
**resolution** | **str** | |
|
||||
**pnl** | [**List[PnLEntry]**](PnLEntry.md) | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_pn_l import AccountPnL
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountPnL from a JSON string
|
||||
account_pn_l_instance = AccountPnL.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountPnL.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_pn_l_dict = account_pn_l_instance.to_dict()
|
||||
# create an instance of AccountPnL from a dict
|
||||
account_pn_l_from_dict = AccountPnL.from_dict(account_pn_l_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# AccountPosition
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**market_id** | **int** | |
|
||||
**symbol** | **str** | |
|
||||
**initial_margin_fraction** | **str** | |
|
||||
**open_order_count** | **int** | |
|
||||
**pending_order_count** | **int** | |
|
||||
**position_tied_order_count** | **int** | |
|
||||
**sign** | **int** | |
|
||||
**position** | **str** | |
|
||||
**avg_entry_price** | **str** | |
|
||||
**position_value** | **str** | |
|
||||
**unrealized_pnl** | **str** | |
|
||||
**realized_pnl** | **str** | |
|
||||
**liquidation_price** | **str** | |
|
||||
**total_funding_paid_out** | **str** | | [optional]
|
||||
**margin_mode** | **int** | |
|
||||
**allocated_margin** | **str** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_position import AccountPosition
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountPosition from a JSON string
|
||||
account_position_instance = AccountPosition.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountPosition.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_position_dict = account_position_instance.to_dict()
|
||||
# create an instance of AccountPosition from a dict
|
||||
account_position_from_dict = AccountPosition.from_dict(account_position_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# AccountStats
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**collateral** | **str** | |
|
||||
**portfolio_value** | **str** | |
|
||||
**leverage** | **str** | |
|
||||
**available_balance** | **str** | |
|
||||
**margin_usage** | **str** | |
|
||||
**buying_power** | **str** | |
|
||||
**cross_stats** | [**AccountMarginStats**](AccountMarginStats.md) | |
|
||||
**total_stats** | [**AccountMarginStats**](AccountMarginStats.md) | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_stats import AccountStats
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountStats from a JSON string
|
||||
account_stats_instance = AccountStats.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountStats.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_stats_dict = account_stats_instance.to_dict()
|
||||
# create an instance of AccountStats from a dict
|
||||
account_stats_from_dict = AccountStats.from_dict(account_stats_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# AccountTradeStats
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**daily_trades_count** | **int** | |
|
||||
**daily_volume** | **float** | |
|
||||
**weekly_trades_count** | **int** | |
|
||||
**weekly_volume** | **float** | |
|
||||
**monthly_trades_count** | **int** | |
|
||||
**monthly_volume** | **float** | |
|
||||
**total_trades_count** | **int** | |
|
||||
**total_volume** | **float** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.account_trade_stats import AccountTradeStats
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of AccountTradeStats from a JSON string
|
||||
account_trade_stats_instance = AccountTradeStats.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(AccountTradeStats.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
account_trade_stats_dict = account_trade_stats_instance.to_dict()
|
||||
# create an instance of AccountTradeStats from a dict
|
||||
account_trade_stats_from_dict = AccountTradeStats.from_dict(account_trade_stats_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Announcement
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**title** | **str** | |
|
||||
**content** | **str** | |
|
||||
**created_at** | **int** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.announcement import Announcement
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of Announcement from a JSON string
|
||||
announcement_instance = Announcement.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(Announcement.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
announcement_dict = announcement_instance.to_dict()
|
||||
# create an instance of Announcement from a dict
|
||||
announcement_from_dict = Announcement.from_dict(announcement_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# lighter.AnnouncementApi
|
||||
|
||||
All URIs are relative to *https://mainnet.zklighter.elliot.ai*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**announcement**](AnnouncementApi.md#announcement) | **GET** /api/v1/announcement | announcement
|
||||
|
||||
|
||||
# **announcement**
|
||||
> Announcements announcement()
|
||||
|
||||
announcement
|
||||
|
||||
Get announcement
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.announcements import Announcements
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.AnnouncementApi(api_client)
|
||||
|
||||
try:
|
||||
# announcement
|
||||
api_response = await api_instance.announcement()
|
||||
print("The response of AnnouncementApi->announcement:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling AnnouncementApi->announcement: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**Announcements**](Announcements.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Announcements
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**code** | **int** | |
|
||||
**message** | **str** | | [optional]
|
||||
**announcements** | [**List[Announcement]**](Announcement.md) | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.announcements import Announcements
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of Announcements from a JSON string
|
||||
announcements_instance = Announcements.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(Announcements.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
announcements_dict = announcements_instance.to_dict()
|
||||
# create an instance of Announcements from a dict
|
||||
announcements_from_dict = Announcements.from_dict(announcements_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# ApiKey
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**account_index** | **int** | |
|
||||
**api_key_index** | **int** | |
|
||||
**nonce** | **int** | |
|
||||
**public_key** | **str** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.api_key import ApiKey
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of ApiKey from a JSON string
|
||||
api_key_instance = ApiKey.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(ApiKey.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
api_key_dict = api_key_instance.to_dict()
|
||||
# create an instance of ApiKey from a dict
|
||||
api_key_from_dict = ApiKey.from_dict(api_key_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Block
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**commitment** | **str** | |
|
||||
**height** | **int** | |
|
||||
**state_root** | **str** | |
|
||||
**priority_operations** | **int** | |
|
||||
**on_chain_l2_operations** | **int** | |
|
||||
**pending_on_chain_operations_pub_data** | **str** | |
|
||||
**committed_tx_hash** | **str** | |
|
||||
**committed_at** | **int** | |
|
||||
**verified_tx_hash** | **str** | |
|
||||
**verified_at** | **int** | |
|
||||
**txs** | [**List[Tx]**](Tx.md) | |
|
||||
**status** | **int** | |
|
||||
**size** | **int** | |
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lighter.models.block import Block
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of Block from a JSON string
|
||||
block_instance = Block.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(Block.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
block_dict = block_instance.to_dict()
|
||||
# create an instance of Block from a dict
|
||||
block_from_dict = Block.from_dict(block_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
# lighter.BlockApi
|
||||
|
||||
All URIs are relative to *https://mainnet.zklighter.elliot.ai*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**block**](BlockApi.md#block) | **GET** /api/v1/block | block
|
||||
[**blocks**](BlockApi.md#blocks) | **GET** /api/v1/blocks | blocks
|
||||
[**current_height**](BlockApi.md#current_height) | **GET** /api/v1/currentHeight | currentHeight
|
||||
|
||||
|
||||
# **block**
|
||||
> Blocks block(by, value)
|
||||
|
||||
block
|
||||
|
||||
Get block by its height or commitment
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.blocks import Blocks
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.BlockApi(api_client)
|
||||
by = 'by_example' # str |
|
||||
value = 'value_example' # str |
|
||||
|
||||
try:
|
||||
# block
|
||||
api_response = await api_instance.block(by, value)
|
||||
print("The response of BlockApi->block:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling BlockApi->block: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**by** | **str**| |
|
||||
**value** | **str**| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**Blocks**](Blocks.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **blocks**
|
||||
> Blocks blocks(limit, index=index, sort=sort)
|
||||
|
||||
blocks
|
||||
|
||||
Get blocks
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.blocks import Blocks
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.BlockApi(api_client)
|
||||
limit = 56 # int |
|
||||
index = 56 # int | (optional)
|
||||
sort = asc # str | (optional) (default to asc)
|
||||
|
||||
try:
|
||||
# blocks
|
||||
api_response = await api_instance.blocks(limit, index=index, sort=sort)
|
||||
print("The response of BlockApi->blocks:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling BlockApi->blocks: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**limit** | **int**| |
|
||||
**index** | **int**| | [optional]
|
||||
**sort** | **str**| | [optional] [default to asc]
|
||||
|
||||
### Return type
|
||||
|
||||
[**Blocks**](Blocks.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **current_height**
|
||||
> CurrentHeight current_height()
|
||||
|
||||
currentHeight
|
||||
|
||||
Get current height
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
```python
|
||||
import lighter
|
||||
from lighter.models.current_height import CurrentHeight
|
||||
from lighter.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = lighter.Configuration(
|
||||
host = "https://mainnet.zklighter.elliot.ai"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
async with lighter.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = lighter.BlockApi(api_client)
|
||||
|
||||
try:
|
||||
# currentHeight
|
||||
api_response = await api_instance.current_height()
|
||||
print("The response of BlockApi->current_height:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling BlockApi->current_height: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**CurrentHeight**](CurrentHeight.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | A successful response. | - |
|
||||
**400** | Bad request | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user