Skip to main content

Sign DerivaDEX requests with EIP-712

Use this when you need a clear implementation of the signing flow DerivaDEX expects. The examples use Python because the procedure is easiest to read there, but the same steps apply in any language that can build the same bytes and hashes. You need the chainId, the DerivaDEX verifying smart contract address, the exact request you plan to send, and the wallet key that controls the trader account. Use the network values you are signing for:
NetworkchainIdDerivaDEX verifying smart contract address
Testnet (Sepolia)111551110x5d1a3b4181d3cad422f404f28e9e972d0ba4dad6
Mainnet10x6fb8aa6fc6f27e591423009194529ae126660027
After the signature is correct, move to How to Encrypt Requests for the Operator.

Follow the signing procedure

  1. Define the EIP-191 prefix.
EIP191_HEADER = b"\x19\x01"
  1. Build the EIP-712 domain separator from the chainId and the DerivaDEX verifying smart contract address. Use the same domain values for every request:
    FieldValue
    nameDerivaDEX
    version1
    chainIdThe Ethereum chain ID for the DerivaDEX network you are signing for
    verifyingContractThe DerivaDEX verifying smart contract address for that same network
  2. Choose the request type you are signing, then build its message hash with the exact field order DerivaDEX expects.
    RequestStruct signatureFields, in order
    OrderOrderParamssymbol, strategy, side, orderType, nonce, amount, price, stopPrice
    Cancel one orderCancelOrderParamssymbol, orderHash, nonce
    Cancel all orders in one strategyCancelAllParamssymbol, strategy, nonce
    Update profileUpdateProfileParamspayFeesInDdxState, referralAddress, deniedDelegatedSessionAction, deniedDelegatedSessionAddress, deniedDelegatedSessionExpiry, nonce
    Withdraw collateralWithdrawParamsstrategyId, currency, amount, nonce
  3. Compute the final digest as keccak256(EIP191_HEADER + domain_separator + message_struct_hash).
  4. Sign that digest with the trader wallet.
  5. Recover the signer locally before submission and confirm that it matches the trader wallet you expect.

Worked examples

Start with the helper functions that stay the same across request types. Each request type can be implemented in two ways here:
  • Direct implementation shows the hashing steps directly.
  • DDX client library shows the shortest path if you already use the library.
Both approaches should produce the same EIP-712 hash for the same input.
from decimal import ROUND_DOWN, Decimal

from eth_abi import encode
from eth_utils.crypto import keccak

EIP191_HEADER = b"\x19\x01"
TOKEN_UNIT_SCALE = 6


def encode_string_as_bytes32(value: str) -> bytes:
    raw = value.encode("utf-8")
    if len(raw) > 31:
        raise ValueError("DerivaDEX string fields must fit in 31 bytes")
    return bytes([len(raw)]) + raw + bytes(31 - len(raw))


def scale_decimal(value: Decimal) -> int:
    fixed = value.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
    return int(fixed * (10 ** TOKEN_UNIT_SCALE))


def compute_eip712_domain_separator(chain_id: int, verifying_contract: str) -> bytes:
    domain_type_hash = keccak(
        b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
    )
    return keccak(
        domain_type_hash
        + keccak(b"DerivaDEX")
        + keccak(b"1")
        + encode(["uint256"], [chain_id])
        + encode(["address"], [verifying_contract])
    )


def compute_eip712_hash(
    chain_id: int,
    verifying_contract: str,
    message_struct_hash: bytes,
) -> str:
    domain_separator = compute_eip712_domain_separator(chain_id, verifying_contract)
    digest = keccak(EIP191_HEADER + domain_separator + message_struct_hash)
    return "0x" + digest.hex()

Example: sign an order request

This example follows the steps above for OrderParams.

Direct implementation

from dataclasses import dataclass
from enum import IntEnum


class OrderSide(IntEnum):
    BID = 0
    ASK = 1


class OrderType(IntEnum):
    LIMIT = 0
    MARKET = 1
    STOP = 2
    LIMIT_POST_ONLY = 3


@dataclass
class OrderIntent:
    symbol: str
    strategy: str
    side: OrderSide
    order_type: OrderType
    nonce: str
    amount: Decimal
    price: Decimal
    stop_price: Decimal


def compute_message_struct_hash(intent: OrderIntent) -> bytes:
    order_type_hash = keccak(
        b"OrderParams(bytes32 symbol,bytes32 strategy,uint256 side,uint256 orderType,bytes32 nonce,uint256 amount,uint256 price,uint256 stopPrice)"
    )
    return keccak(
        order_type_hash
        + encode(["bytes32"], [encode_string_as_bytes32(intent.symbol)])
        + encode(["bytes32"], [encode_string_as_bytes32(intent.strategy)])
        + encode(["uint256"], [intent.side])
        + encode(["uint256"], [intent.order_type])
        + encode(["bytes32"], [bytes.fromhex(intent.nonce.removeprefix("0x"))])
        + encode(["uint256"], [scale_decimal(intent.amount)])
        + encode(["uint256"], [scale_decimal(intent.price)])
        + encode(["uint256"], [scale_decimal(intent.stop_price)])
    )
Example input:
intent = OrderIntent(
    symbol="ETHP",
    strategy="main",
    side=OrderSide.BID,
    order_type=OrderType.LIMIT,
    nonce="0x3137373038373530313938323238333436363300000000000000000000000000",
    amount=Decimal("0.1"),
    price=Decimal("1800"),
    stop_price=Decimal("0"),
)

message_struct_hash = compute_message_struct_hash(intent)
eip712_hash = compute_eip712_hash(
    chain_id=11155111,
    verifying_contract="0x5d1a3b4181d3cad422f404f28e9e972d0ba4dad6",
    message_struct_hash=message_struct_hash,
)
For this input, the target result is:
"0xacdcc010cbe31e9387e8faf29d533bdfd20483d36d599e97b63fb8319933ee16"

DDX client library

If you already use ddx-python, this is the shortest path through the same procedure:
from ddx._rust.common.enums import OrderSide, OrderType
from ddx._rust.common.requests.intents import OrderIntent
from ddx._rust.decimal import Decimal

chain_id = 11155111
verifying_contract = "0x5d1a3b4181d3cad422f404f28e9e972d0ba4dad6"

order_intent = OrderIntent(
    "ETHP",
    "main",
    OrderSide.Bid,
    OrderType.Limit,
    "0x3137373038373530313938323238333436363300000000000000000000000000",
    Decimal("0.1"),
    Decimal("1800"),
    Decimal("0"),
    None,
)

eip712_hash = order_intent.hash_eip712((chain_id, verifying_contract))

print(f"Order hash: {eip712_hash}")
For this input, the DDX client library should return the same result as the native example above:
"0xacdcc010cbe31e9387e8faf29d533bdfd20483d36d599e97b63fb8319933ee16"

Sign the hash and recover the signer

Once the EIP-712 hash is correct, signing it is the short final step. This example uses a generated wallet only to show the mechanics:
from eth_account import Account

account = Account.create()
digest_bytes = bytes.fromhex(eip712_hash.removeprefix("0x"))

signed = account.signHash(digest_bytes)
signature = signed.signature.hex()

recovered_address = Account._recover_hash(digest_bytes, signature=signed.signature)

print(f"Address:   {account.address}")
print(f"Signature: {signature}")
print(f"Recovered: {recovered_address}")
The recovered address should match the wallet address that signed the hash. In production, use the trader wallet that is meant to authorize the request, not a generated example wallet. Use Signed Private Requests for the field tables behind cancel, cancel-all, profile-update, and withdraw request content.

Check your result before you submit

  1. Recover the signing address from the signature and digest.
  2. Confirm that it matches the trader wallet you expect to authorize the request.
If the recovered address is wrong, stop there. The operator will reject the request with SignatureRecoveryMismatch, SignerNotFound, or a later action-specific rejection.

Appendix

Use this section when you need the exact field rules behind the steps and examples.
Field familyRule
symbol, strategy, strategyIdEncode as bytes32 with one length byte, then UTF-8 bytes, then zero padding
nonceKeep it as a 32-byte hex value and make it monotonically increasing for the signer
Decimal valuesScale to fixed-point integers at 10^6, cutting off anything past six decimal places first
Order sideBid = 0, Ask = 1
Order typeLimit = 0, Market = 1, Stop = 2, LimitPostOnly = 3
Withdraw currencySign the ERC-20 token address directly
Profile updates need three extra rules:
FieldRule
payFeesInDdxStatefalse = 0, true = 1
Empty referralAddressUse zero-filled bytes32
Empty delegated-session updateUse action 0, zero address, and expiry 0

Continue with submission

After the signature is correct, fetch the operator key, encrypt the signed JSON, and submit the bytes to POST /v2/request. Use How to Encrypt Requests for the Operator for that step. If the request still fails after local recovery matches the trader wallet, use Error Reference for the exact rejection path.
Last modified on May 1, 2026