import { Balance, ChainWalletConfig, SUPPORTED_VM, UserTokenBalance, TokenInfo, EVMNFT, NFT, NFTCollection } from '../types' import { JsonRpcProvider, Contract, Wallet, TransactionRequest, TransactionResponse, TransactionReceipt, parseUnits, formatUnits, ethers } from 'ethers' import BN from 'bn.js' import { HelperAPI } from '../helpers'; import BigNumber from 'bignumber.js'; import type { TransactionReceipt as EthersTransactionReceipt } from 'ethers' import type { Chain, TransactionReceipt as ViemTransactionReceipt } from 'viem' import { PublicClient, WalletClient, Hex, encodeFunctionData, parseEther, formatEther, createPublicClient, http, } from 'viem' export interface TransactionParams { to: string value?: string | bigint // For native token transfers data?: string // For contract calls gasLimit?: string | bigint gasPrice?: string | bigint maxFeePerGas?: string | bigint // For EIP-1559 maxPriorityFeePerGas?: string | bigint // For EIP-1559 nonce?: number } interface TransactionResult { hash: string receipt: EthersTransactionReceipt | null viemReceipt?: ViemTransactionReceipt success: boolean gasUsed?: bigint effectiveGasPrice?: bigint blockNumber?: number confirmations: number } export interface SwapParams { tokenIn: string; tokenOut: string; amountIn: string; slippageTolerance?: number; recipient?: string; deadline?: number; feeAmount?: string; feeReceiver?: string; isInBps?: boolean; chargeFeeBy?: 'currency_in' | 'currency_out'; } export interface KyberRoute { tokenIn: string; amountIn: string; tokenOut: string; amountOut: string; gas: string; gasPrice: string; gasUsd: number; amountOutUsd: string; receivedUsd: string; swaps: Array<{ pool: string; tokenIn: string; tokenOut: string; swapAmount: string; amountOut: string; limitReturnAmount: string; maxPrice: string; exchange: string; poolLength: number; poolType: string; }>; tokens: { [address: string]: { address: string; symbol: string; name: string; decimals: number; price: number; }; }; } export interface KyberSwapResponse { code: number; message: string; data: { routeSummary: KyberRoute; routerAddress: string; }; } export interface KyberBuildResponse { code: number; message: string; data: { amountIn: string; amountInUsd: string; amountOut: string; amountOutUsd: string; gas: string; gasUsd: string; outputChange: { amount: string; percent: number; level: number; }; data: string; routerAddress: string; }; } const KYBER_SUPPORTED_CHAINS: { [key: string]: string } = { '1': 'ethereum', '137': 'polygon', '56': 'bsc', '43114': 'avalanche', '250': 'fantom', '42161': 'arbitrum', '10': 'optimism', '8453': 'base', '324': 'zksync', '59144': 'linea' }; export const DESERIALIZED_SUPPORTED_CHAINS: { [key: string]: string } = { '16661': '0gMainnet', }; interface KyberSwapParams { chainId: string; tokenIn: string; tokenOut: string; amountIn: string; slippageTolerance?: number; // in bips (e.g., 50 = 0.5%) recipient?: string; sender?: string; deadline?: number; // Unix timestamp feeAmount?: string; feeReceiver?: string; isInBps?: boolean; chargeFeeBy?: 'currency_in' | 'currency_out'; clientId?: string; } // ERC-20 ABI export const ERC20_ABI = [ { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }], outputs: [{ type: 'uint256' }], }, { name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }], }, { name: 'symbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }], }, { name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }], }, { name: 'transfer', type: 'function', stateMutability: 'nonpayable', inputs: [ { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ type: 'bool' }], }, { name: 'approve', type: 'function', stateMutability: 'nonpayable', inputs: [ { name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ type: 'bool' }], }, { name: 'allowance', type: 'function', stateMutability: 'view', inputs: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, ], outputs: [{ type: 'uint256' }], }, ] as const export const fromChainToViemChain = (config: ChainWalletConfig): Chain => { return { rpcUrls: { default: { http: [config.rpcUrl] } }, id: config.chainId, name: config.name, nativeCurrency: { name: config.nativeToken.name, symbol: config.nativeToken.symbol, decimals: config.nativeToken.decimals }, blockExplorers: { default: { name: config.name + " Explorer", url: config.explorerUrl, apiUrl: config.explorerUrl }, }, testnet: config.testnet || false } } export const createPublicClientFromChainConfig = (chain: ChainWalletConfig): PublicClient => { return createPublicClient({ chain: fromChainToViemChain(chain), transport: http(chain.rpcUrl) }); } export function viemReceiptToEthersReceipt( receipt: ViemTransactionReceipt, ): EthersTransactionReceipt { return { to: receipt.to ?? null, from: receipt.from, contractAddress: receipt.contractAddress ?? null, transactionIndex: Number(receipt.transactionIndex), gasUsed: receipt.gasUsed, logsBloom: receipt.logsBloom, blockHash: receipt.blockHash, transactionHash: receipt.transactionHash, logs: receipt.logs.map((log) => ({ address: log.address, topics: log.topics, data: log.data, blockNumber: Number(log.blockNumber), transactionHash: log.transactionHash, transactionIndex: Number(log.transactionIndex), blockHash: log.blockHash, logIndex: Number(log.logIndex), removed: false, })), blockNumber: Number(receipt.blockNumber), confirmations: 0, // ethers usually fills this lazily cumulativeGasUsed: receipt.cumulativeGasUsed, effectiveGasPrice: receipt.effectiveGasPrice, status: receipt.status === 'success' ? 1 : 0, type: receipt.type, } as unknown as EthersTransactionReceipt } export const getNativeBalance = async ( address: Hex, client: PublicClient ): Promise => { const balance = await client.getBalance({ address }) return { balance: new BN(balance.toString()), formatted: Number(formatEther(balance)), decimal: 18, } } export const getTokenInfo = async ( tokenAddress: Hex, client: PublicClient ): Promise => { const [decimals, name, symbol] = await Promise.all([ client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'decimals', }), client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'name', }), client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'symbol', }), ]) return { name, symbol, address: tokenAddress, decimals, } } export const getTokenBalance = async ( tokenAddress: Hex, walletAddress: Hex, client: PublicClient ): Promise => { const [balance, decimals] = await Promise.all([ client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'balanceOf', args: [walletAddress], }), client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'decimals', }), ]) const formatted = balance / 10n ** BigInt(decimals) return { balance: new BN(balance.toString()), formatted: Number(formatted), decimal: decimals, } } /** * Sign, send, and confirm any EVM transaction */ export const signAndSend = async ( walletClient: WalletClient, publicClient: PublicClient, params: { to: Hex data?: Hex value?: bigint gas?: bigint nonce?: number maxFeePerGas?: bigint maxPriorityFeePerGas?: bigint }, confirmations = 1 ): Promise<{ hash: Hex, success: boolean }> => { if (walletClient.account === undefined) { throw new Error("wallet Client is not Initialized with an Account") } const hash = await walletClient.sendTransaction({ to: params.to, data: params.data, value: params.value, gas: params.gas, nonce: params.nonce, maxFeePerGas: params.maxFeePerGas, maxPriorityFeePerGas: params.maxPriorityFeePerGas, account: walletClient.account, chain: publicClient.chain, }) return { hash, success: !!hash, } } /** * Sign, send, and confirm any EVM transaction */ export const signSendAndConfirm = async ( walletClient: WalletClient, publicClient: PublicClient, params: { to: Hex data?: Hex value?: bigint gas?: bigint nonce?: number maxFeePerGas?: bigint maxPriorityFeePerGas?: bigint }, confirmations = 1 ): Promise => { if (walletClient.account === undefined) { throw new Error("wallet Client is not Initialized with an Account") } const hash = await walletClient.sendTransaction({ to: params.to, data: params.data, value: params.value, gas: params.gas, nonce: params.nonce, maxFeePerGas: params.maxFeePerGas, maxPriorityFeePerGas: params.maxPriorityFeePerGas, account: walletClient.account, chain: publicClient.chain, }) const receipt = await publicClient.waitForTransactionReceipt({ hash, confirmations, }) return { hash, receipt: viemReceiptToEthersReceipt(receipt), viemReceipt: receipt, success: receipt.status === 'success', gasUsed: receipt.gasUsed, effectiveGasPrice: receipt.effectiveGasPrice, blockNumber: Number(receipt.blockNumber.toString()), confirmations, } } /** * Send native token (ETH, BNB, MATIC, etc.) */ export const sendNativeToken = async ( walletClient: WalletClient, publicClient: PublicClient, to: Hex, amount: string | bigint, confirmations = 1 ) => { const value = typeof amount === 'string' ? parseEther(amount) : amount return signAndSend( walletClient, publicClient, { to, value }, confirmations ) } /** * Send ERC-20 token */ export const sendERC20Token = async ( walletClient: WalletClient, publicClient: PublicClient, tokenAddress: Hex, to: Hex, amount: bigint, confirmations = 1 ) => { const data = encodeFunctionData({ abi: ERC20_ABI, functionName: 'transfer', args: [to, amount], }) return signAndSend( walletClient, publicClient, { to: tokenAddress, data }, confirmations ) } /** * Get current gas prices (both legacy and EIP-1559) */ export const getGasPrices = async (client: PublicClient) => { const fees = await client.estimateFeesPerGas() return { gasPrice: fees.gasPrice, maxFeePerGas: fees.maxFeePerGas, maxPriorityFeePerGas: fees.maxPriorityFeePerGas, } } /** * Estimate gas for a transaction */ export const estimateGas = async ( client: PublicClient, params: { to: Hex data?: Hex value?: bigint } ) => { return client.estimateGas(params) } export const checkAllowance = async ( client: PublicClient, tokenAddress: Hex, owner: Hex, spender: Hex, ) => { const [allowance, decimals] = await Promise.all([ client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'allowance', args: [owner, spender], }), client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'decimals', }), ]) return { allowance, formatted: (allowance / 10n ** BigInt(decimals)).toString(), decimals, } } /** * Check if allowance is sufficient for a transaction */ export const isAllowanceSufficient = async ( publicClient: PublicClient, tokenAddress: `0x${string}`, owner: `0x${string}`, spender: `0x${string}`, requiredAmount: string | bigint, ) => { const { allowance } = await checkAllowance( publicClient, tokenAddress, owner, spender, ) return allowance >= BigInt(requiredAmount) } /** * Approve ERC-20 token spending */ export const approveToken = async ( walletClient: WalletClient, publicClient: PublicClient, tokenAddress: Hex, spender: Hex, amount: bigint, confirmations = 1 ) => { const data = encodeFunctionData({ abi: ERC20_ABI, functionName: 'approve', args: [spender, amount], }) return signSendAndConfirm( walletClient, publicClient, { to: tokenAddress, data }, confirmations ) } const MAX_UINT256 = 2n ** 256n - 1n /** * Approve unlimited token spending (MaxUint256) */ export const approveTokenUnlimited = async ( walletClient: WalletClient, publicClient: PublicClient, tokenAddress: `0x${string}`, spender: `0x${string}`, gas?: bigint, confirmations = 1, ) => { return approveToken( walletClient, publicClient, tokenAddress, spender, MAX_UINT256, confirmations, ) } /** * Check allowance and approve if necessary */ export const checkAndApprove = async ( walletClient: WalletClient, publicClient: PublicClient, tokenAddress: `0x${string}`, spender: `0x${string}`, requiredAmount: bigint, approvalAmount?: bigint, gas?: bigint, confirmations = 1, ): Promise<{ approvalNeeded: boolean currentAllowance: bigint approvalResult?: TransactionResult }> => { const owner = walletClient.account!.address const allowance = await publicClient.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'allowance', args: [owner, spender], }) if (allowance >= requiredAmount) { return { approvalNeeded: false, currentAllowance: allowance, } } const amountToApprove = approvalAmount ?? requiredAmount const approvalResult = await approveToken( walletClient, publicClient, tokenAddress, spender, amountToApprove, confirmations, ) return { approvalNeeded: true, currentAllowance: allowance, approvalResult, } } /** * Reset token allowance to zero (security best practice before setting new allowance) */ export const resetAllowance = async ( walletClient: WalletClient, publicClient: PublicClient, tokenAddress: `0x${string}`, spender: `0x${string}`, gas?: bigint, confirmations = 1, ) => { return approveToken( walletClient, publicClient, tokenAddress, spender, 0n, confirmations, ) } /** * Safe approve: Reset to zero first, then approve the desired amount * (Some tokens like USDT require this) */ export const safeApprove = async ( walletClient: WalletClient, publicClient: PublicClient, tokenAddress: `0x${string}`, spender: `0x${string}`, amount: bigint, gas?: bigint, confirmations = 1, ): Promise<{ resetResult: TransactionResult approveResult: TransactionResult }> => { const resetResult = await resetAllowance( walletClient, publicClient, tokenAddress, spender, gas, confirmations, ) if (!resetResult.success) { throw new Error('Failed to reset allowance') } const approveResult = await approveToken( walletClient, publicClient, tokenAddress, spender, amount, confirmations, ) return { resetResult, approveResult, } } export const discoverTokens = async (wallet: string, chain: ChainWalletConfig): Promise[]> => { const balances = await HelperAPI.getUserToken(wallet, chain.vmType ?? "EVM", chain.chainId) const formatBalances: UserTokenBalance[] = balances.data.map((token: any) => { return { address: token.contractAddress, name: token.name, symbol: token.symbol, decimals: token.decimals, balance: token.balance, owner: wallet, logoUrl: token.logo } }) return formatBalances } export function calcGasTotal(gasLimit = '0', gasPrice = '0') { return new BN(gasLimit, 16).mul(new BN(gasPrice, 16)).toString(); } export function toPrecisionWithoutTrailingZeros(n: number, precision: number) { return new BigNumber(n) .toPrecision(precision) .replace(/(\.[0-9]*[1-9])0*|(\.0*)/u, '$1'); } /** * @param {number|string|BigNumber} value * @param {number=} decimals * @returns {BigNumber} */ export function calcTokenAmount(value: number | string | BigNumber, decimals?: number): BigNumber { const divisor = new BigNumber(10).pow(decimals ?? 0); return new BigNumber(String(value)).div(divisor); } export const transformEVMNFTToUnified = (nft: EVMNFT): NFT => { // Extract image URL from various sources const imageUrl = nft.image?.cachedUrl || nft.image?.thumbnailUrl || nft.image?.pngUrl || nft.image?.originalUrl || nft.openSeaMetadata?.imageUrl || nft.raw?.metadata?.image || undefined; // Extract attributes const attributes = nft.raw?.metadata?.attributes?.map(attr => ({ trait_type: attr.trait_type, value: attr.value, display_type: attr.display_type })); return { id: `${nft.contract.address}:${nft.tokenId}`, name: nft.name || nft.raw?.metadata?.name || 'Unknown', symbol: nft.contract.symbol, description: nft.description || nft.raw?.metadata?.description || '', image: imageUrl, uri: nft.raw?.tokenUri || '', collection: { address: nft.contract.address, name: nft.openSeaMetadata?.collectionName || nft.contract.name, verified: nft.openSeaMetadata?.safelistRequestStatus === 'verified' }, chainType: 'EVM', balance: nft.balance, attributes, tokenStandard: nft.tokenType, isSpam: nft.contract.isSpam, raw: { evm: nft } }; }; export const discoverNFTs = async (wallet: string, chain: ChainWalletConfig): Promise => { try { const response = await HelperAPI.getUserNFTs(wallet, chain.vmType ?? "EVM", chain.chainId); // Filter out spam NFTs if desired (optional) const evmNfts = response.data.filter((nft: any) => !nft.contract.isSpam); // Transform to unified NFT format const nfts = evmNfts.map(transformEVMNFTToUnified); return nfts; } catch (error) { console.error('discoverNFTs: Error fetching NFTs:', error); console.error('Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } } export const discoverAllNFTs = async (wallet: string, chain: ChainWalletConfig): Promise => { try { const response = await HelperAPI.getUserNFTs(wallet, chain.vmType ?? "EVM", chain.chainId); // Transform to unified NFT format const nfts = response.data.map(transformEVMNFTToUnified); return nfts; } catch (error) { console.error('discoverAllNFTs: Error fetching NFTs:', error); console.error('Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } } /** * Get NFT collection details for a specific collection * @param wallet - User's wallet address * @param collectionAddress - The NFT collection contract address * @param chain - Chain configuration * @returns NFTCollection object with collection details and user's NFTs in that collection */ export const getNFTCollection = async ( wallet: string, collectionAddress: string, chain: ChainWalletConfig ): Promise => { try { // Fetch all NFTs for the user const allNfts = await discoverAllNFTs(wallet, chain); // Filter NFTs by collection address (case-insensitive comparison) const collectionNfts = allNfts.filter( nft => nft.collection.address.toLowerCase() === collectionAddress.toLowerCase() ); if (collectionNfts.length === 0) { return null; } // Extract collection metadata from the first NFT const firstNft = collectionNfts[0]; const rawEvmNft = firstNft.raw?.evm; const collection: NFTCollection = { address: collectionAddress, name: firstNft.collection.name, symbol: firstNft.symbol, description: undefined, image: rawEvmNft?.openSeaMetadata?.imageUrl, verified: firstNft.collection.verified, chainType: 'EVM', nfts: collectionNfts, totalOwned: collectionNfts.length, contractMetadata: rawEvmNft ? { tokenType: rawEvmNft.contract.tokenType, totalSupply: rawEvmNft.contract.totalSupply, contractDeployer: rawEvmNft.contract.contractDeployer, deployedBlockNumber: rawEvmNft.contract.deployedBlockNumber, } : undefined, openSeaMetadata: rawEvmNft?.openSeaMetadata ? { collectionSlug: rawEvmNft.openSeaMetadata.collectionSlug, floorPrice: rawEvmNft.openSeaMetadata.floorPrice, safelistRequestStatus: rawEvmNft.openSeaMetadata.safelistRequestStatus, } : undefined, }; return collection; } catch (error) { console.error('getNFTCollection: Error fetching collection:', error); console.error('Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } }