xccy-sdk-ts
v0.1.13
Published
High-level TypeScript SDK for XCCY trading actions
Readme
xccy-sdk-ts
TypeScript SDK for XCCY integrations. Wraps on-chain contracts and HTTP APIs behind a single XccySdk client for trading, margin, simulations, markets, positions, collateral, history, and leaderboard.
Installation
npm i xccy-sdk-ts ethersWhy this SDK
- One client for write + read flows (
XccySdk). - Normalized API methods per chain (
arbitrum,polygon) viasrc/config.ts. - Token amounts for mint/burn/margin:
TokenAmountInput— eitheramount(decimal string, whole tokens) oramountRaw(smallest units), never both; seeresolveTokenAmountToBaseUnitsandtoLiquidityFromTokenAmount. - Strongly typed requests/responses in
src/types/(barrel:index.ts).
Quick Start
import { JsonRpcProvider, Wallet } from "ethers";
import { XccySdk } from "xccy-sdk-ts";
const provider = new JsonRpcProvider(process.env.RPC_URL!);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const sdk = new XccySdk({
chain: "arbitrum",
wallet, // for write calls
// apiBaseUrl and addresses are optional if defaults are valid for your env
});Call APIs on grouped modules (sdk.tradingModules.trading.*, sdk.tradingModules.account.*, sdk.analytics.*, …). XccySdk wires RPC, contracts, and shared runtime context; it also exposes vammManager and collateralEngine getters for low-level access when you need them.
Read-only client
import { XccySdk } from "xccy-sdk-ts";
const sdk = new XccySdk({
chain: "arbitrum",
rpcUrl: process.env.RPC_URL!,
});SDK components
XccySdk
Main facade: builds XccyRuntimeContext, connects contracts via src/contracts.ts, and instantiates the modules below.
TradingModule
Writes (mint, burn, swap)
On-chain writes go through the connected VAMM manager (sdk.vammManager / internal contracts.vammManager). Each method sends a transaction, waits with await tx.wait() by default (optional second argument confirmations?: number → tx.wait(confirmations)), and returns ConfirmedTx<TParsed>:
transactionHash,gasUsed,rawLogs,receiptparsed: values decoded from the relevant VAMM event in the receipt (Mint,Burn, orSwap), not from an extrastaticCallafter mining.
mint — add liquidity for an AccountId in a tick range on key. You pass a token amount via TokenAmountInput (amount decimal string or amountRaw); the SDK converts it to VAMM liquidity (see toLiquidityFromTokenAmount). Tick range can be explicit tickLower / tickUpper or percent helpers tickLowerPercent / tickUpperPercent. parsed: VammMintOutputs — e.g. feeIncurred from the Mint log.
const mintOut = await sdk.tradingModules.trading.mint({
key: poolKey,
account,
tickLower: -120n,
tickUpper: 120n,
amount: "1000",
tokenDecimals: 6,
});
// mintOut.parsed.feeIncurred, mintOut.rawLogs, mintOut.receiptburn — remove liquidity for the same pool/account/range semantics as mint (same MintBurnParams shape). parsed: VammBurnOutputs — amount and feeIncurred from the Burn log.
const burnOut = await sdk.tradingModules.trading.burn({
key: poolKey,
account,
tickLower: -120n,
tickUpper: 120n,
amount: "250",
});swap — execute a swap on key for account using contract-style SwapParams: amountSpecified / sqrtPriceLimitX96 / tickLower / tickUpper (notional direction and limits as on-chain). parsed: VammSwapOutputs — deltas, fee, priceAfterSwap; fixedTokenDeltaUnbalanced matches fixedTokenDelta from the mined Swap event (use simulateSwap for the simulated unbalanced delta).
const swapOut = await sdk.tradingModules.trading.swap({
key: poolKey,
params: {
account,
amountSpecified: -1000n,
sqrtPriceLimitX96: 0n,
tickLower: -600n,
tickUpper: 600n,
},
});- Simulation (static call + metrics + optional gas):
simulateSwap,simulateLiquidity—src/quotes/swapSimulate.ts,src/quotes/liquiditySimulate.ts; helpers insrc/quotes/metrics.ts. - Read:
getVammState→VammPoolState(tick,sqrtPriceX96,compoundToken). For rawstaticCallonly, usesdk.vammManager.*.staticCall(same runner as the SDK).
AccountModule
Margin, ERC-20 approvals/balances, and Collateral Engine calls go through the typechain instance from connectContracts (CollateralEngineFactory in @xccy/typechains). Helpers in src/helpers/erc20.ts use minimal ERC-20 ABIs for token reads/writes.
setAccountOperator/isAccountOperator: the SDK acceptsAccountIdfor consistency, but the on-chain methods take the owner address and operator — the implementation forwardsaccount.owner(per current CE ABI in@xccy/typechains).calculateAccountObligation: the generated contract type exposes an overload; the SDK calls the(account)overload via the full signature string (see implementation).updateMarginBatchNative: talks to the Operator contract at a caller-supplied address viaOperatorFactoryfrom@xccy/typechains.
AnalyticsModule
REST data: markets, positions, collateral, trades, history, leaderboard, platform metrics. HTTP routes live under src/api/endpoints/ (barrel index.ts: analytics/*, lockYield), with shared client src/api/httpClient.ts.
LockYieldModule
Lock-yield flows via the connected lockYield contract from connectContracts.
Supporting code (for contributors)
- Tick / liquidity math:
src/math/ticks.ts,src/math/liquidity.ts - Shared shapes for on-chain simulation / ticks:
src/models/ - Constants:
src/constants/math.ts - Errors:
src/errors.ts
Core Concepts
PoolKey
Most trading methods work with a pool key (see PoolKey / @xccy/typechains):
const poolKey = {
underlyingAsset: "0x...",
compoundToken: "0x...",
termStartTimestampWad: 0n,
termEndTimestampWad: 0n,
feeWad: 100000000000000n,
tickSpacing: 10,
};AccountId
Account tuple used across write/read APIs (AccountId):
const account = {
owner: "0x...",
id: 0n,
isolatedMarginToken: "0x...",
};Amount semantics
swap: pass contract-styleamountSpecified(bigint) in swap params.mint/burn,simulateLiquidity,depositMargin/withdrawMargin: pass exactly one of:amount: decimal string in whole tokens (e.g."1.5"),amountRaw:bigintalready in token base units (wei).
- Do not pass both; conversion for
amountuses tokendecimals()(ortokenDecimalswhen provided). Implemented inresolveTokenAmountToBaseUnitsandtoLiquidityFromTokenAmount.
Common Usage
Swap
const swapOut = await sdk.tradingModules.trading.swap({
key: poolKey,
params: {
account,
amountSpecified: -1000n,
sqrtPriceLimitX96: 0n,
tickLower: -600n,
tickUpper: 600n, // int24 range bounds as bigint
},
});
// swapOut.rawLogs, swapOut.transactionHash, swapOut.gasUsed, swapOut.receiptAdd liquidity
const mintOut = await sdk.tradingModules.trading.mint({
key: poolKey,
account,
tickLowerPercent: "1.5%", // or tickLower/tickUpper directly
tickUpperPercent: "3.0%",
amount: "1000", // user-facing token amount
});Remove liquidity
const burnOut = await sdk.tradingModules.trading.burn({
key: poolKey,
account,
tickLower: -120n,
tickUpper: 120n,
amount: "250", // or amountRaw: …n when you already have base units
});Deposit margin
await sdk.tradingModules.account.depositMargin({
account,
token: "0x...",
amount: "500", // or amountRaw: 500n * 10n ** 18n when you already have base units
});Simulation
simulateSwap / simulateLiquidity run staticCall plus extra RPC (e.g. pool state, optional estimateGas) and attach metrics. For a dry-run only (no send), call sdk.vammManager.swap.staticCall / mint.staticCall / burn.staticCall with the same arguments you would pass on-chain.
const swapSim = await sdk.tradingModules.trading.simulateSwap({
key: poolKey,
params: {
account,
amountSpecified: -1000n,
sqrtPriceLimitX96: 0n,
tickLower: -600n,
tickUpper: 600n,
},
});
const liqSim = await sdk.tradingModules.trading.simulateLiquidity({
key: poolKey,
account,
amount: "1000",
tickLowerPercent: "1.5%",
tickUpperPercent: "3%",
});Data APIs
Examples (all on AnalyticsModule):
const markets = await sdk.analytics.getMarkets();
const positions = await sdk.analytics.getMyPositions({
owner: "0x...",
accountId: 0n,
isolatedMarginToken: "0x...",
});
const subAccounts = await sdk.analytics.getSubAccounts({ owner: "0x..." });
const collateral = await sdk.analytics.getCollateral({
owner: "0x...",
accountId: 0n,
isolatedMarginToken: "0x...",
});
const floatingHistory = await sdk.analytics.getPoolFloatingRateHistory({
asset: "0x...",
resolution: "5m",
limit: 100,
});API surface (high level)
tradingModules.trading
swap, mint, burn (each returns ConfirmedTx), simulateSwap, simulateLiquidity, getVammState
tradingModules.account
depositMargin, withdrawMargin, updateMarginBatchNative, calculateAccountObligation, getAccountMarginValues, setAccountOperator, isAccountOperator, approveErc20, getErc20Balance, getErc20Allowance
analytics
getMarkets, getPositions, getMyPositions, getSubAccounts, getCollateral, getCollateralTokens, getRecentTrades, getMyLiquidityTrades, getLiquidityTrades, getPoolRateHistory, getPoolFloatingRateHistory, getPnlHistory, getSettledPositions, getLeaderboard, getLeaderboardPosition, getPlatformMetrics, getUserPerformance
tradingModules.lockYield
openLockYield, isLockYieldPoolApproved, isLockYieldPaused, getLockYieldPositionIds, getLockYieldPositionStats, simulateOpenLockYield, getLockYieldPositions, getLockYieldPoolStats
Error Handling
XccySdkError is thrown for SDK-normalized failures (e.g. simulation / gas estimation, via asSimulateError internally). Use isXccySdkError and optional toBotError for structured logging.
import { isXccySdkError, toBotError } from "xccy-sdk-ts";
try {
await sdk.tradingModules.trading.simulateSwap({ key: poolKey, params: swapParams });
} catch (e) {
if (isXccySdkError(e)) {
console.error(e.code, e.message, e.details);
// e.g. QUOTE_INSUFFICIENT_COLLATERAL — user-facing message is short;
// original revert text may appear in e.details?.reason
} else {
console.error(e);
}
}
const bot = toBotError(e); // { name, message, code, retryable, details? }Types: XccySdkError, XccySdkErrorCode.
Direct ethers calls (e.g. sdk.vammManager.swap.staticCall) throw ethers errors (CALL_EXCEPTION, etc.), not XccySdkError.
Development
npm run build
npm run typecheck
npm testSmoke scripts (see package.json; set env vars locally, e.g. in .env):
npm run smoke:api
npm run smoke:markets
npm run smoke:quote
npm run smoke:liquidity:quote
npm run smoke:mint # on-chain mint (needs PRIVATE_KEY, pool env, etc.)
npm run smoke:deposit
npm run smoke:bot:flow
npm run smoke:errorsNode + RPC: RPC_URL is required for smoke scripts (there is no default endpoint in the repo). Put RPC_URL=… in .env as KEY=value (no export prefix) so dotenv in scripts sees it the same as your shell. smoke-mint.ts uses a pinned Arbitrum network (42161, staticNetwork), optional batchMaxCount: 1, and attaches provider.on("error", …) so transient RPC issues during tx polling do not crash the process as an unhandled error event.
Reference style
This README follows the same idea as TacBuild/tac-sdk: documentation, entrypoints, and examples are linked inline so you can jump from the text to the implementation or npm without a separate docs site.
