Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions packages/adapters/rebalance/scripts/binance-check.ts
Original file line number Diff line number Diff line change
@@ -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);
});
2 changes: 2 additions & 0 deletions packages/adapters/rebalance/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
116 changes: 115 additions & 1 deletion packages/admin/src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, string> = {};
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<string, unknown>[] = [];
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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/admin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export enum HttpPaths {
TriggerRebalance = '/trigger/rebalance',
TriggerIntent = '/trigger/intent',
TriggerSwap = '/trigger/swap',
BinanceCheck = '/binance/check',
}

export interface PaginationParams {
Expand Down
Loading