关于 orderly-api-authentication
面向 Orderly Network 开发者的认证技能,说明 EVM 与 Solana 钱包注册、API 密钥管理及请求签名流程,涵盖两层认证机制。
主要功能
- 01EVM 钱包的 EIP-712 签名
- 02Solana 钱包消息签名
- 03Ed25519 API 请求认证
- 04账户注册与 API 密钥管理
- 05读取、交易和资产权限范围
适用场景
- 01为 EVM 或 Solana 用户注册 Orderly 账户
- 02为交易机器人配置 API 密钥
- 03实现直接 API 调用的签名认证
- 04排查钱包或请求签名问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ orderlynetwork/ skills --skill orderly-api-authentication安装后在 Claude Code 中输入/orderly-api-authentication确认已加载。
详细介绍
Orderly Network:API 身份验证
此技能涵盖 Orderly Network 中的两个身份验证层:用于账户注册和密钥管理的钱包签名(EVM 使用 EIP-712,Solana 使用 Ed25519 消息签名),以及用于 API 请求身份验证的 Ed25519 签名。
适用场景
- 设置新的 Orderly 账户和 API 密钥(EVM 或 Solana)
- 构建服务端交易机器人
- 实现直接 API 调用
- 了解双层身份验证流程
- 调试签名问题
前提条件
- Web3 钱包(EVM 使用 MetaMask、WalletConnect;Solana 使用 Phantom、Solflare)
- Broker ID(例如
woofi_dex,或你自己的 ID) - 已安装 Node.js 18+(用于编程方式操作)
- 了解 EIP-712 类型化数据签名(EVM)、Ed25519 消息签名(Solana)以及 Ed25519 密码学
身份验证概览
Orderly Network 使用支持 EVM 和 Solana 钱包的双层身份验证系统:
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Wallet Authentication │
│ ───────────────────────────── │
│ • Account registration │
│ • API key management (add/remove keys) │
│ • Privileged operations (withdrawals, admin) │
│ │
│ EVM: EIP-712 typed data signing │
│ Solana: Ed25519 message signing │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Layer 2: API Authentication (Ed25519) │
│ ───────────────────────────────────── │
│ • Trading operations (place/cancel orders) │
│ • Reading account data (positions, balances) │
│ • WebSocket connections │
│ │
│ Signed by: Ed25519 key pair │
│ Key type: Locally-generated Ed25519 key pair │
└─────────────────────────────────────────────────────────────┘
身份验证流程
1. User connects wallet
2. Wallet signs EIP-712 message to register account
3. Account ID is created
4. User generates Ed25519 key pair
5. Wallet signs EIP-712 message to authorize the Ed25519 key
6. Ed25519 key is used for all subsequent API calls
环境配置
| 环境 | API 基础 URL | WebSocket URL |
|---|---|---|
| 主网 | https://api.orderly.org | wss://ws.orderly.org/ws/stream |
| 测试网 | https://testnet-api.orderly.org | wss://testnet-ws.orderly.org/ws/stream |
注意:这些 API 基础 URL 同时适用于 EVM 和 Solana 钱包。Orderly 的 API 是全链通用的——相同的端点可处理这两条链。
获取支持的链
不要将链 ID 硬编码。请动态获取适用于你的 broker 的链:
// Get supported chains for your broker
const response = await fetch(`https://api.orderly.org/v1/public/chain_info?broker_id=${BROKER_ID}`);
const { data } = await response.json();
// data.chains contains supported chain_ids
// Use these chain IDs for EIP-712 domain configuration
EIP-712 域配置
Orderly 根据操作类型使用两种不同的 EIP-712 域:
| 域类型 | 使用场景 | 主网 | 测试网 |
|---|---|---|---|
| 链下 | 账户注册、API 密钥管理 | 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC | 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC |
| 链上 | 提现、内部转账、结算 PnL | 0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203 | 0x1826B75e2ef249173FC735149AE4B8e9ea10abff |
重要:链上
verifyingContract是 Orderly L2 上的 Ledger 合约。所有链共用此合约(而非每条链各自一个)。每条受支持的 EVM 链上都有用于存款的 Vault 合约,但 Ledger 是链上操作的事实来源。
链下域(注册、API 密钥)
用于不直接与智能合约交互的操作:
const OFFCHAIN_DOMAIN = {
name: 'Orderly',
version: '1',
chainId: 421614, // Connected chain ID (e.g., Arbitrum Sepolia)
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
};
链上域(提现、转账)
用于与 Orderly L2 上的 Ledger 合约交互的操作:
const ONCHAIN_DOMAIN = {
name: 'Orderly',
version: '1',
chainId: 42161, // Connected chain ID
verifyingContract: isTestnet
? '0x1826B75e2ef249173FC735149AE4B8e9ea10abff'
: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203',
};
第 1 部分:EIP-712 钱包身份验证
账户级操作需要通过钱包身份验证来证明所有权。
何时使用 EIP-712
- 账户注册:创建新的 Orderly 账户
- API 密钥管理:添加或移除 Ed25519 API 密钥
- 提现:请求从金库提取代币
- 管理操作:设置 IP 限制、管理账户设置
账户注册
步骤 1:检查现有账户
注册前,确认该钱包是否已有账户:
const BROKER_ID = 'woofi_dex'; // Your broker ID
const walletAddress = '0x...'; // User's wallet address
const response = await fetch(
`https://testnet-api.orderly.org/v1/get_account?broker_id=${BROKER_ID}&user_address=${walletAddress}`
);
const data = await response.json();
// If data.success is true, account already exists
// If not, proceed with registration
步骤 2:获取注册 Nonce
获取注册所需的唯一 nonce(有效期为 2 分钟):
const nonceResponse = await fetch('https://testnet-api.orderly.org/v1/registration_nonce');
const { data: nonce } = await nonceResponse.json();
console.log('Registration nonce:', nonce);
步骤 3:签署注册消息
创建并签署 EIP-712 类型化消息:
// Registration Message Type
const REGISTRATION_TYPES = {
Registration: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'registrationNonce', type: 'uint256' },
],
};
// Create the message
const registerMessage = {
brokerId: BROKER_ID,
chainId: 421614,
timestamp: Date.now(),
registrationNonce: nonce,
};
// Sign with wallet (e.g., MetaMask) - Use OFFCHAIN_DOMAIN for registration
const signature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [
walletAddress,
{
types: REGISTRATION_TYPES,
domain: OFFCHAIN_DOMAIN,
message: registerMessage,
primaryType: 'Registration',
},
],
});
步骤 4:提交注册
发送签名后的载荷以创建 Orderly Account ID:
const registerResponse = await fetch('https://testnet-api.orderly.org/v1/register_account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: registerMessage,
signature: signature,
userAddress: walletAddress,
}),
});
const result = await registerResponse.json();
console.log('Account ID:', result.data.account_id);
// Store this account ID - you'll need it for API authentication
API 密钥管理(Orderly Key)
创建账户后,需要注册 Ed25519 密钥以访问 API。
生成 Ed25519 密钥对
import { getPublicKeyAsync, utils } from '@noble/ed25519';
// Generate 32-byte private key (cryptographically secure)
const privateKey = utils.randomPrivateKey();
// Derive public key
const publicKey = await getPublicKeyAsync(privateKey);
// Encode public key as base58 (required by Orderly)
function encodeBase58(bytes: Uint8Array): string {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let result = '';
let num = 0n;
for (const byte of bytes) {
num = num * 256n + BigInt(byte);
}
while (num > 0n) {
result = ALPHABET[Number(num % 58n)] + result;
num = num / 58n;
}
return result;
}
const orderlyKey = `ed25519:${encodeBase58(publicKey)}`;
// Convert bytes to hex string for storage
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
console.log('Orderly Key:', orderlyKey);
console.log('Private Key (hex):', bytesToHex(privateKey));
// STORE PRIVATE KEY SECURELY - NEVER SHARE IT
签署添加 Orderly 密钥消息
通过 EIP-712 将 Ed25519 密钥与您的账户关联:
const ADD_KEY_TYPES = {
AddOrderlyKey: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'orderlyKey', type: 'string' },
{ name: 'scope', type: 'string' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'expiration', type: 'uint64' },
],
};
const addKeyMessage = {
brokerId: BROKER_ID,
chainId: 421614,
orderlyKey: orderlyKey,
scope: 'read,trading', // Permissions: read, trading, asset (comma-separated)
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year from now
};
// Use OFFCHAIN_DOMAIN for API key management
const addKeySignature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [
walletAddress,
{
types: ADD_KEY_TYPES,
domain: OFFCHAIN_DOMAIN,
message: addKeyMessage,
primaryType: 'AddOrderlyKey',
},
],
});
提交 Orderly 密钥
注册 API 密钥:
const keyResponse = await fetch('https://testnet-api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: addKeyMessage,
signature: addKeySignature,
userAddress: walletAddress,
}),
});
const keyResult = await keyResponse.json();
console.log('Key registered:', keyResult.success);
Orderly 密钥权限范围
注册 API 密钥时,请指定权限:
| 权限范围 | 权限 |
|---|---|
read | 读取持仓、订单和余额 |
trading | 下单、取消和修改订单 |
asset | 存款、提现和内部转账 |
多个权限范围可以用逗号分隔并组合:'read,trading,asset'
移除 Orderly 密钥
要移除密钥(需要使用另一个有效密钥进行 Ed25519 身份验证):
// POST /v1/client/remove_orderly_key
const removeResponse = await signAndSendRequest(
accountId,
privateKey, // Must be a different valid key
'https://api.orderly.org/v1/client/remove_orderly_key',
{
method: 'POST',
body: JSON.stringify({
orderly_key: 'ed25519:...', // Key to remove
}),
}
);
Solana 钱包身份验证
Solana 钱包在账户操作中使用原生 Ed25519 消息签名(而非 EIP-712)。Solana 钱包本身已原生使用 Ed25519 密钥,因此签名过程更简单,但需要采用不同的消息格式。
Solana 与 EVM 身份验证对比
| 方面 | EVM 钱包 | Solana 钱包 |
|---|---|---|
| 签名方式 | EIP-712 类型化数据 | 明文消息签名 |
| 密钥类型 | secp256k1 | Ed25519(原生) |
| 账户查询 | /v1/get_account | /v1/get_account?chain_type=SOL |
| 消息格式 | 结构化 JSON 类型 | 通过适配器传递原始字节 |
| 签名 | Ethereum 签名 | Ed25519 签名 |
账户查询
检查 Solana 钱包是否已有 Orderly 账户:
import { PublicKey } from '@solana/web3.js';
const BROKER_ID = 'woofi_dex';
const solanaAddress = '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU'; // Base58 address
// Solana accounts require chain_type=SOL parameter
const response = await fetch(
`https://testnet-api.orderly.org/v1/get_account?` +
`address=${solanaAddress}&` +
`broker_id=${BROKER_ID}&` +
`chain_type=SOL`
);
const data = await response.json();
// data.data.account_id contains the Orderly account ID
// Account ID format is different from EVM (not a keccak256 hash)
使用 Solana 适配器签署消息
Orderly 提供 Solana 适配器,用于生成格式正确的消息:
import { DefaultSolanaWalletAdapter } from '@orderly.network/default-solana-adapter';
import { Connection, clusterApiUrl, Keypair } from '@solana/web3.js';
import { signAsync } from '@noble/ed25519';
import bs58 from 'bs58';
// Setup wallet adapter
const walletAdapter = new DefaultSolanaWalletAdapter();
// Initialize with wallet details
walletAdapter.active({
address: solanaAddress,
provider: {
connection: new Connection(clusterApiUrl('devnet')), // or 'mainnet-beta'
signMessage: async (msg: Uint8Array) => {
// Sign with Solana wallet (Ed25519)
return await signAsync(msg, privateKeyBytes.slice(0, 32));
},
sendTransaction: async (tx, conn) => {
tx.sign([senderKeypair]);
return conn.sendTransaction(tx);
},
},
chain: {
id: network === 'mainnet' ? 900900900 : 901901901, // Solana chain IDs
},
});
注册流程
步骤 1:获取注册 nonce
const nonceResponse = await fetch('https://testnet-api.orderly.org/v1/registration_nonce');
const { data: nonce } = await nonceResponse.json();
步骤 2:生成并签署注册消息
// Generate registration message using adapter
const registerMessage = await walletAdapter.generateRegisterMessage({
brokerId: BROKER_ID,
timestamp: Date.now(),
registrationNonce: nonce,
});
// Sign with Solana wallet (raw message bytes, not EIP-712)
const signature = await wallet.signMessage(registerMessage.message);
步骤 3:提交注册
const registerResponse = await fetch('https://testnet-api.orderly.org/v1/register_account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: registerMessage.message,
signature: signature,
userAddress: solanaAddress,
chainType: 'SOL', // Required for Solana
}),
});
const result = await registerResponse.json();
console.log('Account ID:', result.data.account_id);
API 密钥管理(Orderly 密钥)
生成 Ed25519 密钥对
与 EVM 相同:在本地生成 Ed25519 密钥对:
import { getPublicKeyAsync, utils } from '@noble/ed25519';
import bs58 from 'bs58';
// Generate key pair
const privateKey = utils.randomPrivateKey();
const publicKey = await getPublicKeyAsync(privateKey);
const orderlyKey = `ed25519:${bs58.encode(publicKey)}`;
签署添加 Orderly 密钥消息
// Generate add key message using adapter
const addKeyMessage = await walletAdapter.generateAddKeyMessage({
brokerId: BROKER_ID,
orderlyKey: orderlyKey,
scope: 'read,trading',
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year
});
// Sign with Solana wallet
const signature = await wallet.signMessage(addKeyMessage.message);
提交 Orderly 密钥
const keyResponse = await fetch('https://testnet-api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: addKeyMessage.message,
signature: signature,
userAddress: solanaAddress,
chainType: 'SOL',
}),
});
提现签名
提现在 EVM 和 Solana 上都需要钱包签名:
// Fetch withdraw nonce
const nonceRes = await fetch(`${BASE_URL}/v1/withdraw_nonce`);
const {
data: { withdraw_nonce },
} = await nonceRes.json();
// Generate withdraw message
const withdrawMessage = await walletAdapter.generateWithdrawMessage({
brokerId: BROKER_ID,
receiver: solanaAddress,
token: 'USDC',
amount: '1000',
timestamp: Date.now(),
nonce: Number(withdraw_nonce),
});
// Sign with Solana wallet
const signature = await wallet.signMessage(withdrawMessage.message);
// Submit withdrawal request
const res = await fetch(`${BASE_URL}/v1/withdraw_request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: withdrawMessage.message,
signature: signature,
userAddress: solanaAddress,
verifyingContract: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203', // Mainnet
// verifyingContract: '0x1826B75e2ef249173FC735149AE4B8e9ea10abff', // Testnet
}),
});
结算 PnL 签名
// Fetch settle nonce
const nonceRes = await fetch(`${BASE_URL}/v1/settle_nonce`);
const {
data: { settle_nonce },
} = await nonceRes.json();
// Generate settle message
const settleMessage = await walletAdapter.generateSettleMessage({
brokerId: BROKER_ID,
timestamp: Date.now(),
settlePnlNonce: settle_nonce,
});
// Sign with Solana wallet
const signature = await wallet.signMessage(settleMessage.message);
// Submit settle request
const res = await fetch(`${BASE_URL}/v1/settle_pnl`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: settleMessage.message,
signature: signature,
userAddress: solanaAddress,
verifyingContract: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203',
}),
});
Solana 专属配置
| 环境 | Solana 链 ID | Solana 集群 | Orderly 金库地址 | 验证合约 |
|---|---|---|---|---|
| 主网 | 900900900 | mainnet-beta | ErBmAD61mGFKvrFNaTJuxoPwqrS8GgtwtqJTJVjFWx9Q | 0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203 |
| 测试网 | 901901901 | devnet | 9shwxWDUNhtwkHocsUAmrNAQfBH2DHh4njdAEdHZZkF2 | 0x1826B75e2ef249173FC735149AE4B8e9ea10abff |
注意:EVM 和 Solana 使用相同的 API 基础 URL。请参阅本技能顶部的环境配置部分。
重要差异
账户 ID 生成
- EVM:
keccak256(address, keccak256(brokerId)) - Solana:由
/v1/get_accountAPI 返回(不是哈希值)
消息签名
- EVM:使用
eth_signTypedData_v4和结构化 EIP-712 类型 - Solana:使用经过 Ed25519 签名的原始消息字节
无域分隔符
Solana 不使用 EIP-712 域配置:
// EVM - requires domain
domain: {
name: 'Orderly',
version: '1',
chainId: 42161,
verifyingContract: '0x...',
}
// Solana - no domain, just raw message
const message = await walletAdapter.generateRegisterMessage({...});
第 2 部分:Ed25519 API 身份验证
通过钱包签名注册 Ed25519 密钥后(EVM 使用 EIP-712,Solana 使用 Ed25519 消息签名),您可以使用该密钥执行所有 API 操作。
必需的标头
| 标头 | 说明 |
|---|---|
orderly-timestamp | Unix 时间戳,单位为毫秒 |
orderly-account-id | 你的 Orderly 账户 ID |
orderly-key | 添加 ed25519: 前缀的公钥 |
orderly-signature | Base64url 编码的 Ed25519 签名 |
生成 Ed25519 密钥对
import { getPublicKeyAsync, utils } from '@noble/ed25519';
// Generate private key (32 cryptographically secure random bytes)
const privateKey = utils.randomPrivateKey();
// Derive public key
const publicKey = await getPublicKeyAsync(privateKey);
// Encode public key as base58 (required by Orderly)
function encodeBase58(bytes: Uint8Array): string {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const BASE = 58n;
let num = 0n;
for (const byte of bytes) {
num = num * 256n + BigInt(byte);
}
let result = '';
while (num > 0n) {
result = ALPHABET[Number(num % BASE)] + result;
num = num / BASE;
}
// Handle leading zeros
for (const byte of bytes) {
if (byte === 0) {
result = '1' + result;
} else {
break;
}
}
return result;
}
const orderlyKey = `ed25519:${encodeBase58(publicKey)}`;
// Convert bytes to hex string (browser & Node.js compatible)
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
console.log('Private Key (hex):', bytesToHex(privateKey));
console.log('Public Key (base58):', orderlyKey);
// STORE PRIVATE KEY SECURELY - NEVER SHARE IT
签署请求
构造消息
function buildSignMessage(timestamp: number, method: string, path: string, body?: string): string {
// Message format: timestamp + method + path + body
// Note: No spaces or separators between parts
return `${timestamp}${method}${path}${body || ''}`;
}
// Examples
const timestamp = Date.now();
// GET request (no body)
const getMessage = buildSignMessage(timestamp, 'GET', '/v1/positions');
// POST request (with body)
const body = JSON.stringify({
symbol: 'PERP_ETH_USDC',
side: 'BUY',
order_type: 'LIMIT',
order_price: '3000',
order_quantity: '0.1',
});
const postMessage = buildSignMessage(timestamp, 'POST', '/v1/order', body);
创建签名
import { signAsync } from '@noble/ed25519';
async function signRequest(
timestamp: number,
method: string,
path: string,
body: string | undefined,
privateKey: Uint8Array
): Promise<string> {
const message = buildSignMessage(timestamp, method, path, body);
// Sign with Ed25519
const signatureBytes = await signAsync(new TextEncoder().encode(message), privateKey);
// Encode as base64url (NOT base64)
// Convert to base64, then make it URL-safe
const base64 = btoa(String.fromCharCode(...signatureBytes));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
签名并发送请求的辅助函数
对于简单、独立的身份验证辅助函数,可始终正确处理查询参数和 Content-Type 标头:
import { getPublicKeyAsync, signAsync } from '@noble/ed25519';
import { encodeBase58 } from 'ethers';
export async function signAndSendRequest(
orderlyAccountId: string,
privateKey: Uint8Array | string,
input: URL | string,
init?: RequestInit | undefined
): Promise<Response> {
const timestamp = Date.now();
const encoder = new TextEncoder();
const url = new URL(input);
let message = `${String(timestamp)}${init?.method ?? 'GET'}${url.pathname}${url.search}`;
if (init?.body) {
message += init.body;
}
const orderlySignature = await signAsync(encoder.encode(message), privateKey);
return fetch(input, {
headers: {
'Content-Type':
init?.method !== 'GET' && init?.method !== 'DELETE'
? 'application/json'
: 'application/x-www-form-urlencoded',
'orderly-timestamp': String(timestamp),
'orderly-account-id': orderlyAccountId,
'orderly-key': `ed25519:${encodeBase58(await getPublicKeyAsync(privateKey))}`,
'orderly-signature': Buffer.from(orderlySignature).toString('base64url'),
...(init?.headers ?? {}),
},
...(init ?? {}),
});
}
此辅助函数:
- 正确解析 URL,以提取路径名和查询参数
- 根据 HTTP 方法正确设置 Content-Type(GET/DELETE 使用
application/x-www-form-urlencoded,其他方法使用application/json) - 使用时间戳 + 方法 + 路径名 + 查询参数 + 请求正文构造签名消息
- 返回 fetch 响应,以便进一步处理
使用示例
const baseUrl = 'https://api.orderly.org';
const accountId = '0x123...';
const privateKey = new Uint8Array(32); // Your private key
// GET request with query parameters
const positions = await signAndSendRequest(accountId, privateKey, `${baseUrl}/v1/positions`);
const positionsData = await positions.json();
// GET request with query params
const orders = await signAndSendRequest(
accountId,
privateKey,
`${baseUrl}/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE`
);
const ordersData = await orders.json();
// POST request with body
const order = await signAndSendRequest(accountId, privateKey, `${baseUrl}/v1/order`, {
method: 'POST',
body: JSON.stringify({
symbol: 'PERP_ETH_USDC',
side: 'BUY',
order_type: 'LIMIT',
order_price: '3000',
order_quantity: '0.1',
}),
});
const orderData = await order.json();
// DELETE request
const cancel = await signAndSendRequest(
accountId,
privateKey,
`${baseUrl}/v1/order?order_id=123&symbol=PERP_ETH_USDC`,
{ method: 'DELETE' }
);
错误处理辅助函数
class OrderlyApiError extends Error {
code: number;
details: any;
constructor(response: any) {
super(response.message || 'API Error');
this.code = response.code;
this.details = response;
}
}
// Usage with error handling
async function apiRequest(
accountId: string,
privateKey: Uint8Array,
url: string,
init?: RequestInit
) {
const response = await signAndSendRequest(accountId, privateKey, url, init);
const result = await response.json();
if (!result.success) {
throw new OrderlyApiError(result);
}
return result.data;
}
查询参数
查询参数必须包含在签名消息中。URL 会被解析,以提取路径名和查询参数:
// Correct - query params are parsed from the URL
const url = new URL('/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE', baseUrl);
// Message: timestamp + method + pathname + search
// Result: "1234567890123GET/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE"
// Wrong - query params added separately after signing
const path = '/v1/orders';
const signature = await sign(timestamp, 'GET', path);
const url = `${path}?symbol=PERP_ETH_USDC`; // Signature mismatch!
常见错误
签名不匹配(代码 10016)
Cause: Signature doesn't match expected value
Check:
1. Message format: timestamp + method + path + body (no spaces)
2. Method is uppercase: GET, POST, DELETE, PUT
3. Path includes query parameters
4. Body is exact JSON string (same whitespace)
5. Signature is base64url encoded (not base64)
时间戳已过期(代码 10017)
Cause: Timestamp is too old or too far in the future
Solution:
- Ensure server clock is synchronized
- Timestamp must be within ±30 seconds
- Generate timestamp immediately before signing
Orderly 密钥无效(代码 10019)
Cause: Public key format incorrect
Solution:
- Must be prefixed with 'ed25519:'
- Public key must be base58 encoded
- Key must be registered to account
Orderly 密钥权限范围
注册 API 密钥时,请指定权限:
| 权限范围 | 权限 |
|---|---|
read | 读取持仓、订单和余额 |
trading | 下单、取消和修改订单 |
asset | 存款、提款和内部转账 |
// When adding key via EIP-712 signing
const addKeyMessage = {
brokerId: 'woofi_dex',
chainId: 42161,
orderlyKey: 'ed25519:...',
scope: 'read,trading', // Multiple scopes comma-separated
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year
};
安全最佳实践
安全存储私钥
// NEVER hardcode private keys
// BAD:
const privateKey = new Uint8Array([1, 2, 3, ...]);
// GOOD: Load from environment
const privateKeyHex = process.env.ORDERLY_PRIVATE_KEY;
// Convert hex string to Uint8Array (browser & Node.js compatible)
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
const privateKey = hexToBytes(privateKeyHex);
// BETTER: Use secure key management
// AWS KMS, HashiCorp Vault, etc.
密钥轮换
定期轮换 API 密钥以确保安全:
// Generate new key pair
const newPrivateKey = utils.randomPrivateKey();
const newPublicKey = await getPublicKeyAsync(newPrivateKey);
// Register new key (requires wallet signature via EIP-712)
// POST /v1/orderly_key - No Ed25519 auth required
const orderlyKey = `ed25519:${encodeBase58(newPublicKey)}`;
const timestamp = Date.now();
const expiration = timestamp + 31536000000; // 1 year
const addKeyMessage = {
brokerId: 'your_broker_id',
chainId: 42161, // Arbitrum mainnet
orderlyKey: orderlyKey,
scope: 'read,trading', // Comma-separated scopes
timestamp: timestamp,
expiration: expiration,
};
// Sign with wallet (EIP-712)
const addKeySignature = await wallet.signTypedData({
domain: {
name: 'Orderly',
version: '1',
chainId: 42161,
verifyingContract: '0x...', // Contract address
},
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
AddOrderlyKey: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'orderlyKey', type: 'string' },
{ name: 'scope', type: 'string' },
{ name: 'timestamp', type: 'uint256' },
{ name: 'expiration', type: 'uint256' },
],
},
primaryType: 'AddOrderlyKey',
message: addKeyMessage,
});
const registerResponse = await fetch('https://api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: addKeyMessage,
signature: addKeySignature,
userAddress: walletAddress,
}),
});
const registerResult = await registerResponse.json();
if (!registerResult.success) {
throw new Error(`Failed to register key: ${registerResult.message}`);
}
// Update your application config
config.privateKey = newPrivateKey;
config.orderlyKey = orderlyKey;
// Remove old key using authenticated request
// POST /v1/client/remove_orderly_key - Requires Ed25519 auth
const oldOrderlyKey = `ed25519:${encodeBase58(oldPublicKey)}`;
const removeResponse = await signAndSendRequest(
accountId,
newPrivateKey, // Use the NEW key to authenticate
'https://api.orderly.org/v1/client/remove_orderly_key',
{
method: 'POST',
body: JSON.stringify({
orderly_key: oldOrderlyKey,
}),
}
);
const removeResult = await removeResponse.json();
if (!removeResult.success) {
throw new Error(`Failed to remove old key: ${removeResult.message}`);
}
IP 限制
// Set IP restriction for key
POST /v1/client/set_orderly_key_ip_restriction
Body: {
orderly_key: 'ed25519:...',
ip_list: ['1.2.3.4', '5.6.7.8'],
}
// Get current restrictions
GET /v1/client/orderly_key_ip_restriction?orderly_key={key}
// Reset (remove) restrictions
POST /v1/client/reset_orderly_key_ip_restriction
Body: { orderly_key: 'ed25519:...' }
WebSocket 身份验证
WebSocket 也需要 Ed25519 身份验证:
const ws = new WebSocket(`wss://ws-private-evm.orderly.org/v2/ws/private/stream/${accountId}`);
ws.onopen = async () => {
const timestamp = Date.now();
const message = timestamp.toString();
const signature = await signAsync(new TextEncoder().encode(message), privateKey);
// Convert to base64url (browser & Node.js compatible)
const base64 = btoa(String.fromCharCode(...signature));
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
ws.send(
JSON.stringify({
id: 'auth_1',
event: 'auth',
params: {
orderly_key: orderlyKey,
sign: base64url,
timestamp: timestamp,
},
})
);
};
测试身份验证
// Verify key is valid
GET /v1/get_orderly_key?orderly_key={key}
// Response
{
"success": true,
"data": {
"account_id": "0x...",
"valid": true,
"scope": "read,trading",
"expires_at": 1735689600000
}
}
支持的链
| 链 | 链 ID | 主网 | 测试网 |
|---|---|---|---|
| Arbitrum | 42161 / 421614 | ✅ | ✅ |
| Optimism | 10 / 11155420 | ✅ | ✅ |
| Base | 8453 / 84532 | ✅ | ✅ |
| Ethereum | 1 / 11155111 | ✅ | ✅ |
| Solana | 900900900 / 901901901 | ✅ | ✅ |
| Mantle | 5000 / 5003 | ✅ | ✅ |
常见问题
EIP-712 错误
“Nonce 已过期”错误
- Nonce 仅在 2 分钟内有效
- 获取新的 nonce 并重试
“账户已存在”错误
- 该钱包已在此经纪商处注册
- 使用
/v1/get_account获取现有账户信息
“签名无效”错误
- 确保 EIP-712 域完全匹配(name、version、chainId、verifyingContract)
- 检查链 ID 是否与网络匹配
- 确认消息结构与类型一致
- 使用
eth_signTypedData_v4,而不是eth_signTypedData
Ed25519 错误
签名不匹配(代码 10016)
Cause: Signature doesn't match expected value
Check:
1. Message format: timestamp + method + path + body (no spaces)
2. Method is uppercase: GET, POST, DELETE, PUT
3. Path includes query parameters
4. Body is exact JSON string (same whitespace)
5. Signature is base64url encoded (not base64)
时间戳已过期(代码 10017)
Cause: Timestamp is too old or too far in the future
Solution:
- Ensure server clock is synchronized
- Timestamp must be within ±30 seconds
- Generate timestamp immediately before signing
Orderly 密钥无效(代码 10019)
Cause: Public key format incorrect
Solution:
- Must be prefixed with 'ed25519:'
- Public key must be base58 encoded
- Key must be registered to account
身份验证方式对比
| 方面 | EIP-712 钱包身份验证 | Ed25519 API 身份验证 |
|---|---|---|
| 用途 | 账户操作、密钥管理 | 交易、读取数据 |
| 签名者 | 用户的 Web3 钱包 | 本地生成的 Ed25519 密钥 |
| 密钥类型 | Ethereum 私钥 | Ed25519 密钥对 |
| 端点 | /v1/register_account、/v1/orderly_key | 所有其他端点 |
| 签名类型 | EIP-712 类型化数据 | 原始 Ed25519 + base64url |
| 范围 | 创建/管理 API 密钥 | 使用 API 密钥进行交易 |
相关技能
- orderly-trading-orders - 使用经过身份验证的端点
- orderly-websocket-streaming - WebSocket 身份验证
- orderly-sdk-react-hooks - 用于简化身份验证的 React SDK
- orderly-deposit-withdraw - 为账户注资