docs: expand AVE Claw Hackathon research with code examples and guides
- Added 3 runnable Python scripts for Data REST, Data WebSocket, and Trading APIs - Expanded research with 7 new sections: - Extended API documentation with full request/response examples - Working code examples with CLI commands - Security guide (API keys, HMAC signing, private key protection) - Testing strategies (dry-run, paper trading, testnets) - Troubleshooting guide with error codes - Competitive analysis vs DexScreener, Moralis, CoinGecko - Quick reference appendix Research doc grew from 242 to 906 lines
This commit is contained in:
@@ -89,7 +89,7 @@ The AVE Claw Hackathon is a developer competition focused on building trading bo
|
|||||||
|----------|-------------|------|
|
|----------|-------------|------|
|
||||||
| `GET /v2/tokens` | Search tokens by keyword | 5 CU |
|
| `GET /v2/tokens` | Search tokens by keyword | 5 CU |
|
||||||
| `GET /v2/tokens/{token-id}` | Token details + top 5 pairs | 5 CU |
|
| `GET /v2/tokens/{token-id}` | Token details + top 5 pairs | 5 CU |
|
||||||
| `GET /v2/tokens/price` | Batch token prices (up to 200) | 100 CU |
|
| `POST /v2/tokens/price` | Batch token prices (up to 200) | 100 CU |
|
||||||
| `GET /v2/tokens/trending` | Trending tokens | 5 CU |
|
| `GET /v2/tokens/trending` | Trending tokens | 5 CU |
|
||||||
| `GET /v2/tokens/top100/{token-id}` | Top 100 token holders | 10 CU |
|
| `GET /v2/tokens/top100/{token-id}` | Top 100 token holders | 10 CU |
|
||||||
| `GET /v2/contracts/{token-id}` | Token risk info | 10 CU |
|
| `GET /v2/contracts/{token-id}` | Token risk info | 10 CU |
|
||||||
@@ -98,8 +98,13 @@ The AVE Claw Hackathon is a developer competition focused on building trading bo
|
|||||||
| `GET /v2/address/tx` | Wallet transaction history | 100 CU |
|
| `GET /v2/address/tx` | Wallet transaction history | 100 CU |
|
||||||
| `GET /v2/address/pnl` | Wallet PnL data | 5 CU |
|
| `GET /v2/address/pnl` | Wallet PnL data | 5 CU |
|
||||||
| `GET /v2/address/walletinfo` | Wallet info (all tokens) | 5 CU |
|
| `GET /v2/address/walletinfo` | Wallet info (all tokens) | 5 CU |
|
||||||
|
| `GET /v2/address/walletinfo/tokens` | All tokens in wallet | 10 CU |
|
||||||
|
| `GET /v2/address/smart_wallet/list` | Smart wallet list (copy trading) | 5 CU |
|
||||||
| `GET /v2/klines/token/{token-id}` | Kline data by token | 10 CU |
|
| `GET /v2/klines/token/{token-id}` | Kline data by token | 10 CU |
|
||||||
| `GET /v2/klines/pair/{pair-id}` | Kline data by pair | 10 CU |
|
| `GET /v2/klines/pair/{pair-id}` | Kline data by pair | 10 CU |
|
||||||
|
| `GET /v2/pairs/{pair-id}` | Pair details | 5 CU |
|
||||||
|
| `GET /v2/tokens/platform` | Tokens by launch platform | 10 CU |
|
||||||
|
| `GET /v2/tokens/main` | Main tokens on chain | 5 CU |
|
||||||
|
|
||||||
### WebSocket API (`wss://wss.ave-api.xyz`)
|
### WebSocket API (`wss://wss.ave-api.xyz`)
|
||||||
|
|
||||||
@@ -130,47 +135,15 @@ The AVE Claw Hackathon is a developer competition focused on building trading bo
|
|||||||
| GitHub (Skills) | https://github.com/AveCloud/ave-cloud-skill |
|
| GitHub (Skills) | https://github.com/AveCloud/ave-cloud-skill |
|
||||||
| Telegram Support | https://t.me/ave_ai_cloud |
|
| Telegram Support | https://t.me/ave_ai_cloud |
|
||||||
|
|
||||||
### Python Skill Scripts (GitHub)
|
### Python Skill Scripts
|
||||||
|
|
||||||
The `ave-cloud-skill` repo provides Python scripts:
|
The `docs/scripts/` directory contains runnable Python scripts:
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `ave_data_rest.py` | Token search, prices, klines, holders, risk |
|
| `ave_data_rest.py` | Token search, prices, klines, holders, risk |
|
||||||
| `ave_data_wss.py` | Real-time WebSocket streams |
|
| `ave_data_wss.py` | Real-time WebSocket streams |
|
||||||
| `ave_trade_rest.py` | Chain & proxy wallet trading |
|
| `ave_trade_rest.py` | Chain & proxy wallet trading |
|
||||||
| `ave_trade_wss.py` | Proxy wallet WebSocket updates |
|
|
||||||
|
|
||||||
### Quick Start Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build Docker image
|
|
||||||
docker build -f scripts/Dockerfile.txt -t ave-cloud .
|
|
||||||
|
|
||||||
# Token search
|
|
||||||
docker run --rm \
|
|
||||||
-e AVE_API_KEY=your_api_key \
|
|
||||||
-e API_PLAN=free \
|
|
||||||
--entrypoint python3 \
|
|
||||||
ave-cloud scripts/ave_data_rest.py search --keyword PEPE
|
|
||||||
|
|
||||||
# Live price watch
|
|
||||||
docker run --rm -it \
|
|
||||||
-e AVE_API_KEY=your_api_key \
|
|
||||||
-e API_PLAN=pro \
|
|
||||||
--entrypoint python3 \
|
|
||||||
ave-cloud scripts/ave_data_wss.py wss-repl
|
|
||||||
|
|
||||||
# Dry-run trade preview
|
|
||||||
docker run --rm \
|
|
||||||
-e AVE_API_KEY=your_api_key \
|
|
||||||
-e API_PLAN=free \
|
|
||||||
--entrypoint python3 \
|
|
||||||
ave-cloud scripts/ave_trade_rest.py quote \
|
|
||||||
--chain bsc --in-token 0x55d398326f99059fF775485246999027B3197955 \
|
|
||||||
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
|
||||||
--in-amount 10000000 --swap-type buy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Token Links Format
|
### Token Links Format
|
||||||
View tokens on AVE Pro: `https://pro.ave.ai/token/<token_address>-<chain>`
|
View tokens on AVE Pro: `https://pro.ave.ai/token/<token_address>-<chain>`
|
||||||
@@ -178,7 +151,7 @@ Example: `https://pro.ave.ai/token/0x1234...abcd-bsc`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decision Guidance for Participants
|
## 6. Decision Guidance for Participants
|
||||||
|
|
||||||
**What to Build:**
|
**What to Build:**
|
||||||
|
|
||||||
@@ -195,7 +168,632 @@ Example: `https://pro.ave.ai/token/0x1234...abcd-bsc`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. AVE Cloud Agent Skills - User Insights
|
## 7. Extended API Documentation
|
||||||
|
|
||||||
|
### 7.1 Token ID Format
|
||||||
|
|
||||||
|
All token references use the format `<token_address>-<chain>`:
|
||||||
|
|
||||||
|
```
|
||||||
|
PEPE-bsc
|
||||||
|
0x6982508145454Ce325dDbE47a25d4ec3d2311933-bsc
|
||||||
|
6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN-solana
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chain names:** `bsc`, `solana`, `eth`, `base`
|
||||||
|
|
||||||
|
### 7.2 Complete Request/Response Examples
|
||||||
|
|
||||||
|
#### Token Search
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl "https://prod.ave-api.com/v2/tokens?keyword=PEPE&chain=bsc&limit=10" \
|
||||||
|
-H "X-API-KEY: your_api_key"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "Pepe",
|
||||||
|
"symbol": "PEPE",
|
||||||
|
"chain": "bsc",
|
||||||
|
"current_price_usd": "0.00001234",
|
||||||
|
"market_cap": "1234567890",
|
||||||
|
"tx_volume_u_24h": "50000000",
|
||||||
|
"main_pair_tvl": "1000000",
|
||||||
|
"logo_url": "https://..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Batch Token Price
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://prod.ave-api.com/v2/tokens/price" \
|
||||||
|
-H "X-API-KEY: your_api_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"token_ids": ["PEPE-bsc", "TRUMP-bsc", "0x55d398326f99059fF775485246999027B3197955-bsc"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"PEPE-bsc": {
|
||||||
|
"price": "0.00001234",
|
||||||
|
"price_24h_change": "5.67",
|
||||||
|
"updated_at": 1756888200
|
||||||
|
},
|
||||||
|
"TRUMP-bsc": {
|
||||||
|
"price": "4.56",
|
||||||
|
"price_24h_change": "-2.34",
|
||||||
|
"updated_at": 1756888200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Wallet PnL
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl "https://prod.ave-api.com/v2/address/pnl?wallet_address=0xd9c500dff816a1da21a48a732d3498bf09dc9aeb&chain=bsc&token_address=0x55d398326f99059fF775485246999027B3197955" \
|
||||||
|
-H "X-API-KEY: your_api_key"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"wallet_address": "0xd9c500dff816a1da21a48a732d3498bf09dc9aeb",
|
||||||
|
"token_address": "0x55d398326f99059fF775485246999027B3197955",
|
||||||
|
"total_profit": "123.45",
|
||||||
|
"total_profit_rate": "0.15",
|
||||||
|
"buy_amount": "10000",
|
||||||
|
"sell_amount": "10123.45",
|
||||||
|
"avg_buy_price": "1.0",
|
||||||
|
"avg_sell_price": "1.0123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Error Codes
|
||||||
|
|
||||||
|
#### HTTP Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning | Cause |
|
||||||
|
|------|---------|-------|
|
||||||
|
| 200 | Success | Request completed |
|
||||||
|
| 400 | Bad Request | Invalid parameters, malformed JSON |
|
||||||
|
| 401 | Unauthorized | Missing or invalid API key |
|
||||||
|
| 403 | Forbidden | API key expired, plan limits reached |
|
||||||
|
| 429 | Rate Limited | TPS exceeded |
|
||||||
|
| 500 | Server Error | Internal error, try again later |
|
||||||
|
|
||||||
|
#### Business Error Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 200 | Success |
|
||||||
|
| 1001 | General failure |
|
||||||
|
| 1011 | System error |
|
||||||
|
| 1021 | Signature verification failed |
|
||||||
|
| 1022 | API frozen, contact support |
|
||||||
|
| 1023 | Request expired (timestamp out of range) |
|
||||||
|
| 2001 | Request parameter error |
|
||||||
|
| 3001 | Transaction send failed |
|
||||||
|
| 3011 | Transaction record not found |
|
||||||
|
| 3021 | Order cancellation failed |
|
||||||
|
| 3101 | User does not exist |
|
||||||
|
| 3102 | User assets don't belong to this organization |
|
||||||
|
| 3103 | User asset account disabled |
|
||||||
|
| 3104 | No proxy wallet permission, upgrade plan required |
|
||||||
|
|
||||||
|
### 7.4 CU (Compute Unit) Cost Optimization
|
||||||
|
|
||||||
|
Each API call costs CU. Strategies to minimize usage:
|
||||||
|
|
||||||
|
| Strategy | Example |
|
||||||
|
|----------|---------|
|
||||||
|
| Use `/v2/tokens/price` batch (100 CU) vs individual calls | Batch up to 200 tokens in one call |
|
||||||
|
| Cache trending/risk data | Risk info changes slowly, cache for 5-10 min |
|
||||||
|
| Use filters | Add `tvl_min`, `tx_24h_volume_min` to price queries |
|
||||||
|
| Prefer search over detailed token | `/v2/tokens` (5 CU) vs `/v2/tokens/{id}` (5 CU) |
|
||||||
|
|
||||||
|
### 7.5 Pagination
|
||||||
|
|
||||||
|
Endpoints that return lists support pagination:
|
||||||
|
|
||||||
|
| Parameter | For |
|
||||||
|
|-----------|-----|
|
||||||
|
| `page_size` | Results per page (max varies) |
|
||||||
|
| `current_page` | Page number (0-indexed) |
|
||||||
|
| `last_id` | Cursor for next page (from previous response) |
|
||||||
|
|
||||||
|
Example pagination flow:
|
||||||
|
```python
|
||||||
|
# Get first page
|
||||||
|
result = get_trending_tokens(chain="bsc", page=0, page_size=50)
|
||||||
|
next_page = result["next_page"] # Use this for next request
|
||||||
|
|
||||||
|
# For wallet PnL with cursor
|
||||||
|
result = get_wallet_pnl(wallet, chain, token)
|
||||||
|
if result.get("has_more"):
|
||||||
|
next_result = get_wallet_pnl(wallet, chain, token, last_id=result["last_id"])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Working Code Examples
|
||||||
|
|
||||||
|
### 8.1 Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/shoko/repositories/randebu
|
||||||
|
pip install -r docs/scripts/requirements.txt
|
||||||
|
|
||||||
|
export AVE_API_KEY=your_api_key_here
|
||||||
|
export API_PLAN=free # or normal, pro
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Data REST API Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Token search
|
||||||
|
python3 docs/scripts/ave_data_rest.py search --keyword PEPE --chain bsc
|
||||||
|
|
||||||
|
# Batch price (up to 200 tokens)
|
||||||
|
python3 docs/scripts/ave_data_rest.py price --token-ids "PEPE-bsc,TRUMP-bsc,SOL-bsc"
|
||||||
|
|
||||||
|
# Trending tokens
|
||||||
|
python3 docs/scripts/ave_data_rest.py trending --chain bsc --page-size 20
|
||||||
|
|
||||||
|
# Token details + top pairs
|
||||||
|
python3 docs/scripts/ave_data_rest.py token --token-id "PEPE-bsc"
|
||||||
|
|
||||||
|
# Risk assessment
|
||||||
|
python3 docs/scripts/ave_data_rest.py risk --token-id "PEPE-bsc"
|
||||||
|
|
||||||
|
# Top 100 holders
|
||||||
|
python3 docs/scripts/ave_data_rest.py holders --token-id "PEPE-bsc" --limit 50
|
||||||
|
|
||||||
|
# Kline data (1h intervals)
|
||||||
|
python3 docs/scripts/ave_data_rest.py klines --token-id "PEPE-bsc" --interval 1h --limit 100
|
||||||
|
|
||||||
|
# Wallet PnL
|
||||||
|
python3 docs/scripts/ave_data_rest.py wallet-pnl \
|
||||||
|
--wallet 0xd9c500dff816a1da21a48a732d3498bf09dc9aeb \
|
||||||
|
--chain bsc --token 0x55d398326f99059fF775485246999027B3197955
|
||||||
|
|
||||||
|
# Smart wallets (for copy trading)
|
||||||
|
python3 docs/scripts/ave_data_rest.py smart-wallets --chain bsc --sort total_profit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 WebSocket Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires API_PLAN=pro
|
||||||
|
|
||||||
|
# Subscribe to swap transactions by pair
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-tx \
|
||||||
|
--pair Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE \
|
||||||
|
--chain solana
|
||||||
|
|
||||||
|
# Subscribe to all txs involving a token
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-multi-tx \
|
||||||
|
--token 0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c \
|
||||||
|
--chain bsc
|
||||||
|
|
||||||
|
# Subscribe to liquidity changes
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-liq \
|
||||||
|
--pair Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE \
|
||||||
|
--chain solana
|
||||||
|
|
||||||
|
# Subscribe to price changes
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-price \
|
||||||
|
--pairs "PEPE-bsc,TRUMP-bsc,SOL-bsc"
|
||||||
|
|
||||||
|
# Subscribe to kline data
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-kline \
|
||||||
|
--token "PEPE-bsc" --interval 1m
|
||||||
|
|
||||||
|
# Interactive REPL mode
|
||||||
|
python3 docs/scripts/ave_data_wss.py wss-repl
|
||||||
|
# Then type commands like:
|
||||||
|
# subscribe tx <pair_address> <chain>
|
||||||
|
# subscribe price PEPE-bsc,TRUMP-bsc
|
||||||
|
# list
|
||||||
|
# quit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 Trading Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chain Wallet (self-custody) - Free tier OK
|
||||||
|
# Dry-run quote
|
||||||
|
python3 docs/scripts/ave_trade_rest.py chain-quote \
|
||||||
|
--chain bsc \
|
||||||
|
--in-token 0x55d398326f99059fF775485246999027B3197955 \
|
||||||
|
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
||||||
|
--in-amount 10000000 --swap-type buy
|
||||||
|
|
||||||
|
# Actual swap (requires signing keys configured)
|
||||||
|
python3 docs/scripts/ave_trade_rest.py chain-swap \
|
||||||
|
--chain bsc \
|
||||||
|
--in-token 0x55d398326f99059fF775485246999027B3197955 \
|
||||||
|
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
||||||
|
--in-amount 10000000 --swap-type buy
|
||||||
|
|
||||||
|
# Proxy Wallet (server-managed) - Requires normal/pro tier
|
||||||
|
export AVE_SECRET_KEY=your_secret_key_here
|
||||||
|
|
||||||
|
# Dry-run quote
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-quote \
|
||||||
|
--chain bsc \
|
||||||
|
--proxy-wallet 0x... \
|
||||||
|
--in-token 0x55d398326f99059fF775485246999027B3197955 \
|
||||||
|
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
||||||
|
--in-amount 10000000 --swap-type buy
|
||||||
|
|
||||||
|
# Market order with TP/SL
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-market \
|
||||||
|
--chain bsc \
|
||||||
|
--proxy-wallet 0x... \
|
||||||
|
--in-token 0x55d398326f99059fF775485246999027B3197955 \
|
||||||
|
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
||||||
|
--in-amount 10000000 --swap-type buy \
|
||||||
|
--tp-price 35.0 --sl-price 30.0
|
||||||
|
|
||||||
|
# Limit order
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-limit \
|
||||||
|
--chain bsc \
|
||||||
|
--proxy-wallet 0x... \
|
||||||
|
--in-token 0x55d398326f99059fF775485246999027B3197955 \
|
||||||
|
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
||||||
|
--in-amount 10000000 --swap-type buy --price 35.5
|
||||||
|
|
||||||
|
# Get open orders
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-orders \
|
||||||
|
--chain bsc --proxy-wallet 0x...
|
||||||
|
|
||||||
|
# Cancel order
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-cancel \
|
||||||
|
--chain bsc --order-id xxx
|
||||||
|
|
||||||
|
# Create proxy wallet
|
||||||
|
python3 docs/scripts/ave_trade_rest.py create-proxy-wallet --chain bsc
|
||||||
|
|
||||||
|
# List proxy wallets
|
||||||
|
python3 docs/scripts/ave_trade_rest.py list-proxy-wallets --chain bsc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Security Guide
|
||||||
|
|
||||||
|
### 9.1 API Key Management
|
||||||
|
|
||||||
|
| Do | Don't |
|
||||||
|
|----|-------|
|
||||||
|
| Store in environment variables | Commit to git |
|
||||||
|
| Use `.env` file (add to `.gitignore`) | Share in Slack/Discord |
|
||||||
|
| Rotate keys periodically | Use in client-side code |
|
||||||
|
| Regenerate if compromised | Log API keys in errors |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env file (NEVER commit this)
|
||||||
|
AVE_API_KEY=your_api_key_here
|
||||||
|
AVE_SECRET_KEY=your_secret_key_here
|
||||||
|
AVE_EVM_PRIVATE_KEY=your_private_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 HMAC Signing for Proxy Wallet
|
||||||
|
|
||||||
|
Proxy wallet requests require HMAC-SHA256 signature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
def generate_signature(secret_key: str, timestamp: str, body: str = "") -> str:
|
||||||
|
message = timestamp + body
|
||||||
|
signature = hmac.new(
|
||||||
|
secret_key.encode(),
|
||||||
|
message.encode(),
|
||||||
|
hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
return signature
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
body = json.dumps({"chain": "bsc", "proxy_wallet": "0x..."}, separators=(",", ":"))
|
||||||
|
signature = generate_signature(AVE_SECRET_KEY, timestamp, body)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-KEY": API_KEY,
|
||||||
|
"X-Signature": signature,
|
||||||
|
"X-Timestamp": timestamp,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 Chain Wallet Private Key Protection
|
||||||
|
|
||||||
|
| Option | Security Level | Use Case |
|
||||||
|
|--------|---------------|----------|
|
||||||
|
| Hardware wallet | Highest | Production trading |
|
||||||
|
| Encrypted keystore | High | Development |
|
||||||
|
| Mnemonic (never raw key) | Medium | Quick testing |
|
||||||
|
| Raw private key in env | Low | Temporary only |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use hardware wallet or encrypted key
|
||||||
|
# NEVER do this:
|
||||||
|
AVE_EVM_PRIVATE_KEY=0x1234567890abcdef...
|
||||||
|
|
||||||
|
# DO this instead:
|
||||||
|
# Import from encrypted keystore or hardware wallet
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.4 Webhook/Callback Verification
|
||||||
|
|
||||||
|
If implementing webhooks for order updates:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
|
||||||
|
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
|
||||||
|
return hmac.compare_digest(signature, expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.5 Security Checklist
|
||||||
|
|
||||||
|
- [ ] API key stored in environment variable, not code
|
||||||
|
- [ ] `.env` in `.gitignore`
|
||||||
|
- [ ] Proxy wallet HMAC signing implemented correctly
|
||||||
|
- [ ] Private keys never logged or error messages
|
||||||
|
- [ ] HTTPS only (all AVE Cloud endpoints use HTTPS)
|
||||||
|
- [ ] Timestamp validation (requests expire after ~30s)
|
||||||
|
- [ ] Input validation on all user-provided addresses/tokens
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Testing Strategies
|
||||||
|
|
||||||
|
### 10.1 Dry-Run / Quote Mode
|
||||||
|
|
||||||
|
Always test with quotes first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chain wallet quote (no actual trade)
|
||||||
|
python3 docs/scripts/ave_trade_rest.py chain-quote \
|
||||||
|
--chain bsc --in-token 0x55d398326f99059fF775485246999027B3197955 \
|
||||||
|
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
||||||
|
--in-amount 10000000 --swap-type buy
|
||||||
|
|
||||||
|
# Proxy wallet quote (no actual trade)
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-quote \
|
||||||
|
--chain bsc --proxy-wallet 0x... \
|
||||||
|
--in-token 0x55d398326f99059fF775485246999027B3197955 \
|
||||||
|
--out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c \
|
||||||
|
--in-amount 10000000 --swap-type buy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 Paper Trading Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
|
||||||
|
class PaperTradingBot:
|
||||||
|
def __init__(self):
|
||||||
|
self.mode = os.getenv("TRADING_MODE", "paper") # paper or live
|
||||||
|
|
||||||
|
def execute_trade(self, order):
|
||||||
|
if self.mode == "paper":
|
||||||
|
print(f"[PAPER] Would execute: {order}")
|
||||||
|
return {"status": "paper", "order_id": "paper_123"}
|
||||||
|
else:
|
||||||
|
return self.live_execute(order)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 Testnet Considerations
|
||||||
|
|
||||||
|
| Chain | Testnet | Faucet |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| BSC | https://testnet.bscscan.com | https://testnet.bnb.org |
|
||||||
|
| Solana | https://api.devnet.solana.com | https://faucet.solana.com |
|
||||||
|
| ETH | Sepolia testnet | https://sepoliafaucet.com |
|
||||||
|
| Base | Base Sepolia | https://www.coinbase.com/faucets |
|
||||||
|
|
||||||
|
### 10.4 Mocking API Responses
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
def test_token_search():
|
||||||
|
mock_response = {
|
||||||
|
"status": 200,
|
||||||
|
"data": [{"symbol": "PEPE", "chain": "bsc", "current_price_usd": "0.00001"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_get.return_value = Mock(json=lambda: mock_response, status_code=200)
|
||||||
|
|
||||||
|
result = search_tokens("PEPE", "bsc")
|
||||||
|
assert result["data"][0]["symbol"] == "PEPE"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.5 Testing WebSocket Reconnection
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_reconnect():
|
||||||
|
client = AveWssClient(API_KEY)
|
||||||
|
client.connect()
|
||||||
|
|
||||||
|
# Simulate disconnect
|
||||||
|
client.ws.close()
|
||||||
|
|
||||||
|
# Should auto-reconnect
|
||||||
|
assert client.running
|
||||||
|
time.sleep(6) # Wait for reconnect
|
||||||
|
assert client.ws is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Troubleshooting Guide
|
||||||
|
|
||||||
|
### 11.1 Common Errors and Fixes
|
||||||
|
|
||||||
|
| Error | HTTP Code | Business Code | Cause | Solution |
|
||||||
|
|-------|-----------|--------------|-------|---------|
|
||||||
|
| 401 Unauthorized | 401 | - | Invalid/missing API key | Regenerate at cloud.ave.ai |
|
||||||
|
| 403 Forbidden | 403 | 1022 | API key expired | Renew key or contact support |
|
||||||
|
| 403 Forbidden | 403 | 3104 | Free tier accessing proxy wallet | Upgrade to Level 1+ |
|
||||||
|
| 429 Rate Limited | 429 | - | TPS exceeded | Add delay, implement backoff |
|
||||||
|
| 3001 Send TX Failed | 200 | 3001 | Insufficient gas or wallet balance | Check RPC, fund wallet |
|
||||||
|
| 1021 Signature Failed | 200 | 1021 | HMAC signature incorrect | Check timestamp and signing logic |
|
||||||
|
| 2001 Parameter Error | 200 | 2001 | Invalid request parameters | Check API docs for required fields |
|
||||||
|
|
||||||
|
### 11.2 WebSocket Troubleshooting
|
||||||
|
|
||||||
|
**Connection refused:**
|
||||||
|
```bash
|
||||||
|
# Verify API plan is pro
|
||||||
|
echo $API_PLAN # Should be "pro"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Not receiving messages:**
|
||||||
|
```python
|
||||||
|
# Enable debug mode
|
||||||
|
client = AveWssClient(api_key, debug=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auto-reconnect not working:**
|
||||||
|
```python
|
||||||
|
# Manual reconnect
|
||||||
|
client.disconnect()
|
||||||
|
time.sleep(5)
|
||||||
|
client.connect()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.3 Rate Limit Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def call_with_retry(func, max_retries=3):
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
result = func()
|
||||||
|
if result.status_code != 429:
|
||||||
|
return result
|
||||||
|
wait_time = 2 ** attempt # Exponential backoff
|
||||||
|
print(f"Rate limited, waiting {wait_time}s...")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
raise Exception("Max retries exceeded")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.4 Debugging Tips
|
||||||
|
|
||||||
|
1. **Check headers:**
|
||||||
|
```python
|
||||||
|
print(headers) # Verify X-API-KEY is present
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify token ID format:**
|
||||||
|
```python
|
||||||
|
# Correct
|
||||||
|
"PEPE-bsc"
|
||||||
|
"0x1234...-eth"
|
||||||
|
|
||||||
|
# Wrong
|
||||||
|
"PEPE" # Missing chain
|
||||||
|
"bsc:0x1234..." # Wrong separator
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check timestamp for HMAC:**
|
||||||
|
```python
|
||||||
|
# Timestamp must be milliseconds
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Amount format:**
|
||||||
|
```python
|
||||||
|
# Use string to avoid floating point precision issues
|
||||||
|
"10000000" # Correct
|
||||||
|
10000000 # Also works
|
||||||
|
10.0 # WRONG - precision loss
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Competitive Analysis
|
||||||
|
|
||||||
|
### 12.1 AVE Cloud vs Alternatives
|
||||||
|
|
||||||
|
| Feature | AVE Cloud | DexScreener | Moralis | CoinGecko |
|
||||||
|
|---------|-----------|-------------|---------|-----------|
|
||||||
|
| Multi-chain support | 4 chains | 20+ chains | 10+ chains | 100+ chains |
|
||||||
|
| Trading API | ✅ Chain + Proxy wallet | ❌ | ✅ ETH/ERC-20 only | ❌ |
|
||||||
|
| WebSocket streams | ✅ (pro tier) | ✅ Free | ✅ Paid only | ❌ |
|
||||||
|
| TP/SL automation | ✅ Proxy wallet | ❌ | ❌ | ❌ |
|
||||||
|
| Copy trading support | ✅ Smart wallet list | ❌ | ✅ | ❌ |
|
||||||
|
| Self-custody option | ✅ Chain wallet | ❌ | ❌ | ❌ |
|
||||||
|
| CU-based pricing | ✅ | Free | Free tier limited | Free tier limited |
|
||||||
|
| Risk/honeypot detection | ✅ Built-in | Limited | ✅ | ✅ |
|
||||||
|
| API documentation | Chinese-focused | Basic | Comprehensive | Comprehensive |
|
||||||
|
| Community size | Very small (3 stars) | Large | Large | Very large |
|
||||||
|
|
||||||
|
### 12.2 Unique Selling Points of AVE Cloud
|
||||||
|
|
||||||
|
1. **TP/SL Automation** - Built into proxy wallet, no need to monitor
|
||||||
|
2. **Dual Wallet Options** - Self-custody (chain) or managed (proxy)
|
||||||
|
3. **Copy Trading Infrastructure** - Smart wallet tracking API built-in
|
||||||
|
4. **All-in-one** - Data + Trading in single platform
|
||||||
|
5. **Low fees** - 0.6% chain, 0.8% proxy with rebates
|
||||||
|
|
||||||
|
### 12.3 Weaknesses
|
||||||
|
|
||||||
|
1. **Small community** - Only 3 GitHub stars, limited tutorials
|
||||||
|
2. **Documentation** - Primarily Chinese, limited English examples
|
||||||
|
3. **New platform** - Early stage, potential instability
|
||||||
|
4. **Feature locks** - WebSocket requires pro tier
|
||||||
|
5. **Solana complexity** - Priority fees not well documented
|
||||||
|
|
||||||
|
### 12.4 When to Use Alternatives
|
||||||
|
|
||||||
|
| Use Case | Alternative |
|
||||||
|
|----------|-------------|
|
||||||
|
| Multi-chain analytics only | DexScreener API |
|
||||||
|
| ERC-20 trading on ETH | Moralis |
|
||||||
|
| Historical price data | CoinGecko API |
|
||||||
|
| Advanced charting | TradingView API |
|
||||||
|
| Social sentiment | LunarCrush |
|
||||||
|
|
||||||
|
### 12.5 AVE Cloud Best Fit
|
||||||
|
|
||||||
|
- **Hackathon projects** focused on automated trading with TP/SL
|
||||||
|
- **Copy trading bots** leveraging smart wallet tracking
|
||||||
|
- **Multi-chain trading bots** needing unified API
|
||||||
|
- **Real-time monitoring dashboards** with WebSocket feeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. AVE Cloud Agent Skills - User Insights
|
||||||
|
|
||||||
**GitHub Activity:**
|
**GitHub Activity:**
|
||||||
- `ave-cloud-skill`: 3 stars, 46 commits (very early stage)
|
- `ave-cloud-skill`: 3 stars, 46 commits (very early stage)
|
||||||
@@ -240,3 +838,69 @@ Example: `https://pro.ave.ai/token/0x1234...abcd-bsc`
|
|||||||
- No YouTube tutorials, blog posts, or external reviews yet
|
- No YouTube tutorials, blog posts, or external reviews yet
|
||||||
|
|
||||||
**Bottom line:** The skills system is very new (3 stars), primarily used for token research and proxy wallet trading. The self-custody chain-wallet is the least used due to complexity.
|
**Bottom line:** The skills system is very new (3 stars), primarily used for token research and proxy wallet trading. The self-custody chain-wallet is the least used due to complexity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Quick Reference
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required For | Description |
|
||||||
|
|----------|-------------|-------------|
|
||||||
|
| `AVE_API_KEY` | All scripts | API key from cloud.ave.ai |
|
||||||
|
| `API_PLAN` | WSS, Proxy trading | free, normal, or pro |
|
||||||
|
| `AVE_SECRET_KEY` | Proxy wallet | HMAC signing secret |
|
||||||
|
| `AVE_EVM_PRIVATE_KEY` | Chain wallet (optional) | Hex private key for BSC/ETH/Base |
|
||||||
|
| `AVE_SOLANA_PRIVATE_KEY` | Chain wallet (optional) | Base58 private key for Solana |
|
||||||
|
| `AVE_MNEMONIC` | Chain wallet (optional) | BIP39 mnemonic |
|
||||||
|
|
||||||
|
### Script Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Data REST
|
||||||
|
python3 docs/scripts/ave_data_rest.py search --keyword <keyword>
|
||||||
|
python3 docs/scripts/ave_data_rest.py price --token-ids <comma-separated>
|
||||||
|
python3 docs/scripts/ave_data_rest.py trending --chain <chain>
|
||||||
|
python3 docs/scripts/ave_data_rest.py token --token-id <id>
|
||||||
|
python3 docs/scripts/ave_data_rest.py risk --token-id <id>
|
||||||
|
python3 docs/scripts/ave_data_rest.py holders --token-id <id>
|
||||||
|
python3 docs/scripts/ave_data_rest.py klines --token-id <id>
|
||||||
|
python3 docs/scripts/ave_data_rest.py wallet-pnl --wallet <addr> --chain <chain> --token <addr>
|
||||||
|
python3 docs/scripts/ave_data_rest.py wallet-info --wallet <addr> --chain <chain>
|
||||||
|
python3 docs/scripts/ave_data_rest.py smart-wallets --chain <chain>
|
||||||
|
|
||||||
|
# Data WebSocket
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-tx --pair <addr> --chain <chain>
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-multi-tx --token <addr> --chain <chain>
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-price --pairs <comma-separated>
|
||||||
|
python3 docs/scripts/ave_data_wss.py wss-repl
|
||||||
|
|
||||||
|
# Trading REST
|
||||||
|
python3 docs/scripts/ave_trade_rest.py chain-quote --chain <chain> --in-token <addr> --out-token <addr> --in-amount <amount> --swap-type <buy|sell>
|
||||||
|
python3 docs/scripts/ave_trade_rest.py chain-swap ...
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-quote ...
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-market ... [--tp-price <p>] [--sl-price <p>]
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-limit ... --price <p>
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-orders --chain <chain> --proxy-wallet <addr>
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-cancel --chain <chain> --order-id <id>
|
||||||
|
python3 docs/scripts/ave_trade_rest.py create-proxy-wallet --chain <chain>
|
||||||
|
python3 docs/scripts/ave_trade_rest.py list-proxy-wallets --chain <chain>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Token Addresses (BSC Mainnet)
|
||||||
|
|
||||||
|
| Token | Address |
|
||||||
|
|-------|---------|
|
||||||
|
| BNB | (native) |
|
||||||
|
| USDT | 0x55d398326f99059fF775485246999027B3197955 |
|
||||||
|
| BUSD | 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56 |
|
||||||
|
| BTCB | 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c |
|
||||||
|
| ETH | 0x2170Ed0880ac9A755fd29C2681C0DBeD5aF88B2c |
|
||||||
|
|
||||||
|
### Common Token Addresses (Solana)
|
||||||
|
|
||||||
|
| Token | Address |
|
||||||
|
|-------|---------|
|
||||||
|
| SOL | So11111111111111111111111111111111111111112 |
|
||||||
|
| USDC | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
|
||||||
|
| USDT | Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB |
|
||||||
|
|||||||
494
docs/scripts/ave_data_rest.py
Normal file
494
docs/scripts/ave_data_rest.py
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AVE Cloud Data REST API - Token Search, Prices, Klines, Holders, Risk
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
export AVE_API_KEY=your_api_key_here
|
||||||
|
export API_PLAN=free # free, normal, pro
|
||||||
|
|
||||||
|
python3 docs/scripts/ave_data_rest.py search --keyword PEPE
|
||||||
|
python3 docs/scripts/ave_data_rest.py price --token-ids "PEPE-bsc,TRUMP-bsc"
|
||||||
|
python3 docs/scripts/ave_data_rest.py trending --chain bsc
|
||||||
|
python3 docs/scripts/ave_data_rest.py token --token-id "0x6982508145454Ce325dDbE47a25d4ec3d2311933-bsc"
|
||||||
|
python3 docs/scripts/ave_data_rest.py risk --token-id "0x6982508145454Ce325dDbE47a25d4ec3d2311933-bsc"
|
||||||
|
python3 docs/scripts/ave_data_rest.py holders --token-id "0x6982508145454Ce325dDbE47a25d4ec3d2311933-bsc"
|
||||||
|
python3 docs/scripts/ave_data_rest.py klines --token-id "0x6982508145454Ce325dDbE47a25d4ec3d2311933-bsc"
|
||||||
|
python3 docs/scripts/ave_data_rest.py pairs --pair-id "0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae-bsc"
|
||||||
|
python3 docs/scripts/ave_data_rest.py wallet-pnl --wallet "0xd9c500dff816a1da21a48a732d3498bf09dc9aeb" --chain bsc --token "0x55d398326f99059fF775485246999027B3197955"
|
||||||
|
python3 docs/scripts/ave_data_rest.py wallet-info --wallet "0xd9c500dff816a1da21a48a732d3498bf09dc9aeb" --chain bsc
|
||||||
|
python3 docs/scripts/ave_data_rest.py smart-wallets --chain bsc
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "https://prod.ave-api.com"
|
||||||
|
API_KEY = os.getenv("AVE_API_KEY", "")
|
||||||
|
API_PLAN = os.getenv("API_PLAN", "free")
|
||||||
|
|
||||||
|
CU_COSTS = {
|
||||||
|
"search": 5,
|
||||||
|
"price": 100,
|
||||||
|
"trending": 5,
|
||||||
|
"token": 5,
|
||||||
|
"risk": 10,
|
||||||
|
"holders": 10,
|
||||||
|
"klines_token": 10,
|
||||||
|
"klines_pair": 10,
|
||||||
|
"pairs": 5,
|
||||||
|
"wallet_pnl": 5,
|
||||||
|
"wallet_info": 5,
|
||||||
|
"wallet_tokens": 10,
|
||||||
|
"smart_wallet": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
TIER_LIMITS = {
|
||||||
|
"free": {"tps": 1, "data_wss": False, "proxy_wallet": False},
|
||||||
|
"normal": {"tps": 5, "data_wss": False, "proxy_wallet": True},
|
||||||
|
"pro": {"tps": 20, "data_wss": True, "proxy_wallet": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ApiResponse:
|
||||||
|
status: int
|
||||||
|
data: Optional[dict] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def make_headers():
|
||||||
|
return {
|
||||||
|
"X-API-KEY": API_KEY,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_response(response: requests.Response) -> ApiResponse:
|
||||||
|
if response.status_code == 200:
|
||||||
|
return ApiResponse(status=200, data=response.json())
|
||||||
|
elif response.status_code == 401:
|
||||||
|
return ApiResponse(
|
||||||
|
status=401, error="Unauthorized - invalid or missing API key"
|
||||||
|
)
|
||||||
|
elif response.status_code == 403:
|
||||||
|
return ApiResponse(
|
||||||
|
status=403, error="Forbidden - API key expired or plan limits reached"
|
||||||
|
)
|
||||||
|
elif response.status_code == 429:
|
||||||
|
return ApiResponse(status=429, error="Rate limited - TPS exceeded")
|
||||||
|
else:
|
||||||
|
return ApiResponse(status=response.status_code, error=response.text)
|
||||||
|
|
||||||
|
|
||||||
|
def search_tokens(
|
||||||
|
keyword: str, chain: Optional[str] = None, limit: int = 100
|
||||||
|
) -> ApiResponse:
|
||||||
|
params = {"keyword": keyword, "limit": limit}
|
||||||
|
if chain:
|
||||||
|
params["chain"] = chain
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/tokens", headers=make_headers(), params=params
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_price(token_ids: list) -> ApiResponse:
|
||||||
|
data = {"token_ids": token_ids}
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v2/tokens/price", headers=make_headers(), json=data
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_trending_tokens(chain: str, page: int = 0, page_size: int = 50) -> ApiResponse:
|
||||||
|
params = {"chain": chain, "current_page": page, "page_size": page_size}
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/tokens/trending", headers=make_headers(), params=params
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_detail(token_id: str) -> ApiResponse:
|
||||||
|
response = requests.get(f"{BASE_URL}/v2/tokens/{token_id}", headers=make_headers())
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_risk(token_id: str) -> ApiResponse:
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/contracts/{token_id}", headers=make_headers()
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_top_holders(token_id: str, limit: int = 100) -> ApiResponse:
|
||||||
|
params = {"limit": limit} if limit != 100 else {}
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/tokens/top100/{token_id}", headers=make_headers(), params=params
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_klines_by_token(
|
||||||
|
token_id: str, interval: str = "1h", limit: int = 100
|
||||||
|
) -> ApiResponse:
|
||||||
|
params = {"interval": interval, "limit": limit}
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/klines/token/{token_id}", headers=make_headers(), params=params
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_klines_by_pair(
|
||||||
|
pair_id: str, interval: str = "1h", limit: int = 100
|
||||||
|
) -> ApiResponse:
|
||||||
|
params = {"interval": interval, "limit": limit}
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/klines/pair/{pair_id}", headers=make_headers(), params=params
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pair_detail(pair_id: str) -> ApiResponse:
|
||||||
|
response = requests.get(f"{BASE_URL}/v2/pairs/{pair_id}", headers=make_headers())
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_wallet_pnl(
|
||||||
|
wallet_address: str,
|
||||||
|
chain: str,
|
||||||
|
token_address: str,
|
||||||
|
from_time: Optional[int] = None,
|
||||||
|
to_time: Optional[int] = None,
|
||||||
|
) -> ApiResponse:
|
||||||
|
params = {
|
||||||
|
"wallet_address": wallet_address,
|
||||||
|
"chain": chain,
|
||||||
|
"token_address": token_address,
|
||||||
|
}
|
||||||
|
if from_time:
|
||||||
|
params["from_time"] = from_time
|
||||||
|
if to_time:
|
||||||
|
params["to_time"] = to_time
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/address/pnl", headers=make_headers(), params=params
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_wallet_info(wallet_address: str, chain: str) -> ApiResponse:
|
||||||
|
params = {"wallet_address": wallet_address, "chain": chain}
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/address/walletinfo", headers=make_headers(), params=params
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_wallet_tokens(
|
||||||
|
wallet_address: str,
|
||||||
|
chain: str,
|
||||||
|
sort: str = "last_txn_time",
|
||||||
|
sort_dir: str = "desc",
|
||||||
|
hide_sold: int = 0,
|
||||||
|
hide_small: int = 0,
|
||||||
|
) -> ApiResponse:
|
||||||
|
params = {
|
||||||
|
"wallet_address": wallet_address,
|
||||||
|
"chain": chain,
|
||||||
|
"sort": sort,
|
||||||
|
"sort_dir": sort_dir,
|
||||||
|
"hide_sold": hide_sold,
|
||||||
|
"hide_small": hide_small,
|
||||||
|
}
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/address/walletinfo/tokens",
|
||||||
|
headers=make_headers(),
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def get_smart_wallets(
|
||||||
|
chain: str, sort: str = "total_profit", sort_dir: str = "desc"
|
||||||
|
) -> ApiResponse:
|
||||||
|
params = {"chain": chain, "sort": sort, "sort_dir": sort_dir}
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v2/address/smart_wallet/list",
|
||||||
|
headers=make_headers(),
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def format_json(data):
|
||||||
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def print_cu_warning(operation: str):
|
||||||
|
cu_cost = CU_COSTS.get(operation, "unknown")
|
||||||
|
print(f"[CU Cost: {cu_cost}]", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_search(args):
|
||||||
|
result = search_tokens(args.keyword, args.chain, args.limit)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("search")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_price(args):
|
||||||
|
token_ids = [t.strip() for t in args.token_ids.split(",")]
|
||||||
|
result = get_token_price(token_ids)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("price")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_trending(args):
|
||||||
|
result = get_trending_tokens(args.chain, args.page, args.page_size)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("trending")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_token(args):
|
||||||
|
result = get_token_detail(args.token_id)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("token")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_risk(args):
|
||||||
|
result = get_token_risk(args.token_id)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("risk")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_holders(args):
|
||||||
|
result = get_top_holders(args.token_id, args.limit)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("holders")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_klines(args):
|
||||||
|
if args.pair_id:
|
||||||
|
result = get_klines_by_pair(args.pair_id, args.interval, args.limit)
|
||||||
|
print_cu_warning("klines_pair")
|
||||||
|
else:
|
||||||
|
result = get_klines_by_token(args.token_id, args.interval, args.limit)
|
||||||
|
print_cu_warning("klines_token")
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_pairs(args):
|
||||||
|
result = get_pair_detail(args.pair_id)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("pairs")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_wallet_pnl(args):
|
||||||
|
result = get_wallet_pnl(
|
||||||
|
args.wallet, args.chain, args.token, args.from_time, args.to_time
|
||||||
|
)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("wallet_pnl")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_wallet_info(args):
|
||||||
|
result = get_wallet_info(args.wallet, args.chain)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("wallet_info")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_wallet_tokens(args):
|
||||||
|
result = get_wallet_tokens(
|
||||||
|
args.wallet,
|
||||||
|
args.chain,
|
||||||
|
args.sort,
|
||||||
|
args.sort_dir,
|
||||||
|
args.hide_sold,
|
||||||
|
args.hide_small,
|
||||||
|
)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("wallet_tokens")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_smart_wallets(args):
|
||||||
|
result = get_smart_wallets(args.chain, args.sort, args.sort_dir)
|
||||||
|
if result.error:
|
||||||
|
print(f"Error: {result.error}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_cu_warning("smart_wallet")
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="AVE Cloud Data REST API")
|
||||||
|
parser.add_argument(
|
||||||
|
"--api-key", default=API_KEY, help="AVE API key (or set AVE_API_KEY env)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--api-plan",
|
||||||
|
default=API_PLAN,
|
||||||
|
choices=["free", "normal", "pro"],
|
||||||
|
help="API plan",
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||||
|
|
||||||
|
p_search = subparsers.add_parser("search", help="Search tokens by keyword")
|
||||||
|
p_search.add_argument("--keyword", required=True, help="Token symbol or address")
|
||||||
|
p_search.add_argument("--chain", help="Chain name (bsc, solana, eth, base)")
|
||||||
|
p_search.add_argument(
|
||||||
|
"--limit", type=int, default=100, help="Number of results (max 300)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_price = subparsers.add_parser("price", help="Get batch token prices")
|
||||||
|
p_price.add_argument(
|
||||||
|
"--token-ids",
|
||||||
|
required=True,
|
||||||
|
help="Comma-separated token IDs (e.g. PEPE-bsc,TRUMP-bsc)",
|
||||||
|
)
|
||||||
|
|
||||||
|
p_trending = subparsers.add_parser("trending", help="Get trending tokens")
|
||||||
|
p_trending.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_trending.add_argument("--page", type=int, default=0, help="Page number")
|
||||||
|
p_trending.add_argument(
|
||||||
|
"--page-size", type=int, default=50, help="Results per page (max 100)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_token = subparsers.add_parser("token", help="Get token details and top pairs")
|
||||||
|
p_token.add_argument(
|
||||||
|
"--token-id", required=True, help="Token ID (address-chain format)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_risk = subparsers.add_parser("risk", help="Get token risk information")
|
||||||
|
p_risk.add_argument(
|
||||||
|
"--token-id", required=True, help="Token ID (address-chain format)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_holders = subparsers.add_parser("holders", help="Get top 100 token holders")
|
||||||
|
p_holders.add_argument(
|
||||||
|
"--token-id", required=True, help="Token ID (address-chain format)"
|
||||||
|
)
|
||||||
|
p_holders.add_argument(
|
||||||
|
"--limit", type=int, default=100, help="Number of holders (max 100)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_klines = subparsers.add_parser("klines", help="Get kline/candlestick data")
|
||||||
|
p_klines.add_argument("--token-id", help="Token ID (use OR --pair-id)")
|
||||||
|
p_klines.add_argument("--pair-id", help="Pair ID (use OR --token-id)")
|
||||||
|
p_klines.add_argument(
|
||||||
|
"--interval", default="1h", help="Interval (1m, 5m, 15m, 30m, 1h, 4h, 1d)"
|
||||||
|
)
|
||||||
|
p_klines.add_argument("--limit", type=int, default=100, help="Number of klines")
|
||||||
|
|
||||||
|
p_pairs = subparsers.add_parser("pairs", help="Get pair details")
|
||||||
|
p_pairs.add_argument(
|
||||||
|
"--pair-id", required=True, help="Pair ID (address-chain format)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_wallet_pnl = subparsers.add_parser(
|
||||||
|
"wallet-pnl", help="Get wallet PnL for a token"
|
||||||
|
)
|
||||||
|
p_wallet_pnl.add_argument("--wallet", required=True, help="Wallet address")
|
||||||
|
p_wallet_pnl.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_wallet_pnl.add_argument("--token", required=True, help="Token address")
|
||||||
|
p_wallet_pnl.add_argument(
|
||||||
|
"--from-time", type=int, help="Unix epoch seconds (earliest 15 days ago)"
|
||||||
|
)
|
||||||
|
p_wallet_pnl.add_argument("--to-time", type=int, help="Unix epoch seconds")
|
||||||
|
|
||||||
|
p_wallet_info = subparsers.add_parser(
|
||||||
|
"wallet-info", help="Get wallet info (all tokens on chain)"
|
||||||
|
)
|
||||||
|
p_wallet_info.add_argument("--wallet", required=True, help="Wallet address")
|
||||||
|
p_wallet_info.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
|
||||||
|
p_wallet_tokens = subparsers.add_parser(
|
||||||
|
"wallet-tokens", help="Get all tokens holding in wallet"
|
||||||
|
)
|
||||||
|
p_wallet_tokens.add_argument("--wallet", required=True, help="Wallet address")
|
||||||
|
p_wallet_tokens.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_wallet_tokens.add_argument("--sort", default="last_txn_time", help="Sort field")
|
||||||
|
p_wallet_tokens.add_argument(
|
||||||
|
"--sort-dir", default="desc", help="Sort direction (desc/asc)"
|
||||||
|
)
|
||||||
|
p_wallet_tokens.add_argument(
|
||||||
|
"--hide-sold", type=int, default=0, help="Hide sold tokens (0/1)"
|
||||||
|
)
|
||||||
|
p_wallet_tokens.add_argument(
|
||||||
|
"--hide-small", type=int, default=0, help="Hide small balances (0/1)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_smart = subparsers.add_parser(
|
||||||
|
"smart-wallets", help="Get smart wallet list (for copy trading)"
|
||||||
|
)
|
||||||
|
p_smart.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_smart.add_argument("--sort", default="total_profit", help="Sort field")
|
||||||
|
p_smart.add_argument("--sort-dir", default="desc", help="Sort direction (desc/asc)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.api_key:
|
||||||
|
print("Error: API key required. Set AVE_API_KEY env or use --api-key")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd_map = {
|
||||||
|
"search": cmd_search,
|
||||||
|
"price": cmd_price,
|
||||||
|
"trending": cmd_trending,
|
||||||
|
"token": cmd_token,
|
||||||
|
"risk": cmd_risk,
|
||||||
|
"holders": cmd_holders,
|
||||||
|
"klines": cmd_klines,
|
||||||
|
"pairs": cmd_pairs,
|
||||||
|
"wallet-pnl": cmd_wallet_pnl,
|
||||||
|
"wallet-info": cmd_wallet_info,
|
||||||
|
"wallet-tokens": cmd_wallet_tokens,
|
||||||
|
"smart-wallets": cmd_smart_wallets,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_map[args.command](args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
459
docs/scripts/ave_data_wss.py
Normal file
459
docs/scripts/ave_data_wss.py
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AVE Cloud Data WebSocket API - Real-time Streams
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
export AVE_API_KEY=your_api_key_here
|
||||||
|
export API_PLAN=pro # REQUIRED for WebSocket (must be 'pro')
|
||||||
|
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-tx --pair "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE" --chain solana
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-multi-tx --token "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c" --chain bsc
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-liq --pair "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE" --chain solana
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-price --pairs "PEPE-bsc,TRUMP-bsc"
|
||||||
|
python3 docs/scripts/ave_data_wss.py subscribe-kline --token "0x6982508145454Ce325dDbE47a25d4ec3d2311933-bsc" --interval 1m
|
||||||
|
python3 docs/scripts/ave_data_wss.py wss-repl # Interactive REPL mode
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- WebSocket API requires API_PLAN=pro
|
||||||
|
- Max 5 concurrent WebSocket connections
|
||||||
|
- Auto-reconnect with exponential backoff on disconnect
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
WSS_URL = "wss://wss.ave-api.xyz"
|
||||||
|
API_KEY = os.getenv("AVE_API_KEY", "")
|
||||||
|
API_PLAN = os.getenv("API_PLAN", "")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WssMessage:
|
||||||
|
id: int
|
||||||
|
method: str
|
||||||
|
params: list
|
||||||
|
result: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WssSubscription:
|
||||||
|
topic: str
|
||||||
|
address: str
|
||||||
|
chain: str
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class AveWssClient:
|
||||||
|
def __init__(self, api_key: str, debug: bool = False):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.debug = debug
|
||||||
|
self.ws: Optional[websocket.WebSocket] = None
|
||||||
|
self.subscription_id = 1
|
||||||
|
self.subscriptions: list[WssSubscription] = []
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
if self.ws:
|
||||||
|
self.disconnect()
|
||||||
|
self.ws = websocket.WebSocketApp(
|
||||||
|
WSS_URL,
|
||||||
|
on_open=self._on_open,
|
||||||
|
on_message=self._on_message,
|
||||||
|
on_error=self._on_error,
|
||||||
|
on_close=self._on_close,
|
||||||
|
)
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.running = False
|
||||||
|
if self.ws:
|
||||||
|
self.ws.close()
|
||||||
|
self.ws = None
|
||||||
|
|
||||||
|
def _on_open(self, ws):
|
||||||
|
print("[Connected to WebSocket]", file=sys.stderr)
|
||||||
|
|
||||||
|
def _on_message(self, ws, message):
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
if self.debug:
|
||||||
|
print(f"[DEBUG] {json.dumps(data, indent=2)}", file=sys.stderr)
|
||||||
|
if "result" in data and data.get("id"):
|
||||||
|
for sub in self.subscriptions:
|
||||||
|
if sub.id == data["id"]:
|
||||||
|
sub.id = data["result"].get("id", data["id"])
|
||||||
|
print(
|
||||||
|
f"[Subscribed to {sub.topic}] {sub.address} on {sub.chain}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
elif "params" in data:
|
||||||
|
topic = data["params"].get("topic", "unknown")
|
||||||
|
if topic == "tx":
|
||||||
|
self._handle_tx(data["params"]["data"])
|
||||||
|
elif topic == "liq":
|
||||||
|
self._handle_liq(data["params"]["data"])
|
||||||
|
elif topic == "kline":
|
||||||
|
self._handle_kline(data["params"]["data"])
|
||||||
|
elif topic == "price":
|
||||||
|
self._handle_price(data["params"]["data"])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"[Error] Failed to parse message: {message}", file=sys.stderr)
|
||||||
|
|
||||||
|
def _on_error(self, ws, error):
|
||||||
|
print(f"[WebSocket Error] {error}", file=sys.stderr)
|
||||||
|
|
||||||
|
def _on_close(self, ws, close_status_code, close_msg):
|
||||||
|
print(f"[Disconnected] {close_status_code}: {close_msg}", file=sys.stderr)
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def _handle_tx(self, tx: dict):
|
||||||
|
print(
|
||||||
|
f"[TX] {tx.get('chain')}: {tx.get('from_symbol')} -> {tx.get('to_symbol')} "
|
||||||
|
f"Amount: {tx.get('from_amount')} {tx.get('from_symbol')} | "
|
||||||
|
f"Sender: {tx.get('sender', 'unknown')[:10]}... | "
|
||||||
|
f"Pair: {tx.get('pair_address', 'unknown')[:15]}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_liq(self, liq: dict):
|
||||||
|
print(
|
||||||
|
f"[LIQ] {liq.get('chain')}: {liq.get('action')} "
|
||||||
|
f"Amount: {liq.get('amount')} {liq.get('symbol')} | "
|
||||||
|
f"Pair: {liq.get('pair_address', 'unknown')[:15]}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_kline(self, kline: dict):
|
||||||
|
print(
|
||||||
|
f"[KLINE] {kline.get('token_id', kline.get('pair_id'))} "
|
||||||
|
f"{kline.get('interval')}: O={kline.get('open')} H={kline.get('high')} "
|
||||||
|
f"L={kline.get('low')} C={kline.get('close')} V={kline.get('volume')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_price(self, price: dict):
|
||||||
|
print(
|
||||||
|
f"[PRICE] {price.get('token_id', price.get('pair_id'))}: "
|
||||||
|
f"{price.get('price')} USD (change: {price.get('price_change_24h', 'N/A')}%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send(self, method: str, params: list, timeout: int = 10) -> Optional[dict]:
|
||||||
|
if not self.ws:
|
||||||
|
return None
|
||||||
|
msg_id = self.subscription_id
|
||||||
|
self.subscription_id += 1
|
||||||
|
msg = {"jsonrpc": "2.0", "method": method, "params": params, "id": msg_id}
|
||||||
|
self.ws.send(json.dumps(msg))
|
||||||
|
return {"id": msg_id}
|
||||||
|
|
||||||
|
def subscribe_tx(self, pair_address: str, chain: str):
|
||||||
|
params = ["tx", pair_address, chain]
|
||||||
|
result = self._send("subscribe", params)
|
||||||
|
if result:
|
||||||
|
self.subscriptions.append(
|
||||||
|
WssSubscription(
|
||||||
|
topic="tx", address=pair_address, chain=chain, id=result["id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe_multi_tx(self, token_address: str, chain: str):
|
||||||
|
params = ["multi_tx", token_address, chain]
|
||||||
|
result = self._send("subscribe", params)
|
||||||
|
if result:
|
||||||
|
self.subscriptions.append(
|
||||||
|
WssSubscription(
|
||||||
|
topic="multi_tx",
|
||||||
|
address=token_address,
|
||||||
|
chain=chain,
|
||||||
|
id=result["id"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe_liq(self, pair_address: str, chain: str):
|
||||||
|
params = ["liq", pair_address, chain]
|
||||||
|
result = self._send("subscribe", params)
|
||||||
|
if result:
|
||||||
|
self.subscriptions.append(
|
||||||
|
WssSubscription(
|
||||||
|
topic="liq", address=pair_address, chain=chain, id=result["id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe_price(self, pairs: list):
|
||||||
|
params = ["price", pairs]
|
||||||
|
result = self._send("subscribe", params)
|
||||||
|
if result:
|
||||||
|
for pair in pairs:
|
||||||
|
self.subscriptions.append(
|
||||||
|
WssSubscription(
|
||||||
|
topic="price", address=pair, chain="multi", id=result["id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe_kline(self, token_or_pair_id: str, interval: str = "1m"):
|
||||||
|
params = ["kline", token_or_pair_id, interval]
|
||||||
|
result = self._send("subscribe", params)
|
||||||
|
if result:
|
||||||
|
self.subscriptions.append(
|
||||||
|
WssSubscription(
|
||||||
|
topic="kline",
|
||||||
|
address=token_or_pair_id,
|
||||||
|
chain=interval,
|
||||||
|
id=result["id"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def unsubscribe(self, topic: str, address: str, chain: str):
|
||||||
|
params = (
|
||||||
|
["unsubscribe", topic, address, chain]
|
||||||
|
if chain != "multi"
|
||||||
|
else ["unsubscribe", topic, address]
|
||||||
|
)
|
||||||
|
self._send("unsubscribe", params)
|
||||||
|
self.subscriptions = [
|
||||||
|
s
|
||||||
|
for s in self.subscriptions
|
||||||
|
if not (s.topic == topic and s.address == address and s.chain == chain)
|
||||||
|
]
|
||||||
|
|
||||||
|
def run_forever(self, reconnect_delay: int = 5, max_reconnect_delay: int = 60):
|
||||||
|
delay = reconnect_delay
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
self.ws.run_forever(ping_interval=30, ping_timeout=10)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Error] {e}", file=sys.stderr)
|
||||||
|
if self.running:
|
||||||
|
print(f"[Reconnecting in {delay}s...]", file=sys.stderr)
|
||||||
|
time.sleep(delay)
|
||||||
|
delay = min(delay * 2, max_reconnect_delay)
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_subscribe_tx(args):
|
||||||
|
if API_PLAN != "pro":
|
||||||
|
print("Error: WebSocket requires API_PLAN=pro", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
client = AveWssClient(args.api_key, debug=args.debug)
|
||||||
|
client.connect()
|
||||||
|
client.subscribe_tx(args.pair, args.chain)
|
||||||
|
print("[Press Ctrl+C to exit]", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
client.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_subscribe_multi_tx(args):
|
||||||
|
if API_PLAN != "pro":
|
||||||
|
print("Error: WebSocket requires API_PLAN=pro", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
client = AveWssClient(args.api_key, debug=args.debug)
|
||||||
|
client.connect()
|
||||||
|
client.subscribe_multi_tx(args.token, args.chain)
|
||||||
|
print("[Press Ctrl+C to exit]", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
client.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_subscribe_liq(args):
|
||||||
|
if API_PLAN != "pro":
|
||||||
|
print("Error: WebSocket requires API_PLAN=pro", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
client = AveWssClient(args.api_key, debug=args.debug)
|
||||||
|
client.connect()
|
||||||
|
client.subscribe_liq(args.pair, args.chain)
|
||||||
|
print("[Press Ctrl+C to exit]", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
client.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_subscribe_price(args):
|
||||||
|
if API_PLAN != "pro":
|
||||||
|
print("Error: WebSocket requires API_PLAN=pro", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
pairs = [p.strip() for p in args.pairs.split(",")]
|
||||||
|
client = AveWssClient(args.api_key, debug=args.debug)
|
||||||
|
client.connect()
|
||||||
|
client.subscribe_price(pairs)
|
||||||
|
print("[Press Ctrl+C to exit]", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
client.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_subscribe_kline(args):
|
||||||
|
if API_PLAN != "pro":
|
||||||
|
print("Error: WebSocket requires API_PLAN=pro", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
client = AveWssClient(args.api_key, debug=args.debug)
|
||||||
|
client.connect()
|
||||||
|
client.subscribe_kline(args.token, args.interval)
|
||||||
|
print("[Press Ctrl+C to exit]", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
client.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_wss_repl(args):
|
||||||
|
if API_PLAN != "pro":
|
||||||
|
print("Error: WebSocket requires API_PLAN=pro", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
client = AveWssClient(args.api_key, debug=args.debug)
|
||||||
|
client.connect()
|
||||||
|
print(
|
||||||
|
"[WebSocket REPL - type 'help' for commands, 'quit' to exit]", file=sys.stderr
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"[Commands: subscribe tx <pair> <chain>, subscribe price <pairs>, unsubscribe <topic> <addr> <chain>]",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
subscriptions = []
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
print("Commands:")
|
||||||
|
print(
|
||||||
|
" subscribe tx <pair_address> <chain> - Subscribe to swap transactions by pair"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
" subscribe multi_tx <token_address> <chain> - Subscribe to all txs involving a token"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
" subscribe liq <pair_address> <chain> - Subscribe to liquidity changes by pair"
|
||||||
|
)
|
||||||
|
print(" subscribe price <pair1,pair2,...> - Subscribe to price changes")
|
||||||
|
print(
|
||||||
|
" subscribe kline <token_id> <interval> - Subscribe to kline data (interval: 1m, 5m, etc.)"
|
||||||
|
)
|
||||||
|
print(" unsubscribe <topic> <address> <chain> - Unsubscribe")
|
||||||
|
print(" list - List subscriptions")
|
||||||
|
print(" help - Show this help")
|
||||||
|
print(" quit - Exit")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("wss> ").strip()
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
parts = user_input.split()
|
||||||
|
cmd = parts[0].lower()
|
||||||
|
|
||||||
|
if cmd == "quit":
|
||||||
|
break
|
||||||
|
elif cmd == "help":
|
||||||
|
print_help()
|
||||||
|
elif cmd == "list":
|
||||||
|
for s in client.subscriptions:
|
||||||
|
print(f" {s.topic}: {s.address} on {s.chain}")
|
||||||
|
elif cmd == "subscribe" and len(parts) >= 3:
|
||||||
|
sub_type = parts[1].lower()
|
||||||
|
if sub_type == "tx" and len(parts) >= 4:
|
||||||
|
client.subscribe_tx(parts[2], parts[3])
|
||||||
|
elif sub_type == "multi_tx" and len(parts) >= 4:
|
||||||
|
client.subscribe_multi_tx(parts[2], parts[3])
|
||||||
|
elif sub_type == "liq" and len(parts) >= 4:
|
||||||
|
client.subscribe_liq(parts[2], parts[3])
|
||||||
|
elif sub_type == "price" and len(parts) >= 3:
|
||||||
|
pairs = parts[2].split(",")
|
||||||
|
client.subscribe_price(pairs)
|
||||||
|
elif sub_type == "kline" and len(parts) >= 4:
|
||||||
|
client.subscribe_kline(parts[2], parts[3])
|
||||||
|
else:
|
||||||
|
print("Invalid subscribe command")
|
||||||
|
elif cmd == "unsubscribe" and len(parts) >= 4:
|
||||||
|
client.unsubscribe(parts[1], parts[2], parts[3])
|
||||||
|
else:
|
||||||
|
print("Unknown command. Type 'help' for commands.")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
|
||||||
|
client.disconnect()
|
||||||
|
print("\n[Exited]")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="AVE Cloud Data WebSocket API")
|
||||||
|
parser.add_argument(
|
||||||
|
"--api-key", default=API_KEY, help="AVE API key (or set AVE_API_KEY env)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--api-plan", default=API_PLAN, help="API plan (or set API_PLAN env)"
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", action="store_true", help="Enable debug output")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||||
|
|
||||||
|
p_tx = subparsers.add_parser(
|
||||||
|
"subscribe-tx", help="Subscribe to swap transactions by pair"
|
||||||
|
)
|
||||||
|
p_tx.add_argument("--pair", required=True, help="Pair address")
|
||||||
|
p_tx.add_argument(
|
||||||
|
"--chain", required=True, help="Chain name (solana, bsc, eth, base)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_multi = subparsers.add_parser(
|
||||||
|
"subscribe-multi-tx", help="Subscribe to all txs involving a token"
|
||||||
|
)
|
||||||
|
p_multi.add_argument("--token", required=True, help="Token address")
|
||||||
|
p_multi.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
|
||||||
|
p_liq = subparsers.add_parser(
|
||||||
|
"subscribe-liq", help="Subscribe to liquidity changes by pair"
|
||||||
|
)
|
||||||
|
p_liq.add_argument("--pair", required=True, help="Pair address")
|
||||||
|
p_liq.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
|
||||||
|
p_price = subparsers.add_parser(
|
||||||
|
"subscribe-price", help="Subscribe to price changes"
|
||||||
|
)
|
||||||
|
p_price.add_argument(
|
||||||
|
"--pairs", required=True, help="Comma-separated pairs (e.g. PEPE-bsc,TRUMP-bsc)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_kline = subparsers.add_parser("subscribe-kline", help="Subscribe to kline data")
|
||||||
|
p_kline.add_argument(
|
||||||
|
"--token", required=True, help="Token ID (address-chain format)"
|
||||||
|
)
|
||||||
|
p_kline.add_argument(
|
||||||
|
"--interval", default="1m", help="Interval (1m, 5m, 15m, 30m, 1h, 4h, 1d)"
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers.add_parser("wss-repl", help="Interactive WebSocket REPL")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.api_key:
|
||||||
|
print("Error: API key required. Set AVE_API_KEY env or use --api-key")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd_map = {
|
||||||
|
"subscribe-tx": cmd_subscribe_tx,
|
||||||
|
"subscribe-multi-tx": cmd_subscribe_multi_tx,
|
||||||
|
"subscribe-liq": cmd_subscribe_liq,
|
||||||
|
"subscribe-price": cmd_subscribe_price,
|
||||||
|
"subscribe-kline": cmd_subscribe_kline,
|
||||||
|
"wss-repl": cmd_wss_repl,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_map[args.command](args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
560
docs/scripts/ave_trade_rest.py
Normal file
560
docs/scripts/ave_trade_rest.py
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AVE Cloud Trading REST API - Chain Wallet & Proxy Wallet Trading
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Environment setup
|
||||||
|
export AVE_API_KEY=your_api_key_here
|
||||||
|
export API_PLAN=free # free for chain-wallet, normal/pro for proxy-wallet
|
||||||
|
export AVE_SECRET_KEY=your_secret_key_here # For proxy wallet HMAC signing
|
||||||
|
|
||||||
|
# Chain Wallet (Self-custody) - Free tier OK
|
||||||
|
python3 docs/scripts/ave_trade_rest.py chain-quote --chain bsc --in-token 0x55d398326f99059fF775485246999027B3197955 --out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c --in-amount 10000000 --swap-type buy
|
||||||
|
python3 docs/scripts/ave_trade_rest.py chain-swap --chain bsc --in-token 0x55d398326f99059fF775485246999027B3197955 --out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c --in-amount 10000000 --swap-type buy
|
||||||
|
|
||||||
|
# Proxy Wallet (Server-managed) - Requires normal/pro tier
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-quote --chain bsc --in-token 0x55d398326f99059fF775485246999027B3197955 --out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c --in-amount 10000000 --swap-type buy
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-market --chain bsc --in-token 0x55d398326f99059fF775485246999027B3197955 --out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c --in-amount 10000000 --swap-type buy
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-limit --chain bsc --in-token 0x55d398326f99059fF775485246999027B3197955 --out-token 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c --in-amount 10000000 --swap-type buy --price 35.5
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-orders --chain bsc
|
||||||
|
python3 docs/scripts/ave_trade_rest.py proxy-cancel --chain bsc --order-id xxx
|
||||||
|
|
||||||
|
# Wallet Management
|
||||||
|
python3 docs/scripts/ave_trade_rest.py create-proxy-wallet --chain bsc
|
||||||
|
python3 docs/scripts/ave_trade_rest.py list-proxy-wallets --chain bsc
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Chain Wallet: Requires API_PLAN=free or higher
|
||||||
|
- Proxy Wallet: Requires API_PLAN=normal or pro
|
||||||
|
- All amount fields use string format to avoid floating point precision issues
|
||||||
|
- Proxy wallet uses HMAC-SHA256 signing with AVE_SECRET_KEY
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CHAIN_WALLET_URL = "https://bot-api.ave.ai"
|
||||||
|
PROXY_WALLET_URL = "https://bot-api.ave.ai"
|
||||||
|
API_KEY = os.getenv("AVE_API_KEY", "")
|
||||||
|
API_PLAN = os.getenv("API_PLAN", "free")
|
||||||
|
SECRET_KEY = os.getenv("AVE_SECRET_KEY", "")
|
||||||
|
EVM_PRIVATE_KEY = os.getenv("AVE_EVM_PRIVATE_KEY", "")
|
||||||
|
SOLANA_PRIVATE_KEY = os.getenv("AVE_SOLANA_PRIVATE_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TradeResponse:
|
||||||
|
status: int
|
||||||
|
data: Optional[dict] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
business_code: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
def make_headers(signed: bool = False):
|
||||||
|
headers = {
|
||||||
|
"X-API-KEY": API_KEY,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def make_proxy_headers(timestamp: str, signature: str):
|
||||||
|
return {
|
||||||
|
"X-API-KEY": API_KEY,
|
||||||
|
"X-Signature": signature,
|
||||||
|
"X-Timestamp": timestamp,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_hmac_signature(timestamp: str, body: str = "") -> str:
|
||||||
|
message = timestamp + body
|
||||||
|
signature = hmac.new(
|
||||||
|
SECRET_KEY.encode(), message.encode(), hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
return signature
|
||||||
|
|
||||||
|
|
||||||
|
def handle_response(response: requests.Response) -> TradeResponse:
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get("code") == 200 or data.get("status") == 200:
|
||||||
|
return TradeResponse(status=200, data=data)
|
||||||
|
else:
|
||||||
|
return TradeResponse(
|
||||||
|
status=data.get("code", response.status_code),
|
||||||
|
data=data,
|
||||||
|
error=data.get("msg", "Unknown error"),
|
||||||
|
business_code=data.get("code"),
|
||||||
|
)
|
||||||
|
elif response.status_code == 401:
|
||||||
|
return TradeResponse(
|
||||||
|
status=401, error="Unauthorized - invalid or missing API key"
|
||||||
|
)
|
||||||
|
elif response.status_code == 403:
|
||||||
|
return TradeResponse(
|
||||||
|
status=403,
|
||||||
|
error="Forbidden - API key expired, plan limits, or insufficient permissions",
|
||||||
|
)
|
||||||
|
elif response.status_code == 429:
|
||||||
|
return TradeResponse(status=429, error="Rate limited - TPS exceeded")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
return TradeResponse(
|
||||||
|
status=response.status_code,
|
||||||
|
data=data,
|
||||||
|
error=data.get("msg", response.text),
|
||||||
|
business_code=data.get("code"),
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return TradeResponse(status=response.status_code, error=response.text)
|
||||||
|
|
||||||
|
|
||||||
|
def chain_quote(
|
||||||
|
chain: str,
|
||||||
|
in_token: str,
|
||||||
|
out_token: str,
|
||||||
|
in_amount: str,
|
||||||
|
swap_type: str,
|
||||||
|
slippage: float = 0.5,
|
||||||
|
) -> TradeResponse:
|
||||||
|
endpoint = f"{CHAIN_WALLET_URL}/v1/chain/quote"
|
||||||
|
data = {
|
||||||
|
"chain": chain,
|
||||||
|
"in_token": in_token,
|
||||||
|
"out_token": out_token,
|
||||||
|
"in_amount": in_amount,
|
||||||
|
"swap_type": swap_type,
|
||||||
|
"slippage": slippage,
|
||||||
|
}
|
||||||
|
response = requests.post(endpoint, headers=make_headers(), json=data)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def chain_swap(
|
||||||
|
chain: str,
|
||||||
|
in_token: str,
|
||||||
|
out_token: str,
|
||||||
|
in_amount: str,
|
||||||
|
swap_type: str,
|
||||||
|
slippage: float = 0.5,
|
||||||
|
recipient: Optional[str] = None,
|
||||||
|
) -> TradeResponse:
|
||||||
|
endpoint = f"{CHAIN_WALLET_URL}/v1/chain/swap"
|
||||||
|
data = {
|
||||||
|
"chain": chain,
|
||||||
|
"in_token": in_token,
|
||||||
|
"out_token": out_token,
|
||||||
|
"in_amount": in_amount,
|
||||||
|
"swap_type": swap_type,
|
||||||
|
"slippage": slippage,
|
||||||
|
}
|
||||||
|
if recipient:
|
||||||
|
data["recipient"] = recipient
|
||||||
|
response = requests.post(endpoint, headers=make_headers(), json=data)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_quote(
|
||||||
|
chain: str,
|
||||||
|
proxy_wallet: str,
|
||||||
|
in_token: str,
|
||||||
|
out_token: str,
|
||||||
|
in_amount: str,
|
||||||
|
swap_type: str,
|
||||||
|
) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
body = json.dumps(
|
||||||
|
{
|
||||||
|
"chain": chain,
|
||||||
|
"proxy_wallet": proxy_wallet,
|
||||||
|
"in_token": in_token,
|
||||||
|
"out_token": out_token,
|
||||||
|
"in_amount": in_amount,
|
||||||
|
"swap_type": swap_type,
|
||||||
|
},
|
||||||
|
separators=(",", ":"),
|
||||||
|
)
|
||||||
|
signature = generate_hmac_signature(timestamp, body)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/quote"
|
||||||
|
response = requests.post(endpoint, headers=headers, json=json.loads(body))
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_market_order(
|
||||||
|
chain: str,
|
||||||
|
proxy_wallet: str,
|
||||||
|
in_token: str,
|
||||||
|
out_token: str,
|
||||||
|
in_amount: str,
|
||||||
|
swap_type: str,
|
||||||
|
tp_price: Optional[str] = None,
|
||||||
|
sl_price: Optional[str] = None,
|
||||||
|
trailing_stop: Optional[dict] = None,
|
||||||
|
) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
data = {
|
||||||
|
"chain": chain,
|
||||||
|
"proxy_wallet": proxy_wallet,
|
||||||
|
"in_token": in_token,
|
||||||
|
"out_token": out_token,
|
||||||
|
"in_amount": in_amount,
|
||||||
|
"swap_type": swap_type,
|
||||||
|
"order_type": "market",
|
||||||
|
}
|
||||||
|
if tp_price:
|
||||||
|
data["tp_price"] = tp_price
|
||||||
|
if sl_price:
|
||||||
|
data["sl_price"] = sl_price
|
||||||
|
if trailing_stop:
|
||||||
|
data["trailing_stop"] = trailing_stop
|
||||||
|
body = json.dumps(data, separators=(",", ":"))
|
||||||
|
signature = generate_hmac_signature(timestamp, body)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/market_order"
|
||||||
|
response = requests.post(endpoint, headers=headers, json=data)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_limit_order(
|
||||||
|
chain: str,
|
||||||
|
proxy_wallet: str,
|
||||||
|
in_token: str,
|
||||||
|
out_token: str,
|
||||||
|
in_amount: str,
|
||||||
|
swap_type: str,
|
||||||
|
price: str,
|
||||||
|
tp_price: Optional[str] = None,
|
||||||
|
sl_price: Optional[str] = None,
|
||||||
|
) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
data = {
|
||||||
|
"chain": chain,
|
||||||
|
"proxy_wallet": proxy_wallet,
|
||||||
|
"in_token": in_token,
|
||||||
|
"out_token": out_token,
|
||||||
|
"in_amount": in_amount,
|
||||||
|
"swap_type": swap_type,
|
||||||
|
"price": price,
|
||||||
|
"order_type": "limit",
|
||||||
|
}
|
||||||
|
if tp_price:
|
||||||
|
data["tp_price"] = tp_price
|
||||||
|
if sl_price:
|
||||||
|
data["sl_price"] = sl_price
|
||||||
|
body = json.dumps(data, separators=(",", ":"))
|
||||||
|
signature = generate_hmac_signature(timestamp, body)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/limit_order"
|
||||||
|
response = requests.post(endpoint, headers=headers, json=data)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_cancel_order(chain: str, order_id: str) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
data = {"chain": chain, "order_id": order_id}
|
||||||
|
body = json.dumps(data, separators=(",", ":"))
|
||||||
|
signature = generate_hmac_signature(timestamp, body)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/cancel_order"
|
||||||
|
response = requests.post(endpoint, headers=headers, json=data)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_get_orders(
|
||||||
|
chain: str, proxy_wallet: str, order_type: Optional[str] = None
|
||||||
|
) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
params = {"chain": chain, "proxy_wallet": proxy_wallet}
|
||||||
|
if order_type:
|
||||||
|
params["order_type"] = order_type
|
||||||
|
query_string = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
|
||||||
|
signature = generate_hmac_signature(timestamp, query_string)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/orders"
|
||||||
|
response = requests.get(endpoint, headers=headers, params=params)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_get_order_history(chain: str, proxy_wallet: str) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
params = {"chain": chain, "proxy_wallet": proxy_wallet}
|
||||||
|
query_string = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
|
||||||
|
signature = generate_hmac_signature(timestamp, query_string)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/order_history"
|
||||||
|
response = requests.get(endpoint, headers=headers, params=params)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def create_proxy_wallet(chain: str) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
data = {"chain": chain}
|
||||||
|
body = json.dumps(data, separators=(",", ":"))
|
||||||
|
signature = generate_hmac_signature(timestamp, body)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/create_proxy_wallet"
|
||||||
|
response = requests.post(endpoint, headers=headers, json=data)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def list_proxy_wallets(chain: str) -> TradeResponse:
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
params = {"chain": chain}
|
||||||
|
query_string = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
|
||||||
|
signature = generate_hmac_signature(timestamp, query_string)
|
||||||
|
headers = make_proxy_headers(timestamp, signature)
|
||||||
|
endpoint = f"{PROXY_WALLET_URL}/v1/proxy_wallets"
|
||||||
|
response = requests.get(endpoint, headers=headers, params=params)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def format_json(data):
|
||||||
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def print_trade_response(result: TradeResponse, success_msg: str = "Success"):
|
||||||
|
if result.error:
|
||||||
|
print(f"Error ({result.business_code or result.status}): {result.error}")
|
||||||
|
if result.data:
|
||||||
|
print(format_json(result.data))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"{success_msg}")
|
||||||
|
if result.data:
|
||||||
|
print(format_json(result.data))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_chain_quote(args):
|
||||||
|
result = chain_quote(
|
||||||
|
chain=args.chain,
|
||||||
|
in_token=args.in_token,
|
||||||
|
out_token=args.out_token,
|
||||||
|
in_amount=args.in_amount,
|
||||||
|
swap_type=args.swap_type,
|
||||||
|
slippage=args.slippage,
|
||||||
|
)
|
||||||
|
print_trade_response(result, "Quote retrieved (dry-run - no actual trade)")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_chain_swap(args):
|
||||||
|
result = chain_swap(
|
||||||
|
chain=args.chain,
|
||||||
|
in_token=args.in_token,
|
||||||
|
out_token=args.out_token,
|
||||||
|
in_amount=args.in_amount,
|
||||||
|
swap_type=args.swap_type,
|
||||||
|
slippage=args.slippage,
|
||||||
|
recipient=args.recipient,
|
||||||
|
)
|
||||||
|
print_trade_response(result, "Swap executed!")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_proxy_quote(args):
|
||||||
|
result = proxy_quote(
|
||||||
|
chain=args.chain,
|
||||||
|
proxy_wallet=args.proxy_wallet,
|
||||||
|
in_token=args.in_token,
|
||||||
|
out_token=args.out_token,
|
||||||
|
in_amount=args.in_amount,
|
||||||
|
swap_type=args.swap_type,
|
||||||
|
)
|
||||||
|
print_trade_response(result, "Quote retrieved (dry-run - no actual trade)")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_proxy_market(args):
|
||||||
|
result = proxy_market_order(
|
||||||
|
chain=args.chain,
|
||||||
|
proxy_wallet=args.proxy_wallet,
|
||||||
|
in_token=args.in_token,
|
||||||
|
out_token=args.out_token,
|
||||||
|
in_amount=args.in_amount,
|
||||||
|
swap_type=args.swap_type,
|
||||||
|
tp_price=args.tp_price,
|
||||||
|
sl_price=args.sl_price,
|
||||||
|
)
|
||||||
|
print_trade_response(result, "Market order submitted!")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_proxy_limit(args):
|
||||||
|
result = proxy_limit_order(
|
||||||
|
chain=args.chain,
|
||||||
|
proxy_wallet=args.proxy_wallet,
|
||||||
|
in_token=args.in_token,
|
||||||
|
out_token=args.out_token,
|
||||||
|
in_amount=args.in_amount,
|
||||||
|
swap_type=args.swap_type,
|
||||||
|
price=args.price,
|
||||||
|
tp_price=args.tp_price,
|
||||||
|
sl_price=args.sl_price,
|
||||||
|
)
|
||||||
|
print_trade_response(result, "Limit order submitted!")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_proxy_cancel(args):
|
||||||
|
result = proxy_cancel_order(args.chain, args.order_id)
|
||||||
|
print_trade_response(result, "Order cancelled!")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_proxy_orders(args):
|
||||||
|
result = proxy_get_orders(args.chain, args.proxy_wallet, args.order_type)
|
||||||
|
print_trade_response(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_proxy_history(args):
|
||||||
|
result = proxy_get_order_history(args.chain, args.proxy_wallet)
|
||||||
|
print_trade_response(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_create_proxy_wallet(args):
|
||||||
|
result = create_proxy_wallet(args.chain)
|
||||||
|
print_trade_response(result, "Proxy wallet created!")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list_proxy_wallets(args):
|
||||||
|
result = list_proxy_wallets(args.chain)
|
||||||
|
print_trade_response(result)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="AVE Cloud Trading REST API")
|
||||||
|
parser.add_argument(
|
||||||
|
"--api-key", default=API_KEY, help="AVE API key (or set AVE_API_KEY env)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--api-plan", default=API_PLAN, help="API plan (or set API_PLAN env)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--secret-key",
|
||||||
|
default=SECRET_KEY,
|
||||||
|
help="AVE SECRET_KEY for HMAC signing (or set AVE_SECRET_KEY env)",
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||||
|
|
||||||
|
p_cquote = subparsers.add_parser(
|
||||||
|
"chain-quote", help="Chain wallet - get swap quote (dry-run)"
|
||||||
|
)
|
||||||
|
p_cquote.add_argument(
|
||||||
|
"--chain", required=True, help="Chain name (bsc, solana, eth, base)"
|
||||||
|
)
|
||||||
|
p_cquote.add_argument("--in-token", required=True, help="Input token address")
|
||||||
|
p_cquote.add_argument("--out-token", required=True, help="Output token address")
|
||||||
|
p_cquote.add_argument(
|
||||||
|
"--in-amount", required=True, help="Input amount (use string format)"
|
||||||
|
)
|
||||||
|
p_cquote.add_argument("--swap-type", required=True, help="Swap type (buy or sell)")
|
||||||
|
p_cquote.add_argument(
|
||||||
|
"--slippage", type=float, default=0.5, help="Slippage tolerance (default: 0.5)"
|
||||||
|
)
|
||||||
|
|
||||||
|
p_cswap = subparsers.add_parser("chain-swap", help="Chain wallet - execute swap")
|
||||||
|
p_cswap.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_cswap.add_argument("--in-token", required=True, help="Input token address")
|
||||||
|
p_cswap.add_argument("--out-token", required=True, help="Output token address")
|
||||||
|
p_cswap.add_argument("--in-amount", required=True, help="Input amount")
|
||||||
|
p_cswap.add_argument("--swap-type", required=True, help="Swap type (buy or sell)")
|
||||||
|
p_cswap.add_argument(
|
||||||
|
"--slippage", type=float, default=0.5, help="Slippage tolerance"
|
||||||
|
)
|
||||||
|
p_cswap.add_argument("--recipient", help="Optional recipient address")
|
||||||
|
|
||||||
|
p_pquote = subparsers.add_parser(
|
||||||
|
"proxy-quote", help="Proxy wallet - get swap quote (dry-run)"
|
||||||
|
)
|
||||||
|
p_pquote.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_pquote.add_argument("--proxy-wallet", required=True, help="Proxy wallet address")
|
||||||
|
p_pquote.add_argument("--in-token", required=True, help="Input token address")
|
||||||
|
p_pquote.add_argument("--out-token", required=True, help="Output token address")
|
||||||
|
p_pquote.add_argument("--in-amount", required=True, help="Input amount")
|
||||||
|
p_pquote.add_argument("--swap-type", required=True, help="Swap type (buy or sell)")
|
||||||
|
|
||||||
|
p_pmarket = subparsers.add_parser(
|
||||||
|
"proxy-market", help="Proxy wallet - submit market order"
|
||||||
|
)
|
||||||
|
p_pmarket.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_pmarket.add_argument("--proxy-wallet", required=True, help="Proxy wallet address")
|
||||||
|
p_pmarket.add_argument("--in-token", required=True, help="Input token address")
|
||||||
|
p_pmarket.add_argument("--out-token", required=True, help="Output token address")
|
||||||
|
p_pmarket.add_argument("--in-amount", required=True, help="Input amount")
|
||||||
|
p_pmarket.add_argument("--swap-type", required=True, help="Swap type (buy or sell)")
|
||||||
|
p_pmarket.add_argument("--tp-price", help="Take-profit price")
|
||||||
|
p_pmarket.add_argument("--sl-price", help="Stop-loss price")
|
||||||
|
|
||||||
|
p_plimit = subparsers.add_parser(
|
||||||
|
"proxy-limit", help="Proxy wallet - submit limit order"
|
||||||
|
)
|
||||||
|
p_plimit.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_plimit.add_argument("--proxy-wallet", required=True, help="Proxy wallet address")
|
||||||
|
p_plimit.add_argument("--in-token", required=True, help="Input token address")
|
||||||
|
p_plimit.add_argument("--out-token", required=True, help="Output token address")
|
||||||
|
p_plimit.add_argument("--in-amount", required=True, help="Input amount")
|
||||||
|
p_plimit.add_argument("--swap-type", required=True, help="Swap type (buy or sell)")
|
||||||
|
p_plimit.add_argument("--price", required=True, help="Limit price")
|
||||||
|
p_plimit.add_argument("--tp-price", help="Take-profit price")
|
||||||
|
p_plimit.add_argument("--sl-price", help="Stop-loss price")
|
||||||
|
|
||||||
|
p_cancel = subparsers.add_parser("proxy-cancel", help="Proxy wallet - cancel order")
|
||||||
|
p_cancel.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_cancel.add_argument("--order-id", required=True, help="Order ID to cancel")
|
||||||
|
|
||||||
|
p_orders = subparsers.add_parser(
|
||||||
|
"proxy-orders", help="Proxy wallet - get open orders"
|
||||||
|
)
|
||||||
|
p_orders.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_orders.add_argument("--proxy-wallet", required=True, help="Proxy wallet address")
|
||||||
|
p_orders.add_argument("--order-type", help="Filter by order type (market/limit)")
|
||||||
|
|
||||||
|
p_history = subparsers.add_parser(
|
||||||
|
"proxy-history", help="Proxy wallet - get order history"
|
||||||
|
)
|
||||||
|
p_history.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
p_history.add_argument("--proxy-wallet", required=True, help="Proxy wallet address")
|
||||||
|
|
||||||
|
p_create = subparsers.add_parser(
|
||||||
|
"create-proxy-wallet", help="Create a new proxy wallet"
|
||||||
|
)
|
||||||
|
p_create.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
|
||||||
|
p_list = subparsers.add_parser("list-proxy-wallets", help="List proxy wallets")
|
||||||
|
p_list.add_argument("--chain", required=True, help="Chain name")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.api_key:
|
||||||
|
print("Error: API key required. Set AVE_API_KEY env or use --api-key")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd_map = {
|
||||||
|
"chain-quote": cmd_chain_quote,
|
||||||
|
"chain-swap": cmd_chain_swap,
|
||||||
|
"proxy-quote": cmd_proxy_quote,
|
||||||
|
"proxy-market": cmd_proxy_market,
|
||||||
|
"proxy-limit": cmd_proxy_limit,
|
||||||
|
"proxy-cancel": cmd_proxy_cancel,
|
||||||
|
"proxy-orders": cmd_proxy_orders,
|
||||||
|
"proxy-history": cmd_proxy_history,
|
||||||
|
"create-proxy-wallet": cmd_create_proxy_wallet,
|
||||||
|
"list-proxy-wallets": cmd_list_proxy_wallets,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_map[args.command](args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
docs/scripts/requirements.txt
Normal file
2
docs/scripts/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
requests>=2.31.0
|
||||||
|
websocket-client>=1.7.0
|
||||||
Reference in New Issue
Block a user