@lunch-money/ethereum-to-lunch-money
Version:
A wrapper around Ethereum APIs for enabling Lunch Money to gather information about a user's wallet.
359 lines • 18.2 kB
JavaScript
"use strict";
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 });
const ethers_1 = require("ethers");
const client_1 = require("./client");
const node_fetch_1 = __importDefault(require("node-fetch"));
// Helper function to wrap promises around provider tests
const wrapPromise = (promise) => promise.then((result) => result, (error) => error);
class EthereumInitializationService {
constructor(integrations, logDebug) {
this.integrations = integrations;
this.logDebug = logDebug;
this.initialized = false;
// Helper function to extract clean error messages from API key tests
this.getCleanErrorMessage = (error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
// Extract just the essential error information
if (errorMessage.includes('401')) {
return 'Authentication failed - invalid API key';
}
else if (errorMessage.includes('403')) {
return 'Access forbidden - check API key permissions';
}
else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
return 'Rate limit exceeded - try again later';
}
else if (errorMessage.includes('network') || errorMessage.includes('connection')) {
return 'Network connection error';
}
else if (errorMessage.includes('timeout')) {
return 'Request timed out';
}
else {
// For other errors, take just the first meaningful part
const firstLine = errorMessage.split('\n')[0];
// Remove verbose details and keep just the core message
if (firstLine.includes('server response')) {
const match = firstLine.match(/server response (\d+)/);
if (match) {
return `Server error ${match[1]}`;
}
}
return firstLine;
}
};
// Define handlers to validate response from Wallet API requests
// Use arrow functions to bind 'this' to the class instance
this.handleMoralisResponse = (response) => __awaiter(this, void 0, void 0, function* () {
if (!response.ok) {
throw new Error(`Moralis API error: ${response.status} ${response.statusText}`);
}
this.logDebug('Moralis API key test: SUCCESS');
return { type: 'API Provider', name: 'Moralis', status: 'SUCCESS', apiKey: process.env.MORALIS_API_KEY };
});
this.handleEtherscanResponse = (response) => __awaiter(this, void 0, void 0, function* () {
const data = yield response.json();
if (data.status === '0' && data.message === 'NOTOK') {
throw new Error(`Etherscan API error: ${data.result}`);
}
console.log('[DEBUG_ETHEREUM] Etherscan API key test: SUCCESS');
return {
type: 'API Provider',
name: 'Etherscan',
status: 'SUCCESS',
apiKey: process.env.ETHERSCAN_API_KEY,
};
});
this.logDebug = logDebug.bind(this);
this.handleError = this.handleError.bind(this);
// TODO: this could us a little more error handling
integrations.ethereum.initialized = false;
this.ethereumIntegration = integrations.ethereum;
}
initialize() {
return __awaiter(this, void 0, void 0, function* () {
if (this.initialized) {
return this.ethereumIntegration;
}
try {
yield this.validateAPIKeys();
if (this.ethereumIntegration.serviceProviderInfo.length > 0) {
// Yay, we got a valid Service Provider Key!
this.ethereumIntegration.walletClient = (0, client_1.createEthereumWalletClient)(this.ethereumIntegration.serviceProviderInfo, this.ethereumIntegration.walletProviderInfo);
this.logDebug('Ethereum initialized with supplied provider(s)');
}
else {
this.ethereumIntegration.walletClient = (0, client_1.createEthereumWalletClient)();
this.logDebug('Ethereum initialized with public provider(s)');
}
this.initialized = true;
this.ethereumIntegration.initialized = true;
}
catch (error) {
const errorMessage = this.getErrorMessage(error);
console.error(`Unexpected error in Ethereum initialization: ${errorMessage}`);
throw new Error(errorMessage);
}
return this.ethereumIntegration;
});
}
validateAPIKeys() {
return __awaiter(this, void 0, void 0, function* () {
try {
this.warnAboutLegacyKeys();
this.logDebug('Starting background testing of private providers and API keys');
// This is a well known test Ethereum wallet address
const testAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const missingKeyResults = [];
let wrappedProviderPromises = [];
// Prepare private provider tests
const privateProviders = this.getPrivateProviders();
if (privateProviders.length === 0) {
this.logDebug('No Ethereum service provider keys (ALCHEMY_API_KEY or INFURA_API_KEY) are set.');
missingKeyResults.push({
type: 'Service Provider',
name: 'Ethereum Service Providers',
status: 'FAILED',
error: 'Missing required Ethereum service provider keys (ALCHEMY_API_KEY or INFURA_API_KEY)',
});
}
else {
wrappedProviderPromises = privateProviders.map((provider) => wrapPromise(this.testProvider(provider, testAddress)));
}
//Prepare API Key Tests
let wrappedApiKeyPromises = [];
const apiKeys = this.getWalletTokenApiKeyTestInfo(testAddress);
if (apiKeys.length === 0) {
missingKeyResults.push({
type: 'API Provider',
name: 'API Keys',
status: 'FAILED',
error: 'Missing required wallet token API keys (ETHERSCAN_API_KEY or MORALIS_API_KEY)',
});
}
else {
wrappedApiKeyPromises = apiKeys.map((apiKeyTestInfo) => wrapPromise(this.testApiKey(apiKeyTestInfo)));
}
// Wrap any existing results in a promise
const missingKeyResultsPromise = Promise.resolve(missingKeyResults);
// Run all tests in parallel, including the results
const allResults = yield Promise.all([
...(yield missingKeyResultsPromise),
...wrappedProviderPromises,
...wrappedApiKeyPromises,
]);
this.processResults(allResults);
}
catch (error) {
const errorMessage = this.getErrorMessage(error);
console.error(`Unexpected error in Ethereum initialization: ${errorMessage}`);
throw new Error(errorMessage);
}
});
}
processResults(allResults) {
// Process Provider results
const successfulProviders = allResults.filter((result) => result.status === 'SUCCESS' && result.type === 'Service Provider');
const failedProviders = allResults.filter((result) => result.status === 'FAILED');
if (successfulProviders.length > 0) {
this.ethereumIntegration.serviceProviderInfo = successfulProviders;
if (failedProviders.length > 0) {
this.logDebug(`Secondary provider validation failed: ${failedProviders.map((p) => `${p.name}: ${p.error}`).join(', ')}`);
if (process.env.DEBUG_ETHEREUM_FAIL_ON_ERROR === 'true') {
this.logDebug('DEBUG_ETHEREUM_FAIL_ON_ERROR is set to true. Exiting...');
process.exit(1);
}
else {
const primaryProvider = this.integrations.ethereum.serviceProviderInfo[0];
this.logDebug(`Continuing with ${primaryProvider.name} as primary provider and no secondary provider.`);
}
}
// }
}
else {
if (failedProviders.length > 0) {
this.logDebug(`Provider validation failed: ${failedProviders.map((p) => `${p.name}: ${p.error}`).join(', ')}`);
if (process.env.DEBUG_ETHEREUM_FAIL_ON_ERROR === 'true') {
this.logDebug('DEBUG_ETHEREUM_FAIL_ON_ERROR is set to true. Exiting...');
process.exit(1);
}
else {
console.log('[DEBUG_ETHEREUM] Will continue with the public provider. THIS IS NOT RECOMMENDED!');
}
}
// Return here since we won't bother with API key tests if the provider tests failed
return;
}
// Process API Key test results
const successfulApiKeys = allResults.filter((result) => result.status === 'SUCCESS' && result.type === 'API Provider');
const failedApiKeys = allResults.filter((result) => result.status === 'FAILED' && result.type === 'API Provider');
if (successfulApiKeys.length === 0) {
if (failedApiKeys.length === 0) {
this.logDebug('No ETHERSCAN or MORALIS wallet API keys were provided, or all API keys failed.');
}
else {
this.logDebug(`API key validation failed: ${failedApiKeys.map((p) => `${p.name}: ${p.error}`).join(', ')}`);
}
if (process.env.DEBUG_ETHEREUM_FAIL_ON_ERROR === 'true') {
this.logDebug('DEBUG_ETHEREUM_FAIL_ON_ERROR is set to true. Exiting...');
process.exit(1);
}
else {
this.logDebug('[DEBUG_ETHEREUM] Will continue without wallet API keys. THIS MAY LEAD TO ETHEREUM SERVICE PROVIDER RATE LIMITING!');
}
}
else {
this.ethereumIntegration.walletProviderInfo = successfulApiKeys;
if (failedApiKeys.length > 0) {
this.logDebug(`API key validation failed: ${failedApiKeys.map((p) => `${p.name}: ${p.error}`).join(', ')}`);
if (process.env.DEBUG_ETHEREUM_FAIL_ON_ERROR === 'true') {
console.error('DEBUG_ETHEREUM_FAIL_ON_ERROR is set to true. Exiting...');
process.exit(1);
}
else {
this.logDebug(`Will continue using the single ${successfulApiKeys[0].name} API key with no secondary API key.`);
}
}
}
}
// Add a utility function for error handling
handleError(providerName, error, type) {
const errorMessage = this.getCleanErrorMessage(error);
this.logDebug(`${providerName} test: FAILED - ${errorMessage}`);
return {
type,
name: providerName,
status: 'FAILED',
error: errorMessage,
};
}
// Check if the provider is valid and create a wallet client if it is
testProvider(providerInfo, testAddress) {
return __awaiter(this, void 0, void 0, function* () {
try {
const balance = yield providerInfo.provider.getBalance(testAddress);
const network = yield providerInfo.provider.getNetwork();
this.logDebug(`${providerInfo.name} API key test: SUCCESS - Network: ${network.name}, Test Balance returned.`);
return {
type: 'Service Provider',
provider: providerInfo.provider,
name: providerInfo.name,
status: 'SUCCESS',
network: network.name,
balance: balance.toString(),
apiKey: providerInfo.apiKey,
};
}
catch (error) {
return this.handleError(providerInfo.name, error, 'Service Provider');
}
});
}
// Check if a wallet token API key is valid
testApiKey(apiKeyTestInfo) {
return __awaiter(this, void 0, void 0, function* () {
try {
let response;
if (apiKeyTestInfo.headers) {
response = yield (0, node_fetch_1.default)(apiKeyTestInfo.url, {
headers: apiKeyTestInfo.headers,
});
}
else {
response = yield (0, node_fetch_1.default)(apiKeyTestInfo.url);
}
return yield apiKeyTestInfo.responseHandler(response);
}
catch (error) {
return this.handleError(apiKeyTestInfo.provider, error, 'API Provider');
}
});
}
// Helper to display masked API Keys in debug messages
maskKey(key) {
if (!key)
return 'undefined';
if (key.length <= 8)
return '*'.repeat(key.length);
return key.substring(0, 4) + '*'.repeat(key.length - 8) + key.substring(key.length - 4);
}
warnAboutLegacyKeys() {
if (process.env.DEBUG_ETHEREUM) {
if (process.env.INFURA_PROJECT_ID || process.env.INFURA_PROJECT_SECRET) {
this.logDebug('[DEBUG_ETHEREUM] Found an Infura API Project ID and/or Secret. Replace this with INFURA_API_KEY.');
if (process.env.DEBUG_ETHEREUM_FAIL_ON_ERROR === 'true') {
this.logDebug('Remove unused INFURA_PROJECT_ID and INFURA_PROJECT_SECRET from your environment variables.');
this.logDebug('DEBUG_ETHEREUM_FAIL_ON_ERROR is set to true. Exiting...');
process.exit(1);
}
}
if (process.env.POCKET_API_KEY) {
this.logDebug('[DEBUG_ETHEREUM] Found a Pocket API key. Pocket is no longer recommended as a service node.');
if (process.env.DEBUG_ETHEREUM_FAIL_ON_ERROR === 'true') {
this.logDebug('Remove unused POCKET_API_KEY from your environment variables.');
this.logDebug('DEBUG_ETHEREUM_FAIL_ON_ERROR is set to true. Exiting...');
process.exit(1);
}
}
}
}
// Helper to get the private providers from the environment variables
getPrivateProviders() {
const providers = [];
if (process.env.ALCHEMY_API_KEY) {
const alchemyProvider = new ethers_1.ethers.AlchemyProvider('mainnet', process.env.ALCHEMY_API_KEY);
providers.push({ name: 'Alchemy', provider: alchemyProvider, apiKey: process.env.ALCHEMY_API_KEY });
this.logDebug(`Found an Alchemy key (masked): ${this.maskKey(process.env.ALCHEMY_API_KEY)}`);
}
if (process.env.INFURA_API_KEY) {
const infuraProvider = new ethers_1.ethers.InfuraProvider('mainnet', process.env.INFURA_API_KEY);
providers.push({ name: 'Infura', provider: infuraProvider, apiKey: process.env.INFURA_API_KEY });
this.logDebug(`Found an Infura key (masked): ${this.maskKey(process.env.INFURA_API_KEY)}`);
}
return providers;
}
// Helper to get the array of wallet token API key tests
getWalletTokenApiKeyTestInfo(testAddress) {
const apiKeyTestInfo = [];
if (process.env.ETHERSCAN_API_KEY || process.env.MORALIS_API_KEY) {
if (process.env.MORALIS_API_KEY && process.env.DEBUG_ETHEREUM) {
this.logDebug(`Found a Moralis key (masked): ${this.maskKey(process.env.MORALIS_API_KEY)}`);
apiKeyTestInfo.push({
provider: 'Moralis',
url: `https://deep-index.moralis.io/api/v2/${testAddress}/balance`,
headers: { 'X-API-Key': process.env.MORALIS_API_KEY },
responseHandler: this.handleMoralisResponse,
});
}
if (process.env.ETHERSCAN_API_KEY && process.env.DEBUG_ETHEREUM) {
this.logDebug(`Found an Etherscan key (masked): ${this.maskKey(process.env.ETHERSCAN_API_KEY)}`);
apiKeyTestInfo.push({
provider: 'Etherscan',
url: `https://api.etherscan.io/api?module=account&action=balance&address=${testAddress}&tag=latest&apikey=${process.env.ETHERSCAN_API_KEY}`,
responseHandler: this.handleEtherscanResponse,
});
}
}
return apiKeyTestInfo;
}
getErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
}
exports.default = EthereumInitializationService;
//# sourceMappingURL=ethereum_init.js.map