UNPKG

@todayapp/avantis-sdk

Version:

Unofficial TypeScript SDK for Avantis DEX - Decentralized perpetual futures trading (BETA)

1,717 lines (1,708 loc) 3.85 MB
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: [ {