TypeScript/JavaScript SDK for interacting with the Ambient Ember perpetual futures DEX on Solana.
npm install @crocswap-libs/ambient-ember
# or
yarn add @crocswap-libs/ambient-ember
# or
pnpm add @crocswap-libs/ambient-emberimport { Connection, PublicKey } from "@solana/web3.js";
import * as ember from "@crocswap-libs/ambient-ember";
const connection = new Connection("https://testnet.fogo.io");
const user = new PublicKey("YourWalletAddress");
// Place a market order
const tx = await ember.buildMarketBuyOrder(
connection,
64n, // Market ID (64 = BTC)
BigInt(Date.now()), // Order ID
100000000n, // 1.0 in 10^8 scale
user,
);Hosted APIs such as /exchange expect REST payloads to include a signed permit. The SDK ships with sdk/scripts/signPayload.mjs to handle this end to end: it injects a millisecond nonce when absent, aligns the permit expiry the same way the on-chain program does, signs with your keypair, and returns the original payload augmented with signature and pubkey.
# Draft your payload (no signature fields yet)
PAYLOAD='{"action":{"type":"faucet","amount":"100","marketId":64}}'
# Produce a signed payload
SIGNED=$(echo "$PAYLOAD" \
| node sdk/scripts/signPayload.mjs ~/.config/solana/id.json --stdin)
# Send to the exchange endpoint
curl -X POST https://your-hosted-api.example/exchange \
-H "Content-Type: application/json" \
-d "$SIGNED"What the helper does for you:
- Auto-populates
nonce = Date.now()(ms) if you leave it out, so permits stay valid for ~60 seconds. - Resets
expiresUnixtofloor(nonce / 1000) + 60, matching the server’s envelope rules. - Emits signatures as hex strings alongside the inferred
pubkey(defaults to the signer unless you override it in the payload). - Keeps things idempotent—on the receiving side the exchange API prepends
InitCmaandInitNonceWindow, so brand-new wallets can immediately execute a permit-backed call.
- Key Concepts
- Order Management
- Position Management
- Margin and Collateral
- Querying Data
- Risk Management
- Types and Constants
- Examples
- Permit Test Fixtures
- API Reference
The SDK uses fixed-point arithmetic with specific decimal scales:
- Prices: Scaled by 10^6 (e.g., $100.50 = 100500000)
- Quantities: Scaled by 10^8 (e.g., 1.5 BTC = 150000000)
- Collateral/USD: Scaled by 10^6 (e.g., $1000 = 1000000000)
- Basis Points (bps): 1 bps = 0.01% (e.g., 500 bps = 5%)
- CMA (Cross Margin Account): User's main trading account that holds margin buckets
- Margin Bucket: Per-market collateral and position tracking
- Order Details: Storage for a user's open orders in a market
- Market Order Log: Append-only log of all market events
// Program ID (can be overridden with EMBER_PROGRAM_ID env var)
export const EMBER_PROGRAM_ID = new PublicKey("ambi3LHRUzmU187u4rP46rX6wrYrLtU1Bmi5U2yCTGE");
// Default USD mint
export const USD_MINT = new PublicKey("fUSDNGgHkZfwckbr5RLLvRbvqvRcTLdH9hcHJiq4jry");
// Default market
export const DFLT_EMBER_MARKET = {
mktId: 64,
ticker: "BTC",
name: "Bitcoin",
};Market orders are IOC (Immediate or Cancel) orders with price=0 convention.
// Market Buy
const buyTx = await ember.buildMarketBuyOrder(
connection,
64n, // Market ID
orderId, // Unique order ID (bigint)
100000000n, // 1.0 BTC in 10^8 scale
userPublicKey,
);
// Market Sell
const sellTx = await ember.buildMarketSellOrder(connection, 64n, orderId, 100000000n, userPublicKey);// Limit Buy Order
const buyTx = await ember.buildBuyOrder(
connection,
64n, // Market ID
orderId, // Unique order ID
100000000n, // Quantity: 1.0 BTC
50000000000n, // Price: $50,000
userPublicKey,
{ type: ember.TimeInForce.GTC }, // Good Till Cancelled
1000, // Optional: 10% user initial margin
);
// Limit Sell with Time Expiry
const sellTx = await ember.buildSellOrder(
connection,
64n,
orderId,
50000000n, // 0.5 BTC
51000000000n, // $51,000
userPublicKey,
{
type: ember.TimeInForce.GTT,
timestamp: BigInt(Date.now() / 1000 + 3600), // Expires in 1 hour
},
);const tx = await ember.buildOrderEntryTransaction(connection, {
marketId: 64n,
orderId: uniqueOrderId,
side: ember.OrderSide.Bid, // 0 = Buy, 1 = Sell
qty: 100000000n, // 1.0 BTC
price: 50000000000n, // $50,000
tif: { type: ember.TimeInForce.GTC },
user: userPublicKey,
actor: signerPublicKey, // Optional: delegated trading
userSetImBps: 1000, // Optional: 10% initial margin
});const cancelTx = await ember.buildCancelOrderTransaction(connection, {
marketId: 64n,
orderId: orderToCancel,
user: userPublicKey,
tombstone: ember.OrderTombstone.UserCancel,
});To close a position, place an order in the opposite direction:
// Close a long position with market order
const closeLongTx = await ember.buildMarketSellOrder(
connection,
marketId,
orderId,
positionSize, // Your current position size
userPublicKey,
);
// Close with limit order
const closeTx = await ember.buildSellOrder(connection, marketId, orderId, positionSize, targetPrice, userPublicKey);// Deposit and commit to margin in one transaction
const depositTx = await ember.buildDepositMarginTx(connection, {
user: userPublicKey,
mint: ember.USD_MINT,
amount: 1000000000n, // $1000 in 10^6 scale
marketId: 64n, // Target market
});// Uncommit and withdraw in one transaction
const withdrawTx = await ember.buildWithdrawMarginTx(connection, {
user: userPublicKey,
mint: ember.USD_MINT,
amount: 500000000n, // $500 in 10^6 scale
marketId: 64n,
});// Set custom initial margin (must be >= market minimum)
const setMarginTx = await ember.createSetUserMarginTransaction({
connection,
user: userPublicKey,
marketId: 64n,
userSetImBps: 2000, // 20% initial margin
});// Get full market data
const market = await ember.getMarketData(connection, 64n);
if (market) {
console.log({
lastBid: Number(market.lastBid) / 1e6,
lastAsk: Number(market.lastAsk) / 1e6,
lastTradePrice: Number(market.lastTradePrice) / 1e6,
lastMarkPrice: Number(market.lastMarkPrice) / 1e6,
spread: Number(market.spread) / 1e6,
isActive: market.isActive,
imBps: market.imBps, // Initial margin
mmBps: market.mmBps, // Maintenance margin
tickSize: market.tickSize,
minOrderSize: market.minOrderSize,
});
}
// Get just prices
const prices = await ember.getMarketPrices(connection, 64n);// Get margin bucket with all calculations
const marginBucket = await ember.getUserMarginBucket(
connection,
userPublicKey,
64n, // Market ID
ember.USD_MINT, // Collateral token
);
if (marginBucket) {
// Position info
console.log({
netPosition: Number(marginBucket.netPosition) / 1e8,
avgEntryPrice: Number(marginBucket.avgEntryPrice) / 1e6,
committedCollateral: Number(marginBucket.committedCollateral) / 1e6,
});
// P&L calculations
console.log({
markPrice: Number(marginBucket.markPrice) / 1e6,
unrealizedPnl: Number(marginBucket.unrealizedPnl) / 1e6,
equity: Number(marginBucket.equity) / 1e6,
});
// Available balances
console.log({
availToBuy: Number(marginBucket.availToBuy) / 1e6,
availToSell: Number(marginBucket.availToSell) / 1e6,
availToWithdraw: Number(marginBucket.availToWithdraw) / 1e6,
});
// Margin requirements
console.log({
userSetImBps: marginBucket.userSetImBps,
marketImBps: marginBucket.marketImBps,
effectiveImBps: marginBucket.effectiveImBps, // max(user, market)
});
}// Current position liquidation price
const liqPrice = ember.calcLiqPrice(
1000, // Collateral: $1000
{
qty: 0.1, // 0.1 BTC long
entryPrice: 50000, // Entry at $50k
},
0.05, // 5% maintenance margin
);
// Liquidation price after a new order
const newLiqPrice = ember.calcLiqPriceOnNewOrder(
1000, // Current collateral
{ qty: 0.1, entryPrice: 50000 }, // Current position
{ qty: 0.05, entryPrice: 51000 }, // New order
0.05, // Maintenance margin
);const result = await ember.validateOrder(connection, {
user: userPublicKey,
marketId: 64n,
side: ember.OrderSide.Bid,
quantity: 100000000n,
price: 50000000000n,
orderType: "limit",
});
if (result.isValid) {
// Order can be placed
} else {
console.error("Validation failed:", result.errors);
// Possible errors:
// - INSUFFICIENT_MARGIN
// - EXCEEDS_POSITION_LIMIT
// - BELOW_MIN_ORDER_SIZE
// - INVALID_PRICE_TICK
}// Order side
enum OrderSide {
Bid = 0, // Buy
Ask = 1, // Sell
}
// Time in force options
enum TimeInForce {
IOC = "IOC", // Immediate or Cancel
FOK = "FOK", // Fill or Kill
GTC = "GTC", // Good Till Cancelled
ALO = "ALO", // Add Liquidity Only
GTT = "GTT", // Good Till Time
}
// Time in force variants
type TimeInForceVariant =
| { type: TimeInForce.IOC }
| { type: TimeInForce.FOK }
| { type: TimeInForce.GTC }
| { type: TimeInForce.ALO }
| { type: TimeInForce.GTT; timestamp: bigint };
// Order cancellation reasons
enum OrderTombstone {
UserCancel = 0,
Liquidation = 1,
Expiry = 2,
}// Base margin bucket
interface MarginBucket {
scope: MarginScope;
marketId: bigint;
mint: PublicKey;
committedCollateral: bigint;
netPosition: bigint;
openBidQty: bigint;
openAskQty: bigint;
avgEntryPrice: bigint;
userSetImBps: number;
}
// With pricing calculations
type MarginBucketPriced = MarginBucket & {
markPrice: bigint;
equity: bigint;
unrealizedPnl: bigint;
marketImBps: number;
effectiveImBps: number;
};
// With available balance calculations
type MarginBucketAvail = MarginBucketPriced & {
availToBuy: bigint;
availToSell: bigint;
availToWithdraw: bigint;
};interface MarketData {
marketId: bigint;
lastBid: bigint;
lastAsk: bigint;
lastTradePrice: bigint;
lastMarkPrice: bigint;
midPrice: bigint;
spread: bigint;
isActive: boolean;
imBps: number;
mmBps: number;
tickSize: bigint;
minOrderSize: bigint;
oracle: PublicKey;
baseToken: PublicKey;
}import * as ember from "@crocswap-libs/ambient-ember";
import { Connection, PublicKey, sendAndConfirmTransaction } from "@solana/web3.js";
async function executeTrade(
connection: Connection,
user: Keypair,
side: "buy" | "sell",
quantity: number, // Human readable (e.g., 0.1 BTC)
price: number, // Human readable (e.g., 50000)
) {
// Convert to scaled values
const scaledQty = BigInt(Math.floor(quantity * 1e8));
const scaledPrice = BigInt(Math.floor(price * 1e6));
const marketId = 64n; // BTC market
// Check market status
const market = await ember.getMarketData(connection, marketId);
if (!market?.isActive) {
throw new Error("Market is not active");
}
// Check user margin
const margin = await ember.getUserMarginBucket(connection, user.publicKey, marketId);
const available = side === "buy" ? margin?.availToBuy || 0n : margin?.availToSell || 0n;
const notional = (scaledQty * scaledPrice) / 100000000n;
const required = (notional * BigInt(market.imBps)) / 10000n;
if (available < required) {
throw new Error(`Insufficient margin. Need: $${Number(required) / 1e6}`);
}
// Build and send order
const orderId = BigInt(Date.now());
const tx =
side === "buy"
? await ember.buildBuyOrder(connection, marketId, orderId, scaledQty, scaledPrice, user.publicKey)
: await ember.buildSellOrder(connection, marketId, orderId, scaledQty, scaledPrice, user.publicKey);
const sig = await sendAndConfirmTransaction(connection, tx, [user]);
console.log("Order placed:", sig);
return sig;
}async function manageMargin(
connection: Connection,
user: Keypair,
action: "deposit" | "withdraw",
amount: number, // USD amount
) {
const scaledAmount = BigInt(Math.floor(amount * 1e6));
if (action === "deposit") {
// Deposit and commit to margin
const tx = await ember.buildDepositMarginTx(connection, {
user: user.publicKey,
mint: ember.USD_MINT,
amount: scaledAmount,
marketId: 64n,
});
const sig = await sendAndConfirmTransaction(connection, tx, [user]);
console.log("Deposited:", sig);
} else {
// Check available to withdraw
const margin = await ember.getUserMarginBucket(connection, user.publicKey, 64n);
if (!margin || margin.availToWithdraw < scaledAmount) {
throw new Error("Insufficient available balance");
}
// Withdraw
const tx = await ember.buildWithdrawMarginTx(connection, {
user: user.publicKey,
mint: ember.USD_MINT,
amount: scaledAmount,
marketId: 64n,
});
const sig = await sendAndConfirmTransaction(connection, tx, [user]);
console.log("Withdrawn:", sig);
}
}The permit serialization tests (tests/permit.spec.ts) use fixtures generated directly from the Rust permit types. Regenerate them whenever you touch core/src/permit.rs, the SDK encoder, or any supporting enums so the byte layout used for signatures stays perfectly aligned.
- Confirm Rust tooling is available (
rustup --version). - From the repository root, run the generator:
This compiles
cargo run -p permit-fixtures
tools/permit-fixturesand rewritessdk/tests/fixtures/permits.jsonwith fresh fixtures. - Re-run the targeted tests to make sure the SDK encoder still matches Rust:
yarn test tests/permit.spec.ts
The generator serializes representative PermitEnvelopeV1 payloads to Borsh and exports the resulting base64 strings. The Vitest suite imports those fixtures and asserts that encodePermitEnvelope produces the exact same byte sequence, which guarantees compatibility with the on-chain signature verifier.
The SDK exposes helpers for signing Hyperliquid-style payloads with the same Borsh layout the program expects:
import {
signPermits,
signPermitsBase64,
createHyperliquidContext,
buildPermitEnvelopesFromExchangeRequest,
signHyperliquidRequest,
} from "@crocswap-libs/ambient-ember/permit";
const { signatures, messages } = signPermits(envelopes, keypair); // hex by default
const base64 = signPermitsBase64(envelopes, keypair); // convenience wrapper
// Hyperliquid-style helper
const context = createHyperliquidContext(markets, programId, keypair.publicKey);
const result = signHyperliquidRequest({ action, nonce }, context, keypair);
fetch("/exchange", {
method: "POST",
body: JSON.stringify({ action, nonce, signature: result.signatures }),
});- Pass either a single
PermitEnvelopeV1or an array. The helper returnssignaturesas a string for single envelopes or astring[]for batches. - The
messagesfield mirrors this shape and contains the serialized permit bytes (hex by default). signatureList/messageListgive access to the array form regardless of input, along with the rawUint8Arraybuffers.
This allows API clients to drop signatures directly into the /exchange signature field. When batching multiple actions, provide each permit in order so the server can match the translated actions to the signed bytes.
buildOrderEntryTransaction()- Full control order entrybuildBuyOrder()- Convenience for limit buy ordersbuildSellOrder()- Convenience for limit sell ordersbuildMarketBuyOrder()- Market buy orderbuildMarketSellOrder()- Market sell orderbuildCancelOrderTransaction()- Cancel an orderbuildDepositMarginTx()- Deposit and commit marginbuildWithdrawMarginTx()- Uncommit and withdraw margincreateSetUserMarginTransaction()- Set custom margin requirements
getMarketData()- Full market informationgetMarketPrices()- Just bid/ask/trade pricesgetUserMarginBucket()- User position and margin infogetMultipleMarketData()- Batch market queries
calcLiqPrice()- Calculate liquidation pricecalcLiqPriceOnNewOrder()- Liquidation price after new orderpriceMarginBucketPnl()- Calculate P&L for margin bucketcalcMarginAvail()- Calculate available balances
orderEntryIx()- Order entry instructioncancelOrderIx()- Cancel order instructiondepositIx()- Deposit instructionwithdrawIx()- Withdraw instructioncommitCollateralIx()- Commit collateral instructionuncommitCollateralIx()- Uncommit collateral instructionsetUserMarginIx()- Set user margin instructioninitCMAIx()- Initialize CMA instructioninitMarketIx()- Initialize market instruction
cmaPda()- Cross Margin Account addressmarketPda()- Market state addressorderDetailsPda()- Order details storage addressmarketOrderLogPda()- Market order log address
try {
const tx = await ember.buildOrderEntryTransaction(connection, params);
} catch (error) {
if (error.message.includes("Market") && error.message.includes("not found")) {
// Market doesn't exist
} else if (error.message.includes("CMA account")) {
// User account not initialized
} else if (error.message.includes("Insufficient margin")) {
// Not enough collateral
}
}- Always use BigInt for numeric values to avoid precision loss
- Check market data before placing orders to ensure the market is active
- Validate orders using the risk module before submission
- Handle account initialization - the SDK automatically adds init instructions when needed
- Monitor margin requirements - effective IM is max(user-set, market minimum)
- Use appropriate commitment levels - 'confirmed' for queries, 'finalized' for critical operations
- Batch operations when possible to reduce transaction costs
The SDK includes example scripts in the scripts/ directory:
deposit.js- Deposit and commit marginwithdraw.js- Uncommit and withdraw marginorderEntry.js- Place orderscancelOrder.js- Cancel ordersqueryMarginBucket.js- Query user positionsqueryMarketData.js- Query market information
Run scripts with:
node scripts/scriptName.js [args]EMBER_PROGRAM_ID- Override default program IDEMBER_USD_MINT- Override default USD mint
UNLICENSED - Proprietary software of Crocodile Labs
For issues and questions:
- GitHub Issues: [Report bugs and feature requests]
- Documentation: [Full API documentation]
- Contact: [email protected]