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 |
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
}