mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
Integrate StandX exchange support by updating configuration files, adding environment variables, and enhancing documentation. Include new API endpoints and authentication details for StandX in README and dedicated documentation files. Update CLI and adapter logic to accommodate StandX functionalities.
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
## StandX Perps Authentication
|
||||
|
||||
⚠️ 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 StandX’s 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`.
|
||||
|
||||
#### TypeScript/ES6 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);
|
||||
```
|
||||
|
||||
### 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 Class Example
|
||||
|
||||
Here’s a complete implementation using a class-based approach:
|
||||
|
||||
```
|
||||
import { ed25519 } from "@noble/curves/ed25519";
|
||||
import { base58 } from "@scure/base";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
import { ethers } from "ethers";
|
||||
|
||||
async function example() {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Last updated on
|
||||
|
||||
[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")
|
||||
Reference in New Issue
Block a user