ault-sdk-ts
v0.1.0
Published
TypeScript SDK for the Ault blockchain with high-level transaction methods and REST query clients
Downloads
1,996
Maintainers
Readme
Ault SDK for TypeScript
A TypeScript SDK for the Ault blockchain that provides high-level transaction methods and REST query clients for the current Ault modules, including License, Miner, Market, CLOB, Agent, Authority, RateLimit, Reward, and TokenRegistry.
Features
- High-level client with simple transaction methods (no manual message building)
- Flexible signer support (viem, ethers, Privy, MetaMask, private keys)
- Automatic address format conversion (0x <-> ault1)
- DEX API client for market data, orders, trades, and UDF charting
- DEX WebSocket client for real-time streaming with type-safe subscriptions and auto-reconnect
- REST query clients for the current Ault modules
- EIP-712 typed data signing
- Works across Node.js, Bun, and browser runtimes
- Full TypeScript support with comprehensive type definitions
- Automatic retry with exponential backoff
Installation
# npm
npm install ault-sdk-ts
# pnpm
pnpm add ault-sdk-ts
# bun
bun add ault-sdk-tsInstalling from the npm registry is the supported path for production use.
Quick Start
import { createClient, getNetworkConfig } from "ault-sdk-ts";
import { privateKeyToAccount } from "viem/accounts";
// Create a client with a viem account
const account = privateKeyToAccount("0x...");
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: account,
});
// Query data
const licenses = await client.license.getOwnedBy(client.address);
const epoch = await client.miner.getCurrentEpoch();
// Execute transactions (no message building required!)
const result = await client.delegateMining({
licenseIds: [1, 2, 3], // accepts numbers, strings, or bigints
operator: "0xOperator...", // accepts 0x or ault1 format
});
console.log(
`TX Hash: ${result.txHash}, Confirmed: ${result.confirmed}, Success: ${result.success}`,
);Creating a Client
The createClient() function accepts multiple signer types and auto-detects the format:
With viem
import { createClient, getNetworkConfig } from "ault-sdk-ts";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount("0x...");
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: account,
});With viem WalletClient (Browser)
import { createClient, getNetworkConfig } from "ault-sdk-ts";
import { createWalletClient, custom } from "viem";
const walletClient = createWalletClient({
transport: custom(window.ethereum),
});
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: walletClient,
});With ethers.js
import { createClient, getNetworkConfig } from "ault-sdk-ts";
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("...");
const signer = new ethers.Wallet("0x...", provider);
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: { type: "ethers", signer },
});With MetaMask (EIP-1193)
import { createClient, getNetworkConfig } from "ault-sdk-ts";
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: {
type: "eip1193",
provider: window.ethereum,
address: accounts[0],
},
});With Privy
import { createClient, getNetworkConfig } from "ault-sdk-ts";
import { useSignTypedData, useWallets } from "@privy-io/react-auth";
const { signTypedData } = useSignTypedData();
const { wallets } = useWallets();
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: {
type: "privy",
signTypedData,
address: wallets[0]?.address,
},
});With Private Key (Server-side)
import { createClient, getNetworkConfig } from "ault-sdk-ts";
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: { type: "privateKey", key: "0x..." },
});Client Options
interface ClientOptions {
network: NetworkConfig; // Required: network configuration
signer: FlexibleSignerInput; // Required: wallet/signer
signerAddress?: string; // Optional: override signer address
fetchFn?: typeof fetch; // Optional: custom fetch
fetchOptions?: FetchWithRetryOptions;
defaultGasLimit?: string; // Optional: default gas limit
defaultMemo?: string; // Optional: default memo
defaultConfirmationTimeoutMs?: number;
manifestOverride?: Manifest; // Optional: caller-supplied manifest layer (see "SDK Manifest")
}SDK Manifest
createClient resolves network endpoints and gas/EIP-712 defaults from three
layers, in increasing precedence:
- The embedded baseline shipped with the SDK (
NetworkConfigplusGAS_CONSTANTS). - The manifest fetched from
<dexApiUrl>/api/v1/sdk-manifestat bootstrap and refreshed every five minutes in the background. The operator rotates URLs, EIP-712 fee defaults, and per-module gas constants by editing the dex-apiSDK_MANIFEST_JSONenv var; older SDK versions pick up the new values within one refresh window without a package bump. - An optional
manifestOverrideyou pass tocreateClient. The override sits on top of both other layers, is frozen for the lifetime of the client, and is not touched by background refreshes.
The override layer covers cases the fetched manifest cannot: pointing the client at a private RPC during local development, pinning a deterministic manifest in tests, surviving a URL rotation cutover by keeping the old endpoint until you redeploy, or tuning gas constants without waiting on a manifest deploy.
import { createClient, getNetworkConfig, type Manifest } from "ault-sdk-ts";
const manifestOverride: Manifest = {
schemaVersion: 1,
urls: {
rpc: "https://rpc.internal.example.com",
},
gas: {
LIMIT_ORDER: 250000,
},
};
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: account,
manifestOverride,
});The override is sparse — every field is optional and only the keys you set
take effect. Disjoint keys still flow through from the fetched manifest, so
{gas: {LIMIT_ORDER: 250000}} does not erase a MARKET_ORDER value the
dex-api emits. Unknown top-level keys pass through untouched for forward
compatibility with future manifest fields.
Supported fields
The Manifest type has four top-level keys. schemaVersion is required; the
three sub-objects are optional and each one is itself sparse.
interface Manifest {
schemaVersion: number; // required, integer >= 1
urls?: Partial<ManifestUrls>;
eip712?: Partial<ManifestEip712>;
gas?: Partial<ManifestGas>;
// unknown top-level keys are preserved for forward compatibility
}urls — endpoint overrides. Each value is a string and applies only if
non-empty. The dexApiUrl itself is not overridable here (it is the trust
root for the fetched manifest).
| Key | Maps to NetworkConfig field |
| ---------- | ----------------------------- |
| rpc | rpcUrl |
| rest | restUrl |
| evm | evmRpcUrl |
| explorer | explorerUrl |
| dexWs | dexWsUrl |
URL rotations propagate to the tx executor on the next call but not to the
REST/WS contexts captured at createClient time. Re-create the client to
pick up new endpoints across the board.
eip712 — fee defaults applied to every transaction unless per-call
options override them.
| Key | Type | Notes |
| ----------- | -------------------------------- | ------------------------------ |
| feeDenom | non-empty string | replaces GAS_CONSTANTS.DENOM |
| feeAmount | non-negative integer string | raw aault, "0" allowed |
| gasLimit | strictly positive integer string | gas units, "0" rejected |
gas — per-message-type gas constants and the fee-free multiplier.
Every value is a positive integer. Keys outside the supported list are
dropped silently and the embedded baseline keeps applying.
The keys most consumers will touch are the CLOB ones plus the fee-free multiplier:
| Key | Used by |
| ------------------------- | ---------------------------------------- |
| FEE_FREE_GAS_MULTIPLIER | fee-free gas computation across all msgs |
| LIMIT_ORDER | placeLimitOrder |
| MARKET_ORDER | placeMarketOrder |
| SCALE_ORDER_BASE | placeScaleOrder (base cost) |
| SCALE_ORDER_PER_SUB | placeScaleOrder (per sub-order) |
| CANCEL_ORDER_BASE | cancelOrders (base cost) |
| CANCEL_ORDER_PER_ID | cancelOrders (per order id) |
The schema also accepts per-message gas constants for the license,
miner, agent, reward, and accountx modules (PER_LICENSE,
PER_DELEGATE_MINING, APPROVE_AGENT, SET_REFERRAL_CODE,
OPEN_ACCOUNT, and the rest). These exist for operator-side
deploy-cycle bypass and are rarely needed in consumer code. The full
list lives in ManifestGasSchema in src/core/manifest-types.ts.
The gas blob never accepts EIP712_FEE_AMOUNT, EIP712_GAS_LIMIT, or
DENOM — those live on eip712 as feeAmount, gasLimit, and
feeDenom. Smuggling them through gas is dropped defensively because
the broadcast layer never reads them off the gas object.
The value validates against ManifestSchema synchronously at createClient
time. A malformed override throws straight away with the schema error
instead of falling back to the baseline the way a broken fetched manifest
does, since override data comes from your code and a typo is a bug worth
surfacing loudly. The caller's object is deep-cloned at construction, so
mutating it after createClient returns does not affect resolver state.
When a non-empty override is present, the resolver emits one
console.info line at boot listing the override key paths (never values,
since urls may carry private endpoints).
If you also use client.attachAgent(...), the agent surface reuses the
same manifest resolver, so the override flows through to agent-signed
transactions without separate plumbing.
The package also re-exports the low-level helpers if you need them:
mergeManifests, fetchManifest, applyNetworkOverrides,
applyGasOverrides, createManifestResolver, createStaticResolver.
Query Methods
The client provides pass-through access to all REST API methods:
// License queries
const license = await client.license.getLicense("1");
const licenses = await client.license.getOwnedBy(client.address);
const balance = await client.license.getBalance(client.address);
const isApproved = await client.license.isApprovedMember(address);
// Miner queries
const epoch = await client.miner.getCurrentEpoch();
const operators = await client.miner.getOperators();
const delegation = await client.miner.getLicenseDelegation("123");
const emission = await client.miner.getEmissionInfo();
// Market and CLOB queries
const markets = await client.market.getMarkets();
const market = await client.market.getMarket(1);
const orders = await client.clob.getOrders({ orderer: client.address, marketId: 1 });
// Reward queries
const rewardParams = await client.reward.getParams();
const referralCode = await client.reward.getReferralCode(client.address);
const builderAllowances = await client.reward.getBuilderAllowances({ user: client.address });
// TokenRegistry queries
const tokenRegistryParams = await client.tokenregistry.getParams();
const tokenPair = await client.tokenregistry.getTokenPair(
"0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
);
const tokenPairs = await client.tokenregistry.getTokenPairs();DEX API
The SDK includes a client for the Ault DEX API — a REST API for market data, order books, trades, balances, and UDF charting. This is a separate service from the on-chain Cosmos REST endpoints.
Market Data
import { createAultClient, getNetworkConfig } from "ault-sdk-ts";
const client = createAultClient({
network: { ...getNetworkConfig("<CHAIN_ID>"), dexApiUrl: "<DEX_API_URL>" },
});
const dex = client.rest.dex;
// Markets and currencies
const markets = await dex.listMarkets();
const currencies = await dex.listCurrencies();
// Tickers
const ticker = await dex.getTicker("AULT/USDT");
const allTickers = await dex.getAllTickers();
// Order book
const orderbook = await dex.getOrderbook("AULT/USDT", { limit: 50 });
// Recent trades
const trades = await dex.listTrades("AULT/USDT", { limit: 100, since: 1773600000000 });
// Trading fees
const fees = await dex.getTradingFees();
// OHLCV candles
const candles = await dex.getOHLCV("AULT/USDT", {
timeframe: "1h",
limit: 100,
});
// Protocol stats (24h window)
const stats = await dex.getStats(); // defaults to { window: "24h" }
console.log(stats.totals.quoteVolume); // "1234567.89" or null when markets span multiple quotes
console.log(stats.totals.tradeCount, stats.totals.traderCount);
for (const m of stats.markets) {
console.log(m.symbol, m.quoteVolume, m.tradeCount);
}Monetary fields like quoteVolume, estimatedFee, and averageSlippageBps come back as canonical decimal strings, so parse them with a decimal library rather than parseFloat. totals.quoteVolume and its related fee fields are null when the markets that traded in the window span more than one quote asset, since summing notionals across quotes is meaningless. markets[] can briefly lag totals while the symbol map updates, so the per-market trade counts need not add up to totals.tradeCount.
User Queries
// Orders for an address
const orders = await dex.listOrders({
address: "ault1...",
status: "open",
symbol: "AULT/USDT",
limit: 50,
});
// Single order by ID
const order = await dex.getOrder("010000000000000000000002");
// User's trade history
const myTrades = await dex.listMyTrades({
address: "ault1...",
symbol: "AULT/USDT",
});
// Balances
const balances = await dex.getBalance({ address: "ault1..." });
console.log(balances.free); // { AULT: 100, USDT: 500 }
console.log(balances.total); // { AULT: 150, USDT: 500 }UDF charting
All UDF (TradingView) charting endpoints are available on dex:
const config = await dex.getUDFConfig();
const serverTime = await dex.getUDFTime();
const symbolInfo = await dex.getUDFSymbol({ symbol: "AULT/USDT" });
const results = await dex.searchUDFSymbols({ query: "AULT", limit: 10 });
const history = await dex.getUDFHistory({
symbol: "AULT/USDT",
resolution: "60",
from: 1773500000,
to: 1773600000,
});
const quotes = await dex.getUDFQuotes({ symbols: "AULT/USDT,BTC/USDT" });
const marks = await dex.getUDFMarks({ symbol: "AULT/USDT", from: 1773500000, to: 1773600000 });
const tsMarks = await dex.getUDFTimescaleMarks({
symbol: "AULT/USDT",
from: 1773500000,
to: 1773600000,
});DEX WebSocket
The SDK ships a WebSocket client for streaming live data from the DEX API. It speaks the v2 subscription protocol: open one connection, subscribe to channels, and receive { channel, data } frames. The payload types are WebSocket-native (they differ from the REST shapes) and are exported from the SDK.
Connecting
import { createAultClient, getNetworkConfig } from "ault-sdk-ts";
const client = createAultClient({
network: { ...getNetworkConfig("<CHAIN_ID>"), dexApiUrl: "<DEX_API_URL>" },
});
// The WS client is created lazily from dexApiUrl (http→ws, appends /ws)
await client.ws.connect();Or use the standalone factory:
import { createDexWs } from "ault-sdk-ts";
const ws = createDexWs("wss://my-dex-api.example.com/ws");
await ws.connect();Subscribing to Channels
Pass a subscription descriptor and the onData callback is typed from its type. Market channels need a coin; the candle channel also needs an interval. Frames flow through onData as they arrive. For snapshot-gated channels the first frame is the snapshot and the rest are deltas, so don't assume the first frame is a delta.
// L2 order book — snapshot first, then per-block deltas (data typed as WsBook)
const sub = client.ws.subscribe(
{ type: "l2Book", coin: "AULT/USDC" },
{
onData: (book) => {
const [bids, asks] = book.levels;
console.log(bids[0], asks[0]); // { px, sz, n }
},
},
);
// Trades in each block (data typed as WsTrade[])
client.ws.subscribe(
{ type: "trades", coin: "AULT/USDC" },
{ onData: (trades) => trades.forEach((t) => console.log(t.side, t.px, t.sz)) },
);
// Best bid / offer (data typed as WsBbo)
client.ws.subscribe(
{ type: "bbo", coin: "AULT/USDC" },
{ onData: (b) => console.log(b.bbo[0], b.bbo[1]) },
);
// Candles (data typed as WsCandle[])
client.ws.subscribe(
{ type: "candle", coin: "AULT/USDC", interval: "1m" },
{ onData: (bars) => console.log(bars[bars.length - 1]) },
);
// Unsubscribe
sub.unsubscribe();Subscription Options
ttl sets a subscription lifetime in seconds (the server caps it at 3,600). The ack carries the effective, clamped value.
client.ws.subscribe(
{ type: "l2Book", coin: "AULT/USDC" },
{
onData: (data) => {
/* ... */
},
onSubscribed: (ack) => console.log("subscribed; effective ttl", ack.ttl),
onExpired: () => console.log("subscription expired"),
onError: (err) => console.error(err),
},
{ ttl: 300 },
);User channels
User channels key on an address (ault1... or 0x..., either case) and expose read-only projections of public on-chain state, so no authentication is required. The address round-trips in the ack in the format you sent.
// Order updates (data typed as WsOrderUpdate[])
client.ws.subscribe(
{ type: "orderUpdates", user: "ault1..." },
{
onData: (orders) => orders.forEach((o) => console.log(o.order.oid, o.status, o.chainStatus)),
},
);
// Spot balances — first frame is the full set, later frames are per-coin deltas
client.ws.subscribe(
{ type: "spotState", user: "ault1..." },
{ onData: (state) => console.log(state.spotState.balances) },
);
// Fills — the snapshot frame carries isSnapshot: true, live frames false
client.ws.subscribe(
{ type: "userFills", user: "ault1..." },
{ onData: (f) => console.log(f.isSnapshot, f.fills) },
);
// userEvents data arrives on the wire "user" channel; the SDK routes it back here
client.ws.subscribe({ type: "userEvents", user: "ault1..." }, { onData: (ev) => console.log(ev) });Connection Events
client.ws.on("open", () => console.log("connected"));
client.ws.on("close", (code, reason) => console.log("closed", code, reason));
client.ws.on("error", (err) => console.error(err));
client.ws.on("reconnecting", (attempt) => console.log("reconnecting...", attempt));
client.ws.on("reconnected", () => console.log("reconnected"));
// Raw message listener
client.ws.on("message", (msg) => console.log(msg.channel, msg.data));Reconnection
Auto-reconnect is enabled by default with exponential backoff. On reconnect, every active subscription is re-sent with its original TTL.
const ws = createDexWs("wss://my-dex-api.example.com/ws", {
reconnect: true, // default: true
reconnectDelay: 1000, // default: 1000ms
maxReconnectDelay: 30000, // default: 30000ms
maxReconnectAttempts: Infinity, // default: Infinity
});Disconnecting
client.ws.disconnect();Parallel Query Helpers
For analyzing large numbers of licenses efficiently, use the parallel query helpers:
// Analyze all licenses for an owner in one call (parallel fetching)
const analysis = await client.analyzeLicenses("ault1...");
console.log(`Total: ${analysis.total}`);
console.log(`Active: ${analysis.active}`);
console.log(`Delegated: ${analysis.delegated}`);
// Group delegations by operator
const byOperator = new Map<string, number>();
for (const d of analysis.delegations) {
byOperator.set(d.operator, (byOperator.get(d.operator) ?? 0) + 1);
}The analyzeLicenses() method fetches all data in parallel batches, making it much faster than sequential queries. For an address with 464 licenses, it completes in ~15-30 seconds instead of several minutes.
Individual Parallel Helpers
For more control, use the individual parallel helpers:
// Get all license IDs owned by an address (parallel batched)
const licenseIds = await client.getAllLicenseIds("ault1...");
// Get license details for multiple IDs in parallel
const licenses = await client.getLicenseDetailsParallel(licenseIds);
// Get delegation status for multiple IDs in parallel
const delegations = await client.getLicenseDelegationsParallel(licenseIds);
// Process results
for (const d of delegations) {
if (d.isDelegated) {
console.log(`License ${d.licenseId} delegated to ${d.operator}`);
}
}Transaction Methods
All transaction methods return a TxResult:
interface TxResult {
txHash: string; // Transaction hash
code: number; // Result code (0 = success)
rawLog?: string; // Raw log from transaction
confirmed?: boolean; // true = DeliverTx observed; false = broadcast-only or poll timeout; undefined = confirmation lookup unavailable
success: boolean; // True only when DeliverTx success was confirmed
}The CLOB order methods (placeLimitOrder, placeMarketOrder,
placeScaleOrder, cancelOrders) default to broadcast-only: they return once
CheckTx accepts the transaction, so the result has confirmed: false and
success: false. Pass wait: true to poll for DeliverTx and get back the
decoded order IDs. See Market And CLOB
Transactions.
License Transactions
// Mint a license
await client.mintLicense({
to: "0x...", // EVM or ault1 address
uri: "https://example.com/metadata.json",
reason: "Minted via SDK", // Optional, defaults to ""
});
// Batch mint
await client.batchMintLicense({
recipients: [
{ to: "0xAddr1...", uri: "https://example.com/1.json" },
{ to: "0xAddr2...", uri: "https://example.com/2.json" },
],
});
// Transfer
await client.transferLicense({
licenseId: 123, // bigint, number, or string
to: "0x...",
});
// Burn/Revoke (admin only)
await client.burnLicense({ licenseId: 123 });
await client.revokeLicense({ licenseId: 123 });
// KYC management
await client.approveMember({ member: "0x..." });
await client.revokeMember({ member: "0x..." });
await client.batchApproveMember({ members: ["0x...", "0x..."] });
// Admin functions
await client.setMinters({ add: ["0x..."], remove: [] });
await client.setKYCApprovers({ add: ["0x..."], remove: [] });Miner Transactions
import { base64ToBytes, MarketType } from "ault-sdk-ts";
// Delegate licenses to an operator
await client.delegateMining({
licenseIds: [1, 2, 3], // bigint[], number[], or string[]
operator: "0xOperator...",
});
// Cancel mining delegation
await client.cancelMiningDelegation({
licenseIds: [1, 2, 3],
});
// Redelegate to a new operator
await client.redelegateMining({
licenseIds: [1, 2, 3],
newOperator: "0xNewOperator...",
});
// Set VRF key (Uint8Array)
await client.setOwnerVrfKey({
vrfPubkey: base64ToBytes("base64..."),
possessionProof: base64ToBytes("base64..."),
nonce: 1n,
});
// Submit mining work (Uint8Array)
await client.submitWork({
licenseId: 123,
epoch: 456,
y: base64ToBytes("base64..."),
proof: base64ToBytes("base64..."),
});
// Batch submit work
await client.batchSubmitWork({
submissions: [
{ licenseId: 1, epoch: 100, y: base64ToBytes("..."), proof: base64ToBytes("...") },
{ licenseId: 2, epoch: 100, y: base64ToBytes("..."), proof: base64ToBytes("...") },
],
});
// Operator management
await client.registerOperator({
commissionRate: 5, // Percentage (0-100)
commissionRecipient: "0x...", // Optional, defaults to signer
});
await client.unregisterOperator();
await client.updateOperatorInfo({
newCommissionRate: 6,
newCommissionRecipient: "0x...",
});Market And CLOB Transactions
import { base64ToBytes } from "ault-sdk-ts";
const marketId = base64ToBytes("base64MarketId...");
// Place a limit order (broadcast-only: returns once CheckTx accepts it).
await client.placeLimitOrder({
marketId,
isBuy: true,
price: "1.5",
quantity: "100",
lifespan: { seconds: 3600n, nanos: 0 }, // 1 hour
});
// Pass `wait: true` to poll for DeliverTx and read back the order ID.
const placed = await client.placeLimitOrder({
marketId,
isBuy: true,
price: "1.5",
quantity: "100",
lifespan: { seconds: 3600n, nanos: 0 },
wait: true,
});
console.log(placed.orderId);
// Place a market order
await client.placeMarketOrder({
marketId,
isBuy: true,
deposit: "100",
});
// Cancel specific orders. With `wait: true` the result lists which IDs
// succeeded and which failed.
await client.cancelOrders({ orderIds: [base64ToBytes("base64OrderId...")] });
// Client order id (cloid): attach an 8-byte id to an order so you can address
// it without the chain-assigned order id. It is echoed back (as hex) on the
// wait-mode result, and cancelOrders accepts a cloid or an order id
// interchangeably.
const cloid = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
const withCloid = await client.placeLimitOrder({
marketId,
isBuy: true,
price: "1.5",
quantity: "100",
deadline: new Date(Date.now() + 3600_000),
cloid,
wait: true,
});
console.log(withCloid.cloid); // "0102030405060708"
await client.cancelOrders({ orderIds: [cloid] });
// Look up an order by (orderer, cloid), chain-first with a dex-api fallback.
// Live orders resolve from the chain; filled/cancelled ones from the indexer.
const found = await client.findOrderByCloid(client.address, cloid);
if (found.source !== "none") console.log(found.source, found.order);
// Create a market
await client.createMarket({
marketType: MarketType.MARKET_TYPE_SPOT,
baseDenom: "uatom",
quoteDenom: "uusdc",
tickPrecision: 6,
});Reward And TokenRegistry Transactions
// Create a referral code as an eligible referrer
await client.setReferralCode({
code: "RW18A001",
});
// Approve a builder fee allowance
await client.approveBuilder({
builder: "0x0000000000000000000000000000000000000001",
maxFeeRate: "0.005",
});
// Register an ERC20 token pair
await client.registerTokenPair({
erc20Address: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
enabled: true,
});
// Toggle module-wide token conversions
await client.disableTokenRegistryTransfers();
await client.enableTokenRegistryTransfers();Transaction Options
All transaction methods accept optional gasLimit and memo:
await client.delegateMining({
licenseIds: [1, 2, 3],
operator: "0x...",
gasLimit: "300000", // Override default gas limit
memo: "Delegating via SDK", // Custom memo
});Agent-Signed Transactions
The chain's agent module lets a master account approve a separate keypair to
submit a restricted set of CLOB transactions on its behalf, without prompting
the master signer for each one. Typical use cases: market-making bots that
sign hot orders with a short-lived key, or trading UIs that sign orders
without a wallet popup per click.
The SDK exposes this through client.attachAgent(...) rather than a separate
client. The agent surface reuses the master client's manifest resolver, REST
contexts, and account-info cache — one network state, one of every cached
resource, no chance of drift between wallet-signed and agent-signed paths.
Approve and attach an agent
import { createClient, encodeEthSecp256k1PubKey, deriveAgentAddress } from "ault-sdk-ts";
import { secp256k1 } from "@noble/curves/secp256k1";
import { hexToBytes } from "viem";
const client = await createClient({ network, signer: masterSigner });
// 1. Generate (or load) the agent keypair.
const agentPrivateKey = `0x${"a".repeat(64)}`; // load from secret store in real usage
const compressed = secp256k1.getPublicKey(hexToBytes(agentPrivateKey), true);
const agentPubkey = {
typeUrl: "/cosmos.evm.crypto.v1.ethsecp256k1.PubKey",
value: encodeEthSecp256k1PubKey(compressed),
};
// 2. Master approves the agent on-chain (wallet-signed).
await client.approveAgent({
agentPubkey,
name: "market-maker-bot",
expiry: { seconds: 86_400n * 30n, nanos: 0 }, // 30 days
});
// 3. Attach the agent surface to this client.
const agent = client.attachAgent({ agentPrivateKey });
console.log(agent.address); // bech32 agent address
console.log(agent.masterAddress); // = client.address
console.log(client.asAgent === agent); // truePlace agent-signed orders
// Same shape as the wallet-signed equivalents on `client.*`, including the
// broadcast-only default and the `wait: true` opt-in for a result with the
// decoded order ID.
const placed = await client.asAgent!.placeLimitOrder({
marketId,
isBuy: true,
price: "1.0",
quantity: "1000000000000000000",
lifespan: { seconds: 3600n, nanos: 0 },
wait: true,
});
console.log(placed.orderId);
await client.asAgent!.placeMarketOrder({
marketId,
isBuy: true,
deposit: "5000000000000000000",
});
await client.asAgent!.cancelOrders({ orderIds: [...] });
await client.asAgent!.placeScaleOrder({
marketId,
isBuy: true,
lowPrice: "0.9",
highPrice: "1.1",
totalQuantity: "10000000000000000000",
weights: [3333, 3334, 3333],
lifespan: { seconds: 3600n, nanos: 0 },
});Rotate or detach the agent
// Rotate: attachAgent replaces the prior agent in place. The previous
// surface is overwritten — read from `client.asAgent` after attaching.
client.attachAgent({ agentPrivateKey: newKey });
// Detach: clears `client.asAgent`. Idempotent.
client.detachAgent();
// Revoke on-chain (wallet-signed) when the agent should no longer sign.
await client.revokeAgent({ agentAddress: agent.address });What attachAgent shares with the master client
| Resource | Behavior |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ManifestResolver | One per Client. The agent path reads getNetwork() / getGas() from the same resolver, so URL and EIP-712 manifest overrides reach both signing paths atomically. |
| AccountCache | The master's accountNumber is fetched once. The agent path reads it from the shared cache; it never increments nextSequence (agent txs always sign with sequence=0). |
| REST/WS contexts | The master client's client.<module> query APIs are the only query surface — the agent surface is tx-only. |
| Lifecycle | client.dispose() clears asAgent and stops the manifest refresh timer. Calling attachAgent after dispose() throws. |
Notes
client.asAgentisnulluntilattachAgentis called. After detach or dispose it isnullagain. Always read it fresh; do not cache references across rotations.attachAgentis synchronous (no network I/O). The first agent tx may trigger onequeryAccountRPC if the cache is cold; subsequent agent txs reuse the cachedaccountNumber.- Two concurrent agents from the same master require two
Clientinstances —attachAgentreplaces, it does not coexist.
Development
Generating Protos
Proto and EIP-712 generation expects both the Ault core repo and the Cosmos SDK repo to be checked out alongside this repo:
../ault/proto
../cosmos-sdk/protoFrom this repo root, run:
pnpm gen:protoNote: pnpm gen:eip712 also reads Cosmos SDK protos (staking/distribution) and requires ../cosmos-sdk/proto.
Error Handling
import { ApiError, NetworkError, TimeoutError } from "ault-sdk-ts";
try {
const result = await client.mintLicense({ to: "0x...", uri: "..." });
if (result.success) {
console.log(`Transaction committed: ${result.txHash}`);
} else if (result.confirmed === false) {
console.warn(`Transaction broadcast but not confirmed yet: ${result.txHash}`);
} else if (result.confirmed === undefined) {
console.warn(
`Transaction accepted by CheckTx, but this node cannot confirm it: ${result.txHash}`,
);
} else {
console.error(`Transaction failed: ${result.rawLog}`);
}
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error (${error.status}): ${error.message}`);
} else if (error instanceof NetworkError) {
console.error(`Network Error: ${error.message}`);
} else if (error instanceof TimeoutError) {
console.error("Request timed out");
}
}Advanced Usage
For advanced use cases, you can access the low-level client:
// Access low-level client
const lowLevel = client._lowLevel;
// Build EIP-712 typed data manually
const typedData = lowLevel.eip712.buildTypedData(context, msgs);
// Use the msg namespace directly
import { msg } from "ault-sdk-ts";
import { privateKeyToAccount } from "viem/accounts";
const mySigner = privateKeyToAccount("0x...");
const delegateMsg = msg.miner.delegateMining({
owner: "ault1...",
operator: "ault1...",
licenseIds: [1n, 2n, 3n],
});
// Sign and broadcast manually
const result = await lowLevel.eip712.signAndBroadcast({
signer: mySigner, // provide the signer explicitly
signerAddress: "ault1...",
msgs: [delegateMsg],
});Low-Level Client
For more control, use createAultClient() directly:
import { createAultClient, getNetworkConfig, msg, signAndBroadcastEip712 } from "ault-sdk-ts";
const client = createAultClient({
network: getNetworkConfig("ault_10904-1"),
});
// Query
const licenses = await client.rest.license.getLicenses();
// Build and broadcast transactions manually
const result = await signAndBroadcastEip712({
network: client.network,
signer: mySigner,
signerAddress: "ault1...",
msgs: [
msg.miner.delegateMining({
owner: "ault1...",
operator: "ault1...",
licenseIds: [1n, 2n, 3n],
}),
],
});Network Configuration
import { getNetworkConfig, NETWORKS } from "ault-sdk-ts";
// Use predefined networks
const testnet = getNetworkConfig("ault_10904-1");
const localnet = getNetworkConfig("ault_30904-1");
// Or define custom network
const customNetwork = {
name: "My Network",
type: "testnet" as const,
chainId: "ault_10904-1",
evmChainId: 10904,
rpcUrl: "https://my-rpc.example.com",
restUrl: "https://my-rest.example.com",
evmRpcUrl: "https://my-evm.example.com",
dexApiUrl: "https://my-dex-api.example.com",
dexWsUrl: "wss://my-dex-api.example.com/ws", // optional, derived from dexApiUrl if omitted
isProduction: false,
};Utility Functions
import {
evmToAult,
aultToEvm,
isValidAultAddress,
isValidEvmAddress,
parseEvmChainIdFromCosmosChainId,
} from "ault-sdk-ts";
// Address conversion
const aultAddr = evmToAult("0x1234...abcd"); // => "ault1..."
const evmAddr = aultToEvm("ault1..."); // => "0x1234...abcd"
// Validation
isValidAultAddress("ault1..."); // true/false
isValidEvmAddress("0x1234..."); // true/false
// Chain ID
const evmChainId = parseEvmChainIdFromCosmosChainId("ault_10904-1"); // => 10904Constants
import { GAS_CONSTANTS, TIMING_CONSTANTS } from "ault-sdk-ts";
GAS_CONSTANTS.EIP712_FEE_AMOUNT; // '5000000000000000'
GAS_CONSTANTS.EIP712_GAS_LIMIT; // '200000'
GAS_CONSTANTS.DENOM; // 'aault'
GAS_CONSTANTS.PER_LICENSE; // 200000
TIMING_CONSTANTS.API_TIMEOUT_MS; // 30000
TIMING_CONSTANTS.API_RETRY_DELAY_MS; // 1000
TIMING_CONSTANTS.API_MAX_BACKOFF_MS; // 30000Runtime Compatibility
- Node.js: Requires Node.js 18+ (for native fetch) or provide custom fetch
- Bun: Full support
- Browser: Full support in modern browsers
For older Node.js versions:
import fetch from "node-fetch";
const client = await createClient({
network: getNetworkConfig("ault_10904-1"),
signer: mySigner,
fetchFn: fetch as unknown as typeof globalThis.fetch,
});Releases
This project uses Semantic Versioning and publishes to npm via GitHub Actions.
Publishing a stable release
Update
CHANGELOG.mdwith the changes since the last release.Bump the version in
package.json:
# Patch release (bug fixes): 0.0.2 -> 0.0.3
npm version patch --no-git-tag-version
# Minor release (new features): 0.0.2 -> 0.1.0
npm version minor --no-git-tag-version
# Major release (breaking changes): 0.0.2 -> 1.0.0
npm version major --no-git-tag-version- Commit, tag, and push:
git add package.json CHANGELOG.md
git commit -m "chore: release v0.1.0"
git tag v0.1.0
git push origin master v0.1.0- The
Publishworkflow runs automatically: validates the build, publishes to npm with provenance, and creates a GitHub Release with auto-generated notes.
Publishing a pre-release (alpha / beta / rc)
Pre-release tags publish to a separate npm dist-tag so they never become the default install.
# Set the pre-release version
git checkout master
git pull
git checkout -b "release/v0.1.0-alpha.1"
npm version 0.1.0-alpha.1 --no-git-tag-version
git add package.json
git commit -m "chore: release v0.1.0-alpha.1"
git push origin release/v0.1.0-alpha.1
# Create PR and merge to master
git checkout master
git pull
git tag v0.1.0-alpha.1
git push origin v0.1.0-alpha.1Consumers install with the corresponding tag:
npm install ault-sdk-ts@alpha # latest alpha
npm install ault-sdk-ts@beta # latest beta
npm install ault-sdk-ts@rc # latest release candidate| Tag pattern | npm dist-tag | GitHub Release |
| ---------------- | ------------ | -------------- |
| v0.1.0 | latest | stable |
| v0.1.0-alpha.* | alpha | pre-release |
| v0.1.0-beta.* | beta | pre-release |
| v0.1.0-rc.* | rc | pre-release |
CI checks
All PRs and pushes to master run:
- Lint (oxlint)
- Typecheck (tsc)
- Unit tests (vitest)
- Build validation
- Package contents check (
npm pack --dry-run)
PR conventions
- PR titles should follow Conventional Commits (e.g.
feat: add batch orders,fix: WebSocket reconnect). A bot will comment if the title doesn't match — this is a warning, not a blocker. - PRs with user-facing changes (
feat:,fix:) should updateCHANGELOG.md.
Repository setup
Required GitHub Actions secrets:
NPM_TOKEN— npm access token with publish rights forault-sdk-ts
License
MIT
