UNPKG

@lunch-money/ethereum-to-lunch-money

Version:

A wrapper around Ethereum APIs for enabling Lunch Money to gather information about a user's wallet.

406 lines 21 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadTokenList = exports.createEthereumWalletClient = exports.MoralisProvider = exports.EtherscanProvider = exports.debug = void 0; const ethscan = __importStar(require("@mycrypto/eth-scan")); const ethers_1 = require("ethers"); const _1inch_json_1 = __importDefault(require("../fixtures/1inch.json")); // Use node-fetch for Node.js compatibility const node_fetch_1 = __importDefault(require("node-fetch")); /** * Debug function that logs to console.log if DEBUG_ETHEREUM environment variable is set */ const debug = (...args) => { if (process.env.DEBUG_ETHEREUM) { const timestamp = new Date().toISOString(); console.log(`[DEBUG_ETHEREUM] [${timestamp}]`, ...args); } }; exports.debug = debug; class EtherscanProvider { constructor(apiKey) { this.baseUrl = 'https://api.etherscan.io/v2/api'; this.apiKey = apiKey; } discoverTokensForWallet(address, chainId) { return __awaiter(this, void 0, void 0, function* () { // Use chainId from provider, default to Ethereum mainnet (1) const targetChainId = chainId ? Number(chainId) : 1; const response = yield (0, node_fetch_1.default)(`${this.baseUrl}?chainid=${targetChainId}&module=account&action=tokentx&address=${address}&startblock=0&endblock=99999999&sort=desc&apikey=${this.apiKey}`); const data = yield response.json(); // Handle valid "no transactions" response if (data.status === '0' && data.message === 'No transactions found') { return []; } // Handle API errors if (data.status === '0' && data.message === 'NOTOK') { throw new Error(`Etherscan V2 API error: ${data.result}`); } if (data.status !== '1') { throw new Error(`Etherscan V2 API error: ${data.message}`); } // Extract unique token contract addresses from transfer events const uniqueTokens = new Set(); for (const tx of data.result) { uniqueTokens.add(tx.contractAddress); } return Array.from(uniqueTokens); }); } } exports.EtherscanProvider = EtherscanProvider; class MoralisProvider { constructor(apiKey) { this.baseUrl = 'https://deep-index.moralis.io/api/v2'; this.apiKey = apiKey; } discoverTokensForWallet(address, chainId) { return __awaiter(this, void 0, void 0, function* () { // Map chainId to Moralis chain format (only supports specific enum values) let chainFormat; if (chainId) { switch (Number(chainId)) { case 1: chainFormat = 'eth'; break; // Ethereum mainnet case 137: chainFormat = 'polygon'; break; // Polygon case 56: chainFormat = 'bsc'; break; // BSC case 42161: chainFormat = 'arbitrum'; break; // Arbitrum case 10: chainFormat = 'optimism'; break; // Optimism case 43114: chainFormat = 'avalanche'; break; // Avalanche case 250: chainFormat = 'fantom'; break; // Fantom default: chainFormat = 'eth'; break; // Default to Ethereum } } else { chainFormat = 'eth'; // Default to Ethereum mainnet } const response = yield (0, node_fetch_1.default)(`${this.baseUrl}/${address}/erc20?chain=${chainFormat}`, { headers: { 'X-API-Key': this.apiKey, }, }); if (!response.ok) { const errorData = yield response.json().catch(() => ({})); if (errorData.message && errorData.message.includes('over 2000 tokens')) { throw new Error(`Moralis API error: Wallet has too many tokens (>2000) - ${errorData.message}`); } throw new Error(`Moralis API error: ${response.status} ${response.statusText}`); } const data = yield response.json(); // Extract unique token contract addresses from balance data const uniqueTokens = new Set(); for (const item of data) { if (item.token_address && item.balance !== '0') { uniqueTokens.add(item.token_address); } } return Array.from(uniqueTokens); }); } } exports.MoralisProvider = MoralisProvider; const createEthereumWalletClient = (serviceProviderInfo = [], walletAPIProviderInfo = []) => { let providers = []; let etherscanProvider = null; let moralisProvider = null; try { // Initialize the custom provider(s) for ethscan to use the ethers client correctly. // A custom ethscan provider implementation is needed to map `call` to `send` for ethscan to use the ethers client correctly. // This is a temporary solution until the ethscan library is updated to support ethers v6. if (serviceProviderInfo.length) { providers = Object.freeze(serviceProviderInfo.map((providerInfo) => { if (providerInfo.type === 'Service Provider' && providerInfo.provider) { return Object.assign(Object.assign({}, providerInfo), { customProvider: { send(method, params) { var _a; // Type pulled from: https://github.com/MyCryptoHQ/eth-scan/blob/master/src/providers/provider.ts#L32 const typedParams = params; return (_a = providerInfo.provider) === null || _a === void 0 ? void 0 : _a.call({ to: typedParams[0].to, data: typedParams[0].data, }); }, } }); } else { throw new Error(`Invalid service provider: ${providerInfo.name} passed to createEthereumWalletClient`); } })); // Initialize any wallet API providers for (const walletKeyInfo of walletAPIProviderInfo) { if (walletKeyInfo.type === 'API Provider') { if (walletKeyInfo.name === 'Etherscan' && walletKeyInfo.apiKey) { etherscanProvider = new EtherscanProvider(walletKeyInfo.apiKey); } else if (walletKeyInfo.name === 'Moralis' && walletKeyInfo.apiKey) { moralisProvider = new MoralisProvider(walletKeyInfo.apiKey); } } else { throw new Error(`Invalid wallet API provider: ${walletKeyInfo.name} passed to createEthereumWalletClient`); } } } else { // Create a wallet using the quorum of free service providers const publicProvider = ethers_1.ethers.getDefaultProvider(); providers = Object.freeze([ ...providers, { type: 'Service Provider', name: 'Public Provider', provider: publicProvider, status: 'SUCCESS', customProvider: { send(method, params) { const typedParams = params; return publicProvider === null || publicProvider === void 0 ? void 0 : publicProvider.call({ to: typedParams[0].to, data: typedParams[0].data }); }, }, }, ]); } } catch (error) { throw new Error(`Error creating Ethereum wallet client: ${error instanceof Error ? error.message : String(error)}`); } const internalGetBalances = (client, walletAddress, negligibleBalanceThreshold, provider, customProvider, obscuredWalletAddress) => __awaiter(void 0, void 0, void 0, function* () { let timeoutId; const timeoutDuration = process.env.ETHEREUM_BALANCE_TIMEOUT_MSECS ? parseInt(process.env.ETHEREUM_BALANCE_TIMEOUT_MSECS) : 60000; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`Ethereum connector getBalances timed out after ${timeoutDuration} milliseconds.`)); }, timeoutDuration); }); const result = yield Promise.race([ (() => __awaiter(void 0, void 0, void 0, function* () { try { const weiBalance = yield client.getWeiBalance(walletAddress, provider); const chainId = yield client.getChainId(provider); let filteredTokens = []; try { let discoveredTokens = []; if (moralisProvider || etherscanProvider) { discoveredTokens = yield client.discoverTokensHybrid(walletAddress, chainId); if (discoveredTokens.length > 0) { filteredTokens = yield (0, exports.loadTokenList)().then((tokens) => tokens.filter((t) => discoveredTokens.includes(t.address) && BigInt(t.chainId) === BigInt(chainId))); (0, exports.debug)(`Wallet ${obscuredWalletAddress}: Filtered to ${filteredTokens.length} tokens on chain ${chainId}`); } else { (0, exports.debug)(`Wallet ${obscuredWalletAddress}: No tokens discovered, using empty token list`); } } else { (0, exports.debug)(`Wallet ${obscuredWalletAddress}: No discovery APIs available, using full token list`); filteredTokens = (yield (0, exports.loadTokenList)()).filter((t) => BigInt(t.chainId) === BigInt(chainId)); } } catch (error) { (0, exports.debug)(`Wallet ${obscuredWalletAddress}: Token discovery failed, falling back to full token list:`, error); filteredTokens = (yield (0, exports.loadTokenList)()).filter((t) => BigInt(t.chainId) === BigInt(chainId)); } (0, exports.debug)(`Wallet ${obscuredWalletAddress}: Checking balances for ETH and ${filteredTokens.length} other tokens`); const map = yield ethscan.getTokensBalance(customProvider, walletAddress, filteredTokens.map((t) => t.address)); return { weiBalance, chainId, map, filteredTokens }; } finally { if (timeoutId) { clearTimeout(timeoutId); } } }))(), timeout, ]); const { weiBalance, chainId, map, filteredTokens } = result; (0, exports.debug)('ethers.getTokensBalance returned for wallet address:', obscuredWalletAddress); const balances = Object.entries(map) .map(([address, balance]) => { const token = filteredTokens.find((t) => t.address === address); if (!token) { throw new Error(`Token ${address} not found in discovered token list for chainId ${chainId}`); } return { asset: token.symbol, undivisedAmount: balance, decimals: token.decimals, }; }) .concat({ asset: 'ETH', undivisedAmount: weiBalance, decimals: 18 }) .map(({ asset, undivisedAmount, decimals }) => ({ asset, amount: ethers_1.ethers.formatUnits(undivisedAmount, decimals) })) .filter((b) => ethers_1.ethers.parseUnits(b.amount, 18) > negligibleBalanceThreshold) .map((b) => ({ asset: b.asset, amount: String(b.amount) })) .sort((a, b) => a.asset.localeCompare(b.asset)); (0, exports.debug)(`Returning from getBalances for ${obscuredWalletAddress}:`, balances); const balanceResult = { providerName: 'wallet_ethereum', balances, }; return balanceResult; }); return { getChainId(provider) { return __awaiter(this, void 0, void 0, function* () { return (yield provider.getNetwork()).chainId; }); }, getWeiBalance(walletAddress, provider) { return __awaiter(this, void 0, void 0, function* () { return yield provider.getBalance(walletAddress); }); }, getBalances(walletAddress, negligibleBalanceThreshold) { return __awaiter(this, void 0, void 0, function* () { const obscuredWalletAddress = `0x..${walletAddress.slice(-6)}`; (0, exports.debug)('getBalances called for wallet address:', obscuredWalletAddress); let result = { providerName: 'wallet_ethereum', balances: [], }; if (providers.length === 0) { throw new Error('No Ethereum providers available'); } let providerIndex = 0; let provider = providers[0].provider; let customProvider = providers[0].customProvider; let providerName = providers[0].name; (0, exports.debug)(`Attempting lookup using primary provider: ${providerName}`); while (result.balances.length === 0) { try { result = yield internalGetBalances(this, walletAddress, negligibleBalanceThreshold, provider, customProvider, obscuredWalletAddress); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); (0, exports.debug)(`Error getting balances using provider ${providerName}: ${errorMessage}`); if (errorMessage.includes('unconfigured name') || errorMessage.includes('bad address checksum')) { throw new Error(`Invalid wallet address. Account needs to be relinked.`); } else if (providerIndex === providers.length - 1) { throw error; } providerIndex = providerIndex + 1; providerName = providers[providerIndex].name; provider = providers[providerIndex].provider; customProvider = providers[providerIndex].customProvider; (0, exports.debug)(`Retrying lookup using provider: ${providerName}`); } } return result; }); }, getTokensBalance(walletAddress, tokenContractAddresses, providerInfo) { return __awaiter(this, void 0, void 0, function* () { if (providerInfo.customProvider) { return ethscan.getTokensBalance(providerInfo.customProvider, walletAddress, tokenContractAddresses); } else { throw new Error(`No ethscan compatible provider found for ${providerInfo.name}`); } }); }, discoverTokensHybrid(walletAddress, chainId) { return __awaiter(this, void 0, void 0, function* () { const allTokens = new Set(); // 1. Moralis Discovery (PRIMARY) if (moralisProvider) { try { const moralisTokens = yield moralisProvider.discoverTokensForWallet(walletAddress, chainId); (0, exports.debug)(`Moralis: Discovered ${moralisTokens.length} tokens`); moralisTokens.forEach((t) => allTokens.add(t)); } catch (error) { (0, exports.debug)(`Moralis: Failed - ${error instanceof Error ? error.message : String(error)}`); // 2. Etherscan Discovery (FALLBACK ONLY) if (etherscanProvider) { try { const etherscanTokens = yield etherscanProvider.discoverTokensForWallet(walletAddress, chainId); (0, exports.debug)(`Etherscan (fallback): Discovered ${etherscanTokens.length} tokens`); etherscanTokens.forEach((t) => allTokens.add(t)); } catch (fallbackError) { (0, exports.debug)(`Etherscan (fallback): Failed - ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`); } } else { (0, exports.debug)('Etherscan: No API key provided for fallback'); } } } else { (0, exports.debug)('Moralis: No API key provided'); // 3. Etherscan Discovery (PRIMARY if no Moralis) if (etherscanProvider) { try { const etherscanTokens = yield etherscanProvider.discoverTokensForWallet(walletAddress, chainId); (0, exports.debug)(`Etherscan: Discovered ${etherscanTokens.length} tokens`); etherscanTokens.forEach((t) => allTokens.add(t)); } catch (error) { (0, exports.debug)(`Etherscan: Failed - ${error instanceof Error ? error.message : String(error)}`); } } else { (0, exports.debug)('Etherscan: No API key provided'); } } return Array.from(allTokens); }); }, }; }; exports.createEthereumWalletClient = createEthereumWalletClient; const loadTokenList = () => __awaiter(void 0, void 0, void 0, function* () { return _1inch_json_1.default.tokens; }); exports.loadTokenList = loadTokenList; //# sourceMappingURL=client.js.map