Skip to main content

What Gets Signed

The signature is computed over a wincode serialization of:
action_discriminant(u32) + action_data + nonce(u64) + account(32 bytes) + signer(32 bytes)
These components are serialized together and then signed with your Ed25519 private key.
Nonce Requirement: Every action includes a nonce field (u64) for replay protection. Use timestamp in nanoseconds: BigInt(Date.now()) * 1_000_000n.
Note: The signature field itself is NOT included in what gets signed.

Transaction Structure

All signed transactions follow this format:
{
  "action": {
    "type": "order",
    "orders": [...],
    "nonce": 1704067200000000000
  },
  "account": "FuueqefENiGEW6uMqZQgmwjzgpnb85EgUcZa5Em4PQh7",
  "signer": "FuueqefENiGEW6uMqZQgmwjzgpnb85EgUcZa5Em4PQh7",
  "signature": "5j7sVt3k2YxPqH4w..."
}
FieldDescription
actionThe operation to perform (includes nonce)
accountAccount public key (base58) - whose account is affected
signerSigner public key (base58) - who’s signing (usually same as account)
signatureEd25519 signature (base58)

Serialization Format (wincode)

The exchange uses wincode (a custom binary format) for signing:
TypeEncoding
Enum variantu32 discriminant (0, 1, 2…)
Pubkey/HashRaw 32 bytes (decoded from base58)
SignatureRaw 64 bytes
Stringu64 length prefix + UTF-8 bytes
Option<T>1 byte (0=None, 1=Some) + T if Some
Vec<T>u64 count + elements
bool1 byte (0 or 1)
u64/f648 bytes little-endian
u324 bytes little-endian

Enum Discriminant Mappings

const ACTION_CODES = {
  order: 0,
  oracle: 1,
  faucet: 2,
  updateUserSettings: 3,
  agentWalletCreation: 4,
};

Working Example

Important: Wincode serialization must match exactly. For production use, always use the official SDKs when available. This implementation is provided for reference only.

Install Dependencies

pnpm install tweetnacl bs58
# or
yarn add tweetnacl bs58

Implementation

import * as nacl from 'tweetnacl';
import bs58 from 'bs58';

// ============================================================================
// Enum Discriminant Mappings
// ============================================================================

const ACTION_CODES: Record<string, number> = {
  order: 0,
  oracle: 1,
  faucet: 2,
  updateUserSettings: 3,
  agentWalletCreation: 4,
};

const ORDER_ITEM_CODES: Record<string, number> = {
  order: 0,
  cancel: 1,
  cancelAll: 2,
};

const TIME_IN_FORCE_CODES: Record<string, number> = {
  GTC: 0,
  IOC: 1,
  ALO: 2,
};

const ORDER_TYPE_CODES: Record<string, number> = {
  limit: 0,
  trigger: 1,
};

// ============================================================================
// Primitive Writers
// ============================================================================

function writeU32(value: number): Uint8Array {
  const buf = new Uint8Array(4);
  new DataView(buf.buffer).setUint32(0, value, true); // little-endian
  return buf;
}

function writeU64(value: number | bigint): Uint8Array {
  const buf = new Uint8Array(8);
  new DataView(buf.buffer).setBigUint64(0, BigInt(value), true); // little-endian
  return buf;
}

function writeBool(value: boolean): Uint8Array {
  return new Uint8Array([value ? 1 : 0]);
}

function writeString(str: string): Uint8Array {
  const bytes = new TextEncoder().encode(str);
  return concatBytes(writeU64(bytes.length), bytes);
}

function writeF64(value: number): Uint8Array {
  const buf = new Uint8Array(8);
  new DataView(buf.buffer).setFloat64(0, value, true); // little-endian
  return buf;
}

function concatBytes(...arrays: Uint8Array[]): Uint8Array {
  const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
  const result = new Uint8Array(totalLength);
  let offset = 0;
  for (const arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
}

// ============================================================================
// Validation Helpers
// ============================================================================

function decodeAndValidateKey(key: string): Uint8Array {
  const bytes = bs58.decode(key);
  if (bytes.length !== 32) {
    throw new Error(`Key must be 32 bytes, got ${bytes.length}`);
  }
  return bytes;
}

function decodeAndValidateHash(hash: string): Uint8Array {
  const bytes = bs58.decode(hash);
  if (bytes.length !== 32) {
    throw new Error(`Hash must be 32 bytes, got ${bytes.length}`);
  }
  return bytes;
}

// ============================================================================
// Order Serialization
// ============================================================================

function serializeOrderItem(orderWrapper: any): Uint8Array {
  // Get the order item type (order, cancel, or cancelAll)
  const itemType = Object.keys(orderWrapper)[0];
  if (!itemType || !(itemType in ORDER_ITEM_CODES)) {
    throw new Error(`Invalid order item type: ${itemType}`);
  }

    const parts: Uint8Array[] = [];
    
  // Write order item discriminant (u32)
  parts.push(writeU32(ORDER_ITEM_CODES[itemType]));

  const data = orderWrapper[itemType];

  if (itemType === 'order') {
    // Order: asset, is_buy, price, size, reduce_only, order_type, client_id
    parts.push(writeString(data.c));           // asset
    parts.push(writeBool(data.b));             // is_buy
    parts.push(writeF64(data.px));             // price
    parts.push(writeF64(data.sz));             // size
    parts.push(writeBool(data.r));             // reduce_only

    // Order type (limit or trigger)
    if (data.t.limit) {
      parts.push(writeU32(ORDER_TYPE_CODES.limit));
      parts.push(writeU32(TIME_IN_FORCE_CODES[data.t.limit.tif] || 0));
    } else if (data.t.trigger) {
      parts.push(writeU32(ORDER_TYPE_CODES.trigger));
      parts.push(writeBool(data.t.trigger.is_market));
      parts.push(writeF64(data.t.trigger.triggerPx));
    } else {
      throw new Error('Order must have either limit or trigger type');
    }

    // client_id: Option<Hash> - 1 byte discriminant + 32 bytes if Some
    if (data.cloid) {
      parts.push(writeBool(true));
      parts.push(decodeAndValidateHash(data.cloid)); // Raw 32 bytes
    } else {
      parts.push(writeBool(false));
    }

  } else if (itemType === 'cancel') {
    // Cancel: asset, oid (Hash as raw 32 bytes)
    parts.push(writeString(data.c));           // asset
    parts.push(decodeAndValidateHash(data.oid)); // oid - raw 32 bytes

  } else if (itemType === 'cancelAll') {
    // CancelAll: assets (Vec<String>)
    const assets = data.c || [];
    parts.push(writeU64(assets.length));
    for (const asset of assets) {
      parts.push(writeString(asset));
    }
  }

  return concatBytes(...parts);
}

function serializeOrders(orders: any[]): Uint8Array {
  const parts: Uint8Array[] = [];

  // Write order count (u64)
  parts.push(writeU64(orders.length));

  // Serialize each order item
  for (const order of orders) {
    parts.push(serializeOrderItem(order));
  }

  return concatBytes(...parts);
}

// ============================================================================
// Action Serialization
// ============================================================================

function serializeFaucet(faucet: any): Uint8Array {
  const parts: Uint8Array[] = [];

  // user: Pubkey (raw 32 bytes)
  parts.push(decodeAndValidateKey(faucet.u));

  // amount: Option<f64>
  if (faucet.amount !== undefined && faucet.amount !== null) {
    parts.push(writeBool(true));
    parts.push(writeF64(faucet.amount));
  } else {
    parts.push(writeBool(false));
  }

  return concatBytes(...parts);
}

function serializeAgentWalletCreation(agent: any): Uint8Array {
  return concatBytes(
    decodeAndValidateKey(agent.a),  // agent: Pubkey (raw 32 bytes)
    writeBool(agent.d)               // delete: bool
  );
}

function serializeUpdateUserSettings(settings: any): Uint8Array {
  const parts: Uint8Array[] = [];

  const leverageMap = settings.m || [];
  parts.push(writeU64(leverageMap.length));

  for (const [symbol, leverage] of leverageMap) {
    parts.push(writeString(symbol));
    parts.push(writeF64(leverage));
  }

  return concatBytes(...parts);
}

function serializeOracle(oracles: any[]): Uint8Array {
  const parts: Uint8Array[] = [];

  parts.push(writeU64(oracles.length));

  for (const oracle of oracles) {
    parts.push(writeU64(oracle.t));      // timestamp
    parts.push(writeString(oracle.c));   // asset
    parts.push(writeF64(oracle.px));     // price
  }

  return concatBytes(...parts);
}

// ============================================================================
// Main Transaction Serialization
// ============================================================================

/**
 * Serialize transaction using wincode format for signing.
 *
 * Format: action_discriminant(u32) + action_data + nonce(u64) + account(32) + signer(32)
 */
export function serializeTransaction(
  action: any,
  account: string,
  signer: string
): Uint8Array {
  const actionType = action.type || '';

  if (!(actionType in ACTION_CODES)) {
    throw new Error(`Invalid action type: ${actionType}`);
  }

  const parts: Uint8Array[] = [];

  // 1. Action discriminant (u32)
  parts.push(writeU32(ACTION_CODES[actionType]));

  // 2. Action-specific data
  switch (actionType) {
    case 'order':
      parts.push(serializeOrders(action.orders || []));
      break;
    case 'oracle':
      parts.push(serializeOracle(action.oracles || []));
      break;
    case 'faucet':
      parts.push(serializeFaucet(action.faucet || {}));
      break;
    case 'updateUserSettings':
      parts.push(serializeUpdateUserSettings(action.settings || {}));
      break;
    case 'agentWalletCreation':
      parts.push(serializeAgentWalletCreation(action.agent || {}));
      break;
  }

  // 3. Nonce (u64)
  parts.push(writeU64(action.nonce));

  // 4. Account (32 bytes)
  parts.push(decodeAndValidateKey(account));

  // 5. Signer (32 bytes)
  parts.push(decodeAndValidateKey(signer));

  return concatBytes(...parts);
}

/**
 * Sign a transaction action for the exchange
 */
export function signTransaction(
    secretKey: Uint8Array,
    action: any,
    account: string,
    signer: string
): string {
  // Serialize the transaction using wincode format
    const message = serializeTransaction(action, account, signer);
    
    // Sign with Ed25519
    const signature = nacl.sign.detached(message, secretKey);
    
    // Encode as base58
    return bs58.encode(signature);
}

Binary Layout Reference

Order Action Binary Layout

[4 bytes]  Action discriminant (0 = order)
[8 bytes]  Order count (u64)

For each order item:
  [4 bytes]  Order item discriminant (0=order, 1=cancel, 2=cancelAll)

  If order (0):
    [8 bytes + N]  Asset string (u64 length + UTF-8 bytes)
    [1 byte]       is_buy (bool)
    [8 bytes]      price (f64)
    [8 bytes]      size (f64)
    [1 byte]       reduce_only (bool)
    [4 bytes]      Order type discriminant (0=limit, 1=trigger)
    If limit:
      [4 bytes]    Time in force (0=GTC, 1=IOC, 2=ALO)
    If trigger:
      [1 byte]     is_market (bool)
      [8 bytes]    trigger_price (f64)
    [1 byte]       client_id Option (0=None, 1=Some)
    If Some:
      [32 bytes]   client_id Hash (raw bytes, NOT base58 string!)

  If cancel (1):
    [8 bytes + N]  Asset string
    [32 bytes]     Order ID Hash (raw bytes)

  If cancelAll (2):
    [8 bytes]      Asset count (u64)
    For each asset:
      [8 bytes + N]  Asset string

[8 bytes]  Nonce (u64)
[32 bytes] Account pubkey (raw bytes)
[32 bytes] Signer pubkey (raw bytes)

Faucet Action Binary Layout

[4 bytes]  Action discriminant (2 = faucet)
[32 bytes] User pubkey (raw bytes, NOT base58 string!)
[1 byte]   Amount Option (0=None, 1=Some)
If Some:
  [8 bytes] Amount (f64)
[8 bytes]  Nonce (u64)
[32 bytes] Account pubkey (raw bytes)
[32 bytes] Signer pubkey (raw bytes)

Agent Wallet Action Binary Layout

[4 bytes]  Action discriminant (4 = agentWalletCreation)
[32 bytes] Agent pubkey (raw bytes)
[1 byte]   Delete flag (bool)
[8 bytes]  Nonce (u64)
[32 bytes] Account pubkey (raw bytes)
[32 bytes] Signer pubkey (raw bytes)

Update User Settings Binary Layout

[4 bytes]  Action discriminant (3 = updateUserSettings)
[8 bytes]  Leverage map count (u64)
For each entry:
  [8 bytes + N]  Symbol string (u64 length + UTF-8 bytes)
  [8 bytes]      Leverage (f64)
[8 bytes]  Nonce (u64)
[32 bytes] Account pubkey (raw bytes)
[32 bytes] Signer pubkey (raw bytes)

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.
const nonce = BigInt(Date.now()) * 1_000_000n;

const transaction = {
  action: { 
    type: "order", 
    orders: [{ order: orderDetails }],
    nonce: nonce
  },
  account: myPublicKey,    // Your account
  signer: myPublicKey      // You're signing
};

Agent Wallet (Different Signer)

Agent wallet trading on behalf of user:
const nonce = BigInt(Date.now()) * 1_000_000n;

const transaction = {
  action: { 
    type: "order", 
    orders: [{ order: orderDetails }],
    nonce: nonce
  },
  account: userPublicKey,   // User's account being traded
  signer: agentPublicKey    // Agent signing (must be pre-authorized)
};
Agent Wallets: If signer != account, the signer must be pre-authorized via the /agent-wallet endpoint first.

Implementation Notes

Official SDKs Available: Official SDKs for TypeScript, Python, Rust, and other languages handle all wincode serialization correctly and provide a simple, type-safe API.For production use, always prefer SDKs over manual implementation to ensure correctness and maintainability.

Common Issues

  • Verify action discriminant is u32 (not length-prefixed string)
  • Check that Pubkey/Hash fields are raw 32 bytes (not base58 strings)
  • Ensure Option types have proper 1-byte discriminant
  • Verify field order matches the binary layout exactly
  • Ensure nonce is included in serialization
If signer != account, the signer must be pre-authorized via /agent-wallet endpoint.
Account must be funded first. Use /private/faucet on testnet.
Each nonce can only be used once. Use nanosecond timestamps or incrementing counters.