Implement on-chain token balance retrieval and enhance SpotBot logging
- Added a new method in IWeb3ProxyService to retrieve token balances directly from the blockchain, ensuring accurate decimal handling. - Updated ExchangeService to utilize the new on-chain balance method, replacing the previous balance retrieval logic. - Enhanced SpotBot logging to provide clearer warnings when token balances are significantly lower than expected, and to log cases of excess token balances. - Introduced a new API endpoint for fetching token balances on-chain, improving the overall functionality of the service.
This commit is contained in:
@@ -20,6 +20,8 @@ namespace Managing.Application.Abstractions.Services
|
|||||||
|
|
||||||
Task<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains);
|
Task<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains);
|
||||||
|
|
||||||
|
Task<Balance> GetTokenBalanceOnChainAsync(string address, Ticker ticker, int? chainId = null);
|
||||||
|
|
||||||
Task<decimal> GetEstimatedGasFeeUsdAsync();
|
Task<decimal> GetEstimatedGasFeeUsdAsync();
|
||||||
|
|
||||||
Task<GasFeeData> GetGasFeeDataAsync();
|
Task<GasFeeData> GetGasFeeDataAsync();
|
||||||
|
|||||||
@@ -47,11 +47,14 @@ namespace Managing.Application.Tests
|
|||||||
evmProcessor
|
evmProcessor
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var web3ProxyService = new Mock<IWeb3ProxyService>().Object;
|
||||||
|
|
||||||
return new ExchangeService(
|
return new ExchangeService(
|
||||||
loggerFactory.CreateLogger<ExchangeService>(),
|
loggerFactory.CreateLogger<ExchangeService>(),
|
||||||
GetCandleRepository(),
|
GetCandleRepository(),
|
||||||
exchangeProcessors,
|
exchangeProcessors,
|
||||||
evmManager);
|
evmManager,
|
||||||
|
web3ProxyService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ILogger<TradingBotBase> CreateTradingBotLogger()
|
public static ILogger<TradingBotBase> CreateTradingBotLogger()
|
||||||
|
|||||||
@@ -392,27 +392,45 @@ public class SpotBot : TradingBotBase
|
|||||||
|
|
||||||
if (tokenBalance is { Amount: > 0 })
|
if (tokenBalance is { Amount: > 0 })
|
||||||
{
|
{
|
||||||
// Verify that the token balance matches the position amount with 0.1% tolerance
|
// Verify that the token balance matches the position amount
|
||||||
var positionQuantity = internalPosition.Open.Quantity;
|
var positionQuantity = internalPosition.Open.Quantity;
|
||||||
var tokenBalanceAmount = tokenBalance.Amount;
|
var tokenBalanceAmount = tokenBalance.Amount;
|
||||||
|
|
||||||
if (positionQuantity > 0)
|
if (positionQuantity > 0)
|
||||||
|
{
|
||||||
|
// Only check tolerance if token balance is LESS than position quantity
|
||||||
|
// If balance is greater, it could be orphaned tokens from previous positions
|
||||||
|
if (tokenBalanceAmount < positionQuantity)
|
||||||
{
|
{
|
||||||
var tolerance = positionQuantity * 0.006m; // 0.6% tolerance to account for slippage
|
var tolerance = positionQuantity * 0.006m; // 0.6% tolerance to account for slippage
|
||||||
var difference = Math.Abs(tokenBalanceAmount - positionQuantity);
|
var difference = positionQuantity - tokenBalanceAmount;
|
||||||
|
|
||||||
if (difference > tolerance)
|
if (difference > tolerance)
|
||||||
{
|
{
|
||||||
await LogWarningAsync(
|
await LogWarningAsync(
|
||||||
$"⚠️ Token Balance Mismatch - Position Verification Failed\n" +
|
$"⚠️ Token Balance Below Position Quantity\n" +
|
||||||
$"Position: `{internalPosition.Identifier}`\n" +
|
$"Position: `{internalPosition.Identifier}`\n" +
|
||||||
$"Position Quantity: `{positionQuantity:F5}`\n" +
|
$"Position Quantity: `{positionQuantity:F5}`\n" +
|
||||||
$"Token Balance: `{tokenBalanceAmount:F5}`\n" +
|
$"Token Balance: `{tokenBalanceAmount:F5}`\n" +
|
||||||
$"Difference: `{difference:F5}`\n" +
|
$"Difference: `{difference:F5}`\n" +
|
||||||
$"Tolerance (0.6%): `{tolerance:F5}`\n" +
|
$"Tolerance (0.6%): `{tolerance:F5}`\n" +
|
||||||
$"Token balance does not match position amount within tolerance\n" +
|
$"Token balance is significantly lower than expected\n" +
|
||||||
$"Skipping position synchronization");
|
$"Skipping position synchronization");
|
||||||
return; // Skip processing if amounts don't match
|
return; // Skip processing if balance is too low
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (tokenBalanceAmount > positionQuantity)
|
||||||
|
{
|
||||||
|
// Token balance is higher than position - likely orphaned tokens
|
||||||
|
// Log but continue with synchronization
|
||||||
|
var excess = tokenBalanceAmount - positionQuantity;
|
||||||
|
await LogDebugAsync(
|
||||||
|
$"ℹ️ Token Balance Exceeds Position Quantity\n" +
|
||||||
|
$"Position: `{internalPosition.Identifier}`\n" +
|
||||||
|
$"Position Quantity: `{positionQuantity:F5}`\n" +
|
||||||
|
$"Token Balance: `{tokenBalanceAmount:F5}`\n" +
|
||||||
|
$"Excess: `{excess:F5}`\n" +
|
||||||
|
$"Proceeding with position synchronization");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,17 +18,20 @@ namespace Managing.Infrastructure.Exchanges
|
|||||||
private readonly ICandleRepository _candleRepository;
|
private readonly ICandleRepository _candleRepository;
|
||||||
private readonly IEnumerable<IExchangeProcessor> _exchangeProcessor;
|
private readonly IEnumerable<IExchangeProcessor> _exchangeProcessor;
|
||||||
private readonly IEvmManager _evmManager;
|
private readonly IEvmManager _evmManager;
|
||||||
|
private readonly IWeb3ProxyService _web3ProxyService;
|
||||||
|
|
||||||
public ExchangeService(
|
public ExchangeService(
|
||||||
ILogger<ExchangeService> logger,
|
ILogger<ExchangeService> logger,
|
||||||
ICandleRepository candleRepository,
|
ICandleRepository candleRepository,
|
||||||
IEnumerable<IExchangeProcessor> processor,
|
IEnumerable<IExchangeProcessor> processor,
|
||||||
IEvmManager evmManager)
|
IEvmManager evmManager,
|
||||||
|
IWeb3ProxyService web3ProxyService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_candleRepository = candleRepository;
|
_candleRepository = candleRepository;
|
||||||
_exchangeProcessor = processor;
|
_exchangeProcessor = processor;
|
||||||
_evmManager = evmManager;
|
_evmManager = evmManager;
|
||||||
|
_web3ProxyService = web3ProxyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Trades
|
#region Trades
|
||||||
@@ -271,22 +274,11 @@ namespace Managing.Infrastructure.Exchanges
|
|||||||
string.Equals(balance.TokenName, ticker.ToString(), StringComparison.InvariantCultureIgnoreCase));
|
string.Equals(balance.TokenName, ticker.ToString(), StringComparison.InvariantCultureIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
var evmBalance = await _evmManager.GetTokenBalance(Constants.Chains.Arbitrum, ticker, account.Key);
|
// Use Web3Proxy's on-chain balance endpoint for accurate decimal-adjusted balances
|
||||||
|
// This uses Viem to call the ERC20 balanceOf function directly, ensuring proper decimal handling
|
||||||
|
var balance = await _web3ProxyService.GetTokenBalanceOnChainAsync(account.Key, ticker, 42161); // Arbitrum chainId
|
||||||
|
|
||||||
if (evmBalance == null)
|
return balance;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Balance
|
|
||||||
{
|
|
||||||
TokenName = evmBalance.TokenName,
|
|
||||||
Price = evmBalance.Price,
|
|
||||||
Value = evmBalance.Value,
|
|
||||||
Amount = evmBalance.Balance,
|
|
||||||
TokenAdress = evmBalance.TokenAddress,
|
|
||||||
Chain = evmBalance.Chain
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<decimal> GetFee(Account account, bool isForPaperTrading = false)
|
public async Task<decimal> GetFee(Account account, bool isForPaperTrading = false)
|
||||||
|
|||||||
@@ -34,12 +34,14 @@ namespace Managing.Infrastructure.Tests
|
|||||||
{
|
{
|
||||||
evmProcessor
|
evmProcessor
|
||||||
};
|
};
|
||||||
|
var web3ProxyService = new Mock<IWeb3ProxyService>().Object;
|
||||||
|
|
||||||
_exchangeService = new ExchangeService(
|
_exchangeService = new ExchangeService(
|
||||||
doesntDoMuch.CreateLogger<ExchangeService>(),
|
doesntDoMuch.CreateLogger<ExchangeService>(),
|
||||||
candleRepository,
|
candleRepository,
|
||||||
exchangeProcessors,
|
exchangeProcessors,
|
||||||
evmManager);
|
evmManager,
|
||||||
|
web3ProxyService);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -153,6 +153,18 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
|
|||||||
public List<Balance> Balances { get; set; }
|
public List<Balance> Balances { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response for single token balance on-chain operations
|
||||||
|
/// </summary>
|
||||||
|
public class Web3ProxyTokenBalanceResponse : Web3ProxyResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Token balance if successful
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("balance")]
|
||||||
|
public Balance Balance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Response model for gas fee information
|
/// Response model for gas fee information
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -402,6 +402,35 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
return response.Balances ?? new List<Balance>();
|
return response.Balances ?? new List<Balance>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Balance> GetTokenBalanceOnChainAsync(string address, Ticker ticker, int? chainId = null)
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
address,
|
||||||
|
ticker = ticker.ToString(),
|
||||||
|
chainId
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await GetPrivyServiceAsync<Web3ProxyTokenBalanceResponse>("/token-balance", payload);
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
{
|
||||||
|
throw new Web3ProxyException("Token balance response is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.Success)
|
||||||
|
{
|
||||||
|
throw new Web3ProxyException($"Token balance request failed: {response.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Balance == null)
|
||||||
|
{
|
||||||
|
throw new Web3ProxyException("Token balance is null in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Balance;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<decimal> GetEstimatedGasFeeUsdAsync()
|
public async Task<decimal> GetEstimatedGasFeeUsdAsync()
|
||||||
{
|
{
|
||||||
var response = await GetGmxServiceAsync<GasFeeResponse>("/gas-fee", null);
|
var response = await GetGmxServiceAsync<GasFeeResponse>("/gas-fee", null);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ declare module 'fastify' {
|
|||||||
initAddress: typeof initAddress;
|
initAddress: typeof initAddress;
|
||||||
sendToken: typeof sendToken;
|
sendToken: typeof sendToken;
|
||||||
getWalletBalance: typeof getWalletBalance;
|
getWalletBalance: typeof getWalletBalance;
|
||||||
|
getTokenBalanceOnChain: typeof getTokenBalanceOnChain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1373,6 +1374,151 @@ export async function sendToken(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the on-chain token balance for a specific address using Viem ERC20 contract call
|
||||||
|
* This provides accurate decimal-adjusted balances directly from the blockchain
|
||||||
|
* @param address The wallet address to get balance for
|
||||||
|
* @param ticker The token ticker (e.g., 'ETH', 'USDC', 'BTC')
|
||||||
|
* @param chainId The chain ID (optional, defaults to ARBITRUM)
|
||||||
|
* @returns Balance object with properly formatted amount and USD value
|
||||||
|
*/
|
||||||
|
export const getTokenBalanceOnChainImpl = async (
|
||||||
|
address: string,
|
||||||
|
ticker: string,
|
||||||
|
chainId?: number
|
||||||
|
): Promise<Balance> => {
|
||||||
|
try {
|
||||||
|
chainId = chainId ?? ARBITRUM;
|
||||||
|
|
||||||
|
// Get SDK client for this address
|
||||||
|
const sdk = await getClientForAddress(address);
|
||||||
|
|
||||||
|
// Get token data from GMX configuration
|
||||||
|
const chainTokens = TOKENS[chainId];
|
||||||
|
const token = chainTokens?.find(t => t.symbol === ticker);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(`Token not found: ${ticker} on chain ${chainId}. Available tokens: ${chainTokens?.map(t => t.symbol).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token balance - different approach for native vs ERC20 tokens
|
||||||
|
let rawBalance: bigint;
|
||||||
|
|
||||||
|
if (token.isNative) {
|
||||||
|
// For native tokens (ETH), get balance directly from the chain
|
||||||
|
rawBalance = await sdk.publicClient.getBalance({ address: address as Address });
|
||||||
|
} else {
|
||||||
|
// For ERC20 tokens, get balance using multicall
|
||||||
|
const result = await sdk.executeMulticall({
|
||||||
|
token: {
|
||||||
|
contractAddress: token.address,
|
||||||
|
abiId: "Token",
|
||||||
|
calls: {
|
||||||
|
balance: {
|
||||||
|
methodName: "balanceOf",
|
||||||
|
params: [address]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract balance from multicall result
|
||||||
|
rawBalance = result?.data?.token?.balance?.returnValues?.[0];
|
||||||
|
|
||||||
|
if (rawBalance === undefined || rawBalance === null) {
|
||||||
|
throw new Error(`Failed to extract balance from multicall result for ${ticker}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to decimal format using token decimals
|
||||||
|
const amount = parseFloat(rawBalance.toString()) / Math.pow(10, token.decimals);
|
||||||
|
|
||||||
|
// Get token price (from last candle or current price)
|
||||||
|
// For now, use 0 or fetch from exchange if needed
|
||||||
|
let price = 0;
|
||||||
|
let usdValue = 0;
|
||||||
|
|
||||||
|
// Try to get price from token data if available
|
||||||
|
try {
|
||||||
|
const {tokensData} = await sdk.tokens.getTokensData();
|
||||||
|
const tokenData = tokensData[token.address];
|
||||||
|
if (tokenData?.prices?.minPrice) {
|
||||||
|
// GMX prices are in USD with 30 decimals
|
||||||
|
price = parseFloat(tokenData.prices.minPrice.toString()) / Math.pow(10, 30);
|
||||||
|
usdValue = amount * price;
|
||||||
|
}
|
||||||
|
} catch (priceError) {
|
||||||
|
console.warn(`Could not fetch price for ${ticker}:`, priceError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get chain information
|
||||||
|
const caip2 = getChainName(chainId);
|
||||||
|
const chain: Chain = {
|
||||||
|
id: caip2,
|
||||||
|
rpcUrl: undefined,
|
||||||
|
name: getChainNameFromCaip2(caip2),
|
||||||
|
chainId: chainId
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokenImage: '',
|
||||||
|
tokenName: ticker.toUpperCase(),
|
||||||
|
amount: amount,
|
||||||
|
price: price,
|
||||||
|
value: usdValue,
|
||||||
|
tokenAdress: token.address,
|
||||||
|
chain: chain
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting token balance on-chain:', error);
|
||||||
|
throw new Error(`Failed to get token balance on-chain: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets on-chain token balance using Viem
|
||||||
|
* @param this The FastifyRequest instance
|
||||||
|
* @param reply The FastifyReply instance
|
||||||
|
* @param address The wallet address to get balance for
|
||||||
|
* @param ticker The token ticker (e.g., 'ETH', 'USDC', 'BTC')
|
||||||
|
* @param chainId The chain ID (optional, defaults to ARBITRUM)
|
||||||
|
* @returns The response object with success status and balance
|
||||||
|
*/
|
||||||
|
export async function getTokenBalanceOnChain(
|
||||||
|
this: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
address: string,
|
||||||
|
ticker: string,
|
||||||
|
chainId?: number
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (!address) {
|
||||||
|
throw new Error('Wallet address is required for balance retrieval');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ticker) {
|
||||||
|
throw new Error('Token ticker is required for balance retrieval');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the implementation function
|
||||||
|
const balance = await getTokenBalanceOnChainImpl(address, ticker, chainId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
balance: balance
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error(error);
|
||||||
|
|
||||||
|
// Return appropriate error response
|
||||||
|
reply.status(500);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'An unknown error occurred'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The use of fastify-plugin is required to be able
|
* The use of fastify-plugin is required to be able
|
||||||
* to export the decorators to the outer scope
|
* to export the decorators to the outer scope
|
||||||
@@ -1401,6 +1547,10 @@ export default fp(async (fastify) => {
|
|||||||
return getWalletBalance.call(this, reply, address, assets, chains);
|
return getWalletBalance.call(this, reply, address, assets, chains);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastify.decorateRequest('getTokenBalanceOnChain', async function(this: FastifyRequest, reply: FastifyReply, address: string, ticker: string, chainId?: number) {
|
||||||
|
return getTokenBalanceOnChain.call(this, reply, address, ticker, chainId);
|
||||||
|
});
|
||||||
|
|
||||||
// Test the Privy client initialization (non-blocking)
|
// Test the Privy client initialization (non-blocking)
|
||||||
// Don't throw on error - let the app start so we can debug
|
// Don't throw on error - let the app start so we can debug
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -175,6 +175,56 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fastify.get(
|
||||||
|
'/token-balance',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
querystring: Type.Object({
|
||||||
|
address: Type.String(),
|
||||||
|
ticker: Type.String(),
|
||||||
|
chainId: Type.Optional(Type.Number())
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: Type.Object({
|
||||||
|
success: Type.Boolean(),
|
||||||
|
balance: Type.Optional(Type.Object({
|
||||||
|
tokenImage: Type.Optional(Type.String()),
|
||||||
|
tokenName: Type.Optional(Type.String()),
|
||||||
|
amount: Type.Optional(Type.Number()),
|
||||||
|
price: Type.Optional(Type.Number()),
|
||||||
|
value: Type.Optional(Type.Number()),
|
||||||
|
tokenAdress: Type.Optional(Type.String()),
|
||||||
|
chain: Type.Optional(Type.Object({
|
||||||
|
id: Type.Optional(Type.String()),
|
||||||
|
rpcUrl: Type.Optional(Type.String()),
|
||||||
|
name: Type.Optional(Type.String()),
|
||||||
|
chainId: Type.Optional(Type.Number())
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
error: Type.Optional(Type.String())
|
||||||
|
}),
|
||||||
|
400: Type.Object({
|
||||||
|
success: Type.Boolean(),
|
||||||
|
error: Type.String()
|
||||||
|
}),
|
||||||
|
500: Type.Object({
|
||||||
|
success: Type.Boolean(),
|
||||||
|
error: Type.String()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tags: ['Privy']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async function (request, reply) {
|
||||||
|
try {
|
||||||
|
const { address, ticker, chainId } = request.query;
|
||||||
|
return await request.getTokenBalanceOnChain(reply, address, ticker, chainId);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(request, reply, error, 'privy/token-balance');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default plugin
|
export default plugin
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert'
|
||||||
|
import { getTokenBalanceOnChainImpl } from '../../src/plugins/custom/privy'
|
||||||
|
|
||||||
|
test('Privy get token balance on-chain', async (t) => {
|
||||||
|
await t.test('should get BTC balance with correct decimals', async () => {
|
||||||
|
const result = await getTokenBalanceOnChainImpl(
|
||||||
|
'0x932167388dD9aad41149b3cA23eBD489E2E2DD78',
|
||||||
|
'BTC',
|
||||||
|
42161 // Arbitrum
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(result, 'Balance result should be defined')
|
||||||
|
assert.strictEqual(result.tokenName, 'BTC', 'Token name should be BTC')
|
||||||
|
assert.ok(typeof result.amount === 'number', 'Amount should be a number')
|
||||||
|
assert.ok(result.amount >= 0, 'Amount should be non-negative')
|
||||||
|
assert.ok(typeof result.price === 'number', 'Price should be a number')
|
||||||
|
assert.ok(result.price >= 0, 'Price should be non-negative')
|
||||||
|
assert.ok(typeof result.value === 'number', 'Value should be a number')
|
||||||
|
// For ERC20 tokens like BTC (WBTC), should have a real contract address
|
||||||
|
assert.ok(result.tokenAdress && result.tokenAdress !== '0x0000000000000000000000000000000000000000', 'BTC should have a valid contract address')
|
||||||
|
assert.ok(result.chain, 'Chain should be defined')
|
||||||
|
assert.strictEqual(result.chain.chainId, 42161, 'Chain ID should be Arbitrum')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user