@todayapp/avantis-sdk
Version:
Unofficial TypeScript SDK for Avantis DEX - Decentralized perpetual futures trading (BETA)
1,717 lines (1,708 loc) • 3.85 MB
JavaScript
import { isAddress, createPublicClient, http, createWalletClient, formatUnits, formatEther, parseEther, encodeFunctionData, getContract, parseUnits, getAddress } from 'viem';
import Decimal from 'decimal.js';
import { EventEmitter } from 'eventemitter3';
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
import { baseSepolia, base } from 'viem/chains';
import { z } from 'zod';
import axios from 'axios';
import WebSocket from 'isomorphic-ws';
var ErrorCode;
(function (ErrorCode) {
// Network errors
ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
ErrorCode["RPC_ERROR"] = "RPC_ERROR";
ErrorCode["CONNECTION_ERROR"] = "CONNECTION_ERROR";
// Transaction errors
ErrorCode["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
ErrorCode["TRANSACTION_REJECTED"] = "TRANSACTION_REJECTED";
ErrorCode["INSUFFICIENT_FUNDS"] = "INSUFFICIENT_FUNDS";
ErrorCode["GAS_ESTIMATION_FAILED"] = "GAS_ESTIMATION_FAILED";
// Trading errors
ErrorCode["INVALID_PAIR"] = "INVALID_PAIR";
ErrorCode["INVALID_SIZE"] = "INVALID_SIZE";
ErrorCode["INVALID_LEVERAGE"] = "INVALID_LEVERAGE";
ErrorCode["POSITION_NOT_FOUND"] = "POSITION_NOT_FOUND";
ErrorCode["MARKET_CLOSED"] = "MARKET_CLOSED";
ErrorCode["INSUFFICIENT_COLLATERAL"] = "INSUFFICIENT_COLLATERAL";
ErrorCode["MAX_LEVERAGE_EXCEEDED"] = "MAX_LEVERAGE_EXCEEDED";
ErrorCode["MIN_SIZE_NOT_MET"] = "MIN_SIZE_NOT_MET";
ErrorCode["MAX_SIZE_EXCEEDED"] = "MAX_SIZE_EXCEEDED";
ErrorCode["SLIPPAGE_EXCEEDED"] = "SLIPPAGE_EXCEEDED";
// Authentication errors
ErrorCode["INVALID_SIGNER"] = "INVALID_SIGNER";
ErrorCode["SIGNER_NOT_SET"] = "SIGNER_NOT_SET";
ErrorCode["INVALID_PRIVATE_KEY"] = "INVALID_PRIVATE_KEY";
// Contract errors
ErrorCode["CONTRACT_NOT_FOUND"] = "CONTRACT_NOT_FOUND";
ErrorCode["CONTRACT_CALL_FAILED"] = "CONTRACT_CALL_FAILED";
ErrorCode["INVALID_CONTRACT_ADDRESS"] = "INVALID_CONTRACT_ADDRESS";
// WebSocket errors
ErrorCode["WS_CONNECTION_FAILED"] = "WS_CONNECTION_FAILED";
ErrorCode["WS_MESSAGE_ERROR"] = "WS_MESSAGE_ERROR";
ErrorCode["WS_SUBSCRIPTION_FAILED"] = "WS_SUBSCRIPTION_FAILED";
// Validation errors
ErrorCode["INVALID_PARAMETER"] = "INVALID_PARAMETER";
ErrorCode["MISSING_PARAMETER"] = "MISSING_PARAMETER";
ErrorCode["VALIDATION_FAILED"] = "VALIDATION_FAILED";
// Generic errors
ErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
ErrorCode["NOT_IMPLEMENTED"] = "NOT_IMPLEMENTED";
ErrorCode["OPERATION_TIMEOUT"] = "OPERATION_TIMEOUT";
})(ErrorCode || (ErrorCode = {}));
class AvantisSDKError extends Error {
code;
details;
timestamp;
constructor(code, message, details) {
super(message);
this.name = 'AvantisSDKError';
this.code = code;
this.details = details;
this.timestamp = new Date();
// Maintains proper stack trace for where our error was thrown
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AvantisSDKError);
}
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
details: this.details,
timestamp: this.timestamp.toISOString(),
stack: this.stack
};
}
}
class NetworkError extends AvantisSDKError {
constructor(message, details) {
super(ErrorCode.NETWORK_ERROR, message, details);
this.name = 'NetworkError';
}
}
class TransactionError extends AvantisSDKError {
transactionHash;
constructor(code, message, transactionHash, details) {
super(code, message, { ...details, transactionHash });
this.name = 'TransactionError';
this.transactionHash = transactionHash;
}
}
class TradingError extends AvantisSDKError {
pair;
positionId;
constructor(code, message, pair, positionId, details) {
super(code, message, { ...details, pair, positionId });
this.name = 'TradingError';
this.pair = pair;
this.positionId = positionId;
}
}
class ValidationError extends AvantisSDKError {
field;
constructor(message, field, details) {
super(ErrorCode.VALIDATION_FAILED, message, { ...details, field });
this.name = 'ValidationError';
this.field = field;
}
}
class WebSocketError extends AvantisSDKError {
constructor(code, message, details) {
super(code, message, details);
this.name = 'WebSocketError';
}
}
function isAvantisSDKError(error) {
return error instanceof AvantisSDKError;
}
function handleError(error) {
if (isAvantisSDKError(error)) {
return error;
}
// Handle ethers.js errors
if (error?.code && typeof error.code === 'string') {
const ethersCode = error.code;
switch (ethersCode) {
case 'INSUFFICIENT_FUNDS':
return new TransactionError(ErrorCode.INSUFFICIENT_FUNDS, 'Insufficient funds for transaction', undefined, error);
case 'NETWORK_ERROR':
return new NetworkError('Network connection failed', error);
case 'TIMEOUT':
return new AvantisSDKError(ErrorCode.OPERATION_TIMEOUT, 'Operation timed out', error);
case 'UNPREDICTABLE_GAS_LIMIT':
return new TransactionError(ErrorCode.GAS_ESTIMATION_FAILED, 'Failed to estimate gas for transaction', undefined, error);
}
}
// Handle generic errors
if (error instanceof Error) {
return new AvantisSDKError(ErrorCode.UNKNOWN_ERROR, error.message, { originalError: error });
}
return new AvantisSDKError(ErrorCode.UNKNOWN_ERROR, 'An unknown error occurred', error);
}
var PositionSide;
(function (PositionSide) {
PositionSide["LONG"] = "long";
PositionSide["SHORT"] = "short";
})(PositionSide || (PositionSide = {}));
var OrderType;
(function (OrderType) {
OrderType["MARKET"] = "market";
OrderType["STOP_LIMIT"] = "stop_limit";
OrderType["LIMIT"] = "limit";
OrderType["MARKET_ZERO_FEE"] = "market_zero_fee";
})(OrderType || (OrderType = {}));
// Numeric values for contract interaction (matches Avantis Python SDK and smart contract)
var OrderTypeValue;
(function (OrderTypeValue) {
OrderTypeValue[OrderTypeValue["MARKET"] = 0] = "MARKET";
OrderTypeValue[OrderTypeValue["STOP_LIMIT"] = 1] = "STOP_LIMIT";
OrderTypeValue[OrderTypeValue["LIMIT"] = 2] = "LIMIT";
OrderTypeValue[OrderTypeValue["MARKET_ZERO_FEE"] = 3] = "MARKET_ZERO_FEE";
})(OrderTypeValue || (OrderTypeValue = {}));
var PositionStatus;
(function (PositionStatus) {
PositionStatus["OPEN"] = "open";
PositionStatus["CLOSED"] = "closed";
PositionStatus["LIQUIDATED"] = "liquidated";
PositionStatus["PENDING"] = "pending";
})(PositionStatus || (PositionStatus = {}));
// Margin update types
var MarginUpdateType;
(function (MarginUpdateType) {
MarginUpdateType[MarginUpdateType["ADD"] = 0] = "ADD";
MarginUpdateType[MarginUpdateType["REMOVE"] = 1] = "REMOVE";
})(MarginUpdateType || (MarginUpdateType = {}));
// Limit order execution types
var LimitOrderType;
(function (LimitOrderType) {
LimitOrderType[LimitOrderType["TP"] = 0] = "TP";
LimitOrderType[LimitOrderType["SL"] = 1] = "SL";
LimitOrderType[LimitOrderType["LIQ"] = 2] = "LIQ";
LimitOrderType[LimitOrderType["OPEN"] = 3] = "OPEN";
})(LimitOrderType || (LimitOrderType = {}));
var TimeInterval;
(function (TimeInterval) {
TimeInterval["M1"] = "1m";
TimeInterval["M5"] = "5m";
TimeInterval["M15"] = "15m";
TimeInterval["M30"] = "30m";
TimeInterval["H1"] = "1h";
TimeInterval["H4"] = "4h";
TimeInterval["D1"] = "1d";
TimeInterval["W1"] = "1w";
})(TimeInterval || (TimeInterval = {}));
// Custom Decimal validation
const DecimalSchema = z.union([
z.instanceof(Decimal),
z.number(),
z.string()
]).transform((val) => {
try {
return new Decimal(val);
}
catch (error) {
throw new ValidationError(`Invalid decimal value: ${val}`);
}
});
// Ethereum address validation
const AddressSchema = z.string().refine((val) => isAddress(val), { message: 'Invalid Ethereum address' });
// Trading pair validation
const TradingPairSchema = z.string().regex(/^[A-Z0-9]+\/[A-Z0-9]+$/, 'Invalid trading pair format. Expected format: BASE/QUOTE (e.g., BTC/USD)');
// Position side validation
const PositionSideSchema = z.nativeEnum(PositionSide);
// Leverage validation
const LeverageSchema = z.number().min(1).max(100);
// Slippage validation (percentage)
const SlippageSchema = z.number().min(0).max(100).optional();
// Open position params validation
const OpenPositionParamsSchema = z.object({
pair: TradingPairSchema,
side: PositionSideSchema,
size: DecimalSchema,
leverage: LeverageSchema,
stopLoss: DecimalSchema.optional(),
takeProfit: DecimalSchema.optional(),
slippage: SlippageSchema,
referrer: AddressSchema.optional()
});
// Close position params validation
const ClosePositionParamsSchema = z.object({
positionId: z.string().regex(/^\d+-\d+$/, 'Position ID must be in format "pairIndex-positionIndex" (e.g., "0-123")'),
size: DecimalSchema.optional(),
slippage: SlippageSchema
});
// Update position params validation
const UpdatePositionParamsSchema = z.object({
positionId: z.string().regex(/^\d+-\d+$/, 'Position ID must be in format "pairIndex-positionIndex" (e.g., "0-123")'),
stopLoss: DecimalSchema.nullable().optional(),
takeProfit: DecimalSchema.nullable().optional()
});
// Network configuration validation
const NetworkConfigSchema = z.object({
chainId: z.union([z.literal(8453), z.literal(84531)]),
name: z.string(),
rpcUrl: z.string().url(),
explorerUrl: z.string().url(),
nativeCurrency: z.object({
name: z.string(),
symbol: z.string(),
decimals: z.number()
}),
contracts: z.object({
trading: AddressSchema,
usdc: AddressSchema,
priceFeed: AddressSchema,
vault: AddressSchema,
router: AddressSchema
}),
websocketUrl: z.string().url().optional()
});
// Private key validation
const PrivateKeySchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'Invalid private key format');
// Transaction hash validation
const TransactionHashSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'Invalid transaction hash format');
/**
* Validates Ethereum address
*/
function validateAddress(address) {
try {
AddressSchema.parse(address);
}
catch (error) {
throw new ValidationError(`Invalid address: ${address}`, 'address');
}
}
/**
* Validates private key format
*/
function validatePrivateKey(privateKey) {
try {
PrivateKeySchema.parse(privateKey);
}
catch (error) {
throw new ValidationError('Invalid private key format', 'privateKey');
}
}
/**
* Validates trading pair format
*/
function validateTradingPair(pair) {
try {
TradingPairSchema.parse(pair);
}
catch (error) {
throw new ValidationError(`Invalid trading pair: ${pair}`, 'pair');
}
}
/**
* Validates leverage value
*/
function validateLeverage(leverage, maxLeverage = 100) {
if (leverage < 1 || leverage > maxLeverage) {
throw new ValidationError(`Leverage must be between 1 and ${maxLeverage}, got ${leverage}`, 'leverage');
}
}
/**
* Validates position size
*/
function validatePositionSize(size, minSize, maxSize) {
const sizeDecimal = new Decimal(size);
if (sizeDecimal.lte(0)) {
throw new ValidationError('Position size must be greater than 0', 'size');
}
if (minSize && sizeDecimal.lt(minSize)) {
throw new ValidationError(`Position size must be at least ${minSize.toString()}`, 'size');
}
if (maxSize && sizeDecimal.gt(maxSize)) {
throw new ValidationError(`Position size must not exceed ${maxSize.toString()}`, 'size');
}
return sizeDecimal;
}
/**
* Validates slippage percentage
*/
function validateSlippage(slippage) {
if (slippage === undefined)
return undefined;
if (slippage < 0 || slippage > 100) {
throw new ValidationError('Slippage must be between 0 and 100 percent', 'slippage');
}
return slippage;
}
/**
* Validates stop loss price
*/
function validateStopLoss(stopLoss, entryPrice, isLong) {
if (!stopLoss)
return undefined;
const slDecimal = new Decimal(stopLoss);
if (isLong && slDecimal.gte(entryPrice)) {
throw new ValidationError('Stop loss must be below entry price for long positions', 'stopLoss');
}
if (!isLong && slDecimal.lte(entryPrice)) {
throw new ValidationError('Stop loss must be above entry price for short positions', 'stopLoss');
}
return slDecimal;
}
/**
* Validates take profit price
*/
function validateTakeProfit(takeProfit, entryPrice, isLong) {
if (!takeProfit)
return undefined;
const tpDecimal = new Decimal(takeProfit);
if (isLong && tpDecimal.lte(entryPrice)) {
throw new ValidationError('Take profit must be above entry price for long positions', 'takeProfit');
}
if (!isLong && tpDecimal.gte(entryPrice)) {
throw new ValidationError('Take profit must be below entry price for short positions', 'takeProfit');
}
return tpDecimal;
}
/**
* Generic validation function using Zod schema
*/
function validate(schema, data) {
try {
return schema.parse(data);
}
catch (error) {
if (error instanceof z.ZodError) {
const issues = error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
throw new ValidationError(`Validation failed: ${issues}`);
}
throw error;
}
}
const NETWORKS = {
base: {
chainId: 8453,
name: "Base",
rpcUrl: "https://mainnet.base.org",
explorerUrl: "https://basescan.org",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
contracts: {
trading: "0x44914408af82bC9983bbb330e3578E1105e11d4e", // Trading contract
tradingStorage: "0x8a311D7048c35985aa31C131B9A13e03a5f7422d", // TradingStorage contract
tradingCallbacks: "0x0000000000000000000000000000000000000000", // TODO: Get TradingCallbacks address
pairInfos: "0x81F22d0Cc22977c91bEfE648C9fddff1f2bd977e", // PairInfos contract
pairStorage: "0x5db3772136e5557EFE028Db05EE95C84D76faEC4", // PairStorage contract
priceAggregator: "0x64e2625621970F8cfA17B294670d61CB883dA511", // PriceAggregator contract
vaultManager: "0x0000000000000000000000000000000000000000", // TODO: Get VaultManager address
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base USDC (Native)
priceFeed: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a", // Pyth Network on Base
vault: "0x0000000000000000000000000000000000000000", // TODO: Get USDC Vault address
router: "0x0000000000000000000000000000000000000000", // TODO: Get Trading Router address
avnt: "0x696F9436B67233384889472Cd7cD58A6fB5DF4f1", // AVNT Token
multicall: "0x7A829c5C97A2Bf8BeFB4b01d96A282E4763848d8", // Multicall contract (custom Avantis)
referral: "0x1A110bBA13A1f16cCa4b79758BD39290f29De82D", // Referral contract
},
websocketUrl: process.env.BASE_WS_URL || process.env.ALCHEMY_API_KEY
? `wss://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
: "wss://mainnet.base.org", // Public WebSocket endpoint as fallback
},
"base-sepolia": {
chainId: 84531,
name: "Base Sepolia",
rpcUrl: "https://sepolia.base.org",
explorerUrl: "https://sepolia.basescan.org",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
contracts: {
trading: "0x0000000000000000000000000000000000000000", // TODO: Get testnet Trading contract
tradingStorage: "0x0000000000000000000000000000000000000000", // TODO: Get testnet TradingStorage
tradingCallbacks: "0x0000000000000000000000000000000000000000", // TODO: Get testnet TradingCallbacks
pairInfos: "0x0000000000000000000000000000000000000000", // TODO: Get testnet PairInfos
pairStorage: "0x0000000000000000000000000000000000000000", // TODO: Get testnet PairStorage
priceAggregator: "0x0000000000000000000000000000000000000000", // TODO: Get testnet PriceAggregator
vaultManager: "0x0000000000000000000000000000000000000000", // TODO: Get testnet VaultManager
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia USDC
priceFeed: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729", // Pyth Network on Base Sepolia
vault: "0x0000000000000000000000000000000000000000", // TODO: Get testnet Vault address
router: "0x0000000000000000000000000000000000000000", // TODO: Get testnet Router address
},
websocketUrl: process.env.BASE_SEPOLIA_WS_URL || process.env.ALCHEMY_API_KEY
? `wss://base-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
: "wss://sepolia.base.org", // Public WebSocket endpoint as fallback
},
};
const DEFAULT_NETWORK = "base";
// Trading pairs configuration (matches on-chain pair indices)
const TRADING_PAIRS = {
// Crypto pairs (indices 0-24)
"BTC/USD": {
feedId: "btc-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 100,
decimals: 8,
category: "crypto",
},
"ETH/USD": {
feedId: "eth-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 100,
decimals: 8,
category: "crypto",
},
"LINK/USD": {
feedId: "link-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"MATIC/USD": {
feedId: "matic-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"DOGE/USD": {
feedId: "doge-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"SOL/USD": {
feedId: "sol-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"ADA/USD": {
feedId: "ada-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"AVAX/USD": {
feedId: "avax-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"ATOM/USD": {
feedId: "atom-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"DOT/USD": {
feedId: "dot-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"FTM/USD": {
feedId: "ftm-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"NEAR/USD": {
feedId: "near-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"ALGO/USD": {
feedId: "algo-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"XRP/USD": {
feedId: "xrp-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"LTC/USD": {
feedId: "ltc-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"BCH/USD": {
feedId: "bch-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"ICP/USD": {
feedId: "icp-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"ETC/USD": {
feedId: "etc-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"XLM/USD": {
feedId: "xlm-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"FIL/USD": {
feedId: "fil-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"UNI/USD": {
feedId: "uni-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"APE/USD": {
feedId: "ape-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"SHIB/USD": {
feedId: "shib-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"ARB/USD": {
feedId: "arb-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
"OP/USD": {
feedId: "op-usd",
minSize: 10,
maxSize: 500000,
maxLeverage: 50,
decimals: 8,
category: "crypto",
},
// Forex pairs (indices 25-34)
"EUR/USD": {
feedId: "eur-usd",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 5,
category: "forex",
},
"GBP/USD": {
feedId: "gbp-usd",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 5,
category: "forex",
},
"USD/JPY": {
feedId: "usd-jpy",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 3,
category: "forex",
},
"USD/CHF": {
feedId: "usd-chf",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 5,
category: "forex",
},
"AUD/USD": {
feedId: "aud-usd",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 5,
category: "forex",
},
"USD/CAD": {
feedId: "usd-cad",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 5,
category: "forex",
},
"NZD/USD": {
feedId: "nzd-usd",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 5,
category: "forex",
},
"EUR/GBP": {
feedId: "eur-gbp",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 5,
category: "forex",
},
"EUR/JPY": {
feedId: "eur-jpy",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 3,
category: "forex",
},
"GBP/JPY": {
feedId: "gbp-jpy",
minSize: 100,
maxSize: 10000000,
maxLeverage: 500,
decimals: 3,
category: "forex",
},
// Commodities (indices 35-38)
"XAU/USD": {
feedId: "xau-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 50,
decimals: 2,
category: "commodity",
},
"XAG/USD": {
feedId: "xag-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 50,
decimals: 3,
category: "commodity",
},
"WTI/USD": {
feedId: "wti-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 50,
decimals: 2,
category: "commodity",
},
"BRENT/USD": {
feedId: "brent-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 50,
decimals: 2,
category: "commodity",
},
// Indices (indices 39-41)
"SPX500/USD": {
feedId: "spx500-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 100,
decimals: 2,
category: "index",
},
"NAS100/USD": {
feedId: "nas100-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 100,
decimals: 2,
category: "index",
},
"US30/USD": {
feedId: "us30-usd",
minSize: 10,
maxSize: 1000000,
maxLeverage: 100,
decimals: 2,
category: "index",
},
};
// Fee configuration (in basis points)
const FEES = {
openPositionFee: 10, // 0.1%
closePositionFee: 10, // 0.1%
borrowFeePerHour: 1, // 0.01% per hour
fundingFeeMax: 100, // 1% max
liquidationFee: 50, // 0.5%
};
// Default configuration values
const DEFAULTS = {
slippage: 0.5, // 0.5%
gasLimit: 500000n,
confirmations: 1,
timeout: 180000, // 120 seconds (2 minutes)
maxRetries: 3,
};
// Map our chain IDs to viem chains
const CHAIN_MAP = {
8453: base,
84531: baseSepolia,
};
class BlockchainProvider {
publicClient;
walletClient;
account;
network;
privyClient; // Privy SDK client for native gas sponsorship
privyWalletId; // Privy wallet ID for API calls
config;
constructor(networkName = DEFAULT_NETWORK, customRpcUrl) {
this.network = NETWORKS[networkName];
if (!this.network) {
throw new NetworkError(`Unknown network: ${networkName}`);
}
const rpcUrl = customRpcUrl || this.network.rpcUrl;
const chain = CHAIN_MAP[this.network.chainId];
if (!chain) {
throw new NetworkError(`Unsupported chain ID: ${this.network.chainId}`);
}
try {
this.publicClient = createPublicClient({
chain,
transport: http(rpcUrl),
});
}
catch (error) {
throw new NetworkError(`Failed to connect to network: ${rpcUrl}`, error);
}
this.config = {
confirmations: DEFAULTS.confirmations,
timeout: DEFAULTS.timeout,
maxRetries: DEFAULTS.maxRetries,
};
}
/**
* Sets the signer for transactions
*/
setSigner(config) {
try {
const chain = CHAIN_MAP[this.network.chainId];
switch (config.type) {
case "privateKey":
if (!config.privateKey) {
throw new ValidationError("Private key is required");
}
validatePrivateKey(config.privateKey);
this.account = privateKeyToAccount(config.privateKey);
this.walletClient = createWalletClient({
account: this.account,
chain,
transport: http(this.network.rpcUrl),
});
break;
case "mnemonic":
if (!config.mnemonic) {
throw new ValidationError("Mnemonic is required");
}
this.account = mnemonicToAccount(config.mnemonic);
this.walletClient = createWalletClient({
account: this.account,
chain,
transport: http(this.network.rpcUrl),
});
break;
case "jsonRpc":
// For JSON-RPC signers, we'll use the injected provider pattern
throw new ValidationError("JSON-RPC signer type not yet supported with viem. Use privateKey or mnemonic instead.");
case "injected":
// For browser-based injected providers (MetaMask, etc.)
if (config.provider) {
// This would need window.ethereum or similar
throw new ValidationError("Injected provider support coming soon. Use privateKey or mnemonic for now.");
}
break;
case "viemClient":
// NEW: Handle viem WalletClient (like kernel client)
if (!config.client) {
throw new ValidationError("Client is required for viemClient type");
}
// Store the viem client directly
this.walletClient = config.client;
// Get the account from the client
if (config.client.account) {
this.account = config.client.account;
}
else {
throw new ValidationError("Client must have an account");
}
console.log("[BlockchainProvider] Set viem wallet client:", this.account.address);
break;
case "privyClient":
// Handle Privy SDK client for native gas sponsorship
if (!config.privyClient) {
throw new ValidationError("Privy client is required for privyClient type");
}
if (!config.privyWalletId) {
throw new ValidationError("Privy wallet ID is required for privyClient type");
}
// Store Privy client and wallet ID
this.privyClient = config.privyClient;
this.privyWalletId = config.privyWalletId;
// Still need a viem client for read operations
// Get the wallet address from Privy client
if (config.client) {
this.walletClient = config.client;
if (config.client.account) {
this.account = config.client.account;
}
else {
throw new ValidationError("Client must have an account");
}
}
else {
throw new ValidationError("Viem client is also required for privyClient type (for read operations)");
}
console.log("[BlockchainProvider] Set Privy client with wallet ID:", this.privyWalletId, "address:", this.account.address);
break;
default:
throw new ValidationError("Invalid signer type");
}
}
catch (error) {
throw handleError(error);
}
}
/**
* Gets the current signer (wallet client)
*/
getSigner() {
if (!this.walletClient) {
throw new ValidationError("Signer not configured", "signer");
}
return this.walletClient;
}
/**
* Gets the current account
*/
getAccount() {
if (!this.account) {
throw new ValidationError("Account not configured", "account");
}
return this.account;
}
/**
* Gets the provider instance (public client)
*/
getProvider() {
return this.publicClient;
}
/**
* Gets the current network configuration
*/
getNetwork() {
return this.network;
}
/**
* Gets the Privy client (if configured)
*/
getPrivyClient() {
return this.privyClient;
}
/**
* Gets the Privy wallet ID (if configured)
*/
getPrivyWalletId() {
return this.privyWalletId;
}
/**
* Checks if Privy client is configured
*/
hasPrivyClient() {
return !!this.privyClient && !!this.privyWalletId;
}
/**
* Gets the chain ID
*/
async getChainId() {
try {
return BigInt(this.network.chainId);
}
catch (error) {
throw new NetworkError("Failed to get chain ID", error);
}
}
/**
* Gets the current block number
*/
async getBlockNumber() {
try {
const blockNumber = await this.publicClient.getBlockNumber();
return Number(blockNumber);
}
catch (error) {
throw new NetworkError("Failed to get block number", error);
}
}
/**
* Gets the balance of an address
*/
async getBalance(address) {
try {
const addr = address || this.getAccount().address;
validateAddress(addr);
return await this.publicClient.getBalance({
address: addr,
});
}
catch (error) {
throw handleError(error);
}
}
/**
* Gets the current gas price
*/
async getGasPrice() {
try {
const gasPrice = await this.publicClient.getGasPrice();
return gasPrice;
}
catch (error) {
throw new NetworkError("Failed to get gas price", error);
}
}
/**
* Estimates gas for a transaction
*/
async estimateGas(transaction) {
try {
const gas = await this.publicClient.estimateGas({
account: this.account,
to: transaction.to,
data: transaction.data,
value: transaction.value,
});
return gas;
}
catch (error) {
throw new TransactionError(ErrorCode.GAS_ESTIMATION_FAILED, "Failed to estimate gas", undefined, error);
}
}
/**
* Sends a transaction
*/
async sendTransaction(transaction, config) {
try {
const walletClient = this.getSigner();
const account = this.getAccount();
// Send the transaction
const hash = await walletClient.sendTransaction({
account,
to: transaction.to,
data: transaction.data,
value: transaction.value,
gas: config?.gasLimit,
gasPrice: config?.gasPrice,
maxFeePerGas: config?.maxFeePerGas,
maxPriorityFeePerGas: config?.maxPriorityFeePerGas,
nonce: config?.nonce,
});
// Wait for confirmation
const receipt = await this.waitForTransaction(hash, this.config.confirmations);
return {
hash,
blockNumber: Number(receipt.blockNumber),
blockHash: receipt.blockHash,
from: receipt.from,
to: receipt.to || "",
value: BigInt(0), // Would need to parse from transaction
gasUsed: receipt.gasUsed,
effectiveGasPrice: receipt.effectiveGasPrice,
status: receipt.status === "success" ? "success" : "failed",
logs: receipt.logs,
};
}
catch (error) {
throw handleError(error);
}
}
/**
* Waits for a transaction to be mined
*/
async waitForTransaction(hash, confirmations = 1) {
try {
const receipt = await this.publicClient.waitForTransactionReceipt({
hash: hash,
confirmations,
timeout: this.config.timeout,
});
if (!receipt) {
throw new TransactionError(ErrorCode.TRANSACTION_FAILED, "Transaction receipt not found", hash);
}
if (receipt.status === "reverted") {
throw new TransactionError(ErrorCode.TRANSACTION_FAILED, "Transaction reverted", hash);
}
return receipt;
}
catch (error) {
throw handleError(error);
}
}
/**
* Gets a transaction by hash
*/
async getTransaction(hash) {
try {
return await this.publicClient.getTransaction({ hash: hash });
}
catch (error) {
throw new NetworkError(`Failed to get transaction: ${hash}`, error);
}
}
/**
* Gets a transaction receipt
*/
async getTransactionReceipt(hash) {
try {
return await this.publicClient.getTransactionReceipt({
hash: hash,
});
}
catch (error) {
throw new NetworkError(`Failed to get transaction receipt: ${hash}`, error);
}
}
/**
* Queries events from the blockchain
*/
async queryEvents(filter) {
try {
const logs = await this.publicClient.getLogs({
address: filter.address,
topics: filter.topics,
fromBlock: typeof filter.fromBlock === "number"
? BigInt(filter.fromBlock)
: filter.fromBlock,
toBlock: typeof filter.toBlock === "number"
? BigInt(filter.toBlock)
: filter.toBlock,
});
return logs.map((log) => ({
address: log.address,
topics: log.topics,
data: log.data,
blockNumber: Number(log.blockNumber),
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockHash: log.blockHash,
logIndex: log.logIndex,
removed: log.removed,
}));
}
catch (error) {
throw new NetworkError("Failed to query events", error);
}
}
/**
* Subscribes to new blocks
*/
onBlock(callback) {
// viem uses watchBlocks
this.publicClient.watchBlocks({
onBlock: (block) => callback(Number(block.number)),
});
}
/**
* Unsubscribes from block events
* Note: viem's watchBlocks returns an unwatch function, so this is a simplified version
*/
offBlock(callback) {
// In viem, you'd call the unwatch function returned by watchBlocks
// This is a simplified implementation - in production you'd store the unwatch function
console.warn("offBlock: viem requires calling the unwatch function returned by watchBlocks");
}
/**
* Checks if connected to the network
*/
async isConnected() {
try {
await this.publicClient.getBlockNumber();
return true;
}
catch {
return false;
}
}
/**
* Disconnects from the network
* Note: viem clients don't have a disconnect method, connections are managed automatically
*/
disconnect() {
// viem handles connection lifecycle automatically
// No explicit disconnect needed
}
}
// Pair index mapping for Avantis protocol
// These indices correspond to the on-chain pair IDs
const PAIR_INDICES = {
// Crypto pairs
'BTC/USD': 0,
'ETH/USD': 1,
'LINK/USD': 2,
'MATIC/USD': 3,
'DOGE/USD': 4,
'SOL/USD': 5,
'ADA/USD': 6,
'AVAX/USD': 7,
'ATOM/USD': 8,
'DOT/USD': 9,
'FTM/USD': 10,
'NEAR/USD': 11,
'ALGO/USD': 12,
'XRP/USD': 13,
'LTC/USD': 14,
'BCH/USD': 15,
'ICP/USD': 16,
'ETC/USD': 17,
'XLM/USD': 18,
'FIL/USD': 19,
'UNI/USD': 20,
'APE/USD': 21,
'SHIB/USD': 22,
'ARB/USD': 23,
'OP/USD': 24,
// Forex pairs
'EUR/USD': 25,
'GBP/USD': 26,
'USD/JPY': 27,
'USD/CHF': 28,
'AUD/USD': 29,
'USD/CAD': 30,
'NZD/USD': 31,
'EUR/GBP': 32,
'EUR/JPY': 33,
'GBP/JPY': 34,
// Commodities
'XAU/USD': 35, // Gold
'XAG/USD': 36, // Silver
'WTI/USD': 37, // Oil
'BRENT/USD': 38, // Brent Oil
// Indices
'SPX500/USD': 39, // S&P 500
'NAS100/USD': 40, // Nasdaq 100
'US30/USD': 41, // Dow Jones
};
// Reverse mapping for getting pair name from index
const PAIR_NAMES = Object.entries(PAIR_INDICES).reduce((acc, [name, index]) => {
acc[index] = name;
return acc;
}, {});
/**
* Gets the pair index for a given pair name
*/
function getPairIndex(pairName) {
const index = PAIR_INDICES[pairName.toUpperCase()];
if (index === undefined) {
throw new Error(`Unknown pair: ${pairName}`);
}
return index;
}
/**
* Gets the pair name for a given index
*/
function getPairName(index) {
const name = PAIR_NAMES[index];
if (!name) {
throw new Error(`Unknown pair index: ${index}`);
}
return name;
}
/**
* Validates if a pair exists
*/
function isPairValid(pairName) {
return PAIR_INDICES[pairName.toUpperCase()] !== undefined;
}
/**
* Gets all available pairs
*/
function getAllPairs() {
return Object.keys(PAIR_INDICES);
}
/**
* Gets pairs by category
*/
function getPairsByCategory(category) {
const categoryRanges = {
crypto: [0, 24],
forex: [25, 34],
commodity: [35, 38],
index: [39, 41]
};
const [start, end] = categoryRanges[category];
const pairs = [];
for (let i = start; i <= end; i++) {
const pairName = PAIR_NAMES[i];
if (pairName) {
pairs.push(pairName);
}
}
return pairs;
}
/**
* Gets the configuration for a specific pair
*/
function getPairConfig(pairName) {
const index = PAIR_INDICES[pairName.toUpperCase()];
if (index === undefined)
return null;
// Determine category based on index
let category;
if (index <= 24)
category = 'crypto';
else if (index <= 34)
category = 'forex';
else if (index <= 38)
category = 'commodity';
else
category = 'index';
// Default configurations (these should be fetched from chain in production)
const configs = {
'crypto': {
minSize: new Decimal(0.001),
maxSize: new Decimal(1000000),
maxLeverage: 100,
decimals: 8
},
'forex': {
minSize: new Decimal(100),
maxSize: new Decimal(10000000),
maxLeverage: 500,
decimals: 5
},
'commodity': {
minSize: new Decimal(0.1),
maxSize: new Decimal(100000),
maxLeverage: 50,
decimals: 3
},
'index': {
minSize: new Decimal(0.01),
maxSize: new Decimal(100000),
maxLeverage: 100,
decimals: 2
}
};
return {
index,
name: pairName.toUpperCase(),
category,
...configs[category]
};
}
/**
* Formats a Decimal value to a string with specified decimal places
*/
function formatDecimal(value, decimals = 2) {
return value.toFixed(decimals);
}
/**
* Formats a price value with appropriate decimal places
*/
function formatPrice(price, decimals = 2) {
const priceDecimal = new Decimal(price);
// For very small values, use more decimal places
if (priceDecimal.lt(0.01) && priceDecimal.gt(0)) {
return priceDecimal.toFixed(6);
}
return priceDecimal.toFixed(decimals);
}
/**
* Formats a percentage value
*/
function formatPercentage(value, decimals = 2) {
const percentage = new Decimal(value).mul(100);
return `${percentage.toFixed(decimals)}%`;
}
/**
* Formats a leverage value
*/
function formatLeverage(leverage) {
return `${leverage}x`;
}
/**
* Formats USDC amount (6 decimals)
*/
function formatUSDC(amount) {
if (typeof amount === "bigint") {
return formatUnits(amount, 6);
}
const amountDecimal = new Decimal(amount.toString());
return amountDecimal.div(1e6).toFixed(2);
}
/**
* Converts USDC amount to raw units (6 decimals)
*/
function toUSDCUnits(amount) {
const amountDecimal = new Decimal(amount);
const units = amountDecimal.mul(1e6).toFixed(0);
return BigInt(units);
}
/**
* Converts leverage to contract units (10 decimals)
* Avantis contracts expect leverage with 10 decimal places
*/
function toLeverageUnits(leverage) {
return BigInt(Math.floor(leverage * 1e10));
}
/**
* Converts price to contract units (10 decimals)
* Avantis contracts expect prices (including TP/SL) with 10 decimal places
*/
function toPriceUnits(price) {
const priceDecimal = new Decimal(price);
const units = priceDecimal.mul(1e10).toFixed(0);
return BigInt(units);
}
/**
* Formats ETH amount (18 decimals)
*/
function formatETH(amount) {
if (typeof amount === "string") {
amount = BigInt(amount);
}
return formatEther(amount);
}
/**
* Converts ETH amount to Wei
*/
function toWei(amount) {
const amountString = new Decimal(amount).toFixed();
return parseEther(amountString);
}
/**
* Truncates an Ethereum address for display
*/
function truncateAddress(address, start = 6, end = 4) {
if (!isAddress(address)) {
return address;
}
return `${address.slice(0, start)}...${address.slice(-end)}`;
}
/**
* Formats a transaction hash for display
*/
function truncateHash(hash, length = 10) {
if (hash.length <= length * 2) {
return hash;
}
return `${hash.slice(0, length)}...${hash.slice(-length)}`;
}
/**
* Formats a date to ISO string
*/
function formatDate(date) {
return date.toISOString();
}
/**
* Formats a timestamp to readable date
*/
function formatTimestamp(timestamp) {
const date = typeof timestamp === "number" ? new Date(timestamp * 1000) : timestamp;
return date.toLocaleString();
}
/**
* Formats PnL value with color indicators
*/
function formatPnL(pnl) {
const pnlDecimal = new Decimal(pnl);
const isProfit = pnlDecimal.gte(0);
const prefix = isProfit ? "+" : "";
return {
value: `${prefix}${pnlDecimal.toFixed(2)}`,
isProfit,
};
}
/**
* Formats trading pair
*/
function formatTradingPair(base, quote) {
return `${base.toUpperCase()}/${quote.toUpperCase()}`;
}
/**
* Parses trading pair string
*/
function parseTradingPair(pair) {
const [base, quote] = pair.split("/");
if (!base || !quote) {
throw new Error(`Invalid trading pair format: ${pair}`);
}
return { base: base.trim(), quote: quote.trim() };
}
/**
* Formats large numbers with abbreviations (K, M, B)
*/
function formatCompactNumber(value) {
const num = new Decimal(value);
if (num.gte(1e9)) {
return `${num.div(1e9).toFixed(2)}B`;
}
else if (num.gte(1e6)) {
return `${num.div(1e6).toFixed(2)}M`;
}
else if (num.gte(1e3)) {
return `${num.div(1e3).toFixed(2)}K`;
}
return num.toFixed(2);
}
/**
* Calculates and formats margin level percentage
*/
function formatMarginLevel(equity, marginUsed) {
if (marginUsed.eq(0)) {
return "∞";
}
const marginLevel = equity.div(marginUsed).mul(100);
return `${marginLevel.toFixed(2)}%`;
}
/**
* Converts basis points to percentage
*/
function bpsToPercentage(bps) {
return bps / 100;
}
/**
* Converts percentage to basis points
*/
function percentageToBps(percentage) {
return Math.round(percentage * 100);
}
var abi$9 = [
{
type: "constructor",
inputs: [
],
stateMutability: "nonpayable"
},
{
type: "function",
name: "_MAX_SLIPPAGE",
inputs: [
],
outputs: [
{
name: "",
type: "uint256",
internalType: "uint256"
}
],
stateMutability: "view"
},
{
type: "function",
name: "__msgSender",
inputs: [
],
outputs: [
{
name: "",
type: "address",
internalType: "address"
}
],
stateMutability: "view"
},
{
type: "function",
name: "cancelOpenLimitOrder",
inputs: [
{
name: "_pairIndex",
type: "uint256",
internalType: "uint256"
},
{
name: "_index",
type: "uint256",
internalType: "uint256"
}
],
outputs: [
],
stateMutability: "nonpayable"
},
{
type: "function",
name: "cancelPendingMarketOrder",
inputs: [
{
name: "_id",
type: "uint256",
internalType: "uint256"
}
],
outputs: [
],
stateMutability: "nonpayable"
},
{
type: "function",
name: "closeTradeMarket",
inputs: [
{
name: "_pairIndex",
type: "uint256",
internalType: "uint256"
},
{
name: "_index",
type: "uint256",
internalType: "uint256"
},
{
name: "_amount",
type: "uint256",
internalType: "uint256"
}
],
outputs: [
{
name: "orderId",
type: "uint256",
internalType: "uint256"
}
],
stateMutability: "payable"
},
{
type: "function",
name: "delegatedAction",
inputs: [
{
name: "trader",
type: "address",
internalType: "address"
},
{
name: "call_data",
type: "bytes",
internalType: "bytes"
}
],
outputs: [
{
name: "",
type: "bytes",
internalType: "bytes"
}
],
stateMutability: "payable"
},
{
type: "function",
name: "delegations",
inputs: [
{
name: "",
type: "address",
internalType: "address"
}
],
outputs: [
{
name: "",
type: "address",
internalType: "address"
}
],
stateMutability: "view"
},
{
type: "function",
name: "executeLimitOrder",
inputs: [
{
name: "_orderType",
type: "uint8",
internalType: "enum ITradingStorage.LimitOrder"
},
{
name: "_trader",
type: "address",
internalType: "address"
},
{
name: "_pairIndex",
type: "uint256",
internalType: "uint256"
},
{
name: "_index",
type: "uint256",
internalType: "uint256"
},
{
name: "priceUpdateData",
type: "bytes[]",
internalType: "bytes[]"
}
],
outputs: [
],
stateMutability: "payable"
},
{
type: "function",
name: "executeMarketOrders",
inputs: [
{