diff --git a/.env.example b/.env.example index 33cadfc..84d7b8c 100644 --- a/.env.example +++ b/.env.example @@ -14,12 +14,14 @@ STANDX_SYMBOL=BTC-USD # STANDX_BASE_URL=https://perps.standx.com # STANDX_WS_URL=wss://perps.standx.com/ws-stream/v1 # 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= -# Token expiry timestamp (Unix seconds). When expired: cancels all orders, stops new orders. -# If position exists, continues close/stop-loss logic. If no position/orders, enters silent mode. -# Example: STANDX_TOKEN_EXPIRY=1737092800 -# STANDX_TOKEN_EXPIRY= +# Token expiry configuration (recommended method: creation date + validity days) +# Get these values when generating API token at https://standx.com/user/session +# STANDX_TOKEN_CREATE_DATE=2025-01-15 # Token creation date (YYYY-MM-DD format) +# 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 TRADE_SYMBOL=BTCUSDT # Trading pair symbol diff --git a/docs/standx/auth.md b/docs/standx/auth.md index 32899ce..d5b13e2 100644 --- a/docs/standx/auth.md +++ b/docs/standx/auth.md @@ -1,5 +1,9 @@ ## StandX Perps Authentication +官网创建的 API 提供了 + +API Token 以及 Ed25519 Private Key,用于签名交易。 + ⚠️ This document is under construction. 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`. -#### TypeScript/ES6 Implementation Reference +#### BSC (EVM) Implementation Reference ``` import { ethers } from "ethers"; @@ -84,6 +88,38 @@ const wallet = new ethers.Wallet(privateKey, provider); 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 = ""; // 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 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 -Here’s 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 { base58 } from "@scure/base"; +import { ethers } from "ethers"; +// Types export type Chain = "bsc" | "solana"; export interface SignedData { @@ -260,6 +320,7 @@ export interface RequestSignatureHeaders { "x-request-signature": string; } +// Authentication Class export class StandXAuth { private ed25519PrivateKey: Uint8Array; private ed25519PublicKey: Uint8Array; @@ -343,9 +404,7 @@ export class StandXAuth { } // Usage Example -import { ethers } from "ethers"; - -async function example() { +async function main() { // Initialize auth const auth = new StandXAuth(); @@ -389,8 +448,270 @@ async function example() { 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") \ No newline at end of file +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 + ): Promise { + const signedDataJwt = await this.prepareSignIn(chain, walletAddress); + const payload = this.parseJwt(signedDataJwt); + const signature = await signMessage(payload.message, payload); + return this.login(chain, signature, signedDataJwt); + } + + private async prepareSignIn(chain: Chain, address: string): Promise { + 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 { + 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(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 wallet’s 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") \ No newline at end of file diff --git a/docs/standx/maker-points-guide.md b/docs/standx/maker-points-guide.md index 9090a89..427e4e1 100644 --- a/docs/standx/maker-points-guide.md +++ b/docs/standx/maker-points-guide.md @@ -38,7 +38,7 @@ bun install --- -## 第三步:获取 StandX 登录凭证(最重要的一步) +## 第三步:获取 StandX API Token(最重要的一步) > ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!** > @@ -47,56 +47,47 @@ bun install > ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!** 策略需要两样东西才能帮你下单: -1. **TOKEN**(登录令牌) +1. **TOKEN**(API 令牌) 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 或其他钱包连接。 - -#### 3.3 点击登录 - -连接钱包后,点击 **"登录"** 按钮。钱包会弹出签名请求,确认签名。 - -#### 3.4 导出登录信息(关键!) - -登录成功后,页面上会出现 **"导出登录信息"** 按钮,**点击它**! - -你会看到类似这样的内容: - -``` -Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx...(很长一串) - -代理钱包私钥: 1234567890abcdef...(64 位十六进制字符) -``` - -> 🔴 **请把这两个值复制保存下来!** -> -> 🔴 **请把这两个值复制保存下来!** -> -> 🔴 **请把这两个值复制保存下来!** - -### 什么是代理钱包? - -- 代理钱包是系统 **自动为你生成** 的一个临时钱包 -- 它 **只用于签名交易**,不存放你的资金 +- 这是系统 **自动为你生成** 的一个 Ed25519 签名私钥 +- 它 **只用于签名交易请求**,不存放你的资金 - 你的资产仍然在你自己的钱包里,非常安全 -- **你不需要手动创建**,登录时系统会自动生成 +- **你不需要手动创建**,生成 API Token 时系统会自动创建 +- 格式为 Base58 编码(类似 `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f`) --- @@ -126,12 +117,12 @@ nano .env # ===== 交易所设置 ===== EXCHANGE=standx -# ===== 你的登录凭证(第三步获取的) ===== -# 把下面的 "你的TOKEN" 替换成你导出的 Token(很长一串以 eyJ 开头的) +# ===== 你的 API 凭证(第三步获取的) ===== +# 把下面的 "你的TOKEN" 替换成你生成的 Token(很长一串以 eyJ 开头的) STANDX_TOKEN=你的TOKEN -# 把下面的 "你的私钥" 替换成页面中标注为 STANDX_REQUEST_PRIVATE_KEY 的那一段内容(按页面原样粘贴即可) -STANDX_REQUEST_PRIVATE_KEY=你的 STANDX_REQUEST_PRIVATE_KEY +# 把下面的 "你的私钥" 替换成页面中显示的代理钱包私钥(按页面原样粘贴即可) +STANDX_REQUEST_PRIVATE_KEY=你的代理钱包私钥 # ===== 交易品种 ===== STANDX_SYMBOL=BTC-USD @@ -147,13 +138,12 @@ MAKER_POINTS_BAND_0_10=true MAKER_POINTS_BAND_10_30=true MAKER_POINTS_BAND_30_100=true -# ===== Token 过期时间配置(可选) ===== -# 配置 Token 过期时间后,策略会在 Token 过期前提醒你,过期后自动进入安全模式 -# 格式1:时间戳(毫秒),例如:1735689600000 -# 格式2:时间戳(秒),例如:1735689600(会自动转换为毫秒) -# 格式3:ISO 日期字符串,例如:2025-01-01T00:00:00Z -# 如果不配置此项,策略不会检查 Token 过期时间 -# STANDX_TOKEN_EXPIRY=1735689600000 +# ===== Token 过期时间配置(推荐配置) ===== +# 填写你创建 API Token 时显示的创建日期和有效期天数 +# 创建日期格式:YYYY-MM-DD(例如:2025-01-15) +STANDX_TOKEN_CREATE_DATE=2025-01-15 +# 有效期天数(例如:30) +STANDX_TOKEN_VALIDITY_DAYS=30 # ===== Telegram 通知配置(可选) ===== # 配置后,策略会通过 Telegram 发送重要通知(订单成交、开仓、平仓、止损、Token过期等) @@ -166,16 +156,18 @@ MAKER_POINTS_BAND_30_100=true ### 正确填写示例 -假设你导出的信息是: -- Token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ` -- 代理钱包私钥: `abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890` +假设你生成的 API Token 信息是: +- Token: `eyJhbGciOiJFUzI1NiIsImtpZCI6IlhnaEJQSVNuN0RQVHlMcWJtLUVHVkVhOU1lMFpwdU9iMk1Qc2gtbUFlencifQ...` +- Ed25519 Private Key: `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f` +- 创建日期: `2025-01-15` +- 有效期: `30` 天 那么你的 `.env` 应该这样写: ```bash EXCHANGE=standx -STANDX_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ -STANDX_REQUEST_PRIVATE_KEY=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 +STANDX_TOKEN=eyJhbGciOiJFUzI1NiIsImtpZCI6IlhnaEJQSVNuN0RQVHlMcWJtLUVHVkVhOU1lMFpwdU9iMk1Qc2gtbUFlencifQ... +STANDX_REQUEST_PRIVATE_KEY=HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f STANDX_SYMBOL=BTC-USD MAKER_POINTS_ORDER_AMOUNT=0.01 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_10_30=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_CHAT_ID=你的ChatID ``` @@ -215,21 +208,22 @@ bun run pm2:start:maker-points | 参数 | 含义 | 新手建议 | |------|------|----------| -| `STANDX_TOKEN` | 登录令牌 | 必填,从第三步获取 | +| `STANDX_TOKEN` | API 令牌 | 必填,从第三步获取 | | `STANDX_REQUEST_PRIVATE_KEY` | 代理钱包私钥 | 必填,从第三步获取 | | `STANDX_SYMBOL` | 交易品种 | 默认 `BTC-USD` | | `MAKER_POINTS_ORDER_AMOUNT` | 每笔挂单数量 | 建议 `0.01` 起步 | | `MAKER_POINTS_CLOSE_THRESHOLD` | 持仓达到多少开始平仓 | 设为 `0` 表示不自动平仓 | | `MAKER_POINTS_STOP_LOSS_USD` | 亏损多少美元强制平仓 | 设为 `0` 表示关闭止损 | | `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_CHAT_ID` | Telegram 聊天 ID | 可选,配合 Bot Token 使用 | | `TELEGRAM_ACCOUNT_LABEL` | Telegram 通知账户标签 | 可选,用于区分多个账户 | ### Token 过期时间配置详解 -`STANDX_TOKEN_EXPIRY` 用于设置 Token 的过期时间。配置后,策略会: +`STANDX_TOKEN_CREATE_DATE` 和 `STANDX_TOKEN_VALIDITY_DAYS` 用于设置 Token 的过期时间。配置后,策略会: 1. **Token 过期前 1 小时**:在日志中提醒你 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 -# 方式1:使用时间戳(毫秒) -STANDX_TOKEN_EXPIRY=1735689600000 +# 创建日期(格式:YYYY-MM-DD) +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 -# 方式3:使用 ISO 日期字符串 +# 方式2:使用 ISO 日期字符串 STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z ``` -> 💡 **提示**:如果不配置此项,策略不会检查 Token 过期时间,但建议配置以便及时收到提醒。 +> 💡 **提示**:推荐使用新的创建日期 + 有效期天数方式,更直观易懂。 ### Telegram 通知配置详解 @@ -302,14 +303,15 @@ STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z ### Q:报错说 Token 无效怎么办? -重新去 https://standx.ritmex.one/ 登录,重新导出 Token。Token 可能过期了。 +重新去 https://standx.com/user/session 生成新的 API Token。Token 可能过期了。 -### Q:代理钱包私钥从哪来的? +### Q:Ed25519 Private Key 从哪来的? -登录 standx.ritmex.one 后点击"导出登录信息"就能看到。 -**你不需要自己创建钱包,系统会自动生成!** -**你不需要自己创建钱包,系统会自动生成!** -**你不需要自己创建钱包,系统会自动生成!** +在 StandX 官网(https://standx.com/user/session)点击 "Generate API Token" 按钮时会显示。 +私钥格式为 Base58 编码(类似 `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f`)。 +**你不需要自己创建,系统会自动生成!** +**你不需要自己创建,系统会自动生成!** +**你不需要自己创建,系统会自动生成!** ### Q:.env 文件放在哪? @@ -327,7 +329,7 @@ STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z ### Q:如何知道 Token 什么时候过期? -配置 `STANDX_TOKEN_EXPIRY` 环境变量,策略会在 Token 过期前 1 小时提醒你。Token 过期后,如果有持仓会进入平仓模式,只允许平仓和止损。 +配置 `STANDX_TOKEN_CREATE_DATE`(创建日期)和 `STANDX_TOKEN_VALIDITY_DAYS`(有效期天数),策略会在 Token 过期前 1 小时提醒你。这两个值在生成 API Token 时会显示。Token 过期后,如果有持仓会进入平仓模式,只允许平仓和止损。 ### Q:Telegram 通知收不到怎么办? diff --git a/src/config.ts b/src/config.ts index 671d92c..16db41b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,22 +10,47 @@ export interface StandxTokenConfig { expiryTimestamp: number | null; } -function parseTimestamp(value: string | undefined): number | null { - if (!value || !value.trim()) return null; - const trimmed = value.trim(); - const asNumber = Number(trimmed); - if (Number.isFinite(asNumber) && asNumber > 0) { - return asNumber < 1e12 ? asNumber * 1000 : asNumber; +function parseTokenExpiry(): number | null { + // Method 1: Use creation date + validity days (recommended for official API tokens) + const createDate = process.env.STANDX_TOKEN_CREATE_DATE?.trim(); + const validityDays = process.env.STANDX_TOKEN_VALIDITY_DAYS?.trim(); + + 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) { - return asDate; + + // Method 2: Use legacy STANDX_TOKEN_EXPIRY (timestamp or ISO date string) + 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; } export const standxTokenConfig: StandxTokenConfig = { - expiryTimestamp: parseTimestamp(process.env.STANDX_TOKEN_EXPIRY), + expiryTimestamp: parseTokenExpiry(), }; export function isStandxTokenExpired(): boolean { diff --git a/src/exchanges/standx/gateway.ts b/src/exchanges/standx/gateway.ts index ba11d56..0bc6a47 100644 --- a/src/exchanges/standx/gateway.ts +++ b/src/exchanges/standx/gateway.ts @@ -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 { if (!value) return null; const trimmed = value.trim(); if (!trimmed) return null; + // 0x-prefixed hex if (/^0x[0-9a-fA-F]+$/.test(trimmed)) { 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)) { return Uint8Array.from(Buffer.from(trimmed, "hex")); } - try { - return Uint8Array.from(Buffer.from(trimmed, "base64")); - } catch { - return null; + // Base58 (official StandX API format) + if (/^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed)) { + const decoded = decodeBase58(trimmed); + 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 {