A simple high perf signing lib for BULK txns.
One Rust core, bindings for TypeScript, Python, and direct Rust usage.
| Package | Description | Install |
|---|---|---|
bulk-keychain |
TypeScript/JavaScript (Node.js) | npm install bulk-keychain |
bulk-keychain-wasm |
TypeScript/JavaScript (Browser) | npm install bulk-keychain-wasm |
bulk-keychain |
Python | pip install bulk-keychain |
bulk-keychain |
Rust crate | cargo add bulk-keychain |
import { NativeKeypair, NativeSigner, randomHash } from 'bulk-keychain';
// Generate or import keypair
const keypair = new NativeKeypair();
// Or: NativeKeypair.fromBase58('your-secret-key...')
// Create signer
const signer = new NativeSigner(keypair);
// Sign a single order
const signed = signer.sign({
type: 'order',
symbol: 'BTC-USD',
isBuy: true,
price: 100000,
size: 0.1,
orderType: { type: 'limit', tif: 'GTC' }
});
// Submit to API
await fetch('https://api.bulk.exchange/api/v1/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
actions: JSON.parse(signed.actions),
nonce: signed.nonce,
account: signed.account,
signer: signed.signer,
signature: signed.signature
})
});from bulk_keychain import Keypair, Signer
# Generate or import keypair
keypair = Keypair()
# Or: Keypair.from_base58('your-secret-key...')
# Create signer
signer = Signer(keypair)
# Sign a single order
signed = signer.sign({
"type": "order",
"symbol": "BTC-USD",
"is_buy": True,
"price": 100000.0,
"size": 0.1,
"order_type": {"type": "limit", "tif": "GTC"}
})
# Submit to API
import requests
requests.post(
'https://api.bulk.exchange/api/v1/order',
json={
"actions": signed["actions"],
"nonce": signed["nonce"],
"account": signed["account"],
"signer": signed["signer"],
"signature": signed["signature"],
},
)use bulk_keychain::{Keypair, Signer, Order, TimeInForce};
// Generate or import keypair
let keypair = Keypair::generate();
// Or: Keypair::from_base58("your-secret-key...")?
// Create signer
let mut signer = Signer::new(keypair);
// Sign a single order
let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
let signed = signer.sign(order.into(), None)?;
// Serialize to JSON
let json = signed.to_json()?;| Method | Description | Returns |
|---|---|---|
sign(order) |
Sign a single order/cancel | SignedTransaction |
signAll([orders]) |
Sign multiple orders (each gets own tx, parallel) | SignedTransaction[] |
signGroup([orders]) |
Sign multiple orders atomically (one tx) | SignedTransaction |
signOraclePrices([{ timestamp, asset, price }]) |
Sign oracle px updates |
SignedTransaction |
signPythOracle([{ timestamp, feedIndex, price, exponent }]) |
Sign Pyth oracle o batch |
SignedTransaction |
signWhitelistFaucet(targetPubkey, whitelist) |
Sign whitelist faucet admin action | SignedTransaction |
Python method names are sign_oracle_prices, sign_pyth_oracle, and sign_whitelist_faucet. Rust equivalents are sign_oracle_prices, sign_pyth_oracle, and sign_whitelist_faucet.
Single-order transactions include an optional pre-computed order ID that matches BULK's network order ID generation. This lets you know the order ID before the node responds - useful for optimistic tracking.
Transaction signatures use canonical BULK-SDK bytes:
signature = ed25519_sign( bincode(actions) + nonce_le + account_bytes )
const signed = signer.sign(order);
console.log(`Order ID: ${signed.orderId}`); // Optionalsigned = signer.sign(order)
print(f"Order ID: {signed.get('order_id')}") # Optionallet signed = signer.sign(order.into(), None)?;
println!("Order ID: {:?}", signed.order_id);You can compute an order ID directly from order fields + nonce + account:
use bulk_keychain::{
compute_order_id, Order, Pubkey, TimeInForce,
};
let account = Pubkey::from_base58("your-account-pubkey")?;
let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
let order_id = compute_order_id(&order, 1704067200000, &account).to_base58();from bulk_keychain import compute_order_id_from_order
order_id = compute_order_id_from_order(
{"type": "order", "symbol": "BTC-USD", "is_buy": True, "price": 100000.0, "size": 0.1},
nonce=1704067200000,
account="your-account-pubkey",
)
# Compact API order JSON is also supported:
order_id_compact = compute_order_id_from_order(
{"l": {"c": "BTC-USD", "b": True, "px": 100000.0, "sz": 0.1, "r": False, "tif": "GTC"}},
nonce=1704067200000,
account="your-account-pubkey",
)For multi-order transactions (signGroup / grouped batches), optional order_ids are available when batch order ID computation is enabled.
const signer = new NativeSigner(keypair);
signer.setComputeBatchOrderIds(true); // default false for max performance
const grouped = signer.signGroup([entryOrder, stopLoss, takeProfit]);
console.log(grouped.orderIds); // ["...", "...", "..."]signer = Signer(keypair)
signer.set_compute_batch_order_ids(True) # default False
grouped = signer.sign_group([entry_order, stop_loss, take_profit])
print(grouped.get("order_ids"))let mut signer = Signer::new(keypair).with_batch_order_ids();
let grouped = signer.sign_group(bracket, None)?;
println!("Order IDs: {:?}", grouped.order_ids);Order IDs are derived from canonical BULK-SDK bytes for a single order action:
order_id = SHA256(seqno_le + bincode(single_action) + account_bytes + nonce_le) (base58)
Notes:
seqnois the action index inside the transaction (auto-indexed for grouped txs,0for single-order txs)- for limit/market actions,
px/szuse BULK-SDK fixed-point serialization (round(value * 1e8)asu64) - signer pubkey is not part of the order-ID hash
For high-frequency trading, sign many independent orders in parallel:
// Each order becomes its own transaction (parallel signing)
const orders = [order1, order2, order3];
const signedTxs = signer.signAll(orders); // Returns SignedTransaction[]# Each order becomes its own transaction (parallel signing)
orders = [order1, order2, order3]
signed_txs = signer.sign_all(orders) # Returns list of dicts// Each order becomes its own transaction (parallel signing)
let orders = vec![order1.into(), order2.into(), order3.into()];
let signed_txs = signer.sign_all(orders, None)?; // Returns Vec<SignedTransaction>For bracket orders (entry + stop loss + take profit) that must succeed or fail together:
// All orders in ONE transaction
const bracket = [entryOrder, stopLoss, takeProfit];
const signed = signer.signGroup(bracket); // Returns single SignedTransaction# All orders in ONE transaction
bracket = [entry_order, stop_loss, take_profit]
signed = signer.sign_group(bracket) # Returns single dict// All orders in ONE transaction
let bracket = vec![entry.into(), stop_loss.into(), take_profit.into()];
let signed = signer.sign_group(bracket, None)?; // Returns SignedTransactionFor browser apps using external wallets where you don't have access to the private key, use the prepare/finalize flow:
import { prepareOrder, WasmPreparedMessage } from 'bulk-keychain-wasm';
// Step 1: Prepare the message (no private key needed)
const prepared = prepareOrder(order, {
account: walletPubkey, // The trading account
signer: walletPubkey, // Who signs (defaults to account)
nonce: Date.now() // Optional, auto-generated if omitted
});
// Step 2: Get signature from external wallet
// prepared.messageBytes is Uint8Array - pass to wallet.signMessage()
const { signature } = await wallet.signMessage(prepared.messageBytes);
// Step 3: Finalize into SignedTransaction
const signed = prepared.finalize(bs58.encode(signature));
// Alternative format options:
prepared.messageBase58; // Base58 encoded message
prepared.messageBase64; // Base64 encoded message
prepared.messageHex; // Hex encoded message
prepared.orderId; // Optional pre-computed order IDfrom bulk_keychain import prepare_order, finalize_transaction
# Step 1: Prepare
prepared = prepare_order(order, account=wallet_pubkey)
# Step 2: Sign with external wallet
signature = wallet.sign_message(prepared["message_bytes"])
# Step 3: Finalize
signed = finalize_transaction(prepared, signature)| Function | Description |
|---|---|
prepareOrder(order, options) |
Single order |
prepareAll(orders, options) |
Multiple orders (parallel, each gets own tx) |
prepareGroup(orders, options) |
Atomic multi-order (one tx) |
prepareAgentWallet(agent, delete, options) |
Agent wallet authorization |
prepareFaucet(options) |
Testnet faucet request |
prepareUpdateUserSettings(settings, options) |
Update user settings (leverage) |
When the main account uses an external wallet but trades via an agent:
// Main wallet (Phantom) authorizes agent wallet (Privy)
const prepared = prepareAgentWallet(agentPubkey, false, {
account: mainWalletPubkey, // Phantom
signer: mainWalletPubkey // Phantom signs
});
const { signature } = await phantom.signMessage(prepared.messageBytes);
const signed = prepared.finalize(bs58.encode(signature));{
type: 'order',
symbol: 'BTC-USD',
isBuy: true,
price: 100000,
size: 0.1,
orderType: { type: 'limit', tif: 'GTC' } // GTC, IOC, or ALO
}{
type: 'order',
symbol: 'BTC-USD',
isBuy: true,
price: 0,
size: 0.1,
orderType: { type: 'market', isMarket: true, triggerPx: 0 }
}{
type: 'cancel',
symbol: 'BTC-USD',
orderId: 'order-id-base58'
}{
type: 'cancelAll',
symbols: ['BTC-USD'] // or [] for all symbols
}{
type: 'stop',
symbol: 'BTC-USD',
isBuy: false,
size: 0.1,
triggerPrice: 90000,
limitPrice: 89900, // omit for market-style fill
}{
type: 'takeProfit',
symbol: 'BTC-USD',
isBuy: false,
size: 0.1,
triggerPrice: 110000,
limitPrice: 110100, // omit for market-style fill
}{
type: 'range',
symbol: 'BTC-USD',
isBuy: false,
size: 0.1,
pmin: 90000, // stop-loss trigger price
pmax: 110000, // take-profit trigger price
lmin: 89900, // stop-loss limit price (omit for market-style fill)
lmax: 110100, // take-profit limit price (omit for market-style fill)
}Fires a set of child actions when price crosses a threshold. Nested actions may be: stop, takeProfit, range, order, cancel, cancelAll, modify.
{
type: 'trig',
symbol: 'BTC-USD',
isBuy: true,
triggerPrice: 100000,
actions: [
{ type: 'stop', symbol: 'BTC-USD', isBuy: false, size: 0.1, triggerPrice: 95000 },
{ type: 'takeProfit', symbol: 'BTC-USD', isBuy: false, size: 0.1, triggerPrice: 110000 },
],
}Protective stop that follows price by a fixed distance (trailBps), resetting forward on favorable moves in increments of stepBps. Internally represented as a protective stop leg plus a rotating sentinel trigger leg.
{
type: 'trl', // or 'trailingStop'
symbol: 'BTC-USD',
isBuy: true, // true = protecting a long, false = protecting a short
size: 0.25,
trailBps: 100, // trailing distance in basis points
stepBps: 10, // favorable reset step in basis points
limitPrice: null, // optional: omit or null for market-style trigger
}One-shot follow-up actions executed on the first fill of a parent order in the same transaction. p is the 0-based index of the parent action in the transaction.
// Attach directly to a limit order (auto-promoted to an atomic group):
const limitWithSL = {
type: 'order',
symbol: 'BTC-USD',
isBuy: true,
price: 95000,
size: 0.1,
orderType: { type: 'limit', tif: 'GTC' },
onFill: {
p: 0, // index of the parent order above
actions: [
{ type: 'stop', symbol: 'BTC-USD', isBuy: false, size: 0.1, triggerPrice: 90000 },
],
},
};
const signed = prepareOrder(limitWithSL, { account, signer });
// Or construct the group explicitly (required for market orders):
const signed = prepareGroup([marketOrder, onFillAction], { account, signer });