Files
discountry 86670486a6 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.
2026-01-15 16:11:08 +08:00

717 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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.
## Prerequisites
- Valid wallet address and corresponding private key
- Development environment with `ed25519` algorithm support
## Authentication Flow
### 1\. Prepare Wallet and Temporary ed25519 Key Pair
1. **Prepare Wallet**: Ensure you have a blockchain wallet with its address and private key.
2. **Generate Temporary ed25519 Key Pair and `requestId`**
### 2\. Get Signature Data
Request signature data from the server:
> **Note**: Code examples provided below are for reference purposes only and demonstrate the general implementation approach. Adapt them to your specific production environment.
#### Using curl
```
curl 'https://api.standx.com/v1/offchain/prepare-signin?chain=<chain>' \
-H 'Content-Type: application/json' \
--data-raw '{
"address": "<your_wallet_address>",
"requestId": "<base58_encoded_public_key>"
}'
```
#### TypeScript/ES6 Implementation Reference
#### Request Parameters
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| chain | string | Yes | Blockchain network: `bsc` or `solana` |
| address | string | Yes | Wallet address |
| requestId | string | Yes | Base58-encoded ed25519 public key from step 1 |
#### Success Response
```
{
"success": true,
"signedData": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### 3\. Parse and Verify Signature Data
`signedData` is a JWT string that must be verified using StandXs public key.
#### Get Verification Public Key
```
# Using curl
curl 'https://api.standx.com/v1/offchain/certs'
```
#### Example signedData Payload
### 4\. Sign the Message
Sign `payload.message` with your wallet private key to generate the `signature`.
#### BSC (EVM) Implementation Reference
```
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(
"https://bsc-dataseed.binance.org/"
);
const privateKey = "<your_wallet_private_key>"; // Keep secure; use environment variables
const wallet = new ethers.Wallet(privateKey, provider);
// Sign using the message from the parsed payload
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
Submit the `signature` and original `signedData` to the login endpoint.
**Optional Parameter:**
- `expiresSeconds` (number): Token expiration time in seconds. Defaults to `604800` (7 days) if not specified. This controls how long the JWT access token remains valid before requiring re-authentication.
> **Security Note**: For security best practices, avoid setting excessively long expiration times. Shorter token lifetimes reduce the risk of unauthorized access if a token is compromised. Consider your security requirements when configuring this value.
#### Using curl
#### TypeScript/ES6 Implementation Reference
#### Success Response
```
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"address": "0x...",
"alias": "user123",
"chain": "bsc",
"perpsAlpha": true
}
```
### 6\. Use Access Token
Use the obtained `token` for subsequent API requests by adding `Authorization: Bearer <token>` to the request headers.
## Body Signature Flow
### Basic Flow
1. Prepare a key pair
2. Build message: `{version},{id},{timestamp},{payload}`
3. Sign with private key
4. Base64 encode signature
5. Attach signature to request headers
```
{
...
"authorization": "Bearer <token>",
"x-request-sign-version": "v1",
"x-request-id": "uuid",
"x-request-timestamp": "timestamp",
"x-request-signature": "signature",
...
}
```
### Code example (only for reference):
```
import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base";
import { v4 as uuidv4 } from "uuid";
/**
* Sign request and return Base64-encoded signature.
*/
function encodeRequestSignature(
xRequestVersion: string,
xRequestId: string,
xRequestTimestamp: number,
payload: string,
signingKey: Uint8Array
): string {
// Build message to sign: "{version},{id},{timestamp},{payload}"
const signMsg = \`${xRequestVersion},${xRequestId},${xRequestTimestamp},${payload}\`;
// Sign message with Ed25519 private key
const messageBytes = Buffer.from(signMsg, "utf-8");
const signature = ed25519.sign(messageBytes, signingKey);
// Base64 encode the signature
return Buffer.from(signature).toString("base64");
}
// --- Example Usage ---
// Generate Ed25519 key pair
const privateKey = ed25519.utils.randomSecretKey();
const publicKey = ed25519.getPublicKey(privateKey);
// Generate requestId (base58-encoded public key)
const requestId = base58.encode(publicKey);
// Prepare request parameters
const xRequestVersion = "v1";
const xRequestId = uuidv4();
const xRequestTimestamp = Date.now();
const payloadDict = {
user_id: 12345,
data: "some important information",
};
const payloadStr = JSON.stringify(payloadDict);
// Generate signature
const signature = encodeRequestSignature(
xRequestVersion,
xRequestId,
xRequestTimestamp,
payloadStr,
privateKey
);
// Verify signature (optional)
try {
const verifyMsg = \`v1,${xRequestId},${xRequestTimestamp},${payloadStr}\`;
const signatureBytes = Buffer.from(signature, "base64");
const messageBytes = Buffer.from(verifyMsg, "utf-8");
const isValid = ed25519.verify(signatureBytes, messageBytes, publicKey);
if (!isValid) throw new Error("Verification failed");
} catch (error) {
console.error("Signature verification error:", error.message);
}
// Send Request with Body Signature
fetch("/api/request_need_body_signature", {
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: \`Bearer ${token}\`,
"x-request-sign-version": "v1",
"x-request-id": xRequestId,
"x-request-timestamp": xRequestTimestamp.toString(),
"x-request-signature": signature,
},
body: payloadStr,
});
```
### Complete Authentication Examples
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 {
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) => Promise<string>
): Promise<LoginResponse> {
const signedDataJwt = await this.prepareSignIn(chain, walletAddress);
const payload = this.parseJwt<SignedData>(signedDataJwt);
const signature = await signMessage(payload.message);
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
const provider = new ethers.JsonRpcProvider(
"https://bsc-dataseed.binance.org/"
);
const privateKey = process.env.WALLET_PRIVATE_KEY!;
const wallet = new ethers.Wallet(privateKey, provider);
// Authenticate
const loginResponse = await auth.authenticate(
"bsc",
wallet.address,
async (message) => wallet.signMessage(message)
);
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 `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")