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: Math.floor(Date.now() / 1000), // Unix timestamp in seconds
action: "websocket_auth"
};Place Orders
const placeOrderTypes = {
Order: [
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "orderType", type: "string" },
{ name: "price", type: "string" },
{ name: "triggerPrice", type: "string" },
{ name: "quantity", type: "string" },
{ name: "reduceOnly", type: "bool" },
{ name: "isTriggerMarket", type: "bool" },
{ name: "clientOrderId", type: "string" },
{ name: "closePosition", type: "bool" }
],
PlaceOrders: [
{ name: "subAccountId", type: "uint256" },
{ name: "orders", type: "Order[]" },
{ name: "grouping", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
// Order type values — plain strings, no JSON.stringify needed
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 = {
symbol: "BTC-USDT",
side: "buy",
orderType: "limitGtc", // Plain string value
price: "50000.00", // Required for limit orders, empty string for market
triggerPrice: "", // Required for trigger orders, empty string otherwise
quantity: "0.1",
reduceOnly: false,
isTriggerMarket: false, // For trigger orders: true = market execution
clientOrderId: "0x1234567890abcdef1234567890abcdef",
closePosition: false
};Cancel Orders
const cancelOrderTypes = {
CancelOrders: [
{ name: "subAccountId", type: "uint256" },
{ name: "orderIds", type: "uint256[]" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};Cancel All Orders
const cancelAllOrderTypes = {
CancelAllOrders: [
{ name: "subAccountId", type: "uint256" },
{ name: "symbol", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};Modify Order
const modifyOrderTypes = {
ModifyOrder: [
{ name: "subAccountId", type: "uint256" },
{ name: "orderId", type: "uint256" },
{ name: "price", type: "string" },
{ name: "quantity", type: "string" },
{ name: "triggerPrice", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};Update Leverage
const updateLeverageTypes = {
UpdateLeverage: [
{ name: "subAccountId", type: "uint256" },
{ name: "symbol", type: "string" },
{ name: "leverage", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};Create Subaccount
const createSubaccountTypes = {
CreateSubaccount: [
{ name: "masterSubAccountId", type: "uint256" },
{ name: "name", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
// Note: masterSubAccountId is the ID of an existing subaccount you own.
// It proves ownership and links the new subaccount to your master account.Update SubAccount Name
const updateSubAccountNameTypes = {
UpdateSubAccountName: [
{ name: "subAccountId", type: "uint256" },
{ name: "name", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};Add Delegated Signer
const addDelegatedSignerTypes = {
AddDelegatedSigner: [
{ name: "delegateAddress", type: "address" },
{ name: "subAccountId", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
{ name: "permissions", type: "string[]" }
]
};
// expiresAt: when the delegation permission expires (0 = no expiration)
// expiresAfter: when this signing request expires
// permissions: currently supports ["trading"]Remove All Delegated Signers
const removeAllDelegatedSignersTypes = {
RemoveAllDelegatedSigners: [
{ name: "subAccountId", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};SubAccount Actions
Used for: getPositions, getOpenOrders, getOrderHistory, getTrades, getFundingPayments, getSubAccount, getDelegatedSigners, getBalanceUpdates
const subAccountActionTypes = {
SubAccountAction: [
{ name: "subAccountId", type: "uint256" },
{ name: "action", type: "string" },
{ name: "expiresAfter", type: "uint256" }
]
};Nonce
The nonce field is a replay-protection value required for all state-changing actions (any action not starting with get). Each nonce must be a positive integer, incrementing and unique per request.
- Cryptographically random bytes converted to a uint256 (reference implementation approach)
Date.now()also works as a convenient non-zero unique value
The nonce is not validated as a timestamp — it only needs to be a positive integer, incrementing and unique per request.
expiresAfter
The expiresAfter field is an optional Unix timestamp in milliseconds that sets when the request expires. Use 0 to indicate no expiration.
const expiresAfter = Date.now() + 300000; // 5 minutes from now (milliseconds)JavaScript/TypeScript Implementation
Using viem
import { createWalletClient, http, parseSignature } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
import crypto from 'crypto';
// 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 || Math.floor(Date.now() / 1000)), // Unix seconds
action: "websocket_auth"
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "AuthMessage",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signPlaceOrder(orderData: {
subAccountId: string;
orders: any[];
grouping: string;
nonce?: number;
expiresAfter?: number;
}) {
const types = {
Order: [
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "orderType", type: "string" },
{ name: "price", type: "string" },
{ name: "triggerPrice", type: "string" },
{ name: "quantity", type: "string" },
{ name: "reduceOnly", type: "bool" },
{ name: "isTriggerMarket", type: "bool" },
{ name: "clientOrderId", type: "string" },
{ name: "closePosition", type: "bool" }
],
PlaceOrders: [
{ name: "subAccountId", type: "uint256" },
{ name: "orders", type: "Order[]" },
{ name: "grouping", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(orderData.subAccountId),
orders: orderData.orders,
grouping: orderData.grouping,
nonce: BigInt(orderData.nonce || Date.now()),
expiresAfter: BigInt(orderData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "PlaceOrders",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signCancelOrder(cancelData: {
subAccountId: string;
orderIds: string[];
nonce?: number;
expiresAfter?: number;
}) {
const types = {
CancelOrders: [
{ name: "subAccountId", type: "uint256" },
{ name: "orderIds", type: "uint256[]" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(cancelData.subAccountId),
orderIds: cancelData.orderIds.map(id => BigInt(id)),
nonce: BigInt(cancelData.nonce || Date.now()),
expiresAfter: BigInt(cancelData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "CancelOrders",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signCancelAllOrders(cancelAllData: {
subAccountId: string;
symbol: string;
nonce?: number;
expiresAfter?: number;
}) {
const types = {
CancelAllOrders: [
{ name: "subAccountId", type: "uint256" },
{ name: "symbol", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(cancelAllData.subAccountId),
symbol: cancelAllData.symbol,
nonce: BigInt(cancelAllData.nonce || Date.now()),
expiresAfter: BigInt(cancelAllData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "CancelAllOrders",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signModifyOrder(modifyData: {
subAccountId: string;
orderId: string;
price?: string;
quantity?: string;
triggerPrice?: string;
nonce?: number;
expiresAfter?: number;
}) {
const types = {
ModifyOrder: [
{ name: "subAccountId", type: "uint256" },
{ name: "orderId", type: "uint256" },
{ name: "price", type: "string" },
{ name: "quantity", type: "string" },
{ name: "triggerPrice", 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 || "",
triggerPrice: modifyData.triggerPrice || "",
nonce: BigInt(modifyData.nonce || Date.now()),
expiresAfter: BigInt(modifyData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "ModifyOrder",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signUpdateLeverage(leverageData: {
subAccountId: string;
symbol: string;
leverage: string;
nonce?: number;
expiresAfter?: number;
}) {
const types = {
UpdateLeverage: [
{ name: "subAccountId", type: "uint256" },
{ name: "symbol", type: "string" },
{ name: "leverage", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(leverageData.subAccountId),
symbol: leverageData.symbol,
leverage: leverageData.leverage,
nonce: BigInt(leverageData.nonce || Date.now()),
expiresAfter: BigInt(leverageData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "UpdateLeverage",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signCreateSubaccount(createData: {
masterSubAccountId: string;
name: string;
nonce?: number;
expiresAfter?: number;
}) {
const types = {
CreateSubaccount: [
{ name: "masterSubAccountId", type: "uint256" },
{ name: "name", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
masterSubAccountId: BigInt(createData.masterSubAccountId),
name: createData.name,
nonce: BigInt(createData.nonce || Date.now()),
expiresAfter: BigInt(createData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "CreateSubaccount",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signUpdateSubAccountName(updateData: {
subAccountId: string;
name: string;
nonce?: number;
expiresAfter?: number;
}) {
const types = {
UpdateSubAccountName: [
{ name: "subAccountId", type: "uint256" },
{ name: "name", type: "string" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(updateData.subAccountId),
name: updateData.name,
nonce: BigInt(updateData.nonce || Date.now()),
expiresAfter: BigInt(updateData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "UpdateSubAccountName",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signAddDelegatedSigner(delegateData: {
delegateAddress: string;
subAccountId: string;
nonce?: number;
expiresAfter?: number;
expiresAt?: number;
permissions: string[];
}) {
const types = {
AddDelegatedSigner: [
{ name: "delegateAddress", type: "address" },
{ name: "subAccountId", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
{ name: "permissions", type: "string[]" }
]
};
const message = {
delegateAddress: delegateData.delegateAddress,
subAccountId: BigInt(delegateData.subAccountId),
nonce: BigInt(delegateData.nonce || Date.now()),
expiresAfter: BigInt(delegateData.expiresAfter || 0),
expiresAt: BigInt(delegateData.expiresAt || 0),
permissions: delegateData.permissions
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "AddDelegatedSigner",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signRemoveAllDelegatedSigners(removeData: {
subAccountId: string;
nonce?: number;
expiresAfter?: number;
}) {
const types = {
RemoveAllDelegatedSigners: [
{ name: "subAccountId", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(removeData.subAccountId),
nonce: BigInt(removeData.nonce || Date.now()),
expiresAfter: BigInt(removeData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "RemoveAllDelegatedSigners",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
async signSubAccountAction(actionData: {
subAccountId: string;
action: string;
expiresAfter?: number;
}) {
const types = {
SubAccountAction: [
{ name: "subAccountId", type: "uint256" },
{ name: "action", type: "string" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(actionData.subAccountId),
action: actionData.action,
expiresAfter: BigInt(actionData.expiresAfter || 0)
};
const signature = await this.client.signTypedData({
domain: this.domain,
types,
primaryType: "SubAccountAction",
message
});
const parsedSig = parseSignature(signature);
validateSignature(parsedSig);
return parsedSig;
}
}
// Usage Example
async function example() {
const signer = new SynthetixSigner("0x..." as `0x${string}`);
// WebSocket authentication (timestamp in seconds)
const authSig = await signer.signWebSocketAuth("123456789");
console.log('Auth signature:', authSig);
// Place orders
const orderSig = await signer.signPlaceOrder({
subAccountId: "123456789",
orders: [{
symbol: "BTC-USDT",
side: "buy",
orderType: "limitGtc",
price: "50000",
triggerPrice: "",
quantity: "0.1",
reduceOnly: false,
isTriggerMarket: false,
clientOrderId: "0x1234567890abcdef1234567890abcdef",
closePosition: false
}],
grouping: "na",
nonce: Date.now()
});
console.log('Order signature:', orderSig);
// Cancel orders
const cancelSig = await signer.signCancelOrder({
subAccountId: "123456789",
orderIds: ["987654321", "987654322"],
nonce: Date.now()
});
console.log('Cancel signature:', cancelSig);
// Cancel all orders for a symbol
const cancelAllSig = await signer.signCancelAllOrders({
subAccountId: "123456789",
symbol: "BTC-USDT",
nonce: Date.now()
});
console.log('Cancel all signature:', cancelAllSig);
// Update leverage
const leverageSig = await signer.signUpdateLeverage({
subAccountId: "123456789",
symbol: "BTC-USDT",
leverage: "20",
nonce: Date.now()
});
console.log('Update leverage signature:', leverageSig);
// Create subaccount
const createSig = await signer.signCreateSubaccount({
masterSubAccountId: "123456789", // An existing subaccount you own
name: "New Trading Account",
nonce: Date.now()
});
console.log('Create subaccount signature:', createSig);
// Update subaccount name
const renameSig = await signer.signUpdateSubAccountName({
subAccountId: "123456789",
name: "Renamed Account",
nonce: Date.now()
});
console.log('Update subaccount name signature:', renameSig);
// Add delegated signer
const addDelegateSig = await signer.signAddDelegatedSigner({
delegateAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
subAccountId: "123456789",
permissions: ["trading"],
nonce: Date.now()
});
console.log('Add delegated signer signature:', addDelegateSig);
// Remove all delegated signers
const removeAllSig = await signer.signRemoveAllDelegatedSigners({
subAccountId: "123456789",
nonce: Date.now()
});
console.log('Remove all delegated signers signature:', removeAllSig);
// Get positions (no nonce required for SubAccountAction)
const positionsSig = await signer.signSubAccountAction({
subAccountId: "123456789",
action: "getPositions",
expiresAfter: Date.now() + 300000 // Optional: 5 minute expiration (milliseconds)
});
console.log('Positions signature:', positionsSig);
}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
import os
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_val = signature.get('r')
if not r_val or not re.match(r'^0x[a-fA-F0-9]{64}$', r_val):
raise ValueError("Invalid r value: must be 0x-prefixed 32-byte hex string")
# Validate s - must be 32-byte hex string
s_val = signature.get('s')
if not s_val or not re.match(r'^0x[a-fA-F0-9]{64}$', s_val):
raise ValueError("Invalid s value: must be 0x-prefixed 32-byte hex string")
return True
DOMAIN_FIELDS = [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
]
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 _sign(self, types, primary_type, message, domain=None):
typed_data = {
"types": {
"EIP712Domain": DOMAIN_FIELDS,
**types
},
"primaryType": primary_type,
"domain": domain or self.domain,
"message": message
}
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_websocket_auth(self, sub_account_id, timestamp=None):
if timestamp is None:
timestamp = int(time.time()) # Unix seconds
return self._sign(
types={
"AuthMessage": [
{"name": "subAccountId", "type": "uint256"},
{"name": "timestamp", "type": "uint256"},
{"name": "action", "type": "string"}
]
},
primary_type="AuthMessage",
message={
"subAccountId": sub_account_id,
"timestamp": timestamp,
"action": "websocket_auth"
}
)
def sign_place_order(self, order_data):
return self._sign(
types={
"Order": [
{"name": "symbol", "type": "string"},
{"name": "side", "type": "string"},
{"name": "orderType", "type": "string"},
{"name": "price", "type": "string"},
{"name": "triggerPrice", "type": "string"},
{"name": "quantity", "type": "string"},
{"name": "reduceOnly", "type": "bool"},
{"name": "isTriggerMarket", "type": "bool"},
{"name": "clientOrderId", "type": "string"},
{"name": "closePosition", "type": "bool"}
],
"PlaceOrders": [
{"name": "subAccountId", "type": "uint256"},
{"name": "orders", "type": "Order[]"},
{"name": "grouping", "type": "string"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="PlaceOrders",
message={
"subAccountId": order_data['subAccountId'],
"orders": order_data['orders'],
"grouping": order_data['grouping'],
"nonce": order_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": order_data.get('expiresAfter', 0)
}
)
def sign_cancel_order(self, cancel_data):
return self._sign(
types={
"CancelOrders": [
{"name": "subAccountId", "type": "uint256"},
{"name": "orderIds", "type": "uint256[]"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="CancelOrders",
message={
"subAccountId": cancel_data['subAccountId'],
"orderIds": cancel_data['orderIds'],
"nonce": cancel_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": cancel_data.get('expiresAfter', 0)
}
)
def sign_cancel_all_orders(self, cancel_all_data):
return self._sign(
types={
"CancelAllOrders": [
{"name": "subAccountId", "type": "uint256"},
{"name": "symbol", "type": "string"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="CancelAllOrders",
message={
"subAccountId": cancel_all_data['subAccountId'],
"symbol": cancel_all_data['symbol'],
"nonce": cancel_all_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": cancel_all_data.get('expiresAfter', 0)
}
)
def sign_modify_order(self, modify_data):
return self._sign(
types={
"ModifyOrder": [
{"name": "subAccountId", "type": "uint256"},
{"name": "orderId", "type": "uint256"},
{"name": "price", "type": "string"},
{"name": "quantity", "type": "string"},
{"name": "triggerPrice", "type": "string"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="ModifyOrder",
message={
"subAccountId": modify_data['subAccountId'],
"orderId": modify_data['orderId'],
"price": modify_data.get('price', ""),
"quantity": modify_data.get('quantity', ""),
"triggerPrice": modify_data.get('triggerPrice', ""),
"nonce": modify_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": modify_data.get('expiresAfter', 0)
}
)
def sign_update_leverage(self, leverage_data):
return self._sign(
types={
"UpdateLeverage": [
{"name": "subAccountId", "type": "uint256"},
{"name": "symbol", "type": "string"},
{"name": "leverage", "type": "string"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="UpdateLeverage",
message={
"subAccountId": leverage_data['subAccountId'],
"symbol": leverage_data['symbol'],
"leverage": leverage_data['leverage'],
"nonce": leverage_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": leverage_data.get('expiresAfter', 0)
}
)
def sign_create_subaccount(self, create_data):
return self._sign(
types={
"CreateSubaccount": [
{"name": "masterSubAccountId", "type": "uint256"},
{"name": "name", "type": "string"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="CreateSubaccount",
message={
"masterSubAccountId": create_data['masterSubAccountId'],
"name": create_data['name'],
"nonce": create_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": create_data.get('expiresAfter', 0)
}
)
def sign_update_subaccount_name(self, update_data):
return self._sign(
types={
"UpdateSubAccountName": [
{"name": "subAccountId", "type": "uint256"},
{"name": "name", "type": "string"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="UpdateSubAccountName",
message={
"subAccountId": update_data['subAccountId'],
"name": update_data['name'],
"nonce": update_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": update_data.get('expiresAfter', 0)
}
)
def sign_add_delegated_signer(self, delegate_data):
return self._sign(
types={
"AddDelegatedSigner": [
{"name": "delegateAddress", "type": "address"},
{"name": "subAccountId", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"},
{"name": "expiresAt", "type": "uint256"},
{"name": "permissions", "type": "string[]"}
]
},
primary_type="AddDelegatedSigner",
message={
"delegateAddress": delegate_data['delegateAddress'],
"subAccountId": delegate_data['subAccountId'],
"nonce": delegate_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": delegate_data.get('expiresAfter', 0),
"expiresAt": delegate_data.get('expiresAt', 0),
"permissions": delegate_data['permissions']
}
)
def sign_remove_all_delegated_signers(self, remove_data):
return self._sign(
types={
"RemoveAllDelegatedSigners": [
{"name": "subAccountId", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="RemoveAllDelegatedSigners",
message={
"subAccountId": remove_data['subAccountId'],
"nonce": remove_data.get('nonce', int(time.time() * 1000)),
"expiresAfter": remove_data.get('expiresAfter', 0)
}
)
def sign_subaccount_action(self, action_data):
return self._sign(
types={
"SubAccountAction": [
{"name": "subAccountId", "type": "uint256"},
{"name": "action", "type": "string"},
{"name": "expiresAfter", "type": "uint256"}
]
},
primary_type="SubAccountAction",
message={
"subAccountId": action_data['subAccountId'],
"action": action_data['action'],
"expiresAfter": action_data.get('expiresAfter', 0)
}
)
# Usage Example
def example():
signer = SynthetixSigner(private_key="0x...")
# WebSocket authentication (timestamp in seconds)
auth_sig = signer.sign_websocket_auth(sub_account_id=123456789)
print(f"Auth signature: {auth_sig}")
# Place orders
order_sig = signer.sign_place_order({
"subAccountId": 123456789,
"orders": [{
"symbol": "BTC-USDT",
"side": "buy",
"orderType": "limitGtc",
"price": "50000",
"triggerPrice": "",
"quantity": "0.1",
"reduceOnly": False,
"isTriggerMarket": False,
"clientOrderId": "0x1234567890abcdef1234567890abcdef",
"closePosition": False
}],
"grouping": "na",
"nonce": int(time.time() * 1000)
})
print(f"Order signature: {order_sig}")
# Cancel orders
cancel_sig = signer.sign_cancel_order({
"subAccountId": "123456789",
"orderIds": ["987654321", "987654322"],
"nonce": int(time.time() * 1000)
})
print(f"Cancel signature: {cancel_sig}")
# Cancel all orders for a symbol
cancel_all_sig = signer.sign_cancel_all_orders({
"subAccountId": "123456789",
"symbol": "BTC-USDT",
"nonce": int(time.time() * 1000)
})
print(f"Cancel all signature: {cancel_all_sig}")
# Update leverage
leverage_sig = signer.sign_update_leverage({
"subAccountId": "123456789",
"symbol": "BTC-USDT",
"leverage": "20",
"nonce": int(time.time() * 1000)
})
print(f"Update leverage signature: {leverage_sig}")
# Create subaccount
create_sig = signer.sign_create_subaccount({
"masterSubAccountId": "123456789",
"name": "New Trading Account",
"nonce": int(time.time() * 1000)
})
print(f"Create subaccount signature: {create_sig}")
# Update subaccount name
rename_sig = signer.sign_update_subaccount_name({
"subAccountId": "123456789",
"name": "Renamed Account",
"nonce": int(time.time() * 1000)
})
print(f"Update subaccount name signature: {rename_sig}")
# Add delegated signer
add_delegate_sig = signer.sign_add_delegated_signer({
"delegateAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
"subAccountId": "123456789",
"permissions": ["trading"],
"nonce": int(time.time() * 1000)
})
print(f"Add delegated signer signature: {add_delegate_sig}")
# Remove all delegated signers
remove_all_sig = signer.sign_remove_all_delegated_signers({
"subAccountId": "123456789",
"nonce": int(time.time() * 1000)
})
print(f"Remove all delegated signers signature: {remove_all_sig}")
# Get positions (no nonce required for SubAccountAction)
positions_sig = signer.sign_subaccount_action({
"subAccountId": "123456789",
"action": "getPositions",
"expiresAfter": int(time.time() * 1000) + 300000 # Optional: 5 minute expiration (ms)
})
print(f"Positions signature: {positions_sig}")Go Implementation
Using go-ethereum
package main
import (
"crypto/ecdsa"
"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"
)
var domainFields = apitypes.Types{
"EIP712Domain": {
{"name", "string"},
{"version", "string"},
{"chainId", "uint256"},
{"verifyingContract", "address"},
},
}
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) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"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: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignPlaceOrder signs a place orders request
func (s *SynthetixSigner) SignPlaceOrder(orderData PlaceOrderData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"Order": {
{"symbol", "string"},
{"side", "string"},
{"orderType", "string"},
{"price", "string"},
{"triggerPrice", "string"},
{"quantity", "string"},
{"reduceOnly", "bool"},
{"isTriggerMarket", "bool"},
{"clientOrderId", "string"},
{"closePosition", "bool"},
},
"PlaceOrders": {
{"subAccountId", "uint256"},
{"orders", "Order[]"},
{"grouping", "string"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
},
}
message := apitypes.TypedDataMessage{
"subAccountId": math.NewHexOrDecimal256(int64(orderData.SubAccountID)),
"orders": orderData.Orders,
"grouping": orderData.Grouping,
"nonce": math.NewHexOrDecimal256(orderData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(orderData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "PlaceOrders",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignCancelOrder signs a cancel orders request
func (s *SynthetixSigner) SignCancelOrder(cancelData CancelData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"CancelOrders": {
{"subAccountId", "uint256"},
{"orderIds", "uint256[]"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
},
}
message := apitypes.TypedDataMessage{
"subAccountId": math.NewHexOrDecimal256(int64(cancelData.SubAccountID)),
"orderIds": cancelData.OrderIDs,
"nonce": math.NewHexOrDecimal256(cancelData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(cancelData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "CancelOrders",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignCancelAllOrders signs a cancel all orders request
func (s *SynthetixSigner) SignCancelAllOrders(cancelAllData CancelAllData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"CancelAllOrders": {
{"subAccountId", "uint256"},
{"symbol", "string"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
},
}
message := apitypes.TypedDataMessage{
"subAccountId": math.NewHexOrDecimal256(int64(cancelAllData.SubAccountID)),
"symbol": cancelAllData.Symbol,
"nonce": math.NewHexOrDecimal256(cancelAllData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(cancelAllData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "CancelAllOrders",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignModifyOrder signs a modify order request
func (s *SynthetixSigner) SignModifyOrder(modifyData ModifyData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"ModifyOrder": {
{"subAccountId", "uint256"},
{"orderId", "uint256"},
{"price", "string"},
{"quantity", "string"},
{"triggerPrice", "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,
"triggerPrice": modifyData.TriggerPrice,
"nonce": math.NewHexOrDecimal256(modifyData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(modifyData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "ModifyOrder",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignUpdateLeverage signs an update leverage request
func (s *SynthetixSigner) SignUpdateLeverage(leverageData LeverageData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"UpdateLeverage": {
{"subAccountId", "uint256"},
{"symbol", "string"},
{"leverage", "string"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
},
}
message := apitypes.TypedDataMessage{
"subAccountId": math.NewHexOrDecimal256(int64(leverageData.SubAccountID)),
"symbol": leverageData.Symbol,
"leverage": leverageData.Leverage,
"nonce": math.NewHexOrDecimal256(leverageData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(leverageData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "UpdateLeverage",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignCreateSubaccount signs a create subaccount request
func (s *SynthetixSigner) SignCreateSubaccount(createData CreateSubaccountData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"CreateSubaccount": {
{"masterSubAccountId", "uint256"},
{"name", "string"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
},
}
message := apitypes.TypedDataMessage{
"masterSubAccountId": math.NewHexOrDecimal256(int64(createData.MasterSubAccountID)),
"name": createData.Name,
"nonce": math.NewHexOrDecimal256(createData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(createData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "CreateSubaccount",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignUpdateSubAccountName signs an update subaccount name request
func (s *SynthetixSigner) SignUpdateSubAccountName(updateData UpdateSubAccountNameData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"UpdateSubAccountName": {
{"subAccountId", "uint256"},
{"name", "string"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
},
}
message := apitypes.TypedDataMessage{
"subAccountId": math.NewHexOrDecimal256(int64(updateData.SubAccountID)),
"name": updateData.Name,
"nonce": math.NewHexOrDecimal256(updateData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(updateData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "UpdateSubAccountName",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignAddDelegatedSigner signs an add delegated signer request
func (s *SynthetixSigner) SignAddDelegatedSigner(delegateData AddDelegatedSignerData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"AddDelegatedSigner": {
{"delegateAddress", "address"},
{"subAccountId", "uint256"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
{"expiresAt", "uint256"},
{"permissions", "string[]"},
},
}
message := apitypes.TypedDataMessage{
"delegateAddress": delegateData.DelegateAddress,
"subAccountId": math.NewHexOrDecimal256(int64(delegateData.SubAccountID)),
"nonce": math.NewHexOrDecimal256(delegateData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(delegateData.ExpiresAfter),
"expiresAt": math.NewHexOrDecimal256(delegateData.ExpiresAt),
"permissions": delegateData.Permissions,
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "AddDelegatedSigner",
Domain: s.domain,
Message: message,
}
return s.signTypedData(typedData)
}
// SignRemoveAllDelegatedSigners signs a remove all delegated signers request
func (s *SynthetixSigner) SignRemoveAllDelegatedSigners(removeData RemoveAllDelegatedSignersData) (string, error) {
types := apitypes.Types{
"EIP712Domain": domainFields["EIP712Domain"],
"RemoveAllDelegatedSigners": {
{"subAccountId", "uint256"},
{"nonce", "uint256"},
{"expiresAfter", "uint256"},
},
}
message := apitypes.TypedDataMessage{
"subAccountId": math.NewHexOrDecimal256(int64(removeData.SubAccountID)),
"nonce": math.NewHexOrDecimal256(removeData.Nonce),
"expiresAfter": math.NewHexOrDecimal256(removeData.ExpiresAfter),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "RemoveAllDelegatedSigners",
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)
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 OrderItem struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"orderType"`
Price string `json:"price"`
TriggerPrice string `json:"triggerPrice"`
Quantity string `json:"quantity"`
ReduceOnly bool `json:"reduceOnly"`
IsTriggerMarket bool `json:"isTriggerMarket"`
ClientOrderId string `json:"clientOrderId"`
ClosePosition bool `json:"closePosition"`
}
type PlaceOrderData struct {
SubAccountID uint64 `json:"subAccountId"`
Orders []interface{} `json:"orders"` // Typed data message format
Grouping string `json:"grouping"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
type CancelData struct {
SubAccountID uint64 `json:"subAccountId"`
OrderIDs []interface{} `json:"orderIds"` // uint256[] as typed data
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
type CancelAllData struct {
SubAccountID uint64 `json:"subAccountId"`
Symbol string `json:"symbol"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
type ModifyData struct {
SubAccountID uint64 `json:"subAccountId"`
OrderID uint64 `json:"orderId"`
Price string `json:"price"`
Quantity string `json:"quantity"`
TriggerPrice string `json:"triggerPrice"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
type LeverageData struct {
SubAccountID uint64 `json:"subAccountId"`
Symbol string `json:"symbol"`
Leverage string `json:"leverage"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
type CreateSubaccountData struct {
MasterSubAccountID uint64 `json:"masterSubAccountId"`
Name string `json:"name"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
type UpdateSubAccountNameData struct {
SubAccountID uint64 `json:"subAccountId"`
Name string `json:"name"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
type AddDelegatedSignerData struct {
DelegateAddress string `json:"delegateAddress"`
SubAccountID uint64 `json:"subAccountId"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
ExpiresAt int64 `json:"expiresAt"`
Permissions []string `json:"permissions"`
}
type RemoveAllDelegatedSignersData struct {
SubAccountID uint64 `json:"subAccountId"`
Nonce int64 `json:"nonce"`
ExpiresAfter int64 `json:"expiresAfter"`
}
// Usage example
func main() {
signer, _ := NewSynthetixSigner("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1)
// WebSocket authentication (timestamp in seconds)
authSig, _ := signer.SignWebSocketAuth(123456789, time.Now().Unix())
fmt.Printf("Auth signature: %s\n", authSig)
// Place order
orderData := PlaceOrderData{
SubAccountID: 123456789,
Orders: []interface{}{
map[string]interface{}{
"symbol": "BTC-USDT",
"side": "buy",
"orderType": "limitGtc",
"price": "50000",
"triggerPrice": "",
"quantity": "0.1",
"reduceOnly": false,
"isTriggerMarket": false,
"clientOrderId": "0x1234567890abcdef1234567890abcdef",
"closePosition": false,
},
},
Grouping: "na",
Nonce: time.Now().UnixMilli(),
}
orderSig, _ := signer.SignPlaceOrder(orderData)
fmt.Printf("Order signature: %s\n", orderSig)
// Cancel all orders for a symbol
cancelAllData := CancelAllData{
SubAccountID: 123456789,
Symbol: "BTC-USDT",
Nonce: time.Now().UnixMilli(),
}
cancelAllSig, _ := signer.SignCancelAllOrders(cancelAllData)
fmt.Printf("Cancel all signature: %s\n", cancelAllSig)
// Update leverage
leverageData := LeverageData{
SubAccountID: 123456789,
Symbol: "BTC-USDT",
Leverage: "20",
Nonce: time.Now().UnixMilli(),
}
leverageSig, _ := signer.SignUpdateLeverage(leverageData)
fmt.Printf("Leverage signature: %s\n", leverageSig)
// Create subaccount
createData := CreateSubaccountData{
MasterSubAccountID: 123456789, // An existing subaccount you own
Name: "New Trading Account",
Nonce: time.Now().UnixMilli(),
}
createSig, _ := signer.SignCreateSubaccount(createData)
fmt.Printf("Create subaccount signature: %s\n", createSig)
// Update subaccount name
updateNameData := UpdateSubAccountNameData{
SubAccountID: 123456789,
Name: "Renamed Account",
Nonce: time.Now().UnixMilli(),
}
updateNameSig, _ := signer.SignUpdateSubAccountName(updateNameData)
fmt.Printf("Update subaccount name signature: %s\n", updateNameSig)
// Add delegated signer
addDelegateData := AddDelegatedSignerData{
DelegateAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
SubAccountID: 123456789,
Permissions: []string{"trading"},
Nonce: time.Now().UnixMilli(),
}
addDelegateSig, _ := signer.SignAddDelegatedSigner(addDelegateData)
fmt.Printf("Add delegated signer signature: %s\n", addDelegateSig)
// Remove all delegated signers
removeAllData := RemoveAllDelegatedSignersData{
SubAccountID: 123456789,
Nonce: time.Now().UnixMilli(),
}
removeAllSig, _ := signer.SignRemoveAllDelegatedSigners(removeAllData)
fmt.Printf("Remove all delegated signers signature: %s\n", removeAllSig)
}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
- 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"
};
// orderType is a plain string — no JSON.stringify needed
const order = {
symbol: "BTC-USDT",
side: "buy",
orderType: "limitGtc", // Plain string value
price: "50000",
// ... other fields
};
// Include subAccountId in all trading messages
const message = {
subAccountId: "123456789", // Required field
// ... 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 — the nonce value was already used for a previous request.
Solution:// Each nonce must be a positive integer, incrementing and unique per request
// Option 1: Use crypto random (recommended - guaranteed unique)
const nonce = BigInt('0x' + crypto.randomBytes(8).toString('hex'));
// Option 2: Use timestamp (simple, usually unique)
const nonce = Date.now();
// For WebSocket auth timestamp field (this IS a timestamp, in seconds)
const timestamp = Math.floor(Date.now() / 1000);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,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
const authMessage = {
subAccountId: BigInt(subAccountId),
timestamp: BigInt(Math.floor(Date.now() / 1000)), // Unix timestamp in seconds
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",
orders: [{
symbol: "BTC-USDT",
side: "buy",
orderType: "limitGtc",
price: "99999",
triggerPrice: "",
quantity: "0.001",
reduceOnly: false,
isTriggerMarket: false,
clientOrderId: "0x1234567890abcdef1234567890abcdef",
closePosition: false
}],
grouping: "na",
nonce: Date.now()
});
console.log('Order signature valid');
// Test order cancellation
const cancelSig = await signer.signCancelOrder({
subAccountId: "123456789",
orderIds: ["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
price,quantity, and/ortriggerPrice(all optional) - String Types: Financial values (
price,quantity) are strings for precision - Domain Name: Unified domain name "Synthetix" for all authentication
- Consistent TIF Format: Time in force values use PascalCase 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"(PascalCase)
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