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:- Wrong domain separator (must use zero address as verifying contract)
- Missing
subAccountIdfield - Incorrect message structure
- Order type not JSON.stringify'd (must be '"limitGtc"' not 'limitGtc')
- Wrong domain name (must be "Synthetix")
- Missing or wrong verifyingContract (must be zero address)
// 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 consistencyIssue: 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
- Zero Address Contract: Off-chain authentication uses the zero address (0x0000000000000000000000000000000000000000) as the verifying contract
- SubAccount Required: All trading operations require a
subAccountIdfield - Simplified Modify: Modify orders only allow changing
priceand/orquantity(both optional) - String Types: Financial values (
price,quantity) are strings for precision - JSON Stringify: Order types must be JSON.stringify'd for signing
- Domain Name: Unified domain name "Synthetix" for all authentication
- 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
- Nonce Management - Proper nonce handling strategies
- Troubleshooting - Debug authentication issues
- WebSocket Guide - WebSocket implementation
- REST API Guide - REST endpoint usage