Skip to content

Signing Transactions

Every state-changing action on Blink is submitted as a signed transaction to the sequencer: placing and cancelling orders, moving margin, managing delegations. A transaction wraps a Borsh-encoded RuntimeCall in a signed envelope that carries the signature, a replay-protection value, and fee metadata.

The blink-tx-client crate builds, signs, and submits transactions. The wire format below specifies the bytes a custom client must produce.

Quick start

rust
use blink_tx_client::{TxClient, TxConfig};
use perps_module::CallMessage;

// Derives the address, chain id, and fee defaults from a 32-byte hex key.
let config = TxConfig::default_with_key("<private-key-hex>".to_string());

// Connects to the sequencer over TCP.
let client = TxClient::new_connected("<sequencer-host>", <port>, config).await?;

// Build a call. Fields are documented per call in the API reference.
let call = CallMessage::PlaceOrder { /* ... */ };

// Signs, stamps the uniqueness value, and submits.
let response = client.send_perps_call(call).await?;

TxClient exposes send_perps_call, send_spot_call, send_accounts_call, and cancel_all_orders / cancel_all_spot_orders. Each signs and submits in one call.

Transaction format

A signed transaction is Borsh-encoded and submitted as raw bytes. Its fields, in Borsh order:

FieldTypeDescription
signaturesecp256k1 signature (64 bytes)Signature over the signing payload below
pub_keysecp256k1 public key (65 bytes)The signer's uncompressed public key
runtime_callRuntimeCallThe Borsh-encoded call (accounts, spot, or perps)
uniquenessNonce(u64) or Generation(u64)See Replay protection
detailsfee and chain metadataSee below

details:

FieldTypeDescription
max_priority_fee_bipsu64Unused.
max_feeu128Unused.
gas_limitGas or nullUnused.
chain_idu64Rollup chain id (4321). Used for replay protection.

Only chain_id is enforced. The fee and gas fields are part of the signed envelope but are not currently used; leave them at the client defaults.

What gets signed

The signature covers the unsigned transaction concatenated with the chain's domain separator:

signing_payload = borsh(unsigned_tx) || CHAIN_HASH

unsigned_tx is { runtime_call, uniqueness, details }. Two values provide replay protection:

  • chain_id (4321), a field inside details.
  • CHAIN_HASH, a 32-byte domain separator derived at build time from the rollup's modules and DA configuration. It binds every signature to this exact rollup, so a transaction cannot be replayed on another deployment.

The payload is signed with the account's secp256k1 key.

Keys and addresses

  • Curve: secp256k1 (ECDSA), the same scheme as Ethereum.
  • Private key: 32 bytes, hex-encoded. A leading 0x is accepted.
  • Address: a 20-byte Ethereum-style address, hex-encoded (for example 0x9b08ce57a93751ae790698a2c9ebc76a78f23e25). It is the last 20 bytes of keccak256 over the 64-byte public key, the uncompressed key without its leading 0x04 byte: keccak256(pub_key[1:])[12:]. This address is used for balances, subaccounts, and delegations.

Replay protection

Every transaction carries a uniqueness value that prevents replay. Two modes exist:

  • Nonce(u64): a per-account counter that increments by one per transaction.
  • Generation(u64): a timestamp-style value.

Blink uses Generation, set to the current Unix time in microseconds; the client stamps it automatically. This avoids fetching a sequential nonce and prevents contention between concurrent submissions.

A generation must be unique for the account and recent enough to fall within the rollup's generation window, which tolerates minor clock skew. Duplicate or stale values are rejected. A custom client should use a microsecond clock value and retry with a fresh one if a transaction is rejected as stale.

Submitting

TxClient submits over a persistent TCP connection to the sequencer. A custom client sends the raw Borsh-encoded bytes (not hex or base64) to the sequencer's transaction endpoint.

API trading keys

For automated trading, authorize a separate delegate key with an expiry, scoped to trading actions: placing and cancelling orders and managing leverage. Delegate keys cannot withdraw. Sign with the delegate key and set the call's owner field to the account being traded on behalf of. See Sub-Accounts and Delegated Accounts.