@bulletxyz/sdk-wasm

中文文档
0.0.47发布于 2026/11/05MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 4272

WebAssembly bindings for the Bullet trading SDK — works in Node.js, browsers, and Deno

原始使用文档
文档目录22 节
  1. Install
  2. Quick Start
  3. Degen trading
  4. API Reference
  5. Client
  6. Errors
  7. Transaction Builder
  8. Uniqueness (replay protection)
  9. Executing as another account
  10. External Signing
  11. UnsignedTransaction
  12. Signed transactions
  13. Multisig
  14. Keypair
  15. CallMessage Factories
  16. Warp Bridge Withdrawals
  17. WebSocket
  18. Managed WebSocket (auto-reconnect)
  19. Decimal
  20. REST API Methods
  21. Platform Support
  22. License

@bulletxyz/sdk-wasm

WebAssembly bindings for the Bullet trading API. Works in Node.js, browsers, and Deno.

Install

npm install @bulletxyz/sdk-wasm

Quick Start

import { Client, Keypair, Transaction, User, NewOrderArgs, Side, OrderType } from '@bulletxyz/sdk-wasm';

const keypair = Keypair.fromHex('your-private-key');
const client = await Client.builder()
    .network('mainnet')
    .keypair(keypair)
    .build();

const marketId = client.marketId('BTC-USD');
if (marketId === undefined) throw new Error('unknown market');
const order = new NewOrderArgs('50000.0', '0.1', Side.Bid, OrderType.Limit, false);
const response = await Transaction.builder()
    .callMessage(User.placeOrders(marketId, [order], false))
    .send(client);

For market data without a signer, use await Client.mainnet() and typed REST methods such as client.tickerPrice('BTC-USD').

Degen trading

Use the typed DegenAction factories to open or close a degen position. The action is passed directly into the generated call message without JSON serialization.

open executes all-or-nothing at top-of-book: Side.Bid opens a long and Side.Ask opens a short. The size must be non-zero and aligned to the market's lot size. amountToTransfer funds the isolated balance from cross margin and is capped by the protocol at 10 USDC. close executes IOC and returns the remaining isolated balance to cross margin once the position is flat.

openV1 and closeV1 take an optional maxSlippageBps (1 bps = 0.01%): the transaction reverts if the average fill price is worse than the mark price by more than that. A fill better than mark always passes. Omit it for no limit.

import { DegenAction, Side, User } from '@bulletxyz/sdk-wasm';

const open = User.degenTrade(
    7,
    DegenAction.open('5', Side.Bid, '10'),
);

const close = User.degenTrade(7, DegenAction.close(), 2);

// Revert if the average fill is more than 0.5% worse than mark.
const guardedOpen = User.degenTrade(7, DegenAction.openV1('5', Side.Bid, '10', 50));
const guardedClose = User.degenTrade(7, DegenAction.closeV1(50));

API Reference

Client

Use Client.mainnet(), Client.connect(networkOrUrl), or the builder shown above. The installed package's TypeScript declarations describe all methods and parameters. client.cancelAndPlaceOrders(marketId, cancels, places, expiryTimestamp?, subAccountIndex?) cancels and places orders in one signed transaction. places must not be empty.

By default the client validates every exchange call-message group against the server schema at connection. .userActions(['PlaceOrders', 'CancelOrders']) limits validation to those user actions and rejects other actions before signing. Builder settings supply signing defaults; transaction builders can override them.

Errors

Fallible WASM calls throw BulletSdkError, a JavaScript Error subclass with parseable SDK metadata.

import { BulletSdkError } from '@bulletxyz/sdk-wasm';

try {
    await client.accountBalance(address);
} catch (err) {
    if (err instanceof BulletSdkError) {
        err.kind       // 'api' | 'http' | 'websocket' | 'validation' | ...
        err.status     // HTTP status when the API returned one
        err.details    // structured JSON details when available
        err.errorId    // server-side correlation id, when present (for support)
        err.retryable  // whether retry/backoff is reasonable
        err.message    // human-readable message
    }
}

Transaction Builder

All transaction construction goes through the builder:

// Build and send in one step
const response = await Transaction.builder()
    .callMessage(User.cancelAllOrders())
    .signer(keypair)
    .send(client);

// Build without sending
const tx = Transaction.builder()
    .callMessage(msg)
    .signer(keypair)
    .build(client);

const submitResponse = await client.sendTransaction(tx);
submitResponse.id       // transaction hash
submitResponse.status   // 'submitted' | 'processed' | ...
submitResponse.events   // LedgerEvent[]

Uniqueness (replay protection)

Every transaction carries a uniqueness value. By default the SDK uses window-based uniqueness from a per-client counter that tracks the millisecond unix timestamp and increments per transaction — a monotonic, duplicate-free value that needs no chain round-trip and tolerates many in-flight transactions.

Override it with any one of .window(), .generation(), or .nonce() (these set the same single uniqueness value, so the last call wins):

const tx = Transaction.builder()
    .callMessage(msg)
    .nonce(42n)        // or .generation(n) / .window(n)
    .signer(keypair)
    .send(client);

Executing as another account

By default a transaction executes as the signer's own default address. Use .addressOverride(address) to execute as an account that explicitly authorised the signer's credential (e.g. a multisig or a delegated account). The override is signed as part of the transaction and included in both the Borsh payload (toBytes()) and the Solana offchain JSON (toMessageBytes()). It takes a base58 address and throws on an invalid one.

const tx = Transaction.builder()
    .callMessage(msg)
    .addressOverride(multisigAddress) // base58 string
    .signer(memberKeypair)
    .build(client);

External Signing

For hardware wallets or external signing services that can sign the standard Borsh payload:

// Build unsigned (chain hash is baked in)
const unsigned = Transaction.builder()
    .callMessage(User.cancelAllOrders())
    .buildUnsigned(client);

// Get signable bytes and sign externally
const signable = unsigned.toBytes();
const display = unsigned.toDisplayMessage(); // optional: show in your own confirmation UI
const signature = myExternalSigner(signable);  // 64-byte Ed25519 signature

// Assemble the signed transaction
const signed = SignedTransaction.fromParts(unsigned, signature, pubKey);
await client.sendTransaction(signed);

UnsignedTransaction

Returned by Transaction.builder().buildUnsigned(client). Contains the chain hash so signable bytes can be produced without a client reference.

unsigned.toBytes()  // Uint8Array — borsh signing payload: version byte, tx, chain hash (signable bytes)
unsigned.toDisplayMessage() // string — human-readable unsigned payload for display only
unsigned.toMessageBytes() // Uint8Array — readable JSON bytes for Solana wallets

// Inverse of toBytes(): rebuild an UnsignedTransaction from stored signable
// bytes (chain name comes from the client; mismatched chain hash is rejected).
UnsignedTransaction.fromBytes(bytes, client) // UnsignedTransaction

Use fromBytes when a coordinator (e.g. a multisig UI) persists a transaction as its exact signable bytes and rebuilds it later for display and submission — the rebuilt bytes are byte-identical to what was signed, so the stored bytes are the source of truth rather than a separate JSON representation.

fromBytes only accepts bytes produced by this signing format. Bytes persisted by a pre-fork SDK — which had no leading version byte — cannot be rebuilt and are rejected with a serialization error. The network may still admit a transaction signed over them while the environment's legacy cutoff is open (single-signer transactions only), but rebuild those transactions from their inputs and re-sign them to be safe.

Some external wallets display signMessage bytes as raw UTF-8, so toBytes() can look garbled in the wallet confirmation. That is expected: those bytes are what the network verifies. Use toDisplayMessage() in your app UI to show the transaction contents before asking the wallet to sign toBytes().

For external Solana wallets where the wallet confirmation should show readable JSON, use the Solana offchain path instead. toMessageBytes() returns the readable JSON payload with chain_name, details.chain_hash_fragment and version as the domain fields. SolanaOffchainTransaction.fromParts(...) also carries the chain hash required by the current sequencer offchain authenticator envelope:

const unsigned = Transaction.builder()
    .callMessage(User.cancelAllOrders())
    .buildUnsigned(client);

const message = unsigned.toMessageBytes();
const signature = await wallet.signMessage(message);
const pubKey = wallet.publicKey.toBytes(); // 32-byte Solana public key
const tx = SolanaOffchainTransaction.fromParts(unsigned, signature, pubKey);

await client.sendOffChainTransaction(tx);

Signed transactions

Transaction.builder().build(client) and SignedTransaction.fromParts(...) produce signed transactions. toBytes() serializes the Borsh envelope; toBase64() encodes it for WebSocket submission.

SolanaOffchainTransaction.fromParts(...) wraps a signature over unsigned.toMessageBytes(). Submit it with client.sendOffChainTransaction(tx); its toBytes() and toBase64() serialize the offchain envelope.

Multisig

M-of-N multisig over the spec-compliant Solana offchain format (the format Ledger hardware wallets sign). Every signer signs the same bytes; once the threshold is met the transaction can be submitted.

// 2-of-3 multisig. Keys are canonicalized (sorted) internally, so input
// order never matters.
const config = new MultisigConfig(2, [pubkeyA, pubkeyB, pubkeyC]);

const unsigned = Transaction.builder()
    .callMessage(User.cancelAllOrders())
    .buildUnsigned(client);

// Collect signatures — each signer signs the same signable bytes
const tx = new SolanaLedgerMultisigTransaction(unsigned, config);
const signature = await ledgerWallet.signMessage(tx.signableBytes());
tx.addSignature(pubkeyA, signature);   // validates membership + signature
// ... pass tx.signableBytes() to the other signers ...
tx.addSignature(pubkeyB, signatureB);

// Submit (throws if below threshold)
await client.sendLedgerMultisigTransaction(tx);

The signable bytes can also be produced without assembling a transaction: unsigned.toLedgerMultisigSignableBytes(config) (preamble + JSON) and unsigned.toMultisigMessageBytes(config) (JSON payload only).

Keypair

Use Keypair.generate(), Keypair.fromHex(secret) or Keypair.fromBytes(bytes). The secret is 32 bytes; publicKey() returns the 32-byte Ed25519 public key and sign(bytes) returns a 64-byte signature.

CallMessage Factories

Generated namespaces (User, Public, Keeper, Vault, Admin, Warp) construct call messages. Permissionless actions still require a signed transaction. Use the installed TypeScript declarations for factory signatures.

Use User.transfer for balance movement. TransferEndpoint takes a JSON-encoded balance bucket and an optional sub-account index:

import { TransferEndpoint, User } from '@bulletxyz/sdk-wasm';

const deposit = User.transfer(
    new TransferEndpoint(JSON.stringify('bank')),
    new TransferEndpoint(JSON.stringify('cross')),
    0, '1000.0', '',
);

The older User.deposit, User.withdraw and User.createSubAccount factories remain for wire compatibility, but the exchange rejects those actions.

Delegate expiry is encoded as epoch microseconds. If your app stores epoch milliseconds, pass BigInt(expiresAtMs) * 1000n for expiresAt.

Read configured delegates with accountConfig:

const config = await client.accountConfig(address);

for (const delegate of config.delegates) {
    delegate.address
    delegate.name
    delegate.flags
    delegate.expiresAt  // bigint | undefined, epoch microseconds
}

deriveVaultAddress(name) computes a vault's address from its name. See examples/node/create_vault.ts for a runnable vault example.

User.cancelQueuedWithdrawalV1(vault, withdrawalTimestamp?) cancels the queued withdrawal whose timestamp (from queuedWithdrawals on vault/vaults) matches, or all of the sender's queued withdrawals from the vault when the timestamp is omitted. The older User.cancelQueuedWithdrawal(vault) cancels only the oldest one.

deriveSubAccountAddress(master, index) computes a sub-account's address from its master address and index (0-31). A transfer into a sub-account creates it; pass the index to the destination TransferEndpoint. To trade there, pass the index as the trailing sub_account_index argument on order/cancel factories. Use the derived address for reads such as client.accountInfo(sub). Out-of-range indices throw.

Warp Bridge Withdrawals

Bridge withdrawals use a normal typed call message and the same transaction builder path as exchange user actions:

import { Transaction, Warp } from '@bulletxyz/sdk-wasm';

const callMsg = Warp.transferRemote({
    warpRoute: '0x...',          // bytes32 route
    amount: '1000000',
    destinationDomain: 1234,
    gasPaymentLimit: '400000',
    recipient: '0x...',          // bytes32; 20-byte EVM hex is left-padded
    relayer: null,               // or a string / { Standard: string } / { Vm: string }
});

const unsigned = Transaction.builder()
    .callMessage(callMsg)
    .buildUnsigned(client);

Use decimal strings for amount and gasPaymentLimit values above Number.MAX_SAFE_INTEGER; unsafe JavaScript numbers are rejected to avoid rounding token amounts before signing.

After submission, bridge responses expose the Hyperlane message id when the Trading API includes it in transaction events:

const response = await client.sendOffChainTransaction(tx);
response.messageId  // string | undefined

WebSocket

const ws = await client.connectWs();

// Subscribe to topics
await ws.subscribe([
    Topic.aggTrade('BTC-USD'),
    Topic.depth('ETH-USD', OrderbookDepth.D10),
    Topic.bookTicker('SOL-USD'),
    Topic.kline('BTC-USD', KlineInterval.H1),
    Topic.userDegenPositions('account-address'),
    Topic.allTickers(),
]);

// Receive messages
const msg = await ws.recv();
if (msg.type === 'degenPositionCompleted') {
    console.log(msg.data.position);
}

// Submit orders via WebSocket
const tx = Transaction.builder()
    .callMessage(User.placeOrders(0, [order], false))
    .signer(keypair)
    .build(client);

await ws.orderPlace(tx.toBase64());

Managed WebSocket (auto-reconnect)

For long-running bots, prefer connectWsManaged — it handles reconnection with exponential backoff and replays your subscriptions automatically. Sends are queued; acknowledgements arrive as message events. Calls throw when the command queue is full or the background task has stopped.

Protocol ping/pong keeps the raw socket alive but does not reset managed idle detection. For quiet subscriptions, send application pings, catch timer errors, and clear the interval when the receive loop ends. Pass a ManagedWsConfig to customize reconnect and idle settings; its parameters are in the type declarations:

const ws = await client.connectWsManaged();

ws.subscribe([Topic.depth('BTC-USD', OrderbookDepth.D20)]);

const keepalive = setInterval(() => {
    try {
        ws.ping();
    } catch (err) {
        console.error('keepalive ping failed:', err);
    }
}, 30_000);

try {
    while (true) {
        const evt = await ws.recv();
        if (!evt) break;
        switch (evt.type) {
            case 'message':       handleMessage(evt.message); break;
            case 'reconnecting':  console.log('reconnecting...'); break;
            case 'disconnected':  console.error(evt.reason); return;
        }
    }
} finally {
    clearInterval(keepalive);
}

Decimal

Fixed-precision decimal arithmetic for prices and quantities. Prefer string inputs such as Decimal.fromScientific('1.5e2') to preserve decimal precision. Checked arithmetic (checkedAdd, checkedSub, checkedMul, checkedDiv) returns undefined on failure.

REST API Methods

The client exposes REST endpoints as typed async methods. Response objects have typed getters plus toJSON() for full JSON serialization.

Arguments are generated in alphabetical order, not in "required first" order, so a required argument can follow a nullable one — pass null or undefined for the ones you do not need (for example client.orderBook(null, 'BTC-USD')). Arguments marked ? may be omitted entirely.

userTransfers(action, address, asset, endTime, limit, startTime) accepts an optional asset name (for example 'USDC') and comma-separated movement kinds (for example 'DEPOSIT,WITHDRAWAL'). Both filters are case-insensitive. Empty action entries are ignored; unknown actions or a wholly empty action filter return HTTP 400. When upgrading, insert null or undefined for asset before the existing endTime, limit, and startTime arguments to keep querying all assets.

vaultEquity(address) returns the user's active positions across vaults plus totalEquity. Each position includes vaultAddress, shares, equity, and a nullable percentageReturn.

Each item returned by vaultDepositors includes held, queued, and total shares plus equity including the main vault account and its vault-owned sub-accounts. All pages and pricing come from one committed rollup state version, reported as stateSlot. stateUpdatedAt is when that state was collected (Unix microseconds, refreshed every 30 seconds by default), not the block time. vaultEquity returns HTTP 503 if the last successful collection is over 90 seconds old; vaultDepositors keeps serving last-good rows.

On vaultDepositors, history fields (initialDepositAmount, initialDepositTimestamp, remainingCostBasis, historyUpdatedAt, historyLastTxNumber, historyShares) come from an optional historical projection and are null (or undefined in the bindings) when unavailable; stale history does not block current shares or equity. historyComplete means the projection has complete, valid history, not that it has caught up with the live position.

percentageReturn must be displayed as an estimated unrealized return. On vaultDepositors, percentageReturnIsEstimate is true whenever it is non-null. percentageReturn is null when history is older than 30 minutes, live and historical shares differ, remaining cost basis is zero, or arithmetic overflows. On vaultDepositors, it is also null when the state collection is older than 90 seconds; vaultEquity returns HTTP 503 instead. It is not lifetime cash-flow return and excludes realized withdrawals.

Platform Support

PlatformImport
Node.jsimport { Client } from '@bulletxyz/sdk-wasm'
Denoimport { Client } from '@bulletxyz/sdk-wasm'
Browserimport init, { Client } from '@bulletxyz/sdk-wasm' then await init()

License

MIT

版本与发布记录

@bulletxyz/sdk-wasm 共发布 38 个版本,首次发布于 2026/03/31;过去 12 个月发布了 38 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.0.47latest2026/11/054 天前
0.0.462026/10/319 天前
0.0.452026/10/2910 天前
0.0.442026/10/2613 天前
0.0.432026/10/2119 天前
0.0.422026/10/1326 天前

关于 @bulletxyz/sdk-wasm 的常见问题

@bulletxyz/sdk-wasm 是什么?

WebAssembly bindings for the Bullet trading SDK — works in Node.js, browsers, and Deno

如何安装 @bulletxyz/sdk-wasm?

在项目目录运行 npm install @bulletxyz/sdk-wasm,或使用 pnpm add / yarn add / bun add。

@bulletxyz/sdk-wasm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bulletxyz/sdk-wasm 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bulletxyz/sdk-wasm 可以商用吗?

@bulletxyz/sdk-wasm 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bulletxyz/sdk-wasm 的下载量有多少?

根据 npm 官方下载统计,@bulletxyz/sdk-wasm 在 2026/10/09 – 2026/11/07 这 30 天内被下载 4,272 次,日均约 142 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bulletxyz/sdk-wasm 还在维护吗?

最新版本 0.0.47 发布于 2026/11/05(4 天前);至今共发布 38 个版本,其中过去 12 个月发布了 38 个;首次发布于 2026/03/31。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。