Skip to content

Message Signing

When interacting with the Ethereal platform, many operations like trading and account management require cryptographic signatures to authenticate and authorize your actions. This guide explains how message signing works in the Ethereal Python SDK in a beginner-friendly way.

What is Message Signing?

The Ethereal API uses signed messages to verify the authenticity of requests. EIP-712 signatures are used on all POST endpoints, for example:

  • Submit orders
  • Withdraw funds
  • Link and authorize new signers

Signing Workflow Patterns

The SDK supports two primary workflows for handling signatures:

  1. Simplified Workflow: One-step methods that automatically handle preparing, signing, and submitting operations with user-friendly inputs
  2. Advanced Workflow: Three-step methods that separate operations into prepare -> sign -> submit

Simplified Workflow

When you initialize a client with your private key, the SDK has convenience methods that automatically handle the signing and submissions of orders, withdrawals, and other operations. This is the easiest way to get started.:

from ethereal import RESTClient

# Initialize with your private key
client = RESTClient({
    "base_url": "https://api.etherealtest.net",
    "chain_config": {
        "rpc_url": "https://rpc.etherealtest.net",
        "private_key": "your_private_key"  # Used for signing
    }
})

# Place an order with automatic preparation, signing, and submission
order = client.create_order(
    order_type="LIMIT",
    quantity=0.1,
    side=0,  # Buy
    price=80000.0,
    ticker="BTCUSD"
)

Behind the scenes, when you call methods like create_order, the SDK:

  1. Prepares the order data (prepare_order)
  2. Structures it according to EIP-712 format
  3. Signs it with your private key (sign_order)
  4. Sends the order data and signature to the API (submit_order or dry_run_order)

Using the sign and submit Arguments

These functions also contain sign and submit arguments that allow you to control the signing and submission process:

  • sign: If set to False, the SDK will prepare the order with an empty signature. This is when you want to sign the order using a key management system outside the SDK.
  • submit: If set to False, the SDK will prepare the order but not submit it to the API. The prepared payload will be returned, and can be submitted using the advanced workflow.

Advanced Workflow (Prepare -> Sign -> Submit)

For more control over the signing process, you can break down operations into three distinct steps:

  1. Prepare: Creates the data object required for the operation. Signing at this stage is optional.
  2. Sign: Adds a cryptographic signature to the prepared data object.
  3. Submit: Sends the signed data to the corresponding API endpoint.

This pattern allows for:

  • Inspecting and validating payloads before signing
  • Preparing payloads in bulk without any API calls
  • Dry-running operations to check potential outcomes
  • Using signing keys external to the SDK (e.g., hardware wallets)
  • Implementing custom signing logic

Example with orders:

# 1. PREPARE: Create the unsigned order payload
unsigned_order = client.prepare_order(
    sender=client.chain.address,
    price=50000,
    quantity=0.1,
    side=0,  # Buy
    subaccount=client.subaccounts[0].name,
    onchain_id=1,  # BTCUSD product ID
    order_type="LIMIT",
    time_in_force="GTD",
    post_only=False
)

# Optional: Dry run the order to check outcomes before signing
dry_run_result = client.dry_run_order(unsigned_order)
print(f"Order would result in: {dry_run_result}")

# 2. SIGN: Add your signature to the prepared order
signed_order = client.sign_order(unsigned_order)

# 3. SUBMIT: Send the signed order to the API
order_result = client.submit_order(signed_order)

Private Key Management

If you want to use a different private key for signing, you can specify the private_key argument in any of the sign_* methods. This allows you to use different keys for different operations or to sign messages in a separate environment.

You have two options for preparing the original payload:

  1. Use the simplified workflow with sign and submit set to False
  2. Use the advanced workflow (prepare_*) with include_signature set to False
unsigned_order = client.create_order(
    order_type="LIMIT",
    quantity=0.1,
    side=0,
    price=80000.0,
    ticker="BTCUSD",
    sign=False,
    submit=False,
)
# OR
unsigned_order = client.prepare_order(
    sender=client.chain.address,
    price=80000.0,
    quantity=0.1,
    side=0,
    subaccount=client.subaccounts[0].name,
    onchain_id=1,
    order_type="LIMIT",
    time_in_force="GTD",
    include_signature=False
)

# Sign with a different private key
signed_order = client.sign_order(
    unsigned_order,
    private_key="0xYourOtherPrivateKey"
)

Signature Components

A signature in the Ethereal SDK consists of several key components:

1. Domain Data

Domain data provides context about where the signature will be used. It is stored on the SDK:

domain = client.rpc_config.domain

print(domain.model_dump())
# {
#     'name': 'Ethereal',
#     'version': '1',
#     'chainId': 996353,
#     'verifyingContract': '0xA36D95Cd669f61833a0e9eaA2dC3389229Ee3D47'
# }

2. Types Definition

Types define the structure of the data being signed. These types are also available from the API, and are stored on the SDK at initialization:

types = client.rpc_config.signatureTypes
trade_order_types = client.chain.convert_types(types.TradeOrder)

print(types.model_dump())
# {
#     "LinkSigner": "address sender,address signer,bytes32 subaccount,uint64 nonce,uint64 signedAt",
#     "TradeOrder": "address sender,bytes32 subaccount,uint128 quantity,uint128 price,bool reduceOnly,uint8 side,uint8 engineType,uint32 productId,uint64 nonce,uint64 signedAt",
#     "InitiateWithdraw": "address account,bytes32 subaccount,address token,uint256 amount,uint64 nonce,uint64 signedAt",
#     "UpdateFunding": "uint32 productId,int128 fundingDeltaUsd",
#     "RevokeLinkedSigner": "address sender,address signer,bytes32 subaccount,uint64 nonce,uint64 signedAt",
#     "CancelOrder": "address sender, bytes32 subaccount, uint64 nonce, bytes32[] orderIds",
# }

print(trade_order_types)
# [
#     {"name": "sender", "type": "address"},
#     {"name": "subaccount", "type": "bytes32"},
#     {"name": "quantity", "type": "uint128"},
#     {"name": "price", "type": "uint128"},
#     {"name": "reduceOnly", "type": "bool"},
#     {"name": "side", "type": "uint8"},
#     {"name": "engineType", "type": "uint8"},
#     {"name": "productId", "type": "uint32"},
#     {"name": "nonce", "type": "uint64"},
#     {"name": "signedAt", "type": "uint64"},
# ]

The SDK provides methods to automatically generate these types.

3. Message Data

The message data contains the actual values you're signing:

message = {
    'sender': '0x80606C5b3602b7cbf8FDD06e12308504A941400b',
    'subaccount': '0x7072696d61727900000000000000000000000000000000000000000000000000',
    'quantity': 1000000000,  # Normalized with correct precision
    'price': 50000000000000,
    # ... other values
}

Core Signing Methods

The SDK provides several methods for working with signatures:

sign_message

This is the primary method used to create a signature:

signature = client.chain.sign_message(
    private_key,  # Your private key
    domain,       # Domain data
    types,        # Type definitions
    primary_type, # Primary type (e.g., 'TradeOrder')
    message       # Message data
)

get_signature_types

Converts the API's type definitions into the required EIP-712 format:

types = client.chain.get_signature_types(client.rpc_config, "TradeOrder")

convert_types

Processes type strings into structured type definitions:

# Converting a string like "address sender, bytes32 subaccount"
fields = client.chain.convert_types("address sender, bytes32 subaccount")
# Results in: [{"name": "sender", "type": "address"}, {"name": "subaccount", "type": "bytes32"}]

Complete Implementation Example

Here's a complete example showing both workflows side-by-side:

import time
from ethereal import RESTClient

client = RESTClient({
    "base_url": "https://api.etherealtest.net",
    "chain_config": {
        "rpc_url": "https://rpc.etherealtest.net",
        "private_key": "your_private_key"
    }
})

# Simplified workflow (all-in-one)
simple_order = client.create_order(
    order_type="LIMIT",
    quantity=0.1,
    side=0,
    price=80000.0,
    ticker="BTCUSD"
)

# Advanced workflow (prepare -> sign -> submit)
# 1. Prepare the order
prepared_order = client.prepare_order(
    sender=client.chain.address,
    price="80000.0",
    quantity="0.1",
    side=0,
    subaccount="primary",
    onchain_id=1,  # BTCUSD product ID
    order_type="LIMIT",
    time_in_force="GTD",
    include_signature=False  # Don't sign yet
)

# Optional: Dry run to check potential outcomes
dry_run_result = client.dry_run_order(prepared_order)

# 2. Sign the order when ready
signed_order = client.sign_order(prepared_order)

# 3. Submit the signed order
order_result = client.submit_order(signed_order)

Choosing the Right Approach

  • Use the simplified workflow for quick, straightforward operations where you don't need additional validation
  • Use the advanced workflow when you need:
  • Additional validation before signing (e.g., dry runs)
  • Multiple signatures from different keys
  • Custom signature logic
  • To prepare operations in one system and sign in another
  • To implement specialized transaction workflows