Skip to content

EIP-712 Signing Implementation

This guide provides detailed implementation instructions for EIP-712 signature generation across different programming languages and libraries.

Core Concepts

Domain Separator

The domain separator for off-chain authentication (uses zero address as verifying contract):

const domain = {
  name: "Synthetix",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000000"
};

Message Types

Each API method has its own EIP-712 type definition. Here are the actual implementations:

WebSocket Authentication

const authTypes = {
  AuthMessage: [
    { name: "subAccountId", type: "uint256" },
    { name: "timestamp", type: "uint256" },
    { name: "action", type: "string" }
  ]
};
 
// Example message
const authMessage = {
  subAccountId: "123456789", // Your subaccount ID
  timestamp: Date.now(), // Unix timestamp in milliseconds
  action: "websocket_auth"
};

Place Orders

const placeOrderTypes = {
  NewOrderRequest: [
    { name: "subAccountId", type: "uint64" },
    { name: "symbol", type: "string" },
    { name: "side", type: "string" },
    { name: "quantity", type: "string" },
    { name: "price", type: "string" },
    { name: "orderType", type: "string" }, // MUST be JSON.stringify'd (e.g. '"limitGtc"')
    { name: "reduceOnly", type: "bool" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};
 
// Order type examples - ALWAYS JSON.stringify for EIP-712 signing
// NEW FLATTENED STRUCTURE:
// IMPORTANT: Order types MUST be JSON.stringify'd when used in EIP-712 signatures
const orderTypes = {
  limitGtc: "limitGtc",        // Limit order, Good Till Canceled
  limitIoc: "limitIoc",        // Limit order, Immediate or Cancel
  limitAlo: "limitAlo",        // Limit order, Add Liquidity Only (post-only)
  market: "market",            // Market order
  triggerSl: "triggerSl",      // Trigger Stop Loss
  triggerTp: "triggerTp"       // Trigger Take Profit
};
 
// Example usage in order:
const order = {
  // ... other fields ...
  orderType: "limitGtc",       // Use the enum value
  price: "50000.00",          // Required for limit orders, empty string for market
  triggerPrice: "",           // Required for trigger orders, empty string otherwise
  isTriggerMarket: false      // For trigger orders: true = market execution
};
 
> **Important**: When signing orders with EIP-712, the orderType field MUST be JSON.stringify'd before signing. The API expects the string value "limitGtc" but EIP-712 signing requires JSON.stringify("limitGtc") = '"limitGtc"'.

Cancel Orders

const cancelOrderTypes = {
  CancelOrderRequest: [
    { name: "subAccountId", type: "uint64" },
    { name: "orderId", type: "uint64" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

Modify Order (Simplified Structure)

const modifyOrderTypes = {
  ModifyOrderRequest: [
    { name: "subAccountId", type: "uint64" },
    { name: "orderId", type: "uint64" },
    { name: "price", type: "string" },    // Optional - can be empty string if not changing
    { name: "quantity", type: "string" }, // Optional - can be empty string if not changing
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

JavaScript/TypeScript Implementation

Using viem

import { createWalletClient, http, parseSignature } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
 
// Signature validation function
function validateSignature(signature: { v: number; r: string; s: string }) {
  if (!signature || typeof signature !== 'object') {
    throw new Error('Invalid signature object');
  }
 
  // Validate v - must be 0, 1, 27, or 28
  if (![0, 1, 27, 28].includes(signature.v)) {
    throw new Error(`Invalid v value: ${signature.v}. Must be 0, 1, 27, or 28`);
  }
 
  // Validate r - must be 32-byte hex string
  if (!signature.r || !/^0x[a-fA-F0-9]{64}$/.test(signature.r)) {
    throw new Error('Invalid r value: must be 0x-prefixed 32-byte hex string');
  }
 
  // Validate s - must be 32-byte hex string
  if (!signature.s || !/^0x[a-fA-F0-9]{64}$/.test(signature.s)) {
    throw new Error('Invalid s value: must be 0x-prefixed 32-byte hex string');
  }
 
  return true;
}
 
class SynthetixSigner {
  private account: any;
  private client: any;
  private domain: any;
 
  constructor(privateKey: `0x${string}`, chainId = 1) {
    this.account = privateKeyToAccount(privateKey);
    this.client = createWalletClient({
      account: this.account,
      chain: chainId === 1 ? mainnet : mainnet, // Add other chains as needed
      transport: http()
    });
 
    this.domain = {
      name: "Synthetix",
      version: "1",
      chainId: BigInt(chainId),
      verifyingContract: "0x0000000000000000000000000000000000000000"
    };
  }
 
  async signWebSocketAuth(subAccountId: string, timestamp?: number) {
    const types = {
      AuthMessage: [
        { name: "subAccountId", type: "uint256" },
        { name: "timestamp", type: "uint256" },
        { name: "action", type: "string" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(subAccountId),
      timestamp: BigInt(timestamp || Date.now()),
      action: "websocket_auth"
    };
 
    const signature = await this.client.signTypedData({
      domain: {
        ...this.domain,
        name: "Synthetix"
      },
      types,
      primaryType: "AuthMessage",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signPlaceOrder(orderData: {
    subAccountId: string;
    symbol: string;
    side: "buy" | "sell";
    quantity: string;
    price: string;
    orderType: any;
    reduceOnly: boolean;
    nonce: number;
    expiresAfter?: number;
  }) {
    const types = {
      NewOrderRequest: [
        { name: "subAccountId", type: "uint64" },
        { name: "symbol", type: "string" },
        { name: "side", type: "string" },
        { name: "quantity", type: "string" },
        { name: "price", type: "string" },
        { name: "orderType", type: "string" },
        { name: "reduceOnly", type: "bool" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(orderData.subAccountId),
      symbol: orderData.symbol,
      side: orderData.side,
      quantity: orderData.quantity,
      price: orderData.price,
      orderType: JSON.stringify(orderData.orderType), // ALWAYS stringify for EIP-712
      reduceOnly: orderData.reduceOnly,
      nonce: BigInt(orderData.nonce),
      expiresAfter: BigInt(orderData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "NewOrderRequest",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signCancelOrder(cancelData: {
    subAccountId: string;
    orderId: string;
    nonce: number;
    expiresAfter?: number;
  }) {
    const types = {
      CancelOrderRequest: [
        { name: "subAccountId", type: "uint64" },
        { name: "orderId", type: "uint64" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(cancelData.subAccountId),
      orderId: BigInt(cancelData.orderId),
      nonce: BigInt(cancelData.nonce),
      expiresAfter: BigInt(cancelData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "CancelOrderRequest",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signModifyOrder(modifyData: {
    subAccountId: string;
    orderId: string;
    price?: string;
    quantity?: string;
    nonce: number;
    expiresAfter?: number;
  }) {
    const types = {
      ModifyOrderRequest: [
        { name: "subAccountId", type: "uint64" },
        { name: "orderId", type: "uint64" },
        { name: "price", type: "string" },
        { name: "quantity", type: "string" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(modifyData.subAccountId),
      orderId: BigInt(modifyData.orderId),
      price: modifyData.price || "",
      quantity: modifyData.quantity || "",
      nonce: BigInt(modifyData.nonce),
      expiresAfter: BigInt(modifyData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "ModifyOrderRequest",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
}
 
// Usage Example
async function example() {
  const signer = new SynthetixSigner("0x..." as `0x${string}`);
 
  // WebSocket authentication
  const authSig = await signer.signWebSocketAuth("123456789");
  console.log('Auth signature:', authSig);
 
  // Place limit order
  const orderSig = await signer.signPlaceOrder({
    subAccountId: "123456789",
    symbol: "BTC-USDT",
    side: "buy",
    quantity: "0.1",
    price: "50000",
    orderType: "limitGtc", // This will be JSON.stringify'd in signPlaceOrder
    reduceOnly: false,
    nonce: Date.now()
  });
  console.log('Order signature:', orderSig);
 
  // Cancel order
  const cancelSig = await signer.signCancelOrder({
    subAccountId: "123456789",
    orderId: "987654321",
    nonce: Date.now()
  });
  console.log('Cancel signature:', cancelSig);
}

Python Implementation

Using web3.py and eth_account

from eth_account.messages import encode_structured_data
from eth_account import Account
import json
import time
import re
 
def validate_signature(signature):
    """Validate EIP-712 signature components"""
    if not isinstance(signature, dict):
        raise ValueError("Invalid signature object")
 
    # Validate v - must be 0, 1, 27, or 28
    v = signature.get('v')
    if v not in [0, 1, 27, 28]:
        raise ValueError(f"Invalid v value: {v}. Must be 0, 1, 27, or 28")
 
    # Validate r - must be 32-byte hex string
    r = signature.get('r')
    if not r or not re.match(r'^0x[a-fA-F0-9]{64}$', r):
        raise ValueError("Invalid r value: must be 0x-prefixed 32-byte hex string")
 
    # Validate s - must be 32-byte hex string
    s = signature.get('s')
    if not s or not re.match(r'^0x[a-fA-F0-9]{64}$', s):
        raise ValueError("Invalid s value: must be 0x-prefixed 32-byte hex string")
 
    return True
 
class SynthetixSigner:
    def __init__(self, private_key, chain_id=1):
        self.account = Account.from_key(private_key)
        self.domain = {
            "name": "Synthetix",
            "version": "1",
            "chainId": chain_id,
            "verifyingContract": "0x0000000000000000000000000000000000000000"
        }
 
    def signWebsocketAuth(self, sub_account_id, timestamp=None):
        if timestamp is None:
            timestamp = int(time.time() * 1000)  # Use milliseconds
 
        typed_data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"}
                ],
                "AuthMessage": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "timestamp", "type": "uint256"},
                    {"name": "action", "type": "string"}
                ]
            },
            "primaryType": "AuthMessage",
            "domain": {
                **self.domain,
                "name": "Synthetix"
            },
            "message": {
                "subAccountId": sub_account_id,
                "timestamp": timestamp,
                "action": "websocket_auth"
            }
        }
 
        encoded = encode_structured_data(typed_data)
        signed = self.account.sign_message(encoded)
 
        signature = {
            "v": signed.v,
            "r": "0x" + signed.r.hex(),
            "s": "0x" + signed.s.hex()
        }
        validate_signature(signature)
        return signature
 
    def sign_place_order(self, order_data):
        # ALWAYS stringify the order type for EIP-712
        order_type_str = json.dumps(order_data['orderType'])
 
        typed_data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"}
                ],
                "NewOrderRequest": [
                    {"name": "subAccountId", "type": "uint64"},
                    {"name": "symbol", "type": "string"},
                    {"name": "side", "type": "string"},
                    {"name": "quantity", "type": "string"},
                    {"name": "price", "type": "string"},
                    {"name": "orderType", "type": "string"},
                    {"name": "reduceOnly", "type": "bool"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            "primaryType": "NewOrderRequest",
            "domain": self.domain,
            "message": {
                "subAccountId": order_data['subAccountId'],
                "symbol": order_data['symbol'],
                "side": order_data['side'],
                "quantity": order_data['quantity'],
                "price": order_data['price'],
                "orderType": order_type_str,
                "reduceOnly": order_data['reduceOnly'],
                "nonce": order_data['nonce'],
                "expiresAfter": order_data.get('expiresAfter', 0)
            }
        }
 
        encoded = encode_structured_data(typed_data)
        signed = self.account.sign_message(encoded)
 
        signature = {
            "v": signed.v,
            "r": "0x" + signed.r.hex(),
            "s": "0x" + signed.s.hex()
        }
        validate_signature(signature)
        return signature
 
    def sign_cancel_order(self, cancel_data):
        typed_data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"}
                ],
                "CancelOrderRequest": [
                    {"name": "subAccountId", "type": "uint64"},
                    {"name": "orderId", "type": "uint64"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            "primaryType": "CancelOrderRequest",
            "domain": self.domain,
            "message": {
                "subAccountId": cancel_data['subAccountId'],
                "orderId": cancel_data['orderId'],
                "nonce": cancel_data['nonce'],
                "expiresAfter": cancel_data.get('expiresAfter', 0)
            }
        }
 
        encoded = encode_structured_data(typed_data)
        signed = self.account.sign_message(encoded)
 
        signature = {
            "v": signed.v,
            "r": "0x" + signed.r.hex(),
            "s": "0x" + signed.s.hex()
        }
        validate_signature(signature)
        return signature
 
    def sign_modify_order(self, modify_data):
        typed_data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"}
                ],
                "ModifyOrderRequest": [
                    {"name": "subAccountId", "type": "uint64"},
                    {"name": "orderId", "type": "uint64"},
                    {"name": "price", "type": "string"},
                    {"name": "quantity", "type": "string"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            "primaryType": "ModifyOrderRequest",
            "domain": self.domain,
            "message": {
                "subAccountId": modify_data['subAccountId'],
                "orderId": modify_data['orderId'],
                "price": modify_data.get('price', ""),
                "quantity": modify_data.get('quantity', ""),
                "nonce": modify_data['nonce'],
                "expiresAfter": modify_data.get('expiresAfter', 0)
            }
        }
 
        encoded = encode_structured_data(typed_data)
        signed = self.account.sign_message(encoded)
 
        signature = {
            "v": signed.v,
            "r": "0x" + signed.r.hex(),
            "s": "0x" + signed.s.hex()
        }
        validate_signature(signature)
        return signature
 
# Usage Example
def example():
    signer = SynthetixSigner(private_key="0x...")
 
    # WebSocket authentication
    auth_sig = signer.signWebsocketAuth(sub_account_id=123456789)
    print(f"Auth signature: {auth_sig}")
 
    # Place order
    order_data = {
        "subAccountId": 123456789,
        "symbol": "BTC-USDT",
        "side": "buy",
        "quantity": "0.1",
        "price": "50000",
        "orderType": "limitGtc",  # This will be JSON.stringify'd in sign_place_order
        "reduceOnly": False,
        "nonce": int(time.time() * 1000)
    }
 
    order_sig = signer.sign_place_order(order_data)
    print(f"Order signature: {order_sig}")
 
    # Cancel order
    cancel_data = {
        "subAccountId": "123456789",
        "orderId": "987654321",
        "nonce": int(time.time() * 1000)
    }
 
    cancel_sig = signer.sign_cancel_order(cancel_data)
    print(f"Cancel signature: {cancel_sig}")

Go Implementation

Using go-ethereum

package main
 
import (
    "crypto/ecdsa"
    "encoding/json"
    "fmt"
    "math/big"
    "time"
 
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/math"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/signer/core/apitypes"
)
 
type SynthetixSigner struct {
    privateKey *ecdsa.PrivateKey
    domain     apitypes.TypedDataDomain
}
 
func NewSynthetixSigner(privateKeyHex string, chainID int64) (*SynthetixSigner, error) {
    privateKey, err := crypto.HexToECDSA(privateKeyHex)
    if err != nil {
        return nil, err
    }
 
        domain := apitypes.TypedDataDomain{
        Name:              "Synthetix",
        Version:           "1",
        ChainId:           math.NewHexOrDecimal256(chainID),
        VerifyingContract: "0x0000000000000000000000000000000000000000",
    }
 
    return &SynthetixSigner{
        privateKey: privateKey,
        domain:     domain,
    }, nil
}
 
// SignWebSocketAuth signs authentication for WebSocket connection
func (s *SynthetixSigner) SignWebSocketAuth(subAccountId uint64, timestamp int64) (string, error) {
    // Use WebSocket domain name
    wsDomain := s.domain
    wsDomain.Name = "Synthetix"
 
    types := apitypes.Types{
        "EIP712Domain": {
            {"name", "string"},
            {"version", "string"},
            {"chainId", "uint256"},
        },
        "AuthMessage": {
            {"subAccountId", "uint256"},
            {"timestamp", "uint256"},
            {"action", "string"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(subAccountId)),
        "timestamp":    math.NewHexOrDecimal256(timestamp),
        "action":       "websocket_auth",
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "AuthMessage",
        Domain:      wsDomain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignPlaceOrder signs a new order request
func (s *SynthetixSigner) SignPlaceOrder(orderData OrderData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": {
            {"name", "string"},
            {"version", "string"},
            {"chainId", "uint256"},
        },
        "NewOrderRequest": {
            {"subAccountId", "uint64"},
            {"symbol", "string"},
            {"side", "string"},
            {"quantity", "string"},
            {"price", "string"},
            {"orderType", "string"},
            {"reduceOnly", "bool"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    // ALWAYS stringify order type for EIP-712
    orderTypeJSON, _ := json.Marshal(orderData.OrderType)
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(orderData.SubAccountID)),
        "symbol":       orderData.Symbol,
        "side":         orderData.Side,
        "quantity":     orderData.Quantity,
        "price":        orderData.Price,
        "orderType":    string(orderTypeJSON),
        "reduceOnly":   orderData.ReduceOnly,
        "nonce":        math.NewHexOrDecimal256(orderData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(orderData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "NewOrderRequest",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignCancelOrder signs a cancel order request
func (s *SynthetixSigner) SignCancelOrder(cancelData CancelData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": {
            {"name", "string"},
            {"version", "string"},
            {"chainId", "uint256"},
        },
        "CancelOrderRequest": {
            {"subAccountId", "uint64"},
            {"orderId", "uint64"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(cancelData.SubAccountID)),
        "orderId":      math.NewHexOrDecimal256(int64(cancelData.OrderID)),
        "nonce":        math.NewHexOrDecimal256(cancelData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(cancelData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "CancelOrderRequest",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignModifyOrder signs a modify order request (simplified structure)
func (s *SynthetixSigner) SignModifyOrder(modifyData ModifyData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": {
            {"name", "string"},
            {"version", "string"},
            {"chainId", "uint256"},
        },
        "ModifyOrderRequest": {
            {"subAccountId", "uint64"},
            {"orderId", "uint64"},
            {"price", "string"},
            {"quantity", "string"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(modifyData.SubAccountID)),
        "orderId":      math.NewHexOrDecimal256(int64(modifyData.OrderID)),
        "price":        modifyData.Price,
        "quantity":     modifyData.Quantity,
        "nonce":        math.NewHexOrDecimal256(modifyData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(modifyData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "ModifyOrderRequest",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// validateSignature validates EIP-712 signature components
func validateSignature(sig []byte) error {
    if len(sig) != 65 {
        return fmt.Errorf("invalid signature length: expected 65, got %d", len(sig))
    }
 
    // Validate v (recovery ID) - must be 0, 1, 27, or 28
    v := sig[64]
    if v != 0 && v != 1 && v != 27 && v != 28 {
        return fmt.Errorf("invalid v value: %d. Must be 0, 1, 27, or 28", v)
    }
 
    // r and s are validated by length (32 bytes each)
    // Additional validation could check they are non-zero
    r := new(big.Int).SetBytes(sig[:32])
    s := new(big.Int).SetBytes(sig[32:64])
 
    if r.Sign() == 0 {
        return fmt.Errorf("invalid r value: must be non-zero")
    }
    if s.Sign() == 0 {
        return fmt.Errorf("invalid s value: must be non-zero")
    }
 
    return nil
}
 
// signTypedData is a helper function to sign typed data and return hex signature
func (s *SynthetixSigner) signTypedData(typedData apitypes.TypedData) (string, error) {
    domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
    if err != nil {
        return "", fmt.Errorf("failed to hash domain: %w", err)
    }
 
    typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
    if err != nil {
        return "", fmt.Errorf("failed to hash message: %w", err)
    }
 
    // Calculate the digest
    rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
    digest := crypto.Keccak256Hash(rawData)
 
    // Sign the digest
    signature, err := crypto.Sign(digest.Bytes(), s.privateKey)
    if err != nil {
        return "", fmt.Errorf("failed to sign: %w", err)
    }
 
    // Adjust recovery ID for Ethereum
    if signature[64] < 27 {
        signature[64] += 27
    }
 
    // Validate signature before returning
    if err := validateSignature(signature); err != nil {
        return "", fmt.Errorf("invalid signature: %w", err)
    }
 
    return "0x" + common.Bytes2Hex(signature), nil
}
 
// Data structures
type OrderData struct {
    SubAccountID uint64      `json:"subAccountId"`
    Symbol       string      `json:"symbol"`
    Side         string      `json:"side"`
    Quantity     string      `json:"quantity"`
    Price        string      `json:"price"`
    OrderType    interface{} `json:"orderType"`
    ReduceOnly   bool        `json:"reduceOnly"`
    Nonce        int64       `json:"nonce"`
    ExpiresAfter int64       `json:"expiresAfter"`
}
 
type CancelData struct {
    SubAccountID uint64 `json:"subAccountId"`
    OrderID      uint64 `json:"orderId"`
    Nonce        int64  `json:"nonce"`
    ExpiresAfter int64  `json:"expiresAfter"`
}
 
type ModifyData struct {
    SubAccountID uint64 `json:"subAccountId"`
    OrderID      uint64 `json:"orderId"`
    Price        string `json:"price"`    // Optional - empty string if not changing
    Quantity     string `json:"quantity"` // Optional - empty string if not changing
    Nonce        int64  `json:"nonce"`
    ExpiresAfter int64  `json:"expiresAfter"`
}
 
// Usage example
func main() {
    signer, _ := NewSynthetixSigner("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1)
 
    // WebSocket authentication
    authSig, _ := signer.SignWebSocketAuth(123456789, time.Now().Unix())
    fmt.Printf("Auth signature: %s\n", authSig)
 
    // Place order
    orderData := OrderData{
        SubAccountID: 123456789,
        Symbol:       "BTC-USDT",
        Side:         "buy",
        Quantity:     "0.1",
        Price:        "50000",
        OrderType:    "limitGtc",  // This will be JSON.Marshal'd in SignPlaceOrder
        ReduceOnly:   false,
        Nonce:        time.Now().UnixMilli(),
    }
 
    orderSig, _ := signer.SignPlaceOrder(orderData)
    fmt.Printf("Order signature: %s\n", orderSig)
}

Signature Validation

Local Validation (JavaScript)

function validateSignature(signature) {
  // Check signature structure
  if (!signature || typeof signature !== 'object') {
    throw new Error('Signature must be an object');
  }
 
  const { v, r, s } = signature;
 
  // Validate v component (can be 0, 1, 27, or 28)
  if (typeof v !== 'number' || ![0, 1, 27, 28].includes(v)) {
    throw new Error('Invalid v component: must be 0, 1, 27, or 28');
  }
 
  // Validate r component
  if (typeof r !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(r)) {
    throw new Error('Invalid r component: must be 32-byte hex string');
  }
 
  // Validate s component
  if (typeof s !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(s)) {
    throw new Error('Invalid s component: must be 32-byte hex string');
  }
 
  return true;
}

Common Issues and Solutions

Issue: "Invalid signature" Error

Possible Causes:
  1. Wrong domain separator (must use zero address as verifying contract)
  2. Missing subAccountId field
  3. Incorrect message structure
  4. Order type not JSON.stringify'd (must be '"limitGtc"' not 'limitGtc')
  5. Wrong domain name (must be "Synthetix")
  6. Missing or wrong verifyingContract (must be zero address)
Solution:
// Verify domain matches exactly
const domain = {
  name: "Synthetix",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000000"
};
 
// ALWAYS stringify order types for EIP-712
const processedOrder = {
  ...order,
  orderType: JSON.stringify(order.orderType) // Required: '"limitGtc"' not 'limitGtc'
};
 
// Include subAccountId in all trading messages
const message = {
  subAccountId: "123456789", // Required field
  symbol: "BTC-USDT",
  // ... other fields
};

Issue: "Missing subAccountId" Error

Cause: New required field not included

Solution:
// SubAccountId required for all trading operations
const orderMessage = {
  subAccountId: BigInt(userSubAccountId), // Required
  symbol: "BTC-USDT",
  side: "buy",
  quantity: "0.1",
  price: "50000",
  orderType: "limitGtc",
  triggerPrice: "",
  isTriggerMarket: false,
  reduceOnly: false,
  nonce: BigInt(Date.now()),
  expiresAfter: BigInt(0)
};

Issue: "Nonce already used" Error

Cause: Replay protection triggered

Solution:
// Use consistent timestamp-based nonces (milliseconds)
const nonce = Date.now(); // Millisecond timestamp
// For WebSocket auth timestamp field
const timestamp = Date.now(); // Also use milliseconds for consistency

Issue: WebSocket Authentication Failed

Cause: Wrong domain name or message structure

Solution:
// Use correct domain for WebSocket authentication
const wsDomain = {
  name: "Synthetix",
  version: "1",
  chainId: 1
};
 
const authMessage = {
  subAccountId: BigInt(subAccountId),
  timestamp: BigInt(Date.now()), // Unix timestamp in milliseconds
  action: "websocket_auth" // Exact string required
};

Testing Your Implementation

// Test all signature types
async function testImplementation() {
  const signer = new SynthetixSigner("0x..." as `0x${string}`);
 
  try {
    // Test WebSocket auth
    const authSig = await signer.signWebSocketAuth("123456789");
    console.log('WebSocket auth signature valid');
 
    // Test order placement
    const orderSig = await signer.signPlaceOrder({
      subAccountId: "123456789",
      symbol: "BTC-USDT",
      side: "buy",
      quantity: "0.001",
      price: "99999", // Unfillable price for testing
      orderType: "limitGtc", // Will be JSON.stringify'd in signPlaceOrder
      reduceOnly: false,
      nonce: Date.now()
    });
    console.log('Order signature valid');
 
    // Test order cancellation
    const cancelSig = await signer.signCancelOrder({
      subAccountId: "123456789",
      orderId: "999999999",
      nonce: Date.now()
    });
    console.log('Cancel signature valid');
 
    // Test order modification
    const modifySig = await signer.signModifyOrder({
      subAccountId: "123456789",
      orderId: "999999999",
      price: "99998",
      quantity: "0.002",
      nonce: Date.now()
    });
    console.log('Modify signature valid');
 
  } catch (error) {
    console.error('Signature test failed:', error.message);
  }
}

Key Differences from Standard EIP-712

  1. Zero Address Contract: Off-chain authentication uses the zero address (0x0000000000000000000000000000000000000000) as the verifying contract
  2. SubAccount Required: All trading operations require a subAccountId field
  3. Simplified Modify: Modify orders only allow changing price and/or quantity (both optional)
  4. String Types: Financial values (price, quantity) are strings for precision
  5. JSON Stringify: Order types must be JSON.stringify'd for signing
  6. Domain Name: Unified domain name "Synthetix" for all authentication
  7. Consistent TIF Format: Time in force values use lowercase in both EIP-712 signing and API responses ("gtc", "alo", "ioc")

Important Note on Field Naming

EIP-712 signing and API responses now use consistent format:
  • Both EIP-712 signing and API responses: timeInForce: "gtc" (lowercase, full name)

Consistent format required across all API operations and signing workflows.

Next Steps