Update StandX API documentation and configuration

- Revised `.env.example` to reflect new API token generation process, emphasizing the use of creation date and validity days for token expiry management.
- Enhanced `auth.md` with detailed instructions for obtaining API tokens and signing transactions for both EVM and Solana wallets.
- Updated `maker-points-guide.md` to clarify the API token retrieval process and the significance of the Ed25519 private key.
- Refactored `config.ts` and `gateway.ts` to support new token expiry configuration methods and improved private key handling, including Base58 decoding.
- Improved overall documentation clarity and user guidance for new and existing users.
This commit is contained in:
discountry
2026-01-15 16:11:08 +08:00
parent 6496011d8f
commit 86670486a6
5 changed files with 499 additions and 104 deletions
+7 -5
View File
@@ -14,12 +14,14 @@ STANDX_SYMBOL=BTC-USD
# STANDX_BASE_URL=https://perps.standx.com # STANDX_BASE_URL=https://perps.standx.com
# STANDX_WS_URL=wss://perps.standx.com/ws-stream/v1 # STANDX_WS_URL=wss://perps.standx.com/ws-stream/v1
# STANDX_SESSION_ID= # STANDX_SESSION_ID=
# Optional: request signing key (ed25519 private key, hex or base64) # Optional: request signing key (ed25519 private key, supports hex or base58 format)
# STANDX_REQUEST_PRIVATE_KEY= # STANDX_REQUEST_PRIVATE_KEY=
# Token expiry timestamp (Unix seconds). When expired: cancels all orders, stops new orders. # Token expiry configuration (recommended method: creation date + validity days)
# If position exists, continues close/stop-loss logic. If no position/orders, enters silent mode. # Get these values when generating API token at https://standx.com/user/session
# Example: STANDX_TOKEN_EXPIRY=1737092800 # STANDX_TOKEN_CREATE_DATE=2025-01-15 # Token creation date (YYYY-MM-DD format)
# STANDX_TOKEN_EXPIRY= # STANDX_TOKEN_VALIDITY_DAYS=30 # Token validity period in days
# Legacy method: direct expiry timestamp (Unix seconds)
# STANDX_TOKEN_EXPIRY=1737092800
# Core trading symbol and sizing # Core trading symbol and sizing
TRADE_SYMBOL=BTCUSDT # Trading pair symbol TRADE_SYMBOL=BTCUSDT # Trading pair symbol
+329 -8
View File
@@ -1,5 +1,9 @@
## StandX Perps Authentication ## StandX Perps Authentication
官网创建的 API 提供了
API Token 以及 Ed25519 Private Key,用于签名交易。
⚠️ This document is under construction. ⚠️ This document is under construction.
This document explains how to obtain JWT access tokens for the StandX Perps API through wallet signatures. This document explains how to obtain JWT access tokens for the StandX Perps API through wallet signatures.
@@ -69,7 +73,7 @@ curl 'https://api.standx.com/v1/offchain/certs'
Sign `payload.message` with your wallet private key to generate the `signature`. Sign `payload.message` with your wallet private key to generate the `signature`.
#### TypeScript/ES6 Implementation Reference #### BSC (EVM) Implementation Reference
``` ```
import { ethers } from "ethers"; import { ethers } from "ethers";
@@ -84,6 +88,38 @@ const wallet = new ethers.Wallet(privateKey, provider);
const signature = await wallet.signMessage(payload.message); const signature = await wallet.signMessage(payload.message);
``` ```
#### Solana Implementation Reference
```
import bs58 from "bs58";
import { ed25519 } from "@noble/curves/ed25519";
import { Keypair } from "@solana/web3.js";
const privateKey = "<your_base58_encoded_private_key>"; // Keep secure; use environment variables
const walletKeypair = Keypair.fromSecretKey(bs58.decode(privateKey));
// Sign using the message from the parsed payload
const messageBytes = new TextEncoder().encode(payload.message);
const signatureBytes = ed25519.sign(
messageBytes,
walletKeypair.secretKey.slice(0, 32) // First 32 bytes are the private key
);
// Solana requires a specific signature format
const signature = Buffer.from(
JSON.stringify({
input: payload,
output: {
signedMessage: Array.from(messageBytes),
signature: Array.from(signatureBytes),
account: {
publicKey: Array.from(walletKeypair.publicKey.toBytes()),
},
},
})
).toString("base64");
```
### 5\. Get Access Token ### 5\. Get Access Token
Submit the `signature` and original `signedData` to the login endpoint. Submit the `signature` and original `signedData` to the login endpoint.
@@ -220,14 +256,38 @@ fetch("/api/request_need_body_signature", {
}); });
``` ```
### Complete Authentication Class Example ### Complete Authentication Examples
Heres a complete implementation using a class-based approach: For complete, runnable implementations, see the chain-specific examples:
- [EVM (BSC) Example](https://docs.standx.com/standx-api/perps-auth-evm-example) - Authentication using ethers.js for BSC and other EVM-compatible chains
- [Solana (SVM) Example](https://docs.standx.com/standx-api/perps-auth-svm-example) - Authentication using @solana/web3.js for Solana
Last updated on
[About StandX API](https://docs.standx.com/standx-api/standx-api "About StandX API") [Perps Auth EVM Example](https://docs.standx.com/standx-api/perps-auth-evm-example "Perps Auth EVM Example")
## StandX Perps Authentication - EVM Example
This example demonstrates how to authenticate with the StandX Perps API using an EVM-compatible wallet (e.g., BSC).
## Prerequisites
- Node.js environment with TypeScript support
- EVM wallet with private key
- Required packages:
```
npm install @noble/curves @scure/base ethers
```
## Complete Implementation
``` ```
import { ed25519 } from "@noble/curves/ed25519"; import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base"; import { base58 } from "@scure/base";
import { ethers } from "ethers";
// Types
export type Chain = "bsc" | "solana"; export type Chain = "bsc" | "solana";
export interface SignedData { export interface SignedData {
@@ -260,6 +320,7 @@ export interface RequestSignatureHeaders {
"x-request-signature": string; "x-request-signature": string;
} }
// Authentication Class
export class StandXAuth { export class StandXAuth {
private ed25519PrivateKey: Uint8Array; private ed25519PrivateKey: Uint8Array;
private ed25519PublicKey: Uint8Array; private ed25519PublicKey: Uint8Array;
@@ -343,9 +404,7 @@ export class StandXAuth {
} }
// Usage Example // Usage Example
import { ethers } from "ethers"; async function main() {
async function example() {
// Initialize auth // Initialize auth
const auth = new StandXAuth(); const auth = new StandXAuth();
@@ -389,8 +448,270 @@ async function example() {
body: payload, body: payload,
}); });
} }
main().catch(console.error);
``` ```
Last updated on ## Key Points
[About StandX API](https://docs.standx.com/standx-api/standx-api "About StandX API") [Perps HTTP API](https://docs.standx.com/standx-api/perps-http "Perps HTTP API") 1. **Wallet Setup**: Uses `ethers.js` to create a wallet from a private key
2. **Message Signing**: EVM wallets sign the message directly using `wallet.signMessage()`
3. **Signature Format**: The signature is returned as-is from the wallet (hex string)
## Environment Variables
Create a `.env` file with:
```
WALLET_PRIVATE_KEY=your_private_key_here
```
> **Security Note**: Never commit private keys to version control. Use environment variables or secure key management solutions.
[Perps Auth](https://docs.standx.com/standx-api/perps-auth "Perps Auth") [Perps Auth SVM Example](https://docs.standx.com/standx-api/perps-auth-svm-example "Perps Auth SVM Example")
## StandX Perps Authentication - Solana (SVM) Example
This example demonstrates how to authenticate with the StandX Perps API using a Solana wallet.
## Prerequisites
- Node.js environment with TypeScript support
- Solana wallet with private key (base58-encoded)
- Required packages:
```
npm install @noble/curves @scure/base @solana/web3.js bs58
```
## Complete Implementation
```
import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base";
import bs58 from "bs58";
import { Keypair } from "@solana/web3.js";
// Types
export type Chain = "bsc" | "solana";
export interface SignedData {
domain: string;
uri: string;
statement: string;
version: string;
chainId: number;
nonce: string;
address: string;
requestId: string;
issuedAt: string;
message: string;
exp: number;
iat: number;
}
export interface LoginResponse {
token: string;
address: string;
alias: string;
chain: string;
perpsAlpha: boolean;
}
export interface RequestSignatureHeaders {
"x-request-sign-version": string;
"x-request-id": string;
"x-request-timestamp": string;
"x-request-signature": string;
}
// Authentication Class
export class StandXAuth {
private ed25519PrivateKey: Uint8Array;
private ed25519PublicKey: Uint8Array;
private requestId: string;
private baseUrl = "https://api.standx.com";
constructor() {
const privateKey = ed25519.utils.randomSecretKey();
this.ed25519PrivateKey = privateKey;
this.ed25519PublicKey = ed25519.getPublicKey(privateKey);
this.requestId = base58.encode(this.ed25519PublicKey);
}
async authenticate(
chain: Chain,
walletAddress: string,
signMessage: (msg: string, payload: SignedData) => Promise<string>
): Promise<LoginResponse> {
const signedDataJwt = await this.prepareSignIn(chain, walletAddress);
const payload = this.parseJwt<SignedData>(signedDataJwt);
const signature = await signMessage(payload.message, payload);
return this.login(chain, signature, signedDataJwt);
}
private async prepareSignIn(chain: Chain, address: string): Promise<string> {
const res = await fetch(
\`${this.baseUrl}/v1/offchain/prepare-signin?chain=${chain}\`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address, requestId: this.requestId }),
}
);
const data = await res.json();
if (!data.success) throw new Error("Failed to prepare sign-in");
return data.signedData;
}
private async login(
chain: Chain,
signature: string,
signedData: string,
expiresSeconds: number = 604800 // default: 7 days
): Promise<LoginResponse> {
const res = await fetch(
\`${this.baseUrl}/v1/offchain/login?chain=${chain}\`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ signature, signedData, expiresSeconds }),
}
);
return res.json();
}
signRequest(
payload: string,
requestId: string,
timestamp: number
): RequestSignatureHeaders {
const version = "v1";
const message = \`${version},${requestId},${timestamp},${payload}\`;
const signature = ed25519.sign(
Buffer.from(message, "utf-8"),
this.ed25519PrivateKey
);
return {
"x-request-sign-version": version,
"x-request-id": requestId,
"x-request-timestamp": timestamp.toString(),
"x-request-signature": Buffer.from(signature).toString("base64"),
};
}
private parseJwt<T>(token: string): T {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
return JSON.parse(Buffer.from(base64, "base64").toString("utf-8"));
}
}
// Usage Example
async function main() {
// Initialize auth
const auth = new StandXAuth();
// Setup wallet from base58-encoded private key
const privateKey = process.env.SOLANA_PRIVATE_KEY!;
const walletKeypair = Keypair.fromSecretKey(bs58.decode(privateKey));
const walletAddress = walletKeypair.publicKey.toBase58();
// Authenticate
const loginResponse = await auth.authenticate(
"solana",
walletAddress,
async (message, payload) => {
const messageBytes = new TextEncoder().encode(message);
const signatureBytes = ed25519.sign(
messageBytes,
walletKeypair.secretKey.slice(0, 32) // First 32 bytes are the private key
);
// Solana requires a specific signature format
return Buffer.from(
JSON.stringify({
input: payload,
output: {
signedMessage: Array.from(messageBytes),
signature: Array.from(signatureBytes),
account: {
publicKey: Array.from(walletKeypair.publicKey.toBytes()),
},
},
})
).toString("base64");
}
);
console.log("Access Token:", loginResponse.token);
// Sign a request
const payload = JSON.stringify({
symbol: "BTC-USD",
side: "buy",
order_type: "limit",
qty: "0.1",
price: "50000",
time_in_force: "gtc",
reduce_only: false,
});
const headers = auth.signRequest(payload, crypto.randomUUID(), Date.now());
// Make authenticated request
await fetch("https://perps.standx.com/api/new_order", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: \`Bearer ${loginResponse.token}\`,
...headers,
},
body: payload,
});
}
main().catch(console.error);
```
## Key Points
1. **Wallet Setup**: Uses `@solana/web3.js` Keypair with a base58-encoded private key
2. **Message Signing**: Uses `@noble/curves/ed25519` for Ed25519 signing with `walletKeypair.secretKey.slice(0, 32)` (first 32 bytes are the private key)
3. **Signature Format**: Solana requires a specific JSON structure containing:
- `input`: The original payload from the server
- `output.signedMessage`: The message bytes as an array
- `output.signature`: The signature bytes as an array
- `output.account.publicKey`: The wallets public key bytes as an array
This JSON is then base64-encoded before being sent to the server.
## Signature Format Explanation
Unlike EVM wallets that return a simple hex signature, Solana authentication requires a structured response:
```
{
input: payload, // Original SignedData from server
output: {
signedMessage: [...], // Message bytes as number array
signature: [...], // Ed25519 signature bytes as number array
account: {
publicKey: [...] // Wallet public key bytes as number array
}
}
}
```
This format allows the server to verify both the signature and the signing account.
## Environment Variables
Create a `.env` file with:
```
SOLANA_PRIVATE_KEY=your_base58_encoded_private_key_here
```
> **Security Note**: Never commit private keys to version control. Use environment variables or secure key management solutions.
[Perps Auth EVM Example](https://docs.standx.com/standx-api/perps-auth-evm-example "Perps Auth EVM Example") [Perps HTTP API](https://docs.standx.com/standx-api/perps-http "Perps HTTP API")
+79 -77
View File
@@ -38,7 +38,7 @@ bun install
--- ---
## 第三步:获取 StandX 登录凭证(最重要的一步) ## 第三步:获取 StandX API Token(最重要的一步)
> ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!** > ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!**
> >
@@ -47,56 +47,47 @@ bun install
> ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!** > ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!**
策略需要两样东西才能帮你下单: 策略需要两样东西才能帮你下单:
1. **TOKEN**登录令牌) 1. **TOKEN**API 令牌)
2. **代理钱包私钥**(用于签名交易) 2. **代理钱包私钥**(用于签名交易)
### 获取步骤(图文说明): ### 获取步骤(图文说明):
#### 3.1 打开专用登录页面 #### 3.1 打开 StandX 官方 API 创建页面
在浏览器打开这个网址: 在浏览器打开这个网址:
``` ```
https://standx.ritmex.one/ https://standx.com/user/session
``` ```
> **注意:不是 standx.com!是 standx.ritmex.one** > **现在可以直接在 StandX 官网创建 API Token 了**
#### 3.2 连接你的钱包并登录
如果还没登录,先连接钱包并登录你的 StandX 账户。
#### 3.3 生成 API Token
点击页面上的 **"Generate API Token"** 按钮。
你会看到类似这样的信息:
- **Token**(很长一串以 eyJ 开头的字符串)
- **Ed25519 Private Key**Base58 格式的私钥,类似 `HdsyJD7oWgT756124j3taSPGv...`
- **创建日期**(例如:2025-01-15
- **有效期天数**(例如:30 天)
> 🔴 **请把这些值复制保存下来!**
> >
> **注意:不是 standx.com!是 standx.ritmex.one** > 🔴 **请把这些值复制保存下来**
> >
> **注意:不是 standx.com!是 standx.ritmex.one** > 🔴 **请把这些值复制保存下来**
#### 3.2 连接你的钱包 ### 什么是 Ed25519 Private Key
点击页面上的 **"连接钱包"** 按钮,使用 MetaMask 或其他钱包连接。 - 这是系统 **自动为你生成** 的一个 Ed25519 签名私钥
-**只用于签名交易请求**,不存放你的资金
#### 3.3 点击登录
连接钱包后,点击 **"登录"** 按钮。钱包会弹出签名请求,确认签名。
#### 3.4 导出登录信息(关键!)
登录成功后,页面上会出现 **"导出登录信息"** 按钮,**点击它**
你会看到类似这样的内容:
```
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx...(很长一串)
代理钱包私钥: 1234567890abcdef...64 位十六进制字符)
```
> 🔴 **请把这两个值复制保存下来!**
>
> 🔴 **请把这两个值复制保存下来!**
>
> 🔴 **请把这两个值复制保存下来!**
### 什么是代理钱包?
- 代理钱包是系统 **自动为你生成** 的一个临时钱包
-**只用于签名交易**,不存放你的资金
- 你的资产仍然在你自己的钱包里,非常安全 - 你的资产仍然在你自己的钱包里,非常安全
- **你不需要手动创建**登录时系统会自动生成 - **你不需要手动创建**生成 API Token 时系统会自动创建
- 格式为 Base58 编码(类似 `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f`
--- ---
@@ -126,12 +117,12 @@ nano .env
# ===== 交易所设置 ===== # ===== 交易所设置 =====
EXCHANGE=standx EXCHANGE=standx
# ===== 你的登录凭证(第三步获取的) ===== # ===== 你的 API 凭证(第三步获取的) =====
# 把下面的 "你的TOKEN" 替换成你导出的 Token(很长一串以 eyJ 开头的) # 把下面的 "你的TOKEN" 替换成你生成的 Token(很长一串以 eyJ 开头的)
STANDX_TOKEN=你的TOKEN STANDX_TOKEN=你的TOKEN
# 把下面的 "你的私钥" 替换成页面中标注为 STANDX_REQUEST_PRIVATE_KEY 的那一段内容(按页面原样粘贴即可) # 把下面的 "你的私钥" 替换成页面中显示的代理钱包私钥(按页面原样粘贴即可)
STANDX_REQUEST_PRIVATE_KEY=你的 STANDX_REQUEST_PRIVATE_KEY STANDX_REQUEST_PRIVATE_KEY=你的代理钱包私钥
# ===== 交易品种 ===== # ===== 交易品种 =====
STANDX_SYMBOL=BTC-USD STANDX_SYMBOL=BTC-USD
@@ -147,13 +138,12 @@ MAKER_POINTS_BAND_0_10=true
MAKER_POINTS_BAND_10_30=true MAKER_POINTS_BAND_10_30=true
MAKER_POINTS_BAND_30_100=true MAKER_POINTS_BAND_30_100=true
# ===== Token 过期时间配置(可选 ===== # ===== Token 过期时间配置(推荐配置 =====
# 配置 Token 过期时间后,策略会在 Token 过期前提醒你,过期后自动进入安全模式 # 填写你创建 API Token 时显示的创建日期和有效期天数
# 格式1:时间戳(毫秒),例如:1735689600000 # 创建日期格式:YYYY-MM-DD(例如:2025-01-15
# 格式2:时间戳(秒),例如:1735689600(会自动转换为毫秒) STANDX_TOKEN_CREATE_DATE=2025-01-15
# 格式3:ISO 日期字符串,例如:2025-01-01T00:00:00Z # 有效期天数(例如:30
# 如果不配置此项,策略不会检查 Token 过期时间 STANDX_TOKEN_VALIDITY_DAYS=30
# STANDX_TOKEN_EXPIRY=1735689600000
# ===== Telegram 通知配置(可选) ===== # ===== Telegram 通知配置(可选) =====
# 配置后,策略会通过 Telegram 发送重要通知(订单成交、开仓、平仓、止损、Token过期等) # 配置后,策略会通过 Telegram 发送重要通知(订单成交、开仓、平仓、止损、Token过期等)
@@ -166,16 +156,18 @@ MAKER_POINTS_BAND_30_100=true
### 正确填写示例 ### 正确填写示例
假设你导出的信息是: 假设你生成的 API Token 信息是:
- Token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ` - Token: `eyJhbGciOiJFUzI1NiIsImtpZCI6IlhnaEJQSVNuN0RQVHlMcWJtLUVHVkVhOU1lMFpwdU9iMk1Qc2gtbUFlencifQ...`
- 代理钱包私钥: `abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890` - Ed25519 Private Key: `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f`
- 创建日期: `2025-01-15`
- 有效期: `30`
那么你的 `.env` 应该这样写: 那么你的 `.env` 应该这样写:
```bash ```bash
EXCHANGE=standx EXCHANGE=standx
STANDX_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ STANDX_TOKEN=eyJhbGciOiJFUzI1NiIsImtpZCI6IlhnaEJQSVNuN0RQVHlMcWJtLUVHVkVhOU1lMFpwdU9iMk1Qc2gtbUFlencifQ...
STANDX_REQUEST_PRIVATE_KEY=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 STANDX_REQUEST_PRIVATE_KEY=HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f
STANDX_SYMBOL=BTC-USD STANDX_SYMBOL=BTC-USD
MAKER_POINTS_ORDER_AMOUNT=0.01 MAKER_POINTS_ORDER_AMOUNT=0.01
MAKER_POINTS_CLOSE_THRESHOLD=0.1 MAKER_POINTS_CLOSE_THRESHOLD=0.1
@@ -184,7 +176,8 @@ MAKER_POINTS_MIN_REPRICE_BPS=3
MAKER_POINTS_BAND_0_10=true MAKER_POINTS_BAND_0_10=true
MAKER_POINTS_BAND_10_30=true MAKER_POINTS_BAND_10_30=true
MAKER_POINTS_BAND_30_100=true MAKER_POINTS_BAND_30_100=true
# STANDX_TOKEN_EXPIRY=1735689600000 STANDX_TOKEN_CREATE_DATE=2025-01-15
STANDX_TOKEN_VALIDITY_DAYS=30
# TELEGRAM_BOT_TOKEN=你的BotToken # TELEGRAM_BOT_TOKEN=你的BotToken
# TELEGRAM_CHAT_ID=你的ChatID # TELEGRAM_CHAT_ID=你的ChatID
``` ```
@@ -215,21 +208,22 @@ bun run pm2:start:maker-points
| 参数 | 含义 | 新手建议 | | 参数 | 含义 | 新手建议 |
|------|------|----------| |------|------|----------|
| `STANDX_TOKEN` | 登录令牌 | 必填,从第三步获取 | | `STANDX_TOKEN` | API 令牌 | 必填,从第三步获取 |
| `STANDX_REQUEST_PRIVATE_KEY` | 代理钱包私钥 | 必填,从第三步获取 | | `STANDX_REQUEST_PRIVATE_KEY` | 代理钱包私钥 | 必填,从第三步获取 |
| `STANDX_SYMBOL` | 交易品种 | 默认 `BTC-USD` | | `STANDX_SYMBOL` | 交易品种 | 默认 `BTC-USD` |
| `MAKER_POINTS_ORDER_AMOUNT` | 每笔挂单数量 | 建议 `0.01` 起步 | | `MAKER_POINTS_ORDER_AMOUNT` | 每笔挂单数量 | 建议 `0.01` 起步 |
| `MAKER_POINTS_CLOSE_THRESHOLD` | 持仓达到多少开始平仓 | 设为 `0` 表示不自动平仓 | | `MAKER_POINTS_CLOSE_THRESHOLD` | 持仓达到多少开始平仓 | 设为 `0` 表示不自动平仓 |
| `MAKER_POINTS_STOP_LOSS_USD` | 亏损多少美元强制平仓 | 设为 `0` 表示关闭止损 | | `MAKER_POINTS_STOP_LOSS_USD` | 亏损多少美元强制平仓 | 设为 `0` 表示关闭止损 |
| `MAKER_POINTS_BAND_*` | 三个挂单档位的开关 | 全部 `true` 即可 | | `MAKER_POINTS_BAND_*` | 三个挂单档位的开关 | 全部 `true` 即可 |
| `STANDX_TOKEN_EXPIRY` | Token 过期时间 | 可选,格式见下方说明 | | `STANDX_TOKEN_CREATE_DATE` | Token 创建日期 | 推荐配置,格式 YYYY-MM-DD |
| `STANDX_TOKEN_VALIDITY_DAYS` | Token 有效期天数 | 推荐配置,与创建日期配合使用 |
| `TELEGRAM_BOT_TOKEN` | Telegram 机器人 Token | 可选,用于接收通知 | | `TELEGRAM_BOT_TOKEN` | Telegram 机器人 Token | 可选,用于接收通知 |
| `TELEGRAM_CHAT_ID` | Telegram 聊天 ID | 可选,配合 Bot Token 使用 | | `TELEGRAM_CHAT_ID` | Telegram 聊天 ID | 可选,配合 Bot Token 使用 |
| `TELEGRAM_ACCOUNT_LABEL` | Telegram 通知账户标签 | 可选,用于区分多个账户 | | `TELEGRAM_ACCOUNT_LABEL` | Telegram 通知账户标签 | 可选,用于区分多个账户 |
### Token 过期时间配置详解 ### Token 过期时间配置详解
`STANDX_TOKEN_EXPIRY` 用于设置 Token 的过期时间。配置后,策略会: `STANDX_TOKEN_CREATE_DATE``STANDX_TOKEN_VALIDITY_DAYS` 用于设置 Token 的过期时间。配置后,策略会:
1. **Token 过期前 1 小时**:在日志中提醒你 Token 即将过期 1. **Token 过期前 1 小时**:在日志中提醒你 Token 即将过期
2. **Token 过期后** 2. **Token 过期后**
@@ -237,28 +231,35 @@ bun run pm2:start:maker-points
- 如果无持仓但有挂单:**自动取消所有挂单** - 如果无持仓但有挂单:**自动取消所有挂单**
- 如果无持仓无挂单:进入**静默模式**,只接收数据,不下单 - 如果无持仓无挂单:进入**静默模式**,只接收数据,不下单
**支持的格式** **推荐配置方式(创建日期 + 有效期天数)**
- **时间戳(毫秒)**`1735689600000`
- **时间戳(秒)**`1735689600`(会自动转换为毫秒)
- **ISO 日期字符串**`2025-01-01T00:00:00Z``2025-01-01 00:00:00`
**如何获取 Token 过期时间?** 在 StandX 官网生成 API Token 时,页面会显示创建日期和有效期天数,直接填入即可:
登录 standx.ritmex.one 时,系统会返回 Token 的有效期。你可以在导出登录信息时查看,或者根据登录时设置的过期时间计算。
**示例:**
```bash ```bash
# 方式1:使用时间戳(毫秒 # 创建日期(格式:YYYY-MM-DD
STANDX_TOKEN_EXPIRY=1735689600000 STANDX_TOKEN_CREATE_DATE=2025-01-15
# 有效期天数
STANDX_TOKEN_VALIDITY_DAYS=30
```
# 方式2:使用时间戳(秒) **示例计算:**
- 创建日期:2025-01-15
- 有效期:30 天
- 过期时间:2025-02-14 00:00:00 UTC
**兼容旧版配置(直接指定过期时间戳):**
如果你之前使用的是 `STANDX_TOKEN_EXPIRY`,仍然可以继续使用:
```bash
# 方式1:使用时间戳(秒)
STANDX_TOKEN_EXPIRY=1735689600 STANDX_TOKEN_EXPIRY=1735689600
# 方式3:使用 ISO 日期字符串 # 方式2:使用 ISO 日期字符串
STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z
``` ```
> 💡 **提示**如果不配置此项,策略不会检查 Token 过期时间,但建议配置以便及时收到提醒 > 💡 **提示**推荐使用新的创建日期 + 有效期天数方式,更直观易懂
### Telegram 通知配置详解 ### Telegram 通知配置详解
@@ -302,14 +303,15 @@ STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z
### Q:报错说 Token 无效怎么办? ### Q:报错说 Token 无效怎么办?
重新去 https://standx.ritmex.one/ 登录,重新导出 Token。Token 可能过期了。 重新去 https://standx.com/user/session 生成新的 API Token。Token 可能过期了。
### Q代理钱包私钥从哪来的? ### QEd25519 Private Key 从哪来的?
登录 standx.ritmex.one 后点击"导出登录信息"就能看到 在 StandX 官网(https://standx.com/user/session)点击 "Generate API Token" 按钮时会显示
**你不需要自己创建钱包,系统会自动生成!** 私钥格式为 Base58 编码(类似 `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f`)。
**你不需要自己创建钱包,系统会自动生成!** **你不需要自己创建,系统会自动生成!**
**你不需要自己创建钱包,系统会自动生成!** **你不需要自己创建,系统会自动生成!**
**你不需要自己创建,系统会自动生成!**
### Q.env 文件放在哪? ### Q.env 文件放在哪?
@@ -327,7 +329,7 @@ STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z
### Q:如何知道 Token 什么时候过期? ### Q:如何知道 Token 什么时候过期?
配置 `STANDX_TOKEN_EXPIRY` 环境变量,策略会在 Token 过期前 1 小时提醒你。Token 过期后,如果有持仓会进入平仓模式,只允许平仓和止损。 配置 `STANDX_TOKEN_CREATE_DATE`(创建日期)和 `STANDX_TOKEN_VALIDITY_DAYS`(有效期天数),策略会在 Token 过期前 1 小时提醒你。这两个值在生成 API Token 时会显示。Token 过期后,如果有持仓会进入平仓模式,只允许平仓和止损。
### QTelegram 通知收不到怎么办? ### QTelegram 通知收不到怎么办?
+35 -10
View File
@@ -10,22 +10,47 @@ export interface StandxTokenConfig {
expiryTimestamp: number | null; expiryTimestamp: number | null;
} }
function parseTimestamp(value: string | undefined): number | null { function parseTokenExpiry(): number | null {
if (!value || !value.trim()) return null; // Method 1: Use creation date + validity days (recommended for official API tokens)
const trimmed = value.trim(); const createDate = process.env.STANDX_TOKEN_CREATE_DATE?.trim();
const asNumber = Number(trimmed); const validityDays = process.env.STANDX_TOKEN_VALIDITY_DAYS?.trim();
if (Number.isFinite(asNumber) && asNumber > 0) {
return asNumber < 1e12 ? asNumber * 1000 : asNumber; if (createDate && validityDays) {
// Parse date in YYYY-MM-DD format
const dateMatch = createDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (dateMatch) {
const [, year, month, day] = dateMatch;
const createTimestamp = Date.UTC(
Number(year),
Number(month) - 1, // Month is 0-indexed
Number(day),
0, 0, 0, 0
);
const days = Number(validityDays);
if (Number.isFinite(createTimestamp) && Number.isFinite(days) && days > 0) {
return createTimestamp + days * 24 * 60 * 60 * 1000;
}
}
} }
const asDate = Date.parse(trimmed);
if (Number.isFinite(asDate) && asDate > 0) { // Method 2: Use legacy STANDX_TOKEN_EXPIRY (timestamp or ISO date string)
return asDate; const legacyExpiry = process.env.STANDX_TOKEN_EXPIRY?.trim();
if (legacyExpiry) {
const asNumber = Number(legacyExpiry);
if (Number.isFinite(asNumber) && asNumber > 0) {
return asNumber < 1e12 ? asNumber * 1000 : asNumber;
}
const asDate = Date.parse(legacyExpiry);
if (Number.isFinite(asDate) && asDate > 0) {
return asDate;
}
} }
return null; return null;
} }
export const standxTokenConfig: StandxTokenConfig = { export const standxTokenConfig: StandxTokenConfig = {
expiryTimestamp: parseTimestamp(process.env.STANDX_TOKEN_EXPIRY), expiryTimestamp: parseTokenExpiry(),
}; };
export function isStandxTokenExpired(): boolean { export function isStandxTokenExpired(): boolean {
+49 -4
View File
@@ -107,21 +107,66 @@ class StandxRequestSigner {
} }
} }
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function decodeBase58(input: string): Uint8Array | null {
try {
const bytes: number[] = [];
for (const char of input) {
const value = BASE58_ALPHABET.indexOf(char);
if (value === -1) return null;
let carry = value;
for (let i = 0; i < bytes.length; i += 1) {
const current = bytes[i] ?? 0;
carry += current * 58;
bytes[i] = carry & 0xff;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}
// Handle leading zeros
for (const char of input) {
if (char !== "1") break;
bytes.push(0);
}
return Uint8Array.from(bytes.reverse());
} catch {
return null;
}
}
function parseSigningKey(value?: string): Uint8Array | null { function parseSigningKey(value?: string): Uint8Array | null {
if (!value) return null; if (!value) return null;
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed) return null; if (!trimmed) return null;
// 0x-prefixed hex
if (/^0x[0-9a-fA-F]+$/.test(trimmed)) { if (/^0x[0-9a-fA-F]+$/.test(trimmed)) {
return Uint8Array.from(Buffer.from(trimmed.slice(2), "hex")); return Uint8Array.from(Buffer.from(trimmed.slice(2), "hex"));
} }
// Pure hex (64 chars = 32 bytes for ed25519 private key)
if (/^[0-9a-fA-F]+$/.test(trimmed)) { if (/^[0-9a-fA-F]+$/.test(trimmed)) {
return Uint8Array.from(Buffer.from(trimmed, "hex")); return Uint8Array.from(Buffer.from(trimmed, "hex"));
} }
try { // Base58 (official StandX API format)
return Uint8Array.from(Buffer.from(trimmed, "base64")); if (/^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed)) {
} catch { const decoded = decodeBase58(trimmed);
return null; if (decoded && decoded.length === 32) {
return decoded;
}
} }
// Base64 fallback
try {
const decoded = Uint8Array.from(Buffer.from(trimmed, "base64"));
if (decoded.length === 32) {
return decoded;
}
} catch {
// ignore
}
return null;
} }
function normalizeSymbol(raw: string): string { function normalizeSymbol(raw: string): string {