Signing pattern library for exchange authentication.
Provides a unified interface for signing API requests across the in-scope exchange registry. Authored HMAC recipes cover the common exchange families, with first-party modules for venue-specific signers:
| Pattern | In-scope use | Description |
|---|---|---|
:hmac_sha256_query | Binance family | Sign query string |
:hmac_sha256_headers | Bybit-style | Sign body/headers |
:hmac_sha256_iso_passphrase | OKX-style | ISO timestamp + passphrase |
:deribit | Deribit | Custom Authorization header format |
:hyperliquid | Hyperliquid | EIP-712 / action signing |
:derive | Derive | EIP-712 order/message signing |
:lighter | Lighter | Isolated official zk-Schnorr signer |
:api_key_secret_headers | Alpaca-style | API key and secret headers |
Usage
signed = Bourse.Signing.sign(
:hmac_sha256_headers,
%Bourse.Signing.Request{method: :get, path: "/v5/account/wallet-balance"},
credentials,
signing_config
)Pattern selection is authored in each supported runtime spec. Consumers cannot register reference-only exchanges or inject long-tail signers.
Summary
Functions
Decodes a Base64-encoded string. Raises on invalid input.
Base64-encodes a binary.
Lowercase hex-encodes a binary.
Encodes an ordered list of {key, value} pairs as a query string.
HMAC-SHA256 of data with secret.
HMAC-SHA384 of data with secret.
HMAC-SHA512 of data with secret.
Returns the signing module for a given pattern.
Returns config[:nonce_override] when set, otherwise invokes fallback.
Fallback is a zero-arity function so callers control nonce semantics
(e.g. :erlang.unique_integer vs System.system_time(:microsecond)).
Checks if a pattern is supported.
Returns the list of supported signing patterns.
SHA-256 hash of data.
SHA-512 hash of data.
Signs a request using the specified pattern and configuration.
Current UTC time as ISO 8601 string, truncated to millisecond precision.
Like timestamp_ms_from_config/1 but rendered as ISO 8601 UTC.
Current UTC time in milliseconds.
Returns a preformatted config[:timestamp] first, then
config[:timestamp_ms_override], otherwise current wall time in milliseconds.
Used by pattern modules so dispatch can provide the v4 sign recipe timestamp
and signature-vector tests can inject a frozen provider timestamp.
Current UTC time in seconds.
Like timestamp_ms_from_config/1 but in seconds.
URL-encodes params as a sorted query string.
URL-encodes params as a sorted query string without percent-encoding values.
Types
@type config() :: %{ optional(:api_key_header) => String.t(), optional(:secret_header) => String.t(), optional(:timestamp_header) => String.t(), optional(:signature_header) => String.t(), optional(:passphrase_header) => String.t(), optional(:recv_window_header) => String.t(), optional(:recv_window) => non_neg_integer(), optional(:signature_encoding) => :hex | :base64 | :url, optional(:sign_recipe) => map(), optional(:sign_recipe_section) => String.t(), optional(atom()) => term() }
@type method() :: Bourse.Signing.Request.method()
@type pattern() ::
:hmac_sha256_query
| :hmac_sha256_headers
| :hmac_sha256_iso_passphrase
| :deribit
| :hyperliquid
| :derive
| :lighter
| :api_key_secret_headers
@type request() :: Bourse.Signing.Request.t()
@type signed_request() :: Bourse.Signing.SignedRequest.t()
Functions
Decodes a Base64-encoded string. Raises on invalid input.
Base64-encodes a binary.
Lowercase hex-encodes a binary.
Encodes an ordered list of {key, value} pairs as a query string.
Percent-encodes keys and values the same way as urlencode/1 (space → %20,
not +). See that function's moduledoc for the venue-doc rationale.
Options
:array_style—:brackets(default,key[]=v) or:repeat(key=vrepeated)
HMAC-SHA256 of data with secret.
HMAC-SHA384 of data with secret.
HMAC-SHA512 of data with secret.
Returns the signing module for a given pattern.
Returns config[:nonce_override] when set, otherwise invokes fallback.
Fallback is a zero-arity function so callers control nonce semantics
(e.g. :erlang.unique_integer vs System.system_time(:microsecond)).
Checks if a pattern is supported.
@spec patterns() :: [pattern()]
Returns the list of supported signing patterns.
SHA-256 hash of data.
SHA-512 hash of data.
@spec sign(pattern(), request(), Bourse.Credentials.t(), config()) :: signed_request() | {:error, {:unsupported_signing, term()} | {:lighter_signing, term()}}
Signs a request using the specified pattern and configuration.
Parameters
pattern- The signing pattern atom (e.g.,:hmac_sha256_headers)request-Bourse.Signing.Requestor equivalent map (:method,:path,:body,:params)credentials-Bourse.Credentialsstruct with API key and secretconfig- Pattern-specific configuration from the exchange spec
Returns
A Bourse.Signing.SignedRequest with :url, :method, :headers, and :body.
@spec timestamp_iso8601() :: String.t()
Current UTC time as ISO 8601 string, truncated to millisecond precision.
Like timestamp_ms_from_config/1 but rendered as ISO 8601 UTC.
@spec timestamp_ms() :: non_neg_integer()
Current UTC time in milliseconds.
@spec timestamp_ms_from_config(map()) :: non_neg_integer() | String.t()
Returns a preformatted config[:timestamp] first, then
config[:timestamp_ms_override], otherwise current wall time in milliseconds.
Used by pattern modules so dispatch can provide the v4 sign recipe timestamp
and signature-vector tests can inject a frozen provider timestamp.
@spec timestamp_seconds() :: non_neg_integer()
Current UTC time in seconds.
@spec timestamp_seconds_from_config(map()) :: non_neg_integer() | String.t()
Like timestamp_ms_from_config/1 but in seconds.
URL-encodes params as a sorted query string.
Uses RFC 3986 percent-encoding via URI.encode/2 with URI.char_unreserved?/1
— spaces become %20, never the application/x-www-form-urlencoded +
that Elixir's URI.encode_query/1 emits. Venue authority for the default:
Huobi/HTX spot signing docs require URL-encoded query params with uppercase
hex and show the space as %20 (not +) —
https://huobiapi.github.io/docs/spot/v1/en/#authentication (Signature
Method). Hex digits from URI.encode/2 are
uppercase, matching the same Huobi rule. No first-class venue has been
observed to require www-form + for the signed canonical query — if one
does, register a per-venue carve rather than reverting this default (see
docs/authored-specs.md C21).
List-valued scalar params use empty-bracket keys
(%{"ids" => ["a", "b"]} → "ids%5B%5D=a&ids%5B%5D=b"). That is the form
Deribit's JSON-RPC-over-GET accepts live (OpenAPI lists style=form, explode=true, but the live parser only materializes a list for key[]=…;
Bracket-index ids[0]=… is rejected with value required, and bare repeated
keys with value must be a list. Nested list/map items raise
ArgumentError naming the param — never URI.encode_query's opaque list crash.
Recipe venues that need plain repeated keys use
urlencodeWithArrayRepeat via encode_query_pairs/2.
URL-encodes params as a sorted query string without percent-encoding values.