Author SHA1 Message Date
discountry 7635b7d467 feat: 更新 Lighter 适配器的订单过期逻辑,支持即时或取消订单类型的处理 2025-10-03 03:29:54 +08:00
discountry f565c399e3 feat: 更新 LighterSigner 配置,baseUrl 变为可选以支持离线签名和测试 2025-10-03 03:25:30 +08:00
discountry b43eaa0c64 feat: 在文档中添加 CLI 参数说明,支持通过命令行临时指定交易所 2025-10-03 03:16:44 +08:00
discountry 9a419c36ac feat: 更新 Lighter 适配器的订单过期逻辑,使用绝对未来时间戳以符合链的预期 2025-10-03 03:08:41 +08:00
discountry 0e5026aaa1 feat: 优化 Lighter 适配器的账户消息处理逻辑,支持部分更新和市场订单类型的处理 2025-10-03 02:54:50 +08:00
discountry 6273f3b94a feat: 在 Lighter 适配器中添加订单簿序列跟踪,优化 WebSocket 消息处理逻辑 2025-10-03 02:40:09 +08:00
discountry e478fb4330 feat: 添加 Lighter 适配器的调试日志功能,优化错误处理和订单簿数据规范化 2025-10-03 02:27:58 +08:00
discountry 599b4ec3f3 chore: 移除 grvt-pysdk 项目中的所有文件,清理代码库 2025-10-03 02:12:45 +08:00
discountry 42061017f6 chore: 移除 Lighter 和 Poseidon Crypto 项目中的所有文件,清理代码库 2025-10-03 01:54:08 +08:00
discountry b7cd6eed99 feat: 重构 Lighter 策略,更新签名桥接路径,移除不必要的文件,添加新的签名库支持 2025-10-03 01:51:55 +08:00
discountry abb56b917a chore: 更新 .env.example 文件,调整 Maker 策略的损失限制参数以优化风险管理 2025-10-03 01:34:32 +08:00
discountry 9124c06315 chore: 移除 Maker 策略中的价格追逐阈值配置,简化订单计划逻辑 2025-10-03 01:13:06 +08:00
discountry 56bcaddb22 chore: 更新 .env.example 文件,调整风险管理参数以优化交易策略 2025-10-03 01:01:23 +08:00
discountry 4c4abb3b09 feat: 添加 Lighter 签名桥接和公钥派生脚本,支持新的签名库和环境检测功能 2025-10-01 18:32:02 +08:00
discountry c8b0ab1c8e feat: 添加 Lighter 适配器及相关功能,支持 trailing stops 和新的交易逻辑 2025-09-30 22:08:05 +08:00
discountry e83bdfccf9 docs: 更新 lighter-python-main 文档,添加示例代码和 API 说明,重构部分内容以提高可读性 2025-09-30 18:35:14 +08:00
discountry d6533733c9 docs: 更新 README 文件,添加静默启动和后台运行的说明,包括命令行用法和 pm2 配置示例 2025-09-28 01:39:49 +08:00
discountry 5c1283fac2 feat: 添加命令行参数解析和策略启动功能,支持静默模式和帮助信息 2025-09-28 01:26:00 +08:00
discountry 0e4ae67573 docs: 更新 AGENTS.md 文件,重构项目结构和开发命令部分,移除旧版文档信息并添加新的策略和测试指南 2025-09-28 01:03:25 +08:00
discountry 6ab0759d3d feat: 引入事件发射器和会话交易量跟踪器,重构策略引擎以支持更灵活的事件处理和交易量计算 2025-09-28 00:43:49 +08:00
discountry 25ab1ababf chore: 重构策略引擎,将核心逻辑迁移至策略目录,删除旧的引擎文件并更新相关引用 2025-09-28 00:03:48 +08:00
discountry e60b32a7f8 docs: 更新 README 文件,添加策略运行所需资金、杠杆设置及时间同步的常见问题解答 2025-09-27 23:33:00 +08:00
discountry 46c0551133 fix: 更新 AsterRestClient 和 AsterGateway 中的订单处理逻辑,支持字符串类型的订单 ID 2025-09-27 23:23:53 +08:00
discountry 7e2d312fe3 chore: 更新 bun.lock 文件,添加多个依赖以支持以太坊相关功能 2025-09-27 22:59:28 +08:00
discountry e8dd36f540 chore: 删除 GRVT 适配器的 README 文件,清理项目文档 2025-09-27 22:58:56 +08:00
discountry 47d44c7cb5 chore: 移除 README 中关于旧版文档的说明,以简化文档结构 2025-09-27 22:58:38 +08:00
discountry 048dfb1698 fix: 更新 ethereum-cryptography 依赖至 2.1.3 以支持最新功能 2025-09-27 22:52:57 +08:00
discountry fc1a74174c feat: 在 package.json 中添加 ethereum-cryptography 依赖以支持以太坊相关功能 2025-09-27 22:51:28 +08:00
discountry b45539b7c2 feat: 添加英文文档,更新中文文档以提升用户体验和使用指南的清晰度 2025-09-27 22:46:32 +08:00
discountry a998b3d3a1 fix: 更新最小布林带宽度配置,调整为更小的阈值以支持新策略 2025-09-27 22:33:32 +08:00
discountry d392f54f21 feat: 添加布林带宽度计算功能及相关配置项,优化趋势引擎以支持布林带宽度过滤 2025-09-27 22:29:56 +08:00
284 changed files with 17600 additions and 15091 deletions
+19 -5
View File
@@ -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)
+22 -6
View File
@@ -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.
+109 -173
View File
@@ -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
```
- WindowsPowerShell):
```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 EIP712 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
View File
@@ -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 50100 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.
+13
View File
@@ -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=="],
-57
View File
@@ -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
-39
View File
@@ -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 }}"
-61
View File
@@ -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
-287
View File
@@ -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"]
}
-78
View File
@@ -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
-201
View File
@@ -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.
-59
View File
@@ -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
-300
View File
@@ -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
[![CodeQL](https://github.com/smarlhens/python-boilerplate/workflows/codeql/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml)
[![GitHub CI](https://github.com/smarlhens/python-boilerplate/workflows/ci/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml)
[![GitHub license](https://img.shields.io/github/license/smarlhens/python-boilerplate)](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
```
---
-382
View File
@@ -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
[![CodeQL](https://github.com/smarlhens/python-boilerplate/workflows/codeql/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml)
[![GitHub CI](https://github.com/smarlhens/python-boilerplate/workflows/ci/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml)
[![GitHub license](https://img.shields.io/github/license/smarlhens/python-boilerplate)](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")
-106
View File
@@ -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()
-1961
View File
File diff suppressed because it is too large Load Diff
+78
View File
@@ -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 TransactionApis 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 SignerClients 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. Heres 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. Heres 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 markets 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.
+4
View File
@@ -0,0 +1,4 @@
.idea
vendor
build/*
!build/.keep
+9
View File
@@ -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
+198
View File
@@ -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