diff --git a/packages/adapters/rebalance/scripts/binance-check.ts b/packages/adapters/rebalance/scripts/binance-check.ts new file mode 100644 index 00000000..b014de37 --- /dev/null +++ b/packages/adapters/rebalance/scripts/binance-check.ts @@ -0,0 +1,164 @@ +import { config } from 'dotenv'; +import { resolve } from 'path'; +import { Logger } from '@mark/logger'; +import { BinanceClient } from '../src/adapters/binance/client'; +import { BINANCE_NETWORK_TO_CHAIN_ID } from '../src/adapters/binance/constants'; + +// Load .env from project root +config({ path: resolve(__dirname, '../../../../.env') }); + +const logger = new Logger({ level: 'debug', service: 'binance-check' }); + +async function main() { + const apiKey = process.env.BINANCE_API_KEY; + const apiSecret = process.env.BINANCE_API_SECRET; + + if (!apiKey || !apiSecret) { + logger.error('Missing BINANCE_API_KEY or BINANCE_API_SECRET in .env'); + process.exit(1); + } + + logger.info('Initializing Binance client...'); + const client = new BinanceClient(apiKey, apiSecret, 'https://api.binance.com', logger); + + if (!client.isConfigured()) { + logger.error('Binance client not properly configured'); + process.exit(1); + } + + logger.info('=== Binance USDC Withdrawal Check ==='); + + // Step 1: System status + logger.info('Step 1: Checking Binance system status...'); + const isOperational = await client.isSystemOperational(); + logger.info('System status result', { isOperational }); + if (!isOperational) { + logger.error('Binance system is NOT operational. Aborting.'); + process.exit(1); + } + + // Step 2: Account balances — show all non-zero balances + highlight USDC + logger.info('Step 2: Fetching account balances...'); + const balances = await client.getAccountBalance(); + const nonZeroBalances = Object.entries(balances).filter(([, v]) => parseFloat(v) > 0); + + logger.info('All non-zero balances on Binance account', { + count: nonZeroBalances.length, + balances: Object.fromEntries(nonZeroBalances), + }); + + const usdcBalance = balances['USDC'] || '0'; + const usdtBalance = balances['USDT'] || '0'; + const ethBalance = balances['ETH'] || '0'; + + logger.info('Key asset balances', { + USDC: usdcBalance, + USDT: usdtBalance, + ETH: ethBalance, + }); + + if (parseFloat(usdcBalance) === 0) { + logger.warn('No USDC available on Binance. Nothing to withdraw.'); + process.exit(0); + } + + logger.info(`USDC available for withdrawal: ${usdcBalance} USDC`); + + // Step 3: Withdrawal quota + logger.info('Step 3: Checking withdrawal quota...'); + const quota = await client.getWithdrawQuota(); + const totalQuota = parseFloat(quota.wdQuota); + const usedQuota = parseFloat(quota.usedWdQuota); + const remainingQuota = totalQuota - usedQuota; + + logger.info('Withdrawal quota details', { + totalQuotaUSD: `$${totalQuota.toFixed(2)}`, + usedQuotaUSD: `$${usedQuota.toFixed(2)}`, + remainingQuotaUSD: `$${remainingQuota.toFixed(2)}`, + }); + + const usdcBalanceNum = parseFloat(usdcBalance); + if (usdcBalanceNum > remainingQuota) { + logger.warn('USDC balance exceeds remaining quota', { + usdcBalanceUSD: `$${usdcBalanceNum.toFixed(2)}`, + remainingQuotaUSD: `$${remainingQuota.toFixed(2)}`, + maxWithdrawableByQuota: `$${remainingQuota.toFixed(2)}`, + }); + } else { + logger.info('Quota check passed — sufficient for full balance withdrawal', { + usdcBalanceUSD: `$${usdcBalanceNum.toFixed(2)}`, + remainingQuotaUSD: `$${remainingQuota.toFixed(2)}`, + }); + } + + // Step 4: USDC network config (fees, minimums, enabled status) + logger.info('Step 4: Fetching USDC network configs from Binance...'); + const assetConfigs = await client.getAssetConfig(); + const usdcConfig = assetConfigs.find((c) => c.coin === 'USDC'); + + if (!usdcConfig) { + logger.error('USDC not found in Binance asset config!'); + process.exit(1); + } + + logger.info(`Found ${usdcConfig.networkList.length} USDC networks on Binance`); + + for (const net of usdcConfig.networkList) { + const chainId = BINANCE_NETWORK_TO_CHAIN_ID[net.network as keyof typeof BINANCE_NETWORK_TO_CHAIN_ID] || 'unknown'; + const fee = parseFloat(net.withdrawFee); + const min = parseFloat(net.withdrawMin); + const canWithdraw = net.withdrawEnable && usdcBalanceNum >= min + fee; + + logger.info(`Network: ${net.network}`, { + chainId, + depositEnabled: net.depositEnable, + withdrawEnabled: net.withdrawEnable, + withdrawFee: `${net.withdrawFee} USDC`, + withdrawMin: `${net.withdrawMin} USDC`, + withdrawMax: `${net.withdrawMax} USDC`, + minConfirmations: net.minConfirm, + canWithdrawFullBalance: canWithdraw, + }); + } + + // Step 5: Summary — which chains can we actually withdraw to + logger.info('Step 5: Withdrawal feasibility per supported chain'); + const supportedNetworks = ['ETH', 'ARBITRUM', 'OPTIMISM', 'BASE', 'BSC', 'MATIC', 'AVAXC', 'SCROLL', 'ZKSYNCERA', 'SONIC', 'RON']; + + for (const networkName of supportedNetworks) { + const net = usdcConfig.networkList.find((n) => n.network === networkName); + if (!net) { + logger.debug(`${networkName}: not available on Binance for USDC`); + continue; + } + + const fee = parseFloat(net.withdrawFee); + const min = parseFloat(net.withdrawMin); + const maxWithdrawable = Math.min(usdcBalanceNum - fee, parseFloat(net.withdrawMax), remainingQuota); + const canDo = net.withdrawEnable && usdcBalanceNum >= min + fee && maxWithdrawable > 0; + + if (canDo) { + logger.info(`${networkName}: WITHDRAWAL POSSIBLE`, { + maxWithdrawable: `${maxWithdrawable.toFixed(2)} USDC`, + fee: `${fee} USDC`, + min: `${min} USDC`, + youReceive: `${(maxWithdrawable).toFixed(2)} USDC`, + }); + } else if (!net.withdrawEnable) { + logger.warn(`${networkName}: WITHDRAWAL DISABLED by Binance`); + } else { + logger.warn(`${networkName}: CANNOT WITHDRAW`, { + reason: 'insufficient balance', + required: `${(min + fee).toFixed(2)} USDC`, + available: `${usdcBalanceNum.toFixed(2)} USDC`, + }); + } + } + + logger.info('Done. Check logs above for withdrawal feasibility.'); +} + +main().catch((err) => { + console.error('Error:', err.message || err); + process.exit(1); +}); diff --git a/packages/adapters/rebalance/src/index.ts b/packages/adapters/rebalance/src/index.ts index 2c2de518..7bdd7235 100644 --- a/packages/adapters/rebalance/src/index.ts +++ b/packages/adapters/rebalance/src/index.ts @@ -5,3 +5,5 @@ export { PendleBridgeAdapter } from './adapters/pendle'; export { CHAIN_SELECTORS, CCIP_ROUTER_ADDRESSES, CCIP_SUPPORTED_CHAINS } from './adapters/ccip/types'; export { CCIPBridgeAdapter } from './adapters/ccip'; export { buildTransactionsForAction, DexSwapActionHandler } from './actions'; +export { BinanceClient } from './adapters/binance/client'; +export { BINANCE_NETWORK_TO_CHAIN_ID } from './adapters/binance/constants'; diff --git a/packages/admin/src/api/routes.ts b/packages/admin/src/api/routes.ts index cf650231..0af4b4a8 100644 --- a/packages/admin/src/api/routes.ts +++ b/packages/admin/src/api/routes.ts @@ -17,7 +17,7 @@ import { BPS_MULTIPLIER, } from '@mark/core'; import { encodeFunctionData, erc20Abi, Hex, formatUnits, parseUnits } from 'viem'; -import { MemoizedTransactionRequest } from '@mark/rebalance'; +import { MemoizedTransactionRequest, BinanceClient, BINANCE_NETWORK_TO_CHAIN_ID } from '@mark/rebalance'; import type { SwapExecutionResult } from '@mark/rebalance/src/types'; import { AdminApi } from '../openapi/adminApi'; import { ErrorResponse, ForbiddenResponse } from '../openapi/schemas'; @@ -881,6 +881,112 @@ const handleTriggerSwap = async (context: AdminContext): Promise<{ statusCode: n } }; +const handleBinanceCheck = async (context: AdminContext): Promise<{ statusCode: number; body: string }> => { + const { logger, config } = context; + + try { + const { binance } = config.markConfig; + if (!binance?.apiKey || !binance?.apiSecret) { + return { + statusCode: 400, + body: JSON.stringify({ message: 'Binance API key and secret not configured' }), + }; + } + + const client = new BinanceClient(binance.apiKey, binance.apiSecret, 'https://api.binance.com', logger); + + // 1. System status + const isOperational = await client.isSystemOperational(); + logger.info('Binance system status', { isOperational }); + + if (!isOperational) { + return { + statusCode: 503, + body: JSON.stringify({ message: 'Binance system is not operational', isOperational }), + }; + } + + // 2. Account balances + const balances = await client.getAccountBalance(); + const nonZeroBalances: Record = {}; + for (const [asset, balance] of Object.entries(balances)) { + if (parseFloat(balance as string) > 0) { + nonZeroBalances[asset] = balance as string; + } + } + + logger.info('Binance account balances', { nonZeroBalances }); + + const usdcBalance = balances['USDC'] || '0'; + const usdtBalance = balances['USDT'] || '0'; + const ethBalance = balances['ETH'] || '0'; + + // 3. Withdrawal quota + const quota = await client.getWithdrawQuota(); + const totalQuota = parseFloat(quota.wdQuota); + const usedQuota = parseFloat(quota.usedWdQuota); + const remainingQuota = totalQuota - usedQuota; + + logger.info('Withdrawal quota', { totalQuota, usedQuota, remainingQuota }); + + // 4. USDC network configs + const assetConfigs = await client.getAssetConfig(); + const usdcConfig = assetConfigs.find((c: { coin: string }) => c.coin === 'USDC'); + + const networks: Record[] = []; + if (usdcConfig) { + const usdcBalanceNum = parseFloat(usdcBalance); + for (const net of usdcConfig.networkList) { + const chainId = BINANCE_NETWORK_TO_CHAIN_ID[net.network as keyof typeof BINANCE_NETWORK_TO_CHAIN_ID] || null; + const fee = parseFloat(net.withdrawFee); + const min = parseFloat(net.withdrawMin); + const maxWithdrawable = Math.min(usdcBalanceNum - fee, parseFloat(net.withdrawMax), remainingQuota); + const canWithdraw = net.withdrawEnable && usdcBalanceNum >= min + fee && maxWithdrawable > 0; + + networks.push({ + network: net.network, + chainId, + depositEnabled: net.depositEnable, + withdrawEnabled: net.withdrawEnable, + withdrawFee: net.withdrawFee, + withdrawMin: net.withdrawMin, + withdrawMax: net.withdrawMax, + canWithdraw, + maxWithdrawable: canWithdraw ? maxWithdrawable.toFixed(2) : '0', + }); + } + } + + return { + statusCode: 200, + body: JSON.stringify({ + isOperational, + balances: { + all: nonZeroBalances, + USDC: usdcBalance, + USDT: usdtBalance, + ETH: ethBalance, + }, + quota: { + totalQuotaUSD: totalQuota, + usedQuotaUSD: usedQuota, + remainingQuotaUSD: remainingQuota, + }, + usdcNetworks: networks, + }), + }; + } catch (error) { + logger.error('Binance check failed', { error: jsonifyError(error) }); + return { + statusCode: 500, + body: JSON.stringify({ + message: 'Binance check failed', + error: error instanceof Error ? error.message : 'Unknown error', + }), + }; + } +}; + const handleGetRequest = async ( request: HttpPaths, context: AdminContext, @@ -944,6 +1050,9 @@ const handleGetRequest = async ( } } + case HttpPaths.BinanceCheck: + return handleBinanceCheck(context); + case HttpPaths.GetRebalanceOperationDetails: { const paramsParsed = parsePathParams(AdminApi.getRebalanceOperationDetails.params!, event.pathParameters ?? null); if (isLambdaResponse(paramsParsed)) return paramsParsed; @@ -1268,6 +1377,11 @@ export const extractRequest = (context: AdminContext): HttpPaths | undefined => return undefined; } + // Handle Binance check + if (httpMethod === 'GET' && path.endsWith('/binance/check')) { + return HttpPaths.BinanceCheck; + } + // Handle earmark detail path with ID parameter if (httpMethod === 'GET' && path.includes('/rebalance/earmark/')) { return HttpPaths.GetEarmarkDetails; diff --git a/packages/admin/src/types.ts b/packages/admin/src/types.ts index 7eb8e6de..e9e7ff96 100644 --- a/packages/admin/src/types.ts +++ b/packages/admin/src/types.ts @@ -49,6 +49,7 @@ export enum HttpPaths { TriggerRebalance = '/trigger/rebalance', TriggerIntent = '/trigger/intent', TriggerSwap = '/trigger/swap', + BinanceCheck = '/binance/check', } export interface PaginationParams {