What Gets Signed
The signature is computed over a bincode serialization of:Copy
action + account + signer
signature field itself is NOT included in what gets signed.
Working Example
Important: Bincode serialization is complex and must match exactly. For production use, always use the official SDKs when available. This implementation is provided for reference only.
Install Dependencies
Copy
pnpm install tweetnacl bs58
# or
yarn add tweetnacl bs58
Expected Format
Copy
import * as nacl from 'tweetnacl';
import bs58 from 'bs58';
/**
* Write a u64 little-endian
*/
function writeU64(value: number, buffer: Uint8Array, offset: number): number {
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 8);
view.setBigUint64(0, BigInt(value), true); // little-endian
return offset + 8;
}
/**
* Write a u32 little-endian
*/
function writeU32(value: number, buffer: Uint8Array, offset: number): number {
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 4);
view.setUint32(0, value, true); // little-endian
return offset + 4;
}
/**
* Write a f64 little-endian
*/
function writeF64(value: number, buffer: Uint8Array, offset: number): number {
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 8);
view.setFloat64(0, value, true); // little-endian
return offset + 8;
}
/**
* Write a string with length prefix (u64 LE)
*/
function writeString(str: string, buffer: Uint8Array, offset: number): number {
const encoder = new TextEncoder();
const strBytes = encoder.encode(str);
offset = writeU64(strBytes.length, buffer, offset);
buffer.set(strBytes, offset);
return offset + strBytes.length;
}
/**
* Serialize transaction using the exact bincode format
*/
function serializeTransaction(action: any, account: string, signer: string): Uint8Array {
const parts: Uint8Array[] = [];
// 1. Transaction type
if (action.type === 'order') {
const typeBytes = new TextEncoder().encode('order');
const typePart = new Uint8Array(8 + typeBytes.length);
writeU64(typeBytes.length, typePart, 0);
typePart.set(typeBytes, 8);
parts.push(typePart);
// 2. Quantity of orders
const orders = action.orders || [];
const countPart = new Uint8Array(8);
writeU64(orders.length, countPart, 0);
parts.push(countPart);
// 3. For each order
for (const order of orders) {
const assetBytes = new TextEncoder().encode(order.c);
const assetSize = 8 + assetBytes.length;
const orderSize = assetSize + 1 + 8 + 8 + 1 + 4; // asset + is_buy + price + size + reduce_only + order_type
let orderTypeSize = 0;
if (order.t.limit) {
orderTypeSize = 4; // TIF u32
} else if (order.t.trigger) {
orderTypeSize = 1 + 8; // is_market + triggerPx
}
const totalOrderSize = orderSize + orderTypeSize;
const orderPart = new Uint8Array(totalOrderSize);
let offset = 0;
// Asset
offset = writeString(order.c, orderPart, offset);
// is_buy
orderPart[offset] = order.b ? 1 : 0;
offset += 1;
// price
offset = writeF64(order.px, orderPart, offset);
// size
offset = writeF64(order.sz, orderPart, offset);
// reduce_only
orderPart[offset] = order.r ? 1 : 0;
offset += 1;
// order_type
if (order.t.limit) {
writeU32(0, orderPart, offset); // limit = 0
offset += 4;
// TIF: 0 = GTC, 1 = IOC, 2 = ALO
const tifMap: { [key: string]: number } = { 'GTC': 0, 'IOC': 1, 'ALO': 2 };
writeU32(tifMap[order.t.limit.tif] || 0, orderPart, offset);
offset += 4;
} else if (order.t.trigger) {
writeU32(1, orderPart, offset); // trigger = 1
offset += 4;
// is_market
orderPart[offset] = order.t.trigger.is_market ? 1 : 0;
offset += 1;
// triggerPx
offset = writeF64(order.t.trigger.triggerPx, orderPart, offset);
}
parts.push(orderPart);
}
} else if (action.type === 'cancel') {
const typeBytes = new TextEncoder().encode('cancel');
const typePart = new Uint8Array(8 + typeBytes.length);
writeU64(typeBytes.length, typePart, 0);
typePart.set(typeBytes, 8);
parts.push(typePart);
const cancels = action.cancels || [];
const countPart = new Uint8Array(8);
writeU64(cancels.length, countPart, 0);
parts.push(countPart);
for (const cancel of cancels) {
const cancelSize = 8 + new TextEncoder().encode(cancel.c).length + 8 + new TextEncoder().encode(cancel.oid).length;
const cancelPart = new Uint8Array(cancelSize);
let offset = 0;
offset = writeString(cancel.c, cancelPart, offset);
offset = writeString(cancel.oid, cancelPart, offset);
parts.push(cancelPart);
}
} else if (action.type === 'cancelall') {
const typeBytes = new TextEncoder().encode('cancelall');
const typePart = new Uint8Array(8 + typeBytes.length);
writeU64(typeBytes.length, typePart, 0);
typePart.set(typeBytes, 8);
parts.push(typePart);
const cancels = action.cancels || [];
const countPart = new Uint8Array(8);
writeU64(cancels.length, countPart, 0);
parts.push(countPart);
for (const cancel of cancels) {
const cancelPart = new Uint8Array(8 + new TextEncoder().encode(cancel.c).length);
let offset = 0;
offset = writeString(cancel.c, cancelPart, offset);
parts.push(cancelPart);
}
} else if (action.type === 'updateusersettings') {
const typeBytes = new TextEncoder().encode('updateusersettings');
const typePart = new Uint8Array(8 + typeBytes.length);
writeU64(typeBytes.length, typePart, 0);
typePart.set(typeBytes, 8);
parts.push(typePart);
if (action.settings && action.settings.m) {
const leverageCount = action.settings.m.length;
const countPart = new Uint8Array(8);
writeU64(leverageCount, countPart, 0);
parts.push(countPart);
for (const [symbol, leverage] of action.settings.m) {
const symbolBytes = new TextEncoder().encode(symbol);
const leverageSize = 8 + symbolBytes.length + 8;
const leveragePart = new Uint8Array(leverageSize);
let offset = 0;
offset = writeString(symbol, leveragePart, offset);
offset = writeF64(leverage, leveragePart, offset);
parts.push(leveragePart);
}
} else {
const countPart = new Uint8Array(8);
writeU64(0, countPart, 0);
parts.push(countPart);
}
} else if (action.type === 'agentwalletcreation') {
const typeBytes = new TextEncoder().encode('agentwalletcreation');
const typePart = new Uint8Array(8 + typeBytes.length);
writeU64(typeBytes.length, typePart, 0);
typePart.set(typeBytes, 8);
parts.push(typePart);
if (action.agent) {
const agentBytes = new TextEncoder().encode(action.agent.a);
const agentPart = new Uint8Array(8 + agentBytes.length + 1);
let offset = 0;
offset = writeString(action.agent.a, agentPart, offset);
agentPart[offset] = action.agent.d ? 1 : 0;
parts.push(agentPart);
}
} else if (action.type === 'faucet') {
const typeBytes = new TextEncoder().encode('faucet');
const typePart = new Uint8Array(8 + typeBytes.length);
writeU64(typeBytes.length, typePart, 0);
typePart.set(typeBytes, 8);
parts.push(typePart);
if (action.faucet) {
const userBytes = new TextEncoder().encode(action.faucet.u);
const userPart = new Uint8Array(8 + userBytes.length);
writeU64(userBytes.length, userPart, 0);
userPart.set(userBytes, 8);
parts.push(userPart);
}
}
// 4. Account (32 bytes, decoded from base58)
const accountBytes = bs58.decode(account);
if (accountBytes.length !== 32) {
throw new Error(`Account must be 32 bytes, got ${accountBytes.length}`);
}
parts.push(accountBytes);
// 5. Signer (32 bytes, decoded from base58)
const signerBytes = bs58.decode(signer);
if (signerBytes.length !== 32) {
throw new Error(`Signer must be 32 bytes, got ${signerBytes.length}`);
}
parts.push(signerBytes);
// Concatenate all parts
const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
const result = new Uint8Array(totalLength);
let resultOffset = 0;
for (const part of parts) {
result.set(part, resultOffset);
resultOffset += part.length;
}
return result;
}
/**
* Sign a transaction action for the exchange
*/
function signTransaction(
secretKey: Uint8Array,
action: any,
account: string,
signer: string
): string {
// Serialize the transaction
const message = serializeTransaction(action, account, signer);
// Sign with Ed25519
const signature = nacl.sign.detached(message, secretKey);
// Encode as base58
return bs58.encode(signature);
}
Account vs Signer
The transaction includes two separate public key fields:account: The account being traded (whose positions/orders are affected)signer: Who’s signing the transaction (usually same as account, or authorized agent)
Same Account and Signer
Most common case: you’re trading your own account.Copy
const transaction = {
action: { type: "order", orders: [order] },
account: myPublicKey, // Your account
signer: myPublicKey // You're signing
};
Agent Wallet (Different Signer)
Agent wallet trading on behalf of user:Copy
const transaction = {
action: { type: "order", orders: [order] },
account: userPublicKey, // User's account being traded
signer: agentPublicKey // Agent signing (must be pre-authorized)
};
signer != account, the signer must be pre-authorized via the /agent-wallet endpoint.
Implementation Notes
Official SDKs Available: Official SDKs for TypeScript, Python, Rust, and other languages handle all bincode serialization correctly and provide a simple, type-safe API.For production use, always prefer SDKs over manual implementation to ensure correctness and maintainability.
- Little-endian encoding for all numeric types
- Length-prefixed strings (u64 length prefix)
- Exact format matching for all action types