From ebc15e7e9b50b6e6cb59a7c311f0ee7a5023650c Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Mon, 20 Nov 2023 18:00:11 +0530 Subject: [PATCH 01/28] Pull balances|Add opt block height /w fetching native balance --- src/balances/evm/index.ts | 5 +- src/chain-wallet-manager.ts | 35 ++++- src/index.ts | 1 + src/price-assistant/scheduled-price-feed.ts | 5 +- src/wallet-manager.ts | 99 ++++++++++---- src/wallets/base-wallet.ts | 140 +++++++++++++++----- src/wallets/evm/index.ts | 4 +- src/wallets/index.ts | 1 + 8 files changed, 222 insertions(+), 68 deletions(-) diff --git a/src/balances/evm/index.ts b/src/balances/evm/index.ts index 0feabb5..c83ad4d 100644 --- a/src/balances/evm/index.ts +++ b/src/balances/evm/index.ts @@ -42,9 +42,10 @@ export async function pullEvmTokenBalance( export async function pullEvmNativeBalance( provider: ethers.providers.JsonRpcProvider, address: string, + blockHeight?: number, ): Promise{ - const weiAmount = await provider.getBalance(address); - + const weiAmount = await provider.getBalance(address, blockHeight); + return { isNative: true, rawBalance: weiAmount.toString(), diff --git a/src/chain-wallet-manager.ts b/src/chain-wallet-manager.ts index 624c763..f80f8ef 100644 --- a/src/chain-wallet-manager.ts +++ b/src/chain-wallet-manager.ts @@ -16,7 +16,7 @@ import { import { EVMProvider, EVMWallet } from "./wallets/evm"; import { SolanaProvider, SolanaWallet } from "./wallets/solana"; import { SuiProvider, SuiWallet } from "./wallets/sui"; -import { PriceFeed, WalletPriceFeedConfig, WalletRebalancingConfig } from "./wallet-manager"; +import { PriceFeed, WalletBalanceConfig, WalletPriceFeedConfig, WalletRebalancingConfig } from "./wallet-manager"; import { ScheduledPriceFeed } from "./price-assistant/scheduled-price-feed"; import { OnDemandPriceFeed } from "./price-assistant/ondemand-price-feed"; @@ -37,6 +37,7 @@ export type ChainWalletManagerOptions = { maxGasPrice?: number; gasLimit?: number; }; + balanceConfig: WalletBalanceConfig; priceFeedConfig: WalletPriceFeedConfig; balancePollInterval?: number; walletOptions?: WalletOptions; @@ -266,9 +267,20 @@ export class ChainWalletManager { this.logger.info(`Starting PriceFeed for chain: ${this.options.chainName}`); this.priceFeed?.start(); } - this.interval = setInterval(async () => { - await this.refreshBalances(); - }, this.options.balancePollInterval); + + if (this.options.balanceConfig?.enabled) { + if (this.options.balanceConfig?.scheduled?.enabled) { + this.interval = setInterval(async () => { + await this.refreshBalances(); + }, this.options.balanceConfig?.scheduled?.interval ?? this.options.balancePollInterval); + } else { + // no op: Don't poll balances, fetch on demand instead + } + } else { + this.interval = setInterval(async () => { + await this.refreshBalances(); + }, this.options.balancePollInterval); + } if (this.options.rebalance.enabled) { this.logger.info( @@ -411,4 +423,19 @@ export class ChainWalletManager { this.availableWalletsByChainName[chainName], ); } + + public getBlockHeight () { + return this.walletToolbox.getBlockHeight(); + } + /** Pull balances on demand */ + public async pullBalances () { + const balances = await this.walletToolbox.pullBalances(); + return this.mapBalances(balances); + } + + /** Pull balances on demand with block height */ + public async pullBalancesAtBlockHeight() { + const balances = await this.walletToolbox.pullBalancesAtBlockHeight(); + return this.mapBalances(balances); + } } diff --git a/src/index.ts b/src/index.ts index 1a08a3f..4669340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { export type WalletBalancesByAddress = WBBA; export type WalletInterface = WI; export type {ChainName} from "./wallets"; +export type {Environment} from './price-assistant/supported-tokens.config'; export {isEvmChain, isSolanaChain, isSuiChain} from './wallets'; diff --git a/src/price-assistant/scheduled-price-feed.ts b/src/price-assistant/scheduled-price-feed.ts index db72b8e..c6bfc85 100644 --- a/src/price-assistant/scheduled-price-feed.ts +++ b/src/price-assistant/scheduled-price-feed.ts @@ -3,6 +3,7 @@ import { Logger } from "winston"; import { PriceFeed, TokenPriceData } from "./price-feed"; import { TokenInfo, WalletPriceFeedConfig } from "../wallet-manager"; import { getCoingeckoPrices } from "./helper"; +import { mapConcurrent } from "../utils"; /** * ScheduledPriceFeed is a price feed that periodically fetches token prices from coingecko @@ -28,9 +29,9 @@ export class ScheduledPriceFeed extends PriceFeed { public async pullTokenPrices(tokens: string[]): Promise { const tokenPrices = {} as TokenPriceData; - for await (const token of tokens) { + await mapConcurrent(tokens, async (token) => { tokenPrices[token] = await this.get(token); - } + }) return tokenPrices; } diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index a733693..7e31b8b 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -2,7 +2,7 @@ import { EventEmitter } from "stream"; import { z } from "zod"; import winston from "winston"; -import { createLogger } from "./utils"; +import { createLogger, mapConcurrent } from "./utils"; import { PrometheusExporter } from "./prometheus-exporter"; import { ChainWalletManager, @@ -38,25 +38,37 @@ const TokenInfoSchema = z.object({ chainId: z.number(), coingeckoId: CoinGeckoIdsSchema, symbol: z.string().optional(), -}) +}); -export type TokenInfo = z.infer< - typeof TokenInfoSchema -> +export type TokenInfo = z.infer; export const WalletPriceFeedConfigSchema = z.object({ enabled: z.boolean(), supportedTokens: z.array(TokenInfoSchema), pricePrecision: z.number().optional(), - scheduled: z.object({ + scheduled: z + .object({ + enabled: z.boolean().default(false), + interval: z.number().optional(), + }) + .optional(), +}); + +export const WalletBalanceConfigSchema = z + .object({ enabled: z.boolean().default(false), - interval: z.number().optional(), - }).optional(), -}) + scheduled: z + .object({ + enabled: z.boolean().default(false), + interval: z.number().optional(), + }) + .optional(), + }) + .optional(); -export type WalletPriceFeedConfig = z.infer< - typeof WalletPriceFeedConfigSchema ->; +export type WalletBalanceConfig = z.infer; + +export type WalletPriceFeedConfig = z.infer; export type WalletRebalancingConfig = z.infer< typeof WalletRebalancingConfigSchema @@ -68,7 +80,7 @@ export const WalletManagerChainConfigSchema = z.object({ chainConfig: z.any().optional(), rebalance: WalletRebalancingConfigSchema.optional(), wallets: z.array(WalletConfigSchema), - priceFeedConfig: WalletPriceFeedConfigSchema.optional() + priceFeedConfig: WalletPriceFeedConfigSchema.optional(), }); export const WalletManagerConfigSchema = z.record( @@ -172,7 +184,7 @@ export class WalletManager { const chainManager = new ChainWalletManager( chainManagerConfig, - chainConfig.wallets + chainConfig.wallets, ); chainManager.on("error", error => { @@ -183,7 +195,6 @@ export class WalletManager { chainManager.on( "balances", (balances: WalletBalance[], previousBalances: WalletBalance[]) => { - this.logger.verbose(`Balances updated for ${chainName} (${network})`); this.exporter?.updateBalances(chainName, network, balances); @@ -223,11 +234,19 @@ export class WalletManager { chainManager.on("active-wallets-count", (chainName, network, count) => { this.exporter?.updateActiveWallets(chainName, network, count); - }) + }); - chainManager.on("wallets-lock-period", (chainName, network, walletAddress, lockTime) => { - this.exporter?.updateWalletsLockPeriod(chainName, network, walletAddress, lockTime); - }) + chainManager.on( + "wallets-lock-period", + (chainName, network, walletAddress, lockTime) => { + this.exporter?.updateWalletsLockPeriod( + chainName, + network, + walletAddress, + lockTime, + ); + }, + ); this.managers[chainName] = chainManager; @@ -301,16 +320,50 @@ export class WalletManager { } } - public getAllBalances(): Record { + private async balanceHandlerMapper( + method: "getBalances" | "pullBalancesAtBlockHeight" | "pullBalances", + ) { const balances: Record = {}; - for (const [chainName, manager] of Object.entries(this.managers)) { - balances[chainName] = manager.getBalances(); - } + await mapConcurrent( + Object.entries(this.managers), + async ([chainName, manager]) => { + const balancesByChain = await manager[method](); + balances[chainName] = balancesByChain; + }, + ); return balances; } + public async getAllBalances(): Promise< + Record + > { + return await this.balanceHandlerMapper("getBalances"); + } + + public getBlockHeight(chainName: ChainName): Promise { + const manager = this.managers[chainName]; + if (!manager) + throw new Error(`No wallets configured for chain: ${chainName}`); + + return manager.getBlockHeight(); + } + + // PullBalances doesn't need balances to be refreshed in the background + public async pullBalances(): Promise< + Record + > { + return await this.balanceHandlerMapper("pullBalances"); + } + + // pullBalancesAtBlockHeight doesn't need balances to be refreshed in the background + public async pullBalancesAtBlockHeight(): Promise< + Record + > { + return await this.balanceHandlerMapper("pullBalancesAtBlockHeight"); + } + public getChainBalances(chainName: ChainName): WalletBalancesByAddress { const manager = this.managers[chainName]; if (!manager) diff --git a/src/wallets/base-wallet.ts b/src/wallets/base-wallet.ts index 49e4b6a..aa73b16 100644 --- a/src/wallets/base-wallet.ts +++ b/src/wallets/base-wallet.ts @@ -1,20 +1,20 @@ -import winston from 'winston'; +import winston from "winston"; import { WalletBalance, TokenBalance, WalletOptions, WalletConfig } from "."; import { LocalWalletPool, WalletPool } from "./wallet-pool"; -import { createLogger } from '../utils'; -import { Wallets } from '../chain-wallet-manager'; +import { createLogger } from "../utils"; +import { Wallets } from "../chain-wallet-manager"; export type BaseWalletOptions = { logger: winston.Logger; failOnInvalidTokens: boolean; -} +}; export type TransferRecepit = { - transactionHash: string, - gasUsed: string, - gasPrice: string, - formattedCost: string, -} + transactionHash: string; + gasUsed: string; + gasPrice: string; + formattedCost: string; +}; const DEFAULT_WALLET_ACQUIRE_TIMEOUT = 5000; @@ -42,7 +42,10 @@ export abstract class WalletToolbox { // Should parse tokens received from the user. // The tokens returned should be a list of token addresses used by the chain client // Example: ["DAI", "USDC"] => ["0x00000000", "0x00000001"]; - protected abstract parseTokensConfig(tokens: string[], failOnInvalidTokens: boolean): string[]; + protected abstract parseTokensConfig( + tokens: string[], + failOnInvalidTokens: boolean, + ): string[]; // Should instantiate provider for the chain // calculate data which could be re-utilized (for example token's local addresses, symbol and decimals in evm chains) @@ -51,16 +54,28 @@ export abstract class WalletToolbox { protected abstract getAddressFromPrivateKey(privateKey: string): string; // Should return balances for a native address in the chain - abstract pullNativeBalance(address: string): Promise; + abstract pullNativeBalance( + address: string, + blockHeight?: number, + ): Promise; // Should return balances for tokens in the list for the address specified - abstract pullTokenBalances(address: string, tokens: string[]): Promise; + abstract pullTokenBalances( + address: string, + tokens: string[], + ): Promise; - protected abstract transferNativeBalance(privateKey: string, targetAddress: string, amount: number, maxGasPrice?: number, gasLimit?: number): Promise; + protected abstract transferNativeBalance( + privateKey: string, + targetAddress: string, + amount: number, + maxGasPrice?: number, + gasLimit?: number, + ): Promise; - protected abstract getRawWallet (privateKey: string): Promise; + protected abstract getRawWallet(privateKey: string): Promise; - public abstract getGasPrice (): Promise; + public abstract getGasPrice(): Promise; public abstract getBlockHeight(): Promise; @@ -77,7 +92,7 @@ export abstract class WalletToolbox { this.validateOptions(options); const wallets = {} as Record; - + for (const raw of rawConfig) { const config = this.buildWalletConfig(raw, options.failOnInvalidTokens); this.validateConfig(config, options.failOnInvalidTokens); @@ -92,13 +107,18 @@ export abstract class WalletToolbox { public async pullBalances( isRebalancingEnabled = false, minBalanceThreshold?: number, + blockHeight?: number, ): Promise { if (!this.warm) { - this.logger.debug(`Warming up wallet toolbox for chain ${this.chainName}...`); + this.logger.debug( + `Warming up wallet toolbox for chain ${this.chainName}...`, + ); try { await this.warmup(); } catch (error) { - this.logger.error(`Error warming up wallet toolbox for chain (${this.chainName}): ${error}`); + this.logger.error( + `Error warming up wallet toolbox for chain (${this.chainName}): ${error}`, + ); return []; } this.warm = true; @@ -114,7 +134,7 @@ export abstract class WalletToolbox { let nativeBalance: WalletBalance; try { - nativeBalance = await this.pullNativeBalance(address); + nativeBalance = await this.pullNativeBalance(address, blockHeight); this.addOrDiscardWalletIfRequired( isRebalancingEnabled, @@ -123,11 +143,15 @@ export abstract class WalletToolbox { minBalanceThreshold ?? 0, ); - balances.push(nativeBalance); + balances.push({...nativeBalance, blockHeight}); - this.logger.debug(`Balances for ${address} pulled: ${JSON.stringify(nativeBalance)}`) + this.logger.debug( + `Balances for ${address} pulled: ${JSON.stringify(nativeBalance)}`, + ); } catch (error) { - this.logger.error(`Error pulling native balance for ${address}: ${error}`); + this.logger.error( + `Error pulling native balance for ${address}: ${error}`, + ); continue; } @@ -136,32 +160,53 @@ export abstract class WalletToolbox { continue; } - this.logger.verbose(`Pulling tokens (${tokens.join(', ')}) for ${address}...`); + this.logger.verbose( + `Pulling tokens (${tokens.join(", ")}) for ${address}...`, + ); try { const tokenBalances = await this.pullTokenBalances(address, tokens); - this.logger.debug(`Token balances for ${address} pulled: ${JSON.stringify(tokenBalances)}`); + this.logger.debug( + `Token balances for ${address} pulled: ${JSON.stringify( + tokenBalances, + )}`, + ); nativeBalance.tokens.push(...tokenBalances); } catch (error) { - this.logger.error(`Error pulling token balances for ${address}: ${error}`); + this.logger.error( + `Error pulling token balances for ${address}: ${error}`, + ); } } return balances; } + public async pullBalancesAtBlockHeight( + ) { + const blockHeight = await this.getBlockHeight(); + return this.pullBalances( + false, + undefined, + blockHeight, + ); + } + public async acquire(address?: string, acquireTimeout?: number) { const timeout = acquireTimeout || DEFAULT_WALLET_ACQUIRE_TIMEOUT; // this.grpcClient.acquireWallet(address); - const walletAddress = await this.walletPool.blockAndAcquire(timeout, address); + const walletAddress = await this.walletPool.blockAndAcquire( + timeout, + address, + ); const privateKey = this.wallets[walletAddress].privateKey; return { address: walletAddress, - rawWallet: await this.getRawWallet(privateKey!) + rawWallet: await this.getRawWallet(privateKey!), }; } @@ -169,18 +214,33 @@ export abstract class WalletToolbox { await this.walletPool.release(address); } - public async transferBalance(sourceAddress: string, targetAddress: string, amount: number, maxGasPrice?: number, gasLimit?: number) { + public async transferBalance( + sourceAddress: string, + targetAddress: string, + amount: number, + maxGasPrice?: number, + gasLimit?: number, + ) { const privateKey = this.wallets[sourceAddress].privateKey; if (!privateKey) { throw new Error(`Private key for ${sourceAddress} not found`); } - await this.walletPool.blockAndAcquire(DEFAULT_WALLET_ACQUIRE_TIMEOUT, sourceAddress); + await this.walletPool.blockAndAcquire( + DEFAULT_WALLET_ACQUIRE_TIMEOUT, + sourceAddress, + ); let receipt; try { - receipt = await this.transferNativeBalance(privateKey, targetAddress, amount, maxGasPrice, gasLimit); + receipt = await this.transferNativeBalance( + privateKey, + targetAddress, + amount, + maxGasPrice, + gasLimit, + ); } catch (error) { await this.walletPool.release(sourceAddress); throw error; @@ -193,12 +253,16 @@ export abstract class WalletToolbox { private validateConfig(rawConfig: any, failOnInvalidTokens: boolean) { if (!rawConfig.address && !rawConfig.privateKey) - throw new Error(`Invalid config for chain: ${this.chainName}: Missing address`); + throw new Error( + `Invalid config for chain: ${this.chainName}: Missing address`, + ); if (failOnInvalidTokens && rawConfig.tokens?.length) { rawConfig.tokens.forEach((token: any) => { if (!this.validateTokenAddress(token)) { - throw new Error(`Token not supported for ${this.chainName}[${this.network}]: ${token}`) + throw new Error( + `Token not supported for ${this.chainName}[${this.network}]: ${token}`, + ); } }); } @@ -206,12 +270,18 @@ export abstract class WalletToolbox { return true; } - private buildWalletConfig(rawConfig: any, failOnInvalidTokens: boolean): WalletData { + private buildWalletConfig( + rawConfig: any, + failOnInvalidTokens: boolean, + ): WalletData { const privateKey = rawConfig.privateKey; - const address = rawConfig.address || this.getAddressFromPrivateKey(privateKey); + const address = + rawConfig.address || this.getAddressFromPrivateKey(privateKey); - const tokens = rawConfig.tokens ? this.parseTokensConfig(rawConfig.tokens, failOnInvalidTokens) : []; + const tokens = rawConfig.tokens + ? this.parseTokensConfig(rawConfig.tokens, failOnInvalidTokens) + : []; return { address, privateKey, tokens }; } diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 2ceba1e..84f1e2f 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -226,8 +226,8 @@ export class EvmWalletToolbox extends WalletToolbox { this.logger.debug(`EVM token data: ${JSON.stringify(this.tokenData)}`); } - public async pullNativeBalance(address: string): Promise { - const balance = await pullEvmNativeBalance(this.provider, address); + public async pullNativeBalance(address: string, blockHeight?: number): Promise { + const balance = await pullEvmNativeBalance(this.provider, address, blockHeight); const formattedBalance = ethers.utils.formatEther(balance.rawBalance); return { diff --git a/src/wallets/index.ts b/src/wallets/index.ts index 88208f5..df17d5b 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -59,6 +59,7 @@ export type Balance = { rawBalance: string; formattedBalance: string; usd?: bigint; + blockHeight?: number; }; export type TokenBalance = Balance & { From ec75a58c3194cebba28e7c37f79d8eed59ccb28c Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Mon, 20 Nov 2023 18:42:31 +0530 Subject: [PATCH 02/28] Add commitment in solana getBlockHeight --- src/balances/solana.ts | 6 ++++-- src/balances/sui.ts | 2 ++ src/wallets/solana/index.ts | 4 ++-- src/wallets/solana/solana.config.ts | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/balances/solana.ts b/src/balances/solana.ts index f7ec395..29deeb0 100644 --- a/src/balances/solana.ts +++ b/src/balances/solana.ts @@ -1,12 +1,14 @@ import bs58 from 'bs58'; import { Connection, PublicKey, Keypair } from "@solana/web3.js"; import { Balance } from "./index"; +import { SOLANA_DEFAULT_COMMITMENT } from '../wallets/solana/solana.config'; export async function pullSolanaNativeBalance( connection: Connection, - address: string + address: string, ): Promise { - const lamports = await connection.getBalance(new PublicKey(address)) + // solana web3.js doesn't support passing exact slot(block number) only minSlot, while fetching balance + const lamports = await connection.getBalance(new PublicKey(address), SOLANA_DEFAULT_COMMITMENT) return { isNative: true, diff --git a/src/balances/sui.ts b/src/balances/sui.ts index e6b89d1..a9587af 100644 --- a/src/balances/sui.ts +++ b/src/balances/sui.ts @@ -18,6 +18,8 @@ export interface SuiTransactionDetails { export async function pullSuiNativeBalance(conn: Connection, address: string): Promise { const provider = new JsonRpcProvider(conn); + // mysten SDK doesn't support passing checkpoint (block number) to getBalance + // https://github.com/MystenLabs/sui/issues/14137 const rawBalance = await provider.getBalance({ owner: address }); return { diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index 05d3b1b..5a1d88d 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -6,7 +6,7 @@ import { RecentPrioritizationFees, } from "@solana/web3.js"; import { decode } from "bs58"; -import { SOLANA, SOLANA_CHAIN_CONFIG, SolanaNetworks } from "./solana.config"; +import { SOLANA, SOLANA_CHAIN_CONFIG, SOLANA_DEFAULT_COMMITMENT, SolanaNetworks } from "./solana.config"; import { PYTHNET, PYTHNET_CHAIN_CONFIG } from "./pythnet.config"; import { BaseWalletOptions, @@ -304,6 +304,6 @@ export class SolanaWalletToolbox extends WalletToolbox { } public async getBlockHeight(): Promise { - return this.connection.getBlockHeight(); + return this.connection.getBlockHeight(SOLANA_DEFAULT_COMMITMENT); } } diff --git a/src/wallets/solana/solana.config.ts b/src/wallets/solana/solana.config.ts index 0b626f3..c52349c 100644 --- a/src/wallets/solana/solana.config.ts +++ b/src/wallets/solana/solana.config.ts @@ -4,6 +4,7 @@ const SOLANA_TESTNET = 'solana-devnet'; const SOLANA_CURRENCY_SYMBOL = 'SOL'; export const SOLANA = 'solana'; +export const SOLANA_DEFAULT_COMMITMENT = 'finalized'; export const SOLANA_NETWORKS = { [SOLANA_MAINNET]: 1, From 1890c8cb0742b0429d288cfece07be9ebe3d84bc Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Mon, 20 Nov 2023 19:48:02 +0530 Subject: [PATCH 03/28] Refactor appending token balances in usd --- src/balances/sui.ts | 10 +++++- src/wallets/evm/index.ts | 22 +++++-------- src/wallets/solana/index.ts | 30 ++++++----------- src/wallets/sui/index.ts | 64 +++++++++++++++---------------------- 4 files changed, 52 insertions(+), 74 deletions(-) diff --git a/src/balances/sui.ts b/src/balances/sui.ts index a9587af..d62a5b1 100644 --- a/src/balances/sui.ts +++ b/src/balances/sui.ts @@ -56,7 +56,15 @@ export async function pullSuiTokenData( export async function pullSuiTokenBalances( conn: Connection, address: string -): Promise { +): Promise<{ + coinType: string; + coinObjectCount: number; + totalBalance: string; + lockedBalance: { + number?: number | undefined; + epochId?: number | undefined; + }; +}[]> { const provider = new JsonRpcProvider(conn); return provider.getAllBalances({ owner: address }); diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 84f1e2f..5c93a68 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -243,7 +243,9 @@ export class EvmWalletToolbox extends WalletToolbox { address: string, tokens: string[], ): Promise { - const tokenBalances = await mapConcurrent( + // Pull prices in USD for all the tokens in single network call + await this.priceFeed?.pullTokenPrices(tokens); + return mapConcurrent( tokens, async tokenAddress => { const tokenData = this.tokenData[tokenAddress]; @@ -256,6 +258,10 @@ export class EvmWalletToolbox extends WalletToolbox { balance.rawBalance, tokenData.decimals, ); + + // Add USD price to each token balance + const tokenPrice = await this.priceFeed?.getKey(tokenAddress); + const tokenBalanceInUsd = tokenPrice ? Number(formattedBalance) * Number(tokenPrice) : undefined; return { ...balance, @@ -263,23 +269,11 @@ export class EvmWalletToolbox extends WalletToolbox { tokenAddress, formattedBalance, symbol: tokenData.symbol, + usd: tokenBalanceInUsd, }; }, this.options.tokenPollConcurrency, ); - - // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(tokens); - - // Add USD price to each token balance - return mapConcurrent(tokenBalances, async balance => { - const tokenPrice = await this.priceFeed?.getKey(balance.tokenAddress); - const tokenBalanceInUsd = tokenPrice ? Number(balance.formattedBalance) * Number(tokenPrice) : undefined; - return { - ...balance, - usd: tokenBalanceInUsd, - }; - }, this.options.tokenPollConcurrency); } protected async transferNativeBalance( diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index 5a1d88d..9d93c0a 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -147,8 +147,11 @@ export class SolanaWalletToolbox extends WalletToolbox { ); }); + // Pull prices in USD for all the tokens in single network call + await this.priceFeed?.pullTokenPrices(tokens); + // Assuming that tokens[] is actually an array of mint account addresses. - const balances = await mapConcurrent( + return mapConcurrent( tokens, async token => { const tokenData = this.tokenData[token]; @@ -164,31 +167,18 @@ export class SolanaWalletToolbox extends WalletToolbox { 10 ** tokenData.decimals ).toString(); + // Add USD price to each token balance + const tokenPrice = await this.priceFeed?.getKey(token); + const tokenBalanceInUsd = tokenPrice + ? BigInt(formattedBalance) * tokenPrice + : undefined; + return { isNative: false, rawBalance: tokenBalance.toString(), address, formattedBalance, symbol: tokenKnownSymbol ?? "unknown", - }; - }, - this.options.tokenPollConcurrency, - ) as TokenBalance[]; - - // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(tokens); - - // Add USD price to each token balance - return mapConcurrent( - balances, - async balance => { - const tokenPrice = await this.priceFeed?.getKey(balance.tokenAddress!); - const tokenBalanceInUsd = tokenPrice - ? BigInt(balance.formattedBalance) * tokenPrice - : undefined; - - return { - ...balance, usd: tokenBalanceInUsd, }; }, diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index 639eb5d..188e60a 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -180,57 +180,43 @@ export class SuiWalletToolbox extends WalletToolbox { ): Promise { const uniqueTokens = [...new Set(tokens)]; const allBalances = await pullSuiTokenBalances(this.connection, address); + // Pull prices in USD for all the tokens in single network call + await this.priceFeed?.pullTokenPrices(tokens); - const tokenBalances = await mapConcurrent(uniqueTokens, async (tokenAddress: string) => { + return mapConcurrent(uniqueTokens, async (tokenAddress: string) => { const tokenData = this.tokenData[tokenAddress]; const symbol: string = tokenData?.symbol ? tokenData.symbol : ""; - for (const balance of allBalances) { - if (balance.coinType === tokenData.address) { - - const formattedBalance = formatFixed( - balance.totalBalance, - tokenData?.decimals ? tokenData.decimals : 9 - ); - - return { - tokenAddress, - address, - isNative: false, - rawBalance: balance.totalBalance, - formattedBalance, - symbol, - }; + const balance = allBalances.find(balance => balance.coinType === tokenData.address); + if (!balance) { + return { + tokenAddress, + address, + isNative: false, + rawBalance: "0", + formattedBalance: "0", + symbol, } } + const formattedBalance = formatFixed( + balance.totalBalance, + tokenData?.decimals ? tokenData.decimals : 9 + ); + + const tokenPrice = await this.priceFeed?.getKey(tokenAddress); + const tokenBalanceInUsd = tokenPrice ? BigInt(formattedBalance) * tokenPrice : undefined; + return { tokenAddress, address, isNative: false, - rawBalance: "0", - formattedBalance: "0", + rawBalance: balance.totalBalance, + formattedBalance, symbol, - } - }, this.options.tokenPollConcurrency) as TokenBalance[]; - - // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(tokens); - - // Add USD price to each token balance - return mapConcurrent( - tokenBalances, - async balance => { - const tokenPrice = await this.priceFeed?.getKey(balance.tokenAddress!); - const tokenBalanceInUsd = tokenPrice ? BigInt(balance.formattedBalance) * tokenPrice : undefined; - - return { - ...balance, - usd: tokenBalanceInUsd, - }; - }, - this.options.tokenPollConcurrency, - ); + usd: tokenBalanceInUsd, + }; + }, this.options.tokenPollConcurrency); } protected async transferNativeBalance( From dccaca9d2b9de7fb9e17b2490a032016dd7038c4 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Mon, 20 Nov 2023 22:09:05 +0530 Subject: [PATCH 04/28] Fix minor issue | fix type issue --- examples/wallet-manager.ts | 43 ++++++-- src/chain-wallet-manager.ts | 8 +- src/grpc/client.ts | 205 +++++++++++++++++++++++++----------- src/i-wallet-manager.ts | 7 +- src/wallet-manager.ts | 2 + 5 files changed, 187 insertions(+), 78 deletions(-) diff --git a/examples/wallet-manager.ts b/examples/wallet-manager.ts index 77ec29b..007ef0b 100644 --- a/examples/wallet-manager.ts +++ b/examples/wallet-manager.ts @@ -18,6 +18,12 @@ const allChainWallets: WalletManagerFullConfig['config'] = { tokens: ["WBTC"] } ], + walletBalanceConfig: { + enabled: true, + scheduled: { + enabled: false, + } + }, priceFeedConfig: { scheduled: { enabled: true @@ -41,6 +47,12 @@ const allChainWallets: WalletManagerFullConfig['config'] = { wallets: [ { address: "6VnfVsLdLwNuuCmooLTziQ99PFXZ5vc3yyqyb9tMDhhw", tokens: ['usdc'] }, ], + walletBalanceConfig: { + enabled: true, + scheduled: { + enabled: false, + } + }, }, sui: { rebalance: { @@ -62,6 +74,12 @@ const allChainWallets: WalletManagerFullConfig['config'] = { tokens: ['USDC', 'USDT'] }, ], + walletBalanceConfig: { + enabled: true, + scheduled: { + enabled: false, + } + }, priceFeedConfig: { supportedTokens: [{ chainId: 21, @@ -88,7 +106,13 @@ const allChainWallets: WalletManagerFullConfig['config'] = { address: "0x8d0d970225597085A59ADCcd7032113226C0419d", tokens: [] } - ] + ], + walletBalanceConfig: { + enabled: true, + scheduled: { + enabled: false, + } + }, } } @@ -107,11 +131,12 @@ export const manager = buildWalletManager({ } }); - -// Note: Below code needs wallet's private key to be set in config abopve for aquiring wallet -// manager.withWallet('ethereum', async (wallet) => { -// console.log('Address', wallet.address); -// console.log('Block height', wallet.walletToolbox.getBlockHeight()); -// console.log('Native balances', await wallet.walletToolbox.pullNativeBalance(wallet.address)); -// console.log('Token balances', await wallet.walletToolbox.pullTokenBalances(wallet.address, ['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'])); -// }) +(async () => { + try { + console.time('balances') + const balances = await manager.pullBalancesAtBlockHeight(); + console.timeLog('balances', JSON.stringify(balances)) + } catch (err) { + console.error('Failed to pullBalancesAtBlockHeight', err); + } +})(); diff --git a/src/chain-wallet-manager.ts b/src/chain-wallet-manager.ts index f80f8ef..2ca2c18 100644 --- a/src/chain-wallet-manager.ts +++ b/src/chain-wallet-manager.ts @@ -37,7 +37,7 @@ export type ChainWalletManagerOptions = { maxGasPrice?: number; gasLimit?: number; }; - balanceConfig: WalletBalanceConfig; + walletBalanceConfig: WalletBalanceConfig; priceFeedConfig: WalletPriceFeedConfig; balancePollInterval?: number; walletOptions?: WalletOptions; @@ -268,11 +268,11 @@ export class ChainWalletManager { this.priceFeed?.start(); } - if (this.options.balanceConfig?.enabled) { - if (this.options.balanceConfig?.scheduled?.enabled) { + if (this.options.walletBalanceConfig?.enabled) { + if (this.options.walletBalanceConfig?.scheduled?.enabled) { this.interval = setInterval(async () => { await this.refreshBalances(); - }, this.options.balanceConfig?.scheduled?.interval ?? this.options.balancePollInterval); + }, this.options.walletBalanceConfig?.scheduled?.interval ?? this.options.balancePollInterval); } else { // no op: Don't poll balances, fetch on demand instead } diff --git a/src/grpc/client.ts b/src/grpc/client.ts index bc4fdff..112156a 100644 --- a/src/grpc/client.ts +++ b/src/grpc/client.ts @@ -1,70 +1,149 @@ -import {ChainWalletManager, WalletExecuteOptions, WithWalletExecutor} from "../chain-wallet-manager"; -import {ChainName, isChain} from "../wallets"; -import {getDefaultNetwork, WalletManagerConfig, WalletManagerOptions} from "../wallet-manager"; -import winston from "winston"; -import {createLogger} from "../utils"; -import {IClientWalletManager} from "../i-wallet-manager"; -import {createChannel, createClient} from "nice-grpc"; import { - WalletManagerGRPCServiceDefinition -} from "./out/wallet-manager-grpc-service"; + ChainWalletManager, + WalletBalancesByAddress, + WalletExecuteOptions, + WithWalletExecutor, +} from "../chain-wallet-manager"; +import { ChainName, isChain } from "../wallets"; +import { + getDefaultNetwork, + WalletManagerConfig, + WalletManagerOptions, +} from "../wallet-manager"; +import winston from "winston"; +import { createLogger, mapConcurrent } from "../utils"; +import { IClientWalletManager } from "../i-wallet-manager"; +import { createChannel, createClient } from "nice-grpc"; +import { WalletManagerGRPCServiceDefinition } from "./out/wallet-manager-grpc-service"; export class ClientWalletManager implements IClientWalletManager { - private walletManagerGRPCChannel; - private walletManagerGRPCClient; - private managers; - - protected logger: winston.Logger; - - constructor(private host: string, private port: number, config: WalletManagerConfig, options?: WalletManagerOptions) { - this.logger = createLogger(options?.logger, options?.logLevel, { label: 'WalletManager' }); - this.managers = {} as Record; - - this.walletManagerGRPCChannel = createChannel(`${host}:${port}`) - this.walletManagerGRPCClient = createClient(WalletManagerGRPCServiceDefinition, this.walletManagerGRPCChannel) - - // Constructing a record of manager for the only purpose of extracting the appropriate provider and private key - // to bundle together with the lock acquired from the grpc service. - for (const [chainName, chainConfig] of Object.entries(config)) { - if (!isChain(chainName)) throw new Error(`Invalid chain name: ${chainName}`); - const network = chainConfig.network || getDefaultNetwork(chainName); - - const chainManagerConfig = { - network, - chainName, - logger: this.logger, - rebalance: {...chainConfig.rebalance, enabled: false}, - walletOptions: chainConfig.chainConfig, - }; - - this.managers[chainName] = new ChainWalletManager(chainManagerConfig, chainConfig.wallets); - } + private walletManagerGRPCChannel; + private walletManagerGRPCClient; + private managers; + + protected logger: winston.Logger; + + constructor( + private host: string, + private port: number, + config: WalletManagerConfig, + options?: WalletManagerOptions, + ) { + this.logger = createLogger(options?.logger, options?.logLevel, { + label: "WalletManager", + }); + this.managers = {} as Record; + + this.walletManagerGRPCChannel = createChannel(`${host}:${port}`); + this.walletManagerGRPCClient = createClient( + WalletManagerGRPCServiceDefinition, + this.walletManagerGRPCChannel, + ); + + // Constructing a record of manager for the only purpose of extracting the appropriate provider and private key + // to bundle together with the lock acquired from the grpc service. + for (const [chainName, chainConfig] of Object.entries(config)) { + if (!isChain(chainName)) + throw new Error(`Invalid chain name: ${chainName}`); + const network = chainConfig.network || getDefaultNetwork(chainName); + + const chainManagerConfig = { + network, + chainName, + logger: this.logger, + rebalance: { ...chainConfig.rebalance, enabled: false }, + walletOptions: chainConfig.chainConfig, + }; + + this.managers[chainName] = new ChainWalletManager( + chainManagerConfig, + chainConfig.wallets, + ); } + } + + public async withWallet( + chainName: ChainName, + fn: WithWalletExecutor, + opts?: WalletExecuteOptions, + ): Promise { + const chainManager = this.managers[chainName]; + if (!chainManager) + throw new Error(`No wallets configured for chain: ${chainName}`); - public async withWallet(chainName: ChainName, fn: WithWalletExecutor, opts?: WalletExecuteOptions): Promise { - const chainManager = this.managers[chainName]; - if (!chainManager) throw new Error(`No wallets configured for chain: ${chainName}`); - - const { address: acquiredAddress } = await this.walletManagerGRPCClient.acquireLock({chainName, address: opts?.address, leaseTimeout: opts?.leaseTimeout, acquireTimeout: opts?.waitToAcquireTimeout }) - - // FIXME - // Dirty solution. We are doing as little work as possible to get the same expected WalletInterface after - // locking. - // Unfortunately this is not only inefficient (we lock 2 times) but also nonsense because, if we successfully - // locked a particular address in the wallet manager service, it's impossible that we have it locked here. - // Nevertheless, this should allow us to just make it work right now. - const acquiredWallet = await this.managers[chainName].acquireLock({...opts, address: acquiredAddress}); - - try { - return await fn(acquiredWallet); - } catch (error) { - this.logger.error('The workflow function failed to run within the context of the acquired wallet.', error); - throw error; - } finally { - await Promise.all([ - this.walletManagerGRPCClient.releaseLock({chainName, address: acquiredAddress}), - this.managers[chainName].releaseLock(acquiredAddress) - ]); - } + const { address: acquiredAddress } = + await this.walletManagerGRPCClient.acquireLock({ + chainName, + address: opts?.address, + leaseTimeout: opts?.leaseTimeout, + acquireTimeout: opts?.waitToAcquireTimeout, + }); + + // FIXME + // Dirty solution. We are doing as little work as possible to get the same expected WalletInterface after + // locking. + // Unfortunately this is not only inefficient (we lock 2 times) but also nonsense because, if we successfully + // locked a particular address in the wallet manager service, it's impossible that we have it locked here. + // Nevertheless, this should allow us to just make it work right now. + const acquiredWallet = await this.managers[chainName].acquireLock({ + ...opts, + address: acquiredAddress, + }); + + try { + return await fn(acquiredWallet); + } catch (error) { + this.logger.error( + "The workflow function failed to run within the context of the acquired wallet.", + error, + ); + throw error; + } finally { + await Promise.all([ + this.walletManagerGRPCClient.releaseLock({ + chainName, + address: acquiredAddress, + }), + this.managers[chainName].releaseLock(acquiredAddress), + ]); } + } + + public getBlockHeight(chainName: ChainName): Promise { + const manager = this.managers[chainName]; + if (!manager) + throw new Error(`No wallets configured for chain: ${chainName}`); + + return manager.getBlockHeight(); + } + + private async balanceHandlerMapper( + method: "getBalances" | "pullBalancesAtBlockHeight" | "pullBalances", + ) { + const balances: Record = {}; + + await mapConcurrent( + Object.entries(this.managers), + async ([chainName, manager]) => { + const balancesByChain = await manager[method](); + balances[chainName] = balancesByChain; + }, + ); + + return balances; + } + + // PullBalances doesn't need balances to be refreshed in the background + public async pullBalances(): Promise< + Record + > { + return await this.balanceHandlerMapper("pullBalances"); + } + + // pullBalancesAtBlockHeight doesn't need balances to be refreshed in the background + public async pullBalancesAtBlockHeight(): Promise< + Record + > { + return await this.balanceHandlerMapper("pullBalancesAtBlockHeight"); + } } diff --git a/src/i-wallet-manager.ts b/src/i-wallet-manager.ts index 55b1d6f..b88ba5d 100644 --- a/src/i-wallet-manager.ts +++ b/src/i-wallet-manager.ts @@ -1,4 +1,4 @@ -import {Providers, WalletExecuteOptions, WalletInterface, Wallets, WithWalletExecutor} from "./chain-wallet-manager"; +import {Providers, WalletBalancesByAddress, WalletExecuteOptions, WalletInterface, Wallets, WithWalletExecutor} from "./chain-wallet-manager"; import { ChainName } from "./wallets"; /* @@ -16,7 +16,10 @@ import { ChainName } from "./wallets"; */ interface IWMContextManagedLocks { - withWallet

(chainName: ChainName, fn: WithWalletExecutor, opts?: WalletExecuteOptions): Promise + withWallet

(chainName: ChainName, fn: WithWalletExecutor, opts?: WalletExecuteOptions): Promise; + pullBalances: () => Promise>; + pullBalancesAtBlockHeight: () => Promise>; + getBlockHeight: (chainName: ChainName) => Promise; } interface IWMBareLocks { acquireLock(chainName: ChainName, opts?: WalletExecuteOptions): Promise diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index 7e31b8b..4e58ced 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -79,6 +79,7 @@ export const WalletManagerChainConfigSchema = z.object({ // FIXME: This should be a zod schema chainConfig: z.any().optional(), rebalance: WalletRebalancingConfigSchema.optional(), + walletBalanceConfig: WalletBalanceConfigSchema.optional(), wallets: z.array(WalletConfigSchema), priceFeedConfig: WalletPriceFeedConfigSchema.optional(), }); @@ -178,6 +179,7 @@ export class WalletManager { rebalance: chainConfig.rebalance, walletOptions: chainConfig.chainConfig, priceFeedConfig: chainConfig.priceFeedConfig, + walletBalanceConfig: chainConfig.walletBalanceConfig, balancePollInterval: options?.balancePollInterval, failOnInvalidTokens: options?.failOnInvalidTokens ?? true, }; From 65fb05287244d1ee681df71a8173063cdb271249 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Tue, 21 Nov 2023 14:29:50 +0530 Subject: [PATCH 05/28] Bump with beta version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f989a1..11b41f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.21", + "version": "0.2.22-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.21", + "version": "0.2.22-beta.0", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index d32c857..217e50f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.21", + "version": "0.2.22-beta.0", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts", From fa02da409cf024ca39aca7245315a860d7537032 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Tue, 21 Nov 2023 17:15:42 +0530 Subject: [PATCH 06/28] Make sure to compute tokenBalance in bigint --- examples/wallet-manager.ts | 2 +- src/wallets/base-wallet.ts | 3 ++- src/wallets/evm/index.ts | 5 +++-- src/wallets/solana/index.ts | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/wallet-manager.ts b/examples/wallet-manager.ts index 007ef0b..9a24fb8 100644 --- a/examples/wallet-manager.ts +++ b/examples/wallet-manager.ts @@ -135,7 +135,7 @@ export const manager = buildWalletManager({ try { console.time('balances') const balances = await manager.pullBalancesAtBlockHeight(); - console.timeLog('balances', JSON.stringify(balances)) + console.timeLog('balances', balances) } catch (err) { console.error('Failed to pullBalancesAtBlockHeight', err); } diff --git a/src/wallets/base-wallet.ts b/src/wallets/base-wallet.ts index aa73b16..d7d777b 100644 --- a/src/wallets/base-wallet.ts +++ b/src/wallets/base-wallet.ts @@ -169,7 +169,8 @@ export abstract class WalletToolbox { this.logger.debug( `Token balances for ${address} pulled: ${JSON.stringify( - tokenBalances, + // usd is a big number, so we need to convert it to string + tokenBalances.map(balance => ({...balance, usd: balance?.usd?.toString()})), )}`, ); diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 5c93a68..cba4449 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -261,7 +261,8 @@ export class EvmWalletToolbox extends WalletToolbox { // Add USD price to each token balance const tokenPrice = await this.priceFeed?.getKey(tokenAddress); - const tokenBalanceInUsd = tokenPrice ? Number(formattedBalance) * Number(tokenPrice) : undefined; + // Wrapping with Number below to avoid Bigint issue converting string with decimals, eg: "20.0" + const tokenBalanceInUsd = tokenPrice ? Number(formattedBalance) * Number(tokenPrice): undefined; return { ...balance, @@ -269,7 +270,7 @@ export class EvmWalletToolbox extends WalletToolbox { tokenAddress, formattedBalance, symbol: tokenData.symbol, - usd: tokenBalanceInUsd, + usd: tokenBalanceInUsd ? BigInt(tokenBalanceInUsd): undefined }; }, this.options.tokenPollConcurrency, diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index 9d93c0a..c42772a 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -165,7 +165,7 @@ export class SolanaWalletToolbox extends WalletToolbox { const formattedBalance = ( tokenBalance / 10 ** tokenData.decimals - ).toString(); + ) // Add USD price to each token balance const tokenPrice = await this.priceFeed?.getKey(token); @@ -177,7 +177,7 @@ export class SolanaWalletToolbox extends WalletToolbox { isNative: false, rawBalance: tokenBalance.toString(), address, - formattedBalance, + formattedBalance: formattedBalance.toString(), symbol: tokenKnownSymbol ?? "unknown", usd: tokenBalanceInUsd, }; From f26d81ec2c0ad6dbcd577f7fe3648e06e0c6fd01 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Thu, 23 Nov 2023 21:14:46 +0530 Subject: [PATCH 07/28] Refactor | Add usd balance for native tokens --- src/price-assistant/helper.ts | 39 ++-- src/price-assistant/ondemand-price-feed.ts | 88 ++++---- src/price-assistant/price-feed.ts | 13 +- src/price-assistant/scheduled-price-feed.ts | 37 +-- .../supported-tokens.config.ts | 210 ++++++++++++++++++ src/wallet-manager.ts | 16 +- src/wallets/evm/index.ts | 22 +- src/wallets/index.ts | 3 +- 8 files changed, 333 insertions(+), 95 deletions(-) diff --git a/src/price-assistant/helper.ts b/src/price-assistant/helper.ts index 2089191..9cc9e36 100644 --- a/src/price-assistant/helper.ts +++ b/src/price-assistant/helper.ts @@ -4,26 +4,35 @@ import { Logger } from "winston"; import { CoinGeckoIds } from "./supported-tokens.config"; export type CoinGeckoPriceDict = Partial<{ - [k in CoinGeckoIds]: { - usd: number; - }; + [k in CoinGeckoIds]: { + usd: number; + }; }>; /** * @param tokens - array of coingecko ids for tokens */ -export async function getCoingeckoPrices(tokens: string[] | string, logger: Logger): Promise { - tokens = typeof tokens === "string" ? tokens : tokens.join(","); - const response = await axios.get(`https://api.coingecko.com/api/v3/simple/price?ids=${tokens}&vs_currencies=usd`, { +export async function getCoingeckoPrices( + tokens: string[] | string, + logger: Logger, +): Promise { + const tokensToProcess = + typeof tokens === "string" ? tokens : tokens.join(","); + const response = await axios.get( + `https://api.coingecko.com/api/v3/simple/price?ids=${tokensToProcess}&vs_currencies=usd`, + { headers: { Accept: "application/json", }, - }); - - if (response.status != 200) { - logger.warn(`Failed to get CoinGecko Prices. Response: ${inspect(response)}`); - throw new Error(`HTTP status != 200. Original Status: ${response.status}`); - } - - return response.data; - } \ No newline at end of file + }, + ); + + if (response.status != 200) { + logger.warn( + `Failed to get CoinGecko Prices. Response: ${inspect(response)}`, + ); + throw new Error(`HTTP status != 200. Original Status: ${response.status}`); + } + + return response.data; +} diff --git a/src/price-assistant/ondemand-price-feed.ts b/src/price-assistant/ondemand-price-feed.ts index 4ac95bf..37b6966 100644 --- a/src/price-assistant/ondemand-price-feed.ts +++ b/src/price-assistant/ondemand-price-feed.ts @@ -4,32 +4,33 @@ import { TimeLimitedCache } from "../utils"; import { TokenInfo, WalletPriceFeedConfig } from "../wallet-manager"; import { getCoingeckoPrices } from "./helper"; import { CoinGeckoIds } from "./supported-tokens.config"; -import { PriceFeed, TokenPriceData } from "./price-feed"; +import { PriceFeed } from "./price-feed"; -const DEFAULT_PRICE_PRECISION = 8; const DEFAULT_TOKEN_PRICE_RETENSION_TIME = 5 * 1000; // 5 seconds /** * OnDemandPriceFeed is a price feed that fetches token prices from coingecko on-demand */ export class OnDemandPriceFeed extends PriceFeed{ // here cache key is tokenContractAddress - private cache = new TimeLimitedCache(); + private cache = new TimeLimitedCache(); supportedTokens: TokenInfo[]; tokenPriceGauge?: Gauge; - protected pricePrecision: number; - protected logger: Logger; + private tokenContractToCoingeckoId: Record = {}; constructor( priceAssistantConfig: WalletPriceFeedConfig, logger: Logger, registry?: Registry, ) { - super("ONDEMAND_TOKEN_PRICE", logger, registry, undefined, priceAssistantConfig.pricePrecision) - const { supportedTokens, pricePrecision } = priceAssistantConfig; + super("ONDEMAND_TOKEN_PRICE", logger, registry, undefined) + const { supportedTokens } = priceAssistantConfig; this.supportedTokens = supportedTokens; - this.pricePrecision = pricePrecision || DEFAULT_PRICE_PRECISION; - this.logger = logger; - + + this.tokenContractToCoingeckoId = supportedTokens.reduce((acc, token) => { + acc[token.tokenContract] = token.coingeckoId as CoinGeckoIds; + return acc; + }, {} as Record); + if (registry) { this.tokenPriceGauge = new Gauge({ name: "token_usd_price", @@ -48,60 +49,51 @@ export class OnDemandPriceFeed extends PriceFeed{ // no op } - async update () { - // no op + public getCoinGeckoId (tokenContract: string): CoinGeckoIds | undefined { + return this.tokenContractToCoingeckoId[tokenContract]; } - protected async get (tokenContract: string): Promise { - const cachedPrice = this.cache.get(tokenContract); - if (cachedPrice) { - return cachedPrice; - } - const tokenPrices = await this.pullTokenPrices([tokenContract]); - return tokenPrices[tokenContract]; + protected get (coingeckoId: string): bigint | undefined { + return this.cache.get(coingeckoId as CoinGeckoIds); } - public async pullTokenPrices(tokens: string[]): Promise { + async pullTokenPrices() { + return this.update(); + } + + async update () { const coingekoTokens = []; - const priceDict = {} as TokenPriceData; - for (const token of tokens) { - const supportedToken = this.supportedTokens.find((supportedToken) => supportedToken.tokenContract === token); - if (!supportedToken) { - this.logger.error(`Token ${token} not supported`); - throw new Error(`Token ${token} not supported`); - } + for (const token of this.supportedTokens) { + const { coingeckoId } = token; // Check if we already have the price for this token - const cachedPrice = this.cache.get(token); + const cachedPrice = this.cache.get(coingeckoId); if (cachedPrice) { - priceDict[token] = cachedPrice continue; } - coingekoTokens.push(supportedToken); + coingekoTokens.push(token); + } + + if (coingekoTokens.length === 0) { + // All the cached tokens price are already available and valid + return; } const coingekoTokenIds = coingekoTokens.map(token => token.coingeckoId) - // If we don't have the price, fetch it from an external API const coingeckoData = await getCoingeckoPrices(coingekoTokenIds, this.logger); + for (const token of this.supportedTokens) { + const { coingeckoId, symbol } = token; - for (const token of coingekoTokens) { - const {symbol, coingeckoId, tokenContract} = token; + if (!(coingeckoId in coingeckoData)) { + this.logger.warn(`Token ${symbol} (coingecko: ${coingeckoId}) not found in coingecko response data`); + continue; + } - if (!(coingeckoId in coingeckoData)) { - this.logger.warn(`coingecko: ${coingeckoId} not found in coingecko response data`); - continue; - } - - const tokenPrice = coingeckoData?.[coingeckoId as CoinGeckoIds]?.usd; - if (tokenPrice) { - const preciseTokenPrice = BigInt(Math.round(tokenPrice * 10 ** this.pricePrecision)); - // Token Price is stored by token contract address - this.cache.set(tokenContract, preciseTokenPrice, DEFAULT_TOKEN_PRICE_RETENSION_TIME); - priceDict[tokenContract] = preciseTokenPrice; - this.tokenPriceGauge?.labels({ symbol }).set(Number(tokenPrice)); - } + const tokenPrice = coingeckoData?.[coingeckoId]?.usd; + if (tokenPrice) { + this.cache.set(coingeckoId, BigInt(tokenPrice), DEFAULT_TOKEN_PRICE_RETENSION_TIME); + this.tokenPriceGauge?.labels({ symbol }).set(Number(tokenPrice)); + } } - - return priceDict; } } diff --git a/src/price-assistant/price-feed.ts b/src/price-assistant/price-feed.ts index ec3d943..31faebb 100644 --- a/src/price-assistant/price-feed.ts +++ b/src/price-assistant/price-feed.ts @@ -3,7 +3,6 @@ import { Logger } from "winston"; import { printError } from "../utils"; const DEFAULT_FEED_INTERVAL = 10_000; -const DEFAULT_PRICE_PRECISION = 8; export type TokenPriceData = Partial>; @@ -13,23 +12,21 @@ export abstract class PriceFeed { private locked: boolean; private runIntervalMs: number; private metrics?: BasePriceFeedMetrics; - protected pricePrecision: number; protected logger: Logger; - constructor(name: string, logger: Logger, registry?: Registry, runIntervalMs?: number, pricePrecision?: number) { + constructor(name: string, logger: Logger, registry?: Registry, runIntervalMs?: number) { this.name = name; this.logger = logger; this.locked = false; this.runIntervalMs = runIntervalMs || DEFAULT_FEED_INTERVAL; if (registry) this.metrics = this.initMetrics(registry); - this.pricePrecision = pricePrecision || DEFAULT_PRICE_PRECISION; } protected abstract update(): Promise; - protected abstract get(key: K): Promise; + protected abstract get(key: K): V; - public abstract pullTokenPrices (tokens: string[]): Promise; + public abstract pullTokenPrices (): Promise; public start(): void { this.interval = setInterval(() => this.run(), this.runIntervalMs); @@ -40,9 +37,9 @@ export abstract class PriceFeed { clearInterval(this.interval); } - public async getKey(key: K): Promise { + public getKey(key: K): V { const result = this.get(key); - if (result === undefined || result === null) throw new Error(`Key Not Found: ${key}`); + if (result === undefined || result === null) this.logger.error(`PriceFeed Key Not Found: ${key}`); return result; } diff --git a/src/price-assistant/scheduled-price-feed.ts b/src/price-assistant/scheduled-price-feed.ts index c6bfc85..9f38205 100644 --- a/src/price-assistant/scheduled-price-feed.ts +++ b/src/price-assistant/scheduled-price-feed.ts @@ -3,7 +3,8 @@ import { Logger } from "winston"; import { PriceFeed, TokenPriceData } from "./price-feed"; import { TokenInfo, WalletPriceFeedConfig } from "../wallet-manager"; import { getCoingeckoPrices } from "./helper"; -import { mapConcurrent } from "../utils"; +import { inspect } from "util"; +import { CoinGeckoIds } from "./supported-tokens.config"; /** * ScheduledPriceFeed is a price feed that periodically fetches token prices from coingecko @@ -12,11 +13,18 @@ export class ScheduledPriceFeed extends PriceFeed { private data = {} as TokenPriceData; supportedTokens: TokenInfo[]; tokenPriceGauge?: Gauge; + private tokenContractToCoingeckoId: Record = {}; constructor(priceFeedConfig: WalletPriceFeedConfig, logger: Logger, registry?: Registry) { - const {scheduled, supportedTokens, pricePrecision} = priceFeedConfig; - super("SCHEDULED_TOKEN_PRICE", logger, registry, scheduled?.interval, pricePrecision); + const {scheduled, supportedTokens} = priceFeedConfig; + super("SCHEDULED_TOKEN_PRICE", logger, registry, scheduled?.interval); this.supportedTokens = supportedTokens; + + this.tokenContractToCoingeckoId = supportedTokens.reduce((acc, token) => { + acc[token.tokenContract] = token.coingeckoId as CoinGeckoIds; + return acc; + }, {} as Record); + if (registry) { this.tokenPriceGauge = new Gauge({ name: "token_usd_price", @@ -27,19 +35,19 @@ export class ScheduledPriceFeed extends PriceFeed { } } - public async pullTokenPrices(tokens: string[]): Promise { - const tokenPrices = {} as TokenPriceData; - await mapConcurrent(tokens, async (token) => { - tokenPrices[token] = await this.get(token); - }) - return tokenPrices; + public getCoinGeckoId (tokenContract: string): CoinGeckoIds | undefined { + return this.tokenContractToCoingeckoId[tokenContract]; + } + + async pullTokenPrices () { + // no op } async update() { const coingekoTokenIds = this.supportedTokens.map((token) => token.coingeckoId); const coingeckoData = await getCoingeckoPrices(coingekoTokenIds, this.logger); for (const token of this.supportedTokens) { - const { coingeckoId, symbol, tokenContract } = token; + const { coingeckoId, symbol } = token; if (!(coingeckoId in coingeckoData)) { this.logger.warn(`Token ${symbol} (coingecko: ${coingeckoId}) not found in coingecko response data`); @@ -48,16 +56,15 @@ export class ScheduledPriceFeed extends PriceFeed { const tokenPrice = coingeckoData?.[coingeckoId]?.usd; if (tokenPrice) { - // Token Price is stored by token contract address - this.data[tokenContract] = BigInt(Math.round(tokenPrice * 10 ** this.pricePrecision)); + this.data[coingeckoId] = BigInt(tokenPrice); this.tokenPriceGauge?.labels({ symbol }).set(Number(tokenPrice)); } } - // this.logger.debug(`Updated price feed token prices: ${inspect(this.data)}`); + this.logger.debug(`Updated price feed token prices: ${inspect(this.data)}`); } - protected async get(tokenContract: string): Promise { - return this.data[tokenContract]; + protected get(coingeckoId: string): bigint | undefined { + return this.data[coingeckoId]; } } diff --git a/src/price-assistant/supported-tokens.config.ts b/src/price-assistant/supported-tokens.config.ts index df2e308..2762cf6 100644 --- a/src/price-assistant/supported-tokens.config.ts +++ b/src/price-assistant/supported-tokens.config.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { ChainName } from "../wallets"; export declare enum Environment { MAINNET = "mainnet", @@ -21,6 +22,215 @@ export const CoinGeckoIdsSchema = z z.literal("tether"), z.literal("wrapped-bitcoin"), z.literal("sui"), + z.literal("arbitrum"), + z.literal("optimism"), + z.literal("klay-token"), + z.literal("base"), + z.literal("pyth-network") ]); export type CoinGeckoIds = z.infer; + +export interface TokenInfo { + chainId: number; + coingeckoId: CoinGeckoIds; + symbol: string; + tokenContract: string; +} + +export const coinGeckoIdByChainName = { + "solana": "solana", + "ethereum": "ethereum", + "bsc": "binancecoin", + "polygon": "matic-network", + "avalanche": "avalanche-2", + "fantom": "fantom", + "celo": "celo", + "moonbeam": "moonbeam", + "sui": "sui", + "arbitrum": "arbitrum", + "optimism": "optimism", + "base": "base", + "klaytn": "klay-token", + "pythnet": "pyth-network" +} as const satisfies Record; + +const mainnetTokens = [ + { + chainId: 1, + coingeckoId: "solana", + symbol: "WSOL", + tokenContract: "069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f00000000001", + }, + { + chainId: 2, + coingeckoId: "ethereum", + symbol: "WETH", + tokenContract: "000000000000000000000000C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + }, + { + chainId: 4, + coingeckoId: "binancecoin", + symbol: "WBNB", + tokenContract: "000000000000000000000000bb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + }, + { + chainId: 5, + coingeckoId: "matic-network", + symbol: "WMATIC", + tokenContract: "0000000000000000000000000d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + }, + { + chainId: 6, + coingeckoId: "avalanche-2", + symbol: "WAVAX", + tokenContract: "000000000000000000000000B31f66AA3C1e785363F0875A1B74E27b85FD66c7", + }, + { + chainId: 10, + coingeckoId: "fantom", + symbol: "WFTM", + tokenContract: "00000000000000000000000021be370D5312f44cB42ce377BC9b8a0cEF1A4C83", + }, + { + chainId: 13, + coingeckoId: "klay-token", + symbol: "WKLAY", + tokenContract: "", + }, + { + chainId: 14, + coingeckoId: "celo", + symbol: "WCELO", + tokenContract: "000000000000000000000000471EcE3750Da237f93B8E339c536989b8978a438", + }, + { + chainId: 16, + coingeckoId: "moonbeam", + symbol: "WGLMR", + tokenContract: "000000000000000000000000Acc15dC74880C9944775448304B263D191c6077F", + }, + { + chainId: 21, + coingeckoId: "sui", + symbol: "WSUI", + tokenContract: "9258181f5ceac8dbffb7030890243caed69a9599d2886d957a9cb7656af3bdb3", + }, + { + chainId: 23, + coingeckoId: "arbitrum", + symbol: "WARB", + tokenContract: "0x912CE59144191C1204E64559FE8253a0e49E6548", + }, + { + chainId: 24, + coingeckoId: "optimism", + symbol: "WOP", + tokenContract: "0x4200000000000000000000000000000000000042", + }, + { + chainId: 26, + coingeckoId: "pyth-network", + symbol: "WPYTH", + tokenContract: "", + }, + { + chainId: 30, + coingeckoId: "base", + symbol: "WBASE", + tokenContract: "0x07150e919B4De5fD6a63DE1F9384828396f25fDC", + }, +] satisfies TokenInfo[]; + +const testnetTokens = [ + { + chainId: 1, + coingeckoId: "solana", + symbol: "SOL", + tokenContract: "", + }, + { + chainId: 2, + coingeckoId: "ethereum", + symbol: "ETH", + tokenContract: "000000000000000000000000B4FBF271143F4FBf7B91A5ded31805e42b2208d6", + }, + { + chainId: 4, + coingeckoId: "binancecoin", + symbol: "BNB", + tokenContract: "000000000000000000000000ae13d989daC2f0dEbFf460aC112a837C89BAa7cd", + }, + { + chainId: 5, + coingeckoId: "matic-network", + symbol: "MATIC", + tokenContract: "0000000000000000000000009c3C9283D3e44854697Cd22D3Faa240Cfb032889", + }, + { + chainId: 6, + coingeckoId: "avalanche-2", + symbol: "AVAX", + tokenContract: "000000000000000000000000d00ae08403B9bbb9124bB305C09058E32C39A48c", + }, + { + chainId: 10, + coingeckoId: "fantom", + symbol: "FTM", + tokenContract: "000000000000000000000000f1277d1Ed8AD466beddF92ef448A132661956621", + }, + { + chainId: 13, + coingeckoId: "klay-token", + symbol: "KLAY", + tokenContract: "", + }, + { + chainId: 14, + coingeckoId: "celo", + symbol: "CELO", + tokenContract: "000000000000000000000000F194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9", + }, + { + chainId: 16, + coingeckoId: "moonbeam", + symbol: "GLMR", + tokenContract: "000000000000000000000000D909178CC99d318e4D46e7E66a972955859670E1", + }, + { + chainId: 21, + coingeckoId: "sui", + symbol: "SUI", + tokenContract: "587c29de216efd4219573e08a1f6964d4fa7cb714518c2c8a0f29abfa264327d", + }, + { + chainId: 23, + coingeckoId: "arbitrum", + symbol: "ARB", + tokenContract: "0xF861378B543525ae0C47d33C90C954Dc774Ac1F9", + }, + { + chainId: 24, + coingeckoId: "optimism", + symbol: "OP", + tokenContract: "0x4200000000000000000000000000000000000042", + }, + { + chainId: 26, + coingeckoId: "pyth-network", + symbol: "PYTH", + tokenContract: "", + }, + { + chainId: 30, + coingeckoId: "base", + symbol: "BASE", + tokenContract: "", + }, +] satisfies TokenInfo[]; + +export const supportedTokensByEnv: Record = { + [Environment.MAINNET]: mainnetTokens, + [Environment.TESTNET]: testnetTokens, + [Environment.DEVNET]: [], +}; \ No newline at end of file diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index 4e58ced..158b7a0 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -20,7 +20,7 @@ import { } from "./wallets"; import { TransferRecepit } from "./wallets/base-wallet"; import { RebalanceInstruction } from "./rebalance-strategies"; -import { CoinGeckoIdsSchema } from "./price-assistant/supported-tokens.config"; +import { CoinGeckoIdsSchema, Environment, supportedTokensByEnv } from "./price-assistant/supported-tokens.config"; import { ScheduledPriceFeed } from "./price-assistant/scheduled-price-feed"; import { OnDemandPriceFeed } from "./price-assistant/ondemand-price-feed"; @@ -45,7 +45,6 @@ export type TokenInfo = z.infer; export const WalletPriceFeedConfigSchema = z.object({ enabled: z.boolean(), supportedTokens: z.array(TokenInfoSchema), - pricePrecision: z.number().optional(), scheduled: z .object({ enabled: z.boolean().default(false), @@ -172,6 +171,19 @@ export class WalletManager { } const network = chainConfig.network || getDefaultNetwork(chainName); + // Inject native token into price feed config, if enabled + if (chainConfig.priceFeedConfig?.enabled) { + const {supportedTokens} = chainConfig.priceFeedConfig; + const uniqueChainIds = [...new Set(supportedTokens.map(token => token.chainId))]; + const nativeTokens = supportedTokensByEnv[network as Environment]; + for (const chainId of uniqueChainIds) { + const nativeTokensByChainId = nativeTokens.filter(token => token.chainId === chainId); + if (nativeTokensByChainId.length > 0) { + supportedTokens.push(...nativeTokensByChainId); + } + } + } + const chainManagerConfig = { network, chainName, diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index cba4449..53ea2e1 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -48,6 +48,7 @@ import { import { KlaytnNetwork, KLAYTN, KLAYTN_CHAIN_CONFIG } from "./klaytn.config"; import { BaseNetwork, BASE, BASE_CHAIN_CONFIG } from "./base.config"; import { PriceFeed } from "../../wallet-manager"; +import { coinGeckoIdByChainName } from "../../price-assistant/supported-tokens.config"; const EVM_HEX_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; @@ -229,6 +230,12 @@ export class EvmWalletToolbox extends WalletToolbox { public async pullNativeBalance(address: string, blockHeight?: number): Promise { const balance = await pullEvmNativeBalance(this.provider, address, blockHeight); const formattedBalance = ethers.utils.formatEther(balance.rawBalance); + + // Pull prices in USD for all the native tokens in single network call + await this.priceFeed?.pullTokenPrices(); + const coingeckoId = coinGeckoIdByChainName[this.chainName]; + const tokenUsdPrice = this.priceFeed?.getKey(coingeckoId); + const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(18n)) * tokenUsdPrice: undefined; return { ...balance, @@ -236,6 +243,8 @@ export class EvmWalletToolbox extends WalletToolbox { formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, + balanceUsd, + tokenUsdPrice }; } @@ -244,7 +253,7 @@ export class EvmWalletToolbox extends WalletToolbox { tokens: string[], ): Promise { // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(tokens); + await this.priceFeed?.pullTokenPrices(); return mapConcurrent( tokens, async tokenAddress => { @@ -258,11 +267,11 @@ export class EvmWalletToolbox extends WalletToolbox { balance.rawBalance, tokenData.decimals, ); - + // Add USD price to each token balance - const tokenPrice = await this.priceFeed?.getKey(tokenAddress); - // Wrapping with Number below to avoid Bigint issue converting string with decimals, eg: "20.0" - const tokenBalanceInUsd = tokenPrice ? Number(formattedBalance) * Number(tokenPrice): undefined; + const coinGeckoId = this.priceFeed?.getCoinGeckoId(tokenAddress); + const tokenUsdPrice = this.priceFeed?.getKey(coinGeckoId!); + const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(tokenData.decimals ?? 1n)) * tokenUsdPrice: undefined; return { ...balance, @@ -270,7 +279,8 @@ export class EvmWalletToolbox extends WalletToolbox { tokenAddress, formattedBalance, symbol: tokenData.symbol, - usd: tokenBalanceInUsd ? BigInt(tokenBalanceInUsd): undefined + balanceUsd, + tokenUsdPrice }; }, this.options.tokenPollConcurrency, diff --git a/src/wallets/index.ts b/src/wallets/index.ts index df17d5b..4f36c90 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -58,8 +58,9 @@ export type Balance = { isNative: boolean; rawBalance: string; formattedBalance: string; - usd?: bigint; blockHeight?: number; + tokenUsdPrice?: bigint; + balanceUsd?: bigint; }; export type TokenBalance = Balance & { From d8c9754644f70fe651efbefcd29a4e9306f2718c Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Thu, 23 Nov 2023 23:07:19 +0530 Subject: [PATCH 08/28] Update code for all the chains --- src/prometheus-exporter.ts | 6 +-- src/wallets/base-wallet.ts | 2 +- src/wallets/evm/index.ts | 4 +- src/wallets/solana/index.ts | 80 +++++++++++++++++++++---------------- src/wallets/sui/index.ts | 28 +++++++++---- 5 files changed, 72 insertions(+), 48 deletions(-) diff --git a/src/prometheus-exporter.ts b/src/prometheus-exporter.ts index 7042943..a69e4e1 100644 --- a/src/prometheus-exporter.ts +++ b/src/prometheus-exporter.ts @@ -14,14 +14,14 @@ function updateBalancesGauge(gauge: Gauge, chainName: string, network: string, b } function updateBalancesInUsdGauge(gauge: Gauge, chainName: string, network: string, balance: WalletBalance | TokenBalance) { - const { symbol, address, isNative, usd } = balance; + const { symbol, address, isNative, balanceUsd } = balance; - if (!usd) return; + if (!balanceUsd) return; const tokenAddress = (balance as TokenBalance).tokenAddress || ''; gauge .labels(chainName, network, symbol, isNative.toString(), tokenAddress, address) - .set(Number(usd.toString())); + .set(Number(balanceUsd.toString())); } function updateAvailableWalletsGauge(gauge: Gauge, chainName: string, network: string, count: number) { diff --git a/src/wallets/base-wallet.ts b/src/wallets/base-wallet.ts index d7d777b..e045793 100644 --- a/src/wallets/base-wallet.ts +++ b/src/wallets/base-wallet.ts @@ -170,7 +170,7 @@ export abstract class WalletToolbox { this.logger.debug( `Token balances for ${address} pulled: ${JSON.stringify( // usd is a big number, so we need to convert it to string - tokenBalances.map(balance => ({...balance, usd: balance?.usd?.toString()})), + tokenBalances.map(balance => ({...balance, usd: balance?.balanceUsd?.toString(), tokenUsdPrice: balance?.tokenUsdPrice?.toString()})), )}`, ); diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 53ea2e1..7d59445 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -235,7 +235,7 @@ export class EvmWalletToolbox extends WalletToolbox { await this.priceFeed?.pullTokenPrices(); const coingeckoId = coinGeckoIdByChainName[this.chainName]; const tokenUsdPrice = this.priceFeed?.getKey(coingeckoId); - const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(18n)) * tokenUsdPrice: undefined; + const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(10 ** 18)) * tokenUsdPrice: undefined; return { ...balance, @@ -271,7 +271,7 @@ export class EvmWalletToolbox extends WalletToolbox { // Add USD price to each token balance const coinGeckoId = this.priceFeed?.getCoinGeckoId(tokenAddress); const tokenUsdPrice = this.priceFeed?.getKey(coinGeckoId!); - const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(tokenData.decimals ?? 1n)) * tokenUsdPrice: undefined; + const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(10 ** tokenData.decimals)) * tokenUsdPrice: undefined; return { ...balance, diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index c42772a..8527f9c 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -6,7 +6,12 @@ import { RecentPrioritizationFees, } from "@solana/web3.js"; import { decode } from "bs58"; -import { SOLANA, SOLANA_CHAIN_CONFIG, SOLANA_DEFAULT_COMMITMENT, SolanaNetworks } from "./solana.config"; +import { + SOLANA, + SOLANA_CHAIN_CONFIG, + SOLANA_DEFAULT_COMMITMENT, + SolanaNetworks, +} from "./solana.config"; import { PYTHNET, PYTHNET_CHAIN_CONFIG } from "./pythnet.config"; import { BaseWalletOptions, @@ -26,6 +31,7 @@ import { import { getMint, Mint, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { findMedian, mapConcurrent } from "../../utils"; import { PriceFeed } from "../../wallet-manager"; +import { coinGeckoIdByChainName } from "../../price-assistant/supported-tokens.config"; export type SolanaChainConfig = { chainName: string; @@ -115,12 +121,20 @@ export class SolanaWalletToolbox extends WalletToolbox { Number(balance.rawBalance) / LAMPORTS_PER_SOL ).toString(); + // Pull prices in USD for all the native tokens in single network call + await this.priceFeed?.pullTokenPrices(); + const coingeckoId = coinGeckoIdByChainName[this.chainName]; + const tokenUsdPrice = this.priceFeed?.getKey(coingeckoId); + const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(LAMPORTS_PER_SOL)) * tokenUsdPrice: undefined; + return { ...balance, address, formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, + balanceUsd, + tokenUsdPrice }; } @@ -148,42 +162,38 @@ export class SolanaWalletToolbox extends WalletToolbox { }); // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(tokens); + await this.priceFeed?.pullTokenPrices(); // Assuming that tokens[] is actually an array of mint account addresses. - return mapConcurrent( - tokens, - async token => { - const tokenData = this.tokenData[token]; - const tokenKnownInfo = Object.entries( - this.chainConfig.knownTokens[this.network], - ).find(([_, value]) => value === token); - const tokenKnownSymbol = tokenKnownInfo ? tokenKnownInfo[0] : undefined; - - // We are choosing to show a balance of 0 for a token that is not owned by the address. - const tokenBalance = tokenBalancesDistinct.get(token) ?? 0; - const formattedBalance = ( - tokenBalance / - 10 ** tokenData.decimals - ) - - // Add USD price to each token balance - const tokenPrice = await this.priceFeed?.getKey(token); - const tokenBalanceInUsd = tokenPrice - ? BigInt(formattedBalance) * tokenPrice - : undefined; - - return { - isNative: false, - rawBalance: tokenBalance.toString(), - address, - formattedBalance: formattedBalance.toString(), - symbol: tokenKnownSymbol ?? "unknown", - usd: tokenBalanceInUsd, - }; - }, - this.options.tokenPollConcurrency, - ); + return tokens.map(token => { + const tokenData = this.tokenData[token]; + const tokenKnownInfo = Object.entries( + this.chainConfig.knownTokens[this.network], + ).find(([_, value]) => value === token); + const tokenKnownSymbol = tokenKnownInfo ? tokenKnownInfo[0] : undefined; + + // We are choosing to show a balance of 0 for a token that is not owned by the address. + const tokenBalance = tokenBalancesDistinct.get(token) ?? 0; + const formattedBalance = tokenBalance / 10 ** tokenData.decimals; + + // Add USD price to each token balance + const coinGeckoId = this.priceFeed?.getCoinGeckoId(token); + const tokenUsdPrice = this.priceFeed?.getKey(coinGeckoId!); + const balanceUsd = tokenUsdPrice + ? (BigInt(tokenBalance) / BigInt(10 ** tokenData.decimals)) * + tokenUsdPrice + : undefined; + + return { + isNative: false, + rawBalance: tokenBalance.toString(), + address, + formattedBalance: formattedBalance.toString(), + symbol: tokenKnownSymbol ?? "unknown", + balanceUsd, + tokenUsdPrice, + }; + }); } protected validateChainName(chainName: string): chainName is SolanaChainName { diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index 188e60a..e7c5d59 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -23,6 +23,7 @@ import { getSuiAddressFromPrivateKey } from "../../balances/sui"; import {mapConcurrent} from "../../utils"; import { formatFixed } from "@ethersproject/bignumber"; import { PriceFeed } from "../../wallet-manager"; +import { coinGeckoIdByChainName } from "../../price-assistant/supported-tokens.config"; export const SUI_CHAINS = { [SUI]: 1, @@ -165,12 +166,21 @@ export class SuiWalletToolbox extends WalletToolbox { public async pullNativeBalance(address: string): Promise { const balance = await pullSuiNativeBalance(this.connection, address); const formattedBalance = String(+balance.rawBalance / 10 ** 9); + + // Pull prices in USD for all the native tokens in single network call + await this.priceFeed?.pullTokenPrices(); + const coingeckoId = coinGeckoIdByChainName[this.chainName]; + const tokenUsdPrice = this.priceFeed?.getKey(coingeckoId); + const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(10 ** 9)) * tokenUsdPrice: undefined; + return { ...balance, address, formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, + balanceUsd, + tokenUsdPrice }; } @@ -181,9 +191,9 @@ export class SuiWalletToolbox extends WalletToolbox { const uniqueTokens = [...new Set(tokens)]; const allBalances = await pullSuiTokenBalances(this.connection, address); // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(tokens); + await this.priceFeed?.pullTokenPrices(); - return mapConcurrent(uniqueTokens, async (tokenAddress: string) => { + return uniqueTokens.map(tokenAddress => { const tokenData = this.tokenData[tokenAddress]; const symbol: string = tokenData?.symbol ? tokenData.symbol : ""; @@ -199,13 +209,16 @@ export class SuiWalletToolbox extends WalletToolbox { } } + const tokenDecimals = tokenData?.decimals ?? 9; const formattedBalance = formatFixed( balance.totalBalance, - tokenData?.decimals ? tokenData.decimals : 9 + tokenDecimals ); - const tokenPrice = await this.priceFeed?.getKey(tokenAddress); - const tokenBalanceInUsd = tokenPrice ? BigInt(formattedBalance) * tokenPrice : undefined; + // Add USD price to each token balance + const coinGeckoId = this.priceFeed?.getCoinGeckoId(tokenAddress); + const tokenUsdPrice = this.priceFeed?.getKey(coinGeckoId!); + const balanceUsd = tokenUsdPrice ? (BigInt(balance.totalBalance) / BigInt(10 ** tokenDecimals)) * tokenUsdPrice: undefined; return { tokenAddress, @@ -214,9 +227,10 @@ export class SuiWalletToolbox extends WalletToolbox { rawBalance: balance.totalBalance, formattedBalance, symbol, - usd: tokenBalanceInUsd, + balanceUsd, + tokenUsdPrice }; - }, this.options.tokenPollConcurrency); + }); } protected async transferNativeBalance( From 2d0b57e3e43aa3b902f74711c884b1c1cbc1accd Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 00:25:22 +0530 Subject: [PATCH 09/28] Fix type issue --- src/index.ts | 3 +-- src/price-assistant/supported-tokens.config.ts | 8 +------- src/wallet-manager.ts | 3 ++- src/wallets/index.ts | 6 ++++++ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4669340..9e03a0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,8 +13,7 @@ import { export type WalletBalancesByAddress = WBBA; export type WalletInterface = WI; -export type {ChainName} from "./wallets"; -export type {Environment} from './price-assistant/supported-tokens.config'; +export type {ChainName, Environment} from "./wallets"; export {isEvmChain, isSolanaChain, isSuiChain} from './wallets'; diff --git a/src/price-assistant/supported-tokens.config.ts b/src/price-assistant/supported-tokens.config.ts index 2762cf6..6588b94 100644 --- a/src/price-assistant/supported-tokens.config.ts +++ b/src/price-assistant/supported-tokens.config.ts @@ -1,11 +1,5 @@ import { z } from "zod"; -import { ChainName } from "../wallets"; - -export declare enum Environment { - MAINNET = "mainnet", - TESTNET = "testnet", - DEVNET = "devnet", -} +import { ChainName, Environment } from "../wallets"; export const CoinGeckoIdsSchema = z .union([ diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index 158b7a0..d564705 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -13,6 +13,7 @@ import { } from "./chain-wallet-manager"; import { ChainName, + Environment, isChain, KNOWN_CHAINS, WalletBalance, @@ -20,7 +21,7 @@ import { } from "./wallets"; import { TransferRecepit } from "./wallets/base-wallet"; import { RebalanceInstruction } from "./rebalance-strategies"; -import { CoinGeckoIdsSchema, Environment, supportedTokensByEnv } from "./price-assistant/supported-tokens.config"; +import { CoinGeckoIdsSchema, supportedTokensByEnv } from "./price-assistant/supported-tokens.config"; import { ScheduledPriceFeed } from "./price-assistant/scheduled-price-feed"; import { OnDemandPriceFeed } from "./price-assistant/ondemand-price-feed"; diff --git a/src/wallets/index.ts b/src/wallets/index.ts index 4f36c90..c64d4a0 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -2,6 +2,12 @@ import { z } from "zod"; export const DEVNET = "devnet"; +export const enum Environment { + MAINNET = "mainnet", + TESTNET = "testnet", + DEVNET = "devnet", +} + import { EvmWalletOptions, EvmWalletToolbox, From ac1b75a453512a4fec1f59931ec6b813d1a3721b Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 11:46:45 +0530 Subject: [PATCH 10/28] Use number instead of bigint to avoid conversion issues --- src/price-assistant/ondemand-price-feed.ts | 15 +++++++-------- src/price-assistant/price-feed.ts | 2 +- src/price-assistant/scheduled-price-feed.ts | 8 ++++---- src/wallets/base-wallet.ts | 2 +- src/wallets/evm/index.ts | 17 +++++++++-------- src/wallets/index.ts | 4 ++-- src/wallets/solana/index.ts | 20 +++++++++----------- src/wallets/sui/index.ts | 17 +++++++++-------- 8 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/price-assistant/ondemand-price-feed.ts b/src/price-assistant/ondemand-price-feed.ts index 37b6966..0550d9c 100644 --- a/src/price-assistant/ondemand-price-feed.ts +++ b/src/price-assistant/ondemand-price-feed.ts @@ -10,9 +10,9 @@ const DEFAULT_TOKEN_PRICE_RETENSION_TIME = 5 * 1000; // 5 seconds /** * OnDemandPriceFeed is a price feed that fetches token prices from coingecko on-demand */ -export class OnDemandPriceFeed extends PriceFeed{ +export class OnDemandPriceFeed extends PriceFeed{ // here cache key is tokenContractAddress - private cache = new TimeLimitedCache(); + private cache = new TimeLimitedCache(); supportedTokens: TokenInfo[]; tokenPriceGauge?: Gauge; private tokenContractToCoingeckoId: Record = {}; @@ -23,10 +23,9 @@ export class OnDemandPriceFeed extends PriceFeed{ registry?: Registry, ) { super("ONDEMAND_TOKEN_PRICE", logger, registry, undefined) - const { supportedTokens } = priceAssistantConfig; - this.supportedTokens = supportedTokens; + this.supportedTokens = priceAssistantConfig.supportedTokens; - this.tokenContractToCoingeckoId = supportedTokens.reduce((acc, token) => { + this.tokenContractToCoingeckoId = this.supportedTokens.reduce((acc, token) => { acc[token.tokenContract] = token.coingeckoId as CoinGeckoIds; return acc; }, {} as Record); @@ -53,7 +52,7 @@ export class OnDemandPriceFeed extends PriceFeed{ return this.tokenContractToCoingeckoId[tokenContract]; } - protected get (coingeckoId: string): bigint | undefined { + protected get (coingeckoId: string): number | undefined { return this.cache.get(coingeckoId as CoinGeckoIds); } @@ -91,8 +90,8 @@ export class OnDemandPriceFeed extends PriceFeed{ const tokenPrice = coingeckoData?.[coingeckoId]?.usd; if (tokenPrice) { - this.cache.set(coingeckoId, BigInt(tokenPrice), DEFAULT_TOKEN_PRICE_RETENSION_TIME); - this.tokenPriceGauge?.labels({ symbol }).set(Number(tokenPrice)); + this.cache.set(coingeckoId, tokenPrice, DEFAULT_TOKEN_PRICE_RETENSION_TIME); + this.tokenPriceGauge?.labels({ symbol }).set(tokenPrice); } } } diff --git a/src/price-assistant/price-feed.ts b/src/price-assistant/price-feed.ts index 31faebb..026d393 100644 --- a/src/price-assistant/price-feed.ts +++ b/src/price-assistant/price-feed.ts @@ -4,7 +4,7 @@ import { printError } from "../utils"; const DEFAULT_FEED_INTERVAL = 10_000; -export type TokenPriceData = Partial>; +export type TokenPriceData = Partial>; export abstract class PriceFeed { private name: string; diff --git a/src/price-assistant/scheduled-price-feed.ts b/src/price-assistant/scheduled-price-feed.ts index 9f38205..3a734f5 100644 --- a/src/price-assistant/scheduled-price-feed.ts +++ b/src/price-assistant/scheduled-price-feed.ts @@ -9,7 +9,7 @@ import { CoinGeckoIds } from "./supported-tokens.config"; /** * ScheduledPriceFeed is a price feed that periodically fetches token prices from coingecko */ -export class ScheduledPriceFeed extends PriceFeed { +export class ScheduledPriceFeed extends PriceFeed { private data = {} as TokenPriceData; supportedTokens: TokenInfo[]; tokenPriceGauge?: Gauge; @@ -56,14 +56,14 @@ export class ScheduledPriceFeed extends PriceFeed { const tokenPrice = coingeckoData?.[coingeckoId]?.usd; if (tokenPrice) { - this.data[coingeckoId] = BigInt(tokenPrice); - this.tokenPriceGauge?.labels({ symbol }).set(Number(tokenPrice)); + this.data[coingeckoId] = tokenPrice; + this.tokenPriceGauge?.labels({ symbol }).set(tokenPrice); } } this.logger.debug(`Updated price feed token prices: ${inspect(this.data)}`); } - protected get(coingeckoId: string): bigint | undefined { + protected get(coingeckoId: string): number | undefined { return this.data[coingeckoId]; } } diff --git a/src/wallets/base-wallet.ts b/src/wallets/base-wallet.ts index e045793..374c9d4 100644 --- a/src/wallets/base-wallet.ts +++ b/src/wallets/base-wallet.ts @@ -170,7 +170,7 @@ export abstract class WalletToolbox { this.logger.debug( `Token balances for ${address} pulled: ${JSON.stringify( // usd is a big number, so we need to convert it to string - tokenBalances.map(balance => ({...balance, usd: balance?.balanceUsd?.toString(), tokenUsdPrice: balance?.tokenUsdPrice?.toString()})), + tokenBalances.map(balance => ({...balance, balanceUsd: balance?.balanceUsd?.toString(), tokenUsdPrice: balance?.tokenUsdPrice?.toString()})), )}`, ); diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 7d59445..80570a3 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -235,7 +235,6 @@ export class EvmWalletToolbox extends WalletToolbox { await this.priceFeed?.pullTokenPrices(); const coingeckoId = coinGeckoIdByChainName[this.chainName]; const tokenUsdPrice = this.priceFeed?.getKey(coingeckoId); - const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(10 ** 18)) * tokenUsdPrice: undefined; return { ...balance, @@ -243,8 +242,10 @@ export class EvmWalletToolbox extends WalletToolbox { formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, - balanceUsd, - tokenUsdPrice + ...(tokenUsdPrice && { + balanceUsd: Number(formattedBalance) * tokenUsdPrice, + tokenUsdPrice + }) }; } @@ -268,10 +269,8 @@ export class EvmWalletToolbox extends WalletToolbox { tokenData.decimals, ); - // Add USD price to each token balance const coinGeckoId = this.priceFeed?.getCoinGeckoId(tokenAddress); - const tokenUsdPrice = this.priceFeed?.getKey(coinGeckoId!); - const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(10 ** tokenData.decimals)) * tokenUsdPrice: undefined; + const tokenUsdPrice = coinGeckoId && this.priceFeed?.getKey(coinGeckoId); return { ...balance, @@ -279,8 +278,10 @@ export class EvmWalletToolbox extends WalletToolbox { tokenAddress, formattedBalance, symbol: tokenData.symbol, - balanceUsd, - tokenUsdPrice + ...(tokenUsdPrice && { + balanceUsd: Number(formattedBalance) * tokenUsdPrice, + tokenUsdPrice + }) }; }, this.options.tokenPollConcurrency, diff --git a/src/wallets/index.ts b/src/wallets/index.ts index c64d4a0..9879da8 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -65,8 +65,8 @@ export type Balance = { rawBalance: string; formattedBalance: string; blockHeight?: number; - tokenUsdPrice?: bigint; - balanceUsd?: bigint; + tokenUsdPrice?: number; + balanceUsd?: number; }; export type TokenBalance = Balance & { diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index 8527f9c..b61a310 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -125,7 +125,6 @@ export class SolanaWalletToolbox extends WalletToolbox { await this.priceFeed?.pullTokenPrices(); const coingeckoId = coinGeckoIdByChainName[this.chainName]; const tokenUsdPrice = this.priceFeed?.getKey(coingeckoId); - const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(LAMPORTS_PER_SOL)) * tokenUsdPrice: undefined; return { ...balance, @@ -133,8 +132,10 @@ export class SolanaWalletToolbox extends WalletToolbox { formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, - balanceUsd, - tokenUsdPrice + ...(tokenUsdPrice && { + balanceUsd: Number(formattedBalance) * tokenUsdPrice, + tokenUsdPrice + }) }; } @@ -176,13 +177,8 @@ export class SolanaWalletToolbox extends WalletToolbox { const tokenBalance = tokenBalancesDistinct.get(token) ?? 0; const formattedBalance = tokenBalance / 10 ** tokenData.decimals; - // Add USD price to each token balance const coinGeckoId = this.priceFeed?.getCoinGeckoId(token); - const tokenUsdPrice = this.priceFeed?.getKey(coinGeckoId!); - const balanceUsd = tokenUsdPrice - ? (BigInt(tokenBalance) / BigInt(10 ** tokenData.decimals)) * - tokenUsdPrice - : undefined; + const tokenUsdPrice = coinGeckoId && this.priceFeed?.getKey(coinGeckoId); return { isNative: false, @@ -190,8 +186,10 @@ export class SolanaWalletToolbox extends WalletToolbox { address, formattedBalance: formattedBalance.toString(), symbol: tokenKnownSymbol ?? "unknown", - balanceUsd, - tokenUsdPrice, + ...(tokenUsdPrice && { + balanceUsd: Number(formattedBalance) * tokenUsdPrice, + tokenUsdPrice + }) }; }); } diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index e7c5d59..214a06f 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -171,7 +171,6 @@ export class SuiWalletToolbox extends WalletToolbox { await this.priceFeed?.pullTokenPrices(); const coingeckoId = coinGeckoIdByChainName[this.chainName]; const tokenUsdPrice = this.priceFeed?.getKey(coingeckoId); - const balanceUsd = tokenUsdPrice ? (BigInt(balance.rawBalance) / BigInt(10 ** 9)) * tokenUsdPrice: undefined; return { ...balance, @@ -179,8 +178,10 @@ export class SuiWalletToolbox extends WalletToolbox { formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, - balanceUsd, - tokenUsdPrice + ...(tokenUsdPrice && { + balanceUsd: Number(formattedBalance) * tokenUsdPrice, + tokenUsdPrice + }) }; } @@ -215,10 +216,8 @@ export class SuiWalletToolbox extends WalletToolbox { tokenDecimals ); - // Add USD price to each token balance const coinGeckoId = this.priceFeed?.getCoinGeckoId(tokenAddress); - const tokenUsdPrice = this.priceFeed?.getKey(coinGeckoId!); - const balanceUsd = tokenUsdPrice ? (BigInt(balance.totalBalance) / BigInt(10 ** tokenDecimals)) * tokenUsdPrice: undefined; + const tokenUsdPrice = coinGeckoId && this.priceFeed?.getKey(coinGeckoId); return { tokenAddress, @@ -227,8 +226,10 @@ export class SuiWalletToolbox extends WalletToolbox { rawBalance: balance.totalBalance, formattedBalance, symbol, - balanceUsd, - tokenUsdPrice + ...(tokenUsdPrice && { + balanceUsd: Number(formattedBalance) * tokenUsdPrice, + tokenUsdPrice + }) }; }); } From 99b731c914cf274e76b616815e0a570ee3a0078a Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 14:03:44 +0530 Subject: [PATCH 11/28] Fix cache invalidation of token prices --- examples/wallet-manager.ts | 148 ++++++++++---------- src/price-assistant/ondemand-price-feed.ts | 74 ++++++---- src/price-assistant/price-feed.ts | 2 +- src/price-assistant/scheduled-price-feed.ts | 12 +- src/wallets/evm/index.ts | 4 +- src/wallets/solana/index.ts | 4 +- src/wallets/sui/index.ts | 4 +- test/wallets/sui/sui.test.ts | 1 - 8 files changed, 136 insertions(+), 113 deletions(-) diff --git a/examples/wallet-manager.ts b/examples/wallet-manager.ts index 9a24fb8..d421393 100644 --- a/examples/wallet-manager.ts +++ b/examples/wallet-manager.ts @@ -25,8 +25,9 @@ const allChainWallets: WalletManagerFullConfig['config'] = { } }, priceFeedConfig: { + enabled: true, scheduled: { - enabled: true + enabled: false }, supportedTokens: [{ chainId: 2, @@ -40,80 +41,79 @@ const allChainWallets: WalletManagerFullConfig['config'] = { coingeckoId: "wrapped-bitcoin", symbol: "WBTC" }], - enabled: true - } - }, - solana: { - wallets: [ - { address: "6VnfVsLdLwNuuCmooLTziQ99PFXZ5vc3yyqyb9tMDhhw", tokens: ['usdc'] }, - ], - walletBalanceConfig: { - enabled: true, - scheduled: { - enabled: false, - } - }, - }, - sui: { - rebalance: { - enabled: false, - strategy: 'default', - interval: 10000, - minBalanceThreshold: 0.1, - }, - wallets: [ - { privateKey: 'ODV9VYi3eSljEWWmpWh8s9m/P2BNNxU/Vp8jwADeNuw=' }, - { address: '0x934042d46762fadf9f61ef07aa265fc14d28525c7051224a5f1cba2409aef307' }, - { address: '0x341ab296bbe653b426e996b17af33718db62af3c250c04fe186c9124db5bd8b7' }, - { address: '0x7d20dcdb2bca4f508ea9613994683eb4e76e9c4ed371169677c1be02aaf0b58e' }, - { address: '0x18337b4c5b964b7645506542589c5ed496e794af82f98b3789fed96f61a94c96' }, - { address: '0x9ae846f88db3476d7c9f2d8fc49722f7085a3b46aad998120dd11ebeab83e021' }, - { address: '0xcb39d897bf0561af7531d37db9781e54528269fed4761275931ce32f20352977' }, - { - address: '0x8f11fe7121be742f46e2b3bc2eba081efdc3027697c317a917a2d16fd9b59ab1', - tokens: ['USDC', 'USDT'] - }, - ], - walletBalanceConfig: { - enabled: true, - scheduled: { - enabled: false, - } - }, - priceFeedConfig: { - supportedTokens: [{ - chainId: 21, - tokenContract: "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf", - coingeckoId: "usd-coin", - symbol: "USDC" - }], - enabled: false, } }, - klatyn: { - rebalance: { - enabled: false, - strategy: 'default', - interval: 10000, - minBalanceThreshold: 0.1, - }, - wallets: [ - { - address: "0x80C67432656d59144cEFf962E8fAF8926599bCF8", - tokens: ["USDC", "DAI"] - }, - { - address: "0x8d0d970225597085A59ADCcd7032113226C0419d", - tokens: [] - } - ], - walletBalanceConfig: { - enabled: true, - scheduled: { - enabled: false, - } - }, - } + // solana: { + // wallets: [ + // { address: "6VnfVsLdLwNuuCmooLTziQ99PFXZ5vc3yyqyb9tMDhhw", tokens: ['usdc'] }, + // ], + // walletBalanceConfig: { + // enabled: true, + // scheduled: { + // enabled: false, + // } + // }, + // }, + // sui: { + // rebalance: { + // enabled: false, + // strategy: 'default', + // interval: 10000, + // minBalanceThreshold: 0.1, + // }, + // wallets: [ + // { privateKey: 'ODV9VYi3eSljEWWmpWh8s9m/P2BNNxU/Vp8jwADeNuw=' }, + // { address: '0x934042d46762fadf9f61ef07aa265fc14d28525c7051224a5f1cba2409aef307' }, + // { address: '0x341ab296bbe653b426e996b17af33718db62af3c250c04fe186c9124db5bd8b7' }, + // { address: '0x7d20dcdb2bca4f508ea9613994683eb4e76e9c4ed371169677c1be02aaf0b58e' }, + // { address: '0x18337b4c5b964b7645506542589c5ed496e794af82f98b3789fed96f61a94c96' }, + // { address: '0x9ae846f88db3476d7c9f2d8fc49722f7085a3b46aad998120dd11ebeab83e021' }, + // { address: '0xcb39d897bf0561af7531d37db9781e54528269fed4761275931ce32f20352977' }, + // { + // address: '0x8f11fe7121be742f46e2b3bc2eba081efdc3027697c317a917a2d16fd9b59ab1', + // tokens: ['USDC', 'USDT'] + // }, + // ], + // walletBalanceConfig: { + // enabled: true, + // scheduled: { + // enabled: false, + // } + // }, + // priceFeedConfig: { + // supportedTokens: [{ + // chainId: 21, + // tokenContract: "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf", + // coingeckoId: "usd-coin", + // symbol: "USDC" + // }], + // enabled: false, + // } + // }, + // klatyn: { + // rebalance: { + // enabled: false, + // strategy: 'default', + // interval: 10000, + // minBalanceThreshold: 0.1, + // }, + // wallets: [ + // { + // address: "0x80C67432656d59144cEFf962E8fAF8926599bCF8", + // tokens: ["USDC", "DAI"] + // }, + // { + // address: "0x8d0d970225597085A59ADCcd7032113226C0419d", + // tokens: [] + // } + // ], + // walletBalanceConfig: { + // enabled: true, + // scheduled: { + // enabled: false, + // } + // }, + // } } export const manager = buildWalletManager({ @@ -135,7 +135,7 @@ export const manager = buildWalletManager({ try { console.time('balances') const balances = await manager.pullBalancesAtBlockHeight(); - console.timeLog('balances', balances) + console.timeLog('balances', JSON.stringify(balances)) } catch (err) { console.error('Failed to pullBalancesAtBlockHeight', err); } diff --git a/src/price-assistant/ondemand-price-feed.ts b/src/price-assistant/ondemand-price-feed.ts index 0550d9c..9f85f47 100644 --- a/src/price-assistant/ondemand-price-feed.ts +++ b/src/price-assistant/ondemand-price-feed.ts @@ -4,13 +4,13 @@ import { TimeLimitedCache } from "../utils"; import { TokenInfo, WalletPriceFeedConfig } from "../wallet-manager"; import { getCoingeckoPrices } from "./helper"; import { CoinGeckoIds } from "./supported-tokens.config"; -import { PriceFeed } from "./price-feed"; +import { PriceFeed, TokenPriceData } from "./price-feed"; const DEFAULT_TOKEN_PRICE_RETENSION_TIME = 5 * 1000; // 5 seconds /** * OnDemandPriceFeed is a price feed that fetches token prices from coingecko on-demand */ -export class OnDemandPriceFeed extends PriceFeed{ +export class OnDemandPriceFeed extends PriceFeed { // here cache key is tokenContractAddress private cache = new TimeLimitedCache(); supportedTokens: TokenInfo[]; @@ -22,14 +22,17 @@ export class OnDemandPriceFeed extends PriceFeed{ logger: Logger, registry?: Registry, ) { - super("ONDEMAND_TOKEN_PRICE", logger, registry, undefined) + super("ONDEMAND_TOKEN_PRICE", logger, registry, undefined); this.supportedTokens = priceAssistantConfig.supportedTokens; - - this.tokenContractToCoingeckoId = this.supportedTokens.reduce((acc, token) => { - acc[token.tokenContract] = token.coingeckoId as CoinGeckoIds; - return acc; - }, {} as Record); - + + this.tokenContractToCoingeckoId = this.supportedTokens.reduce( + (acc, token) => { + acc[token.tokenContract] = token.coingeckoId as CoinGeckoIds; + return acc; + }, + {} as Record, + ); + if (registry) { this.tokenPriceGauge = new Gauge({ name: "token_usd_price", @@ -40,59 +43,72 @@ export class OnDemandPriceFeed extends PriceFeed{ } } - start () { + start() { // no op } - stop () { + stop() { // no op } - public getCoinGeckoId (tokenContract: string): CoinGeckoIds | undefined { + public getCoinGeckoId(tokenContract: string): CoinGeckoIds | undefined { return this.tokenContractToCoingeckoId[tokenContract]; } - protected get (coingeckoId: string): number | undefined { + protected get(coingeckoId: string): number | undefined { return this.cache.get(coingeckoId as CoinGeckoIds); } async pullTokenPrices() { - return this.update(); - } - - async update () { const coingekoTokens = []; + const priceDict: TokenPriceData = {}; for (const token of this.supportedTokens) { - const { coingeckoId } = token; + const { coingeckoId } = token; - // Check if we already have the price for this token - const cachedPrice = this.cache.get(coingeckoId); - if (cachedPrice) { - continue; - } - coingekoTokens.push(token); + // Check if we already have the price for this token + const cachedPrice = this.cache.get(coingeckoId); + if (cachedPrice) { + priceDict[coingeckoId] = cachedPrice; + continue; + } + coingekoTokens.push(token); } if (coingekoTokens.length === 0) { // All the cached tokens price are already available and valid - return; + return priceDict; } - const coingekoTokenIds = coingekoTokens.map(token => token.coingeckoId) - const coingeckoData = await getCoingeckoPrices(coingekoTokenIds, this.logger); + const coingekoTokenIds = coingekoTokens.map(token => token.coingeckoId); + const coingeckoData = await getCoingeckoPrices( + coingekoTokenIds, + this.logger, + ); for (const token of this.supportedTokens) { const { coingeckoId, symbol } = token; if (!(coingeckoId in coingeckoData)) { - this.logger.warn(`Token ${symbol} (coingecko: ${coingeckoId}) not found in coingecko response data`); + this.logger.warn( + `Token ${symbol} (coingecko: ${coingeckoId}) not found in coingecko response data`, + ); continue; } const tokenPrice = coingeckoData?.[coingeckoId]?.usd; if (tokenPrice) { - this.cache.set(coingeckoId, tokenPrice, DEFAULT_TOKEN_PRICE_RETENSION_TIME); + this.cache.set( + coingeckoId, + tokenPrice, + DEFAULT_TOKEN_PRICE_RETENSION_TIME, + ); + priceDict[coingeckoId] = tokenPrice; this.tokenPriceGauge?.labels({ symbol }).set(tokenPrice); } } + return priceDict; + } + + async update() { + // no op } } diff --git a/src/price-assistant/price-feed.ts b/src/price-assistant/price-feed.ts index 026d393..34f3db2 100644 --- a/src/price-assistant/price-feed.ts +++ b/src/price-assistant/price-feed.ts @@ -26,7 +26,7 @@ export abstract class PriceFeed { protected abstract get(key: K): V; - public abstract pullTokenPrices (): Promise; + public abstract pullTokenPrices (): Promise; public start(): void { this.interval = setInterval(() => this.run(), this.runIntervalMs); diff --git a/src/price-assistant/scheduled-price-feed.ts b/src/price-assistant/scheduled-price-feed.ts index 3a734f5..4f982c3 100644 --- a/src/price-assistant/scheduled-price-feed.ts +++ b/src/price-assistant/scheduled-price-feed.ts @@ -39,8 +39,16 @@ export class ScheduledPriceFeed extends PriceFeed { return this.tokenContractToCoingeckoId[tokenContract]; } - async pullTokenPrices () { - // no op + async pullTokenPrices (): Promise { + const priceDict: TokenPriceData = {}; + for (const token of this.supportedTokens) { + const { coingeckoId } = token; + const tokenPrice = this.get(coingeckoId); + if (tokenPrice) { + priceDict[coingeckoId] = tokenPrice; + } + } + return priceDict } async update() { diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 80570a3..c5a2601 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -254,7 +254,7 @@ export class EvmWalletToolbox extends WalletToolbox { tokens: string[], ): Promise { // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(); + const tokenPrices = await this.priceFeed?.pullTokenPrices(); return mapConcurrent( tokens, async tokenAddress => { @@ -270,7 +270,7 @@ export class EvmWalletToolbox extends WalletToolbox { ); const coinGeckoId = this.priceFeed?.getCoinGeckoId(tokenAddress); - const tokenUsdPrice = coinGeckoId && this.priceFeed?.getKey(coinGeckoId); + const tokenUsdPrice = coinGeckoId && tokenPrices?.[coinGeckoId]; return { ...balance, diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index b61a310..ad64b38 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -163,7 +163,7 @@ export class SolanaWalletToolbox extends WalletToolbox { }); // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(); + const tokenPrices = await this.priceFeed?.pullTokenPrices(); // Assuming that tokens[] is actually an array of mint account addresses. return tokens.map(token => { @@ -178,7 +178,7 @@ export class SolanaWalletToolbox extends WalletToolbox { const formattedBalance = tokenBalance / 10 ** tokenData.decimals; const coinGeckoId = this.priceFeed?.getCoinGeckoId(token); - const tokenUsdPrice = coinGeckoId && this.priceFeed?.getKey(coinGeckoId); + const tokenUsdPrice = coinGeckoId && tokenPrices?.[coinGeckoId]; return { isNative: false, diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index 214a06f..4b30b28 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -192,7 +192,7 @@ export class SuiWalletToolbox extends WalletToolbox { const uniqueTokens = [...new Set(tokens)]; const allBalances = await pullSuiTokenBalances(this.connection, address); // Pull prices in USD for all the tokens in single network call - await this.priceFeed?.pullTokenPrices(); + const tokenPrices = await this.priceFeed?.pullTokenPrices(); return uniqueTokens.map(tokenAddress => { const tokenData = this.tokenData[tokenAddress]; @@ -217,7 +217,7 @@ export class SuiWalletToolbox extends WalletToolbox { ); const coinGeckoId = this.priceFeed?.getCoinGeckoId(tokenAddress); - const tokenUsdPrice = coinGeckoId && this.priceFeed?.getKey(coinGeckoId); + const tokenUsdPrice = coinGeckoId && tokenPrices?.[coinGeckoId]; return { tokenAddress, diff --git a/test/wallets/sui/sui.test.ts b/test/wallets/sui/sui.test.ts index 753f2e2..24bfccf 100644 --- a/test/wallets/sui/sui.test.ts +++ b/test/wallets/sui/sui.test.ts @@ -98,7 +98,6 @@ describe("sui wallet tests", () => { "rawBalance": "100000000", "formattedBalance": "100.0", "symbol": "USDC", - "usd": undefined } ]); }); From 64ced960e986dfb7a1d0fda4d546033bf4ca23e7 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 16:36:55 +0530 Subject: [PATCH 12/28] Accept blockHeightByChain as option in pullBalances method --- src/chain-wallet-manager.ts | 4 ++-- src/wallet-manager.ts | 25 ++++++++++++++++++++++--- src/wallets/base-wallet.ts | 18 ++++++++---------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/chain-wallet-manager.ts b/src/chain-wallet-manager.ts index 2ca2c18..ff7eede 100644 --- a/src/chain-wallet-manager.ts +++ b/src/chain-wallet-manager.ts @@ -434,8 +434,8 @@ export class ChainWalletManager { } /** Pull balances on demand with block height */ - public async pullBalancesAtBlockHeight() { - const balances = await this.walletToolbox.pullBalancesAtBlockHeight(); + public async pullBalancesAtBlockHeight(blockHeight: number) { + const balances = await this.walletToolbox.pullBalancesAtBlockHeight(blockHeight); return this.mapBalances(balances); } } diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index d564705..8c8346f 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -336,7 +336,7 @@ export class WalletManager { } private async balanceHandlerMapper( - method: "getBalances" | "pullBalancesAtBlockHeight" | "pullBalances", + method: "getBalances" | "pullBalances", ) { const balances: Record = {}; @@ -373,10 +373,29 @@ export class WalletManager { } // pullBalancesAtBlockHeight doesn't need balances to be refreshed in the background - public async pullBalancesAtBlockHeight(): Promise< + public async pullBalancesAtBlockHeight(blockHeightByChain?: Record): Promise< Record > { - return await this.balanceHandlerMapper("pullBalancesAtBlockHeight"); + const balances: Record = {}; + if (blockHeightByChain) { + // Validate blockHeightByChain + for (const chain in blockHeightByChain) { + const manager = this.managers[chain as ChainName]; + if (!manager) + throw new Error(`No wallets configured for chain: ${chain}`); + } + } + + await mapConcurrent( + Object.entries(this.managers), + async ([chainName, manager]) => { + const blockHeight = blockHeightByChain?.[chainName as ChainName] ?? await manager.getBlockHeight(); + const balancesByChain = await manager.pullBalancesAtBlockHeight(blockHeight) + balances[chainName] = balancesByChain; + }, + ); + + return balances; } public getChainBalances(chainName: ChainName): WalletBalancesByAddress { diff --git a/src/wallets/base-wallet.ts b/src/wallets/base-wallet.ts index 374c9d4..0088673 100644 --- a/src/wallets/base-wallet.ts +++ b/src/wallets/base-wallet.ts @@ -143,7 +143,7 @@ export abstract class WalletToolbox { minBalanceThreshold ?? 0, ); - balances.push({...nativeBalance, blockHeight}); + balances.push({ ...nativeBalance, blockHeight }); this.logger.debug( `Balances for ${address} pulled: ${JSON.stringify(nativeBalance)}`, @@ -170,7 +170,11 @@ export abstract class WalletToolbox { this.logger.debug( `Token balances for ${address} pulled: ${JSON.stringify( // usd is a big number, so we need to convert it to string - tokenBalances.map(balance => ({...balance, balanceUsd: balance?.balanceUsd?.toString(), tokenUsdPrice: balance?.tokenUsdPrice?.toString()})), + tokenBalances.map(balance => ({ + ...balance, + balanceUsd: balance?.balanceUsd?.toString(), + tokenUsdPrice: balance?.tokenUsdPrice?.toString(), + })), )}`, ); @@ -185,14 +189,8 @@ export abstract class WalletToolbox { return balances; } - public async pullBalancesAtBlockHeight( - ) { - const blockHeight = await this.getBlockHeight(); - return this.pullBalances( - false, - undefined, - blockHeight, - ); + public async pullBalancesAtBlockHeight(blockHeight: number) { + return this.pullBalances(false, undefined, blockHeight); } public async acquire(address?: string, acquireTimeout?: number) { From ae5b0a9dee28b5d0bdfc76785c89a56bafe37cd8 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 16:44:03 +0530 Subject: [PATCH 13/28] Sync client wallet manager with wallet-manager changes --- src/grpc/client.ts | 36 ++++++++++++++++++++++++++++-------- src/wallet-manager.ts | 33 +++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/grpc/client.ts b/src/grpc/client.ts index 112156a..9858a74 100644 --- a/src/grpc/client.ts +++ b/src/grpc/client.ts @@ -117,9 +117,7 @@ export class ClientWalletManager implements IClientWalletManager { return manager.getBlockHeight(); } - private async balanceHandlerMapper( - method: "getBalances" | "pullBalancesAtBlockHeight" | "pullBalances", - ) { + private async balanceHandlerMapper(method: "getBalances" | "pullBalances") { const balances: Record = {}; await mapConcurrent( @@ -140,10 +138,32 @@ export class ClientWalletManager implements IClientWalletManager { return await this.balanceHandlerMapper("pullBalances"); } - // pullBalancesAtBlockHeight doesn't need balances to be refreshed in the background - public async pullBalancesAtBlockHeight(): Promise< - Record - > { - return await this.balanceHandlerMapper("pullBalancesAtBlockHeight"); + public async pullBalancesAtBlockHeight( + blockHeightByChain?: Record, + ): Promise> { + const balances: Record = {}; + if (blockHeightByChain) { + // Validate blockHeightByChain + for (const chain in blockHeightByChain) { + const manager = this.managers[chain as ChainName]; + if (!manager) + throw new Error(`No wallets configured for chain: ${chain}`); + } + } + + await mapConcurrent( + Object.entries(this.managers), + async ([chainName, manager]) => { + const blockHeight = + blockHeightByChain?.[chainName as ChainName] ?? + (await manager.getBlockHeight()); + const balancesByChain = await manager.pullBalancesAtBlockHeight( + blockHeight, + ); + balances[chainName] = balancesByChain; + }, + ); + + return balances; } } diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index 8c8346f..dc9749f 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -21,7 +21,10 @@ import { } from "./wallets"; import { TransferRecepit } from "./wallets/base-wallet"; import { RebalanceInstruction } from "./rebalance-strategies"; -import { CoinGeckoIdsSchema, supportedTokensByEnv } from "./price-assistant/supported-tokens.config"; +import { + CoinGeckoIdsSchema, + supportedTokensByEnv, +} from "./price-assistant/supported-tokens.config"; import { ScheduledPriceFeed } from "./price-assistant/scheduled-price-feed"; import { OnDemandPriceFeed } from "./price-assistant/ondemand-price-feed"; @@ -174,11 +177,15 @@ export class WalletManager { // Inject native token into price feed config, if enabled if (chainConfig.priceFeedConfig?.enabled) { - const {supportedTokens} = chainConfig.priceFeedConfig; - const uniqueChainIds = [...new Set(supportedTokens.map(token => token.chainId))]; + const { supportedTokens } = chainConfig.priceFeedConfig; + const uniqueChainIds = [ + ...new Set(supportedTokens.map(token => token.chainId)), + ]; const nativeTokens = supportedTokensByEnv[network as Environment]; for (const chainId of uniqueChainIds) { - const nativeTokensByChainId = nativeTokens.filter(token => token.chainId === chainId); + const nativeTokensByChainId = nativeTokens.filter( + token => token.chainId === chainId, + ); if (nativeTokensByChainId.length > 0) { supportedTokens.push(...nativeTokensByChainId); } @@ -335,9 +342,7 @@ export class WalletManager { } } - private async balanceHandlerMapper( - method: "getBalances" | "pullBalances", - ) { + private async balanceHandlerMapper(method: "getBalances" | "pullBalances") { const balances: Record = {}; await mapConcurrent( @@ -373,9 +378,9 @@ export class WalletManager { } // pullBalancesAtBlockHeight doesn't need balances to be refreshed in the background - public async pullBalancesAtBlockHeight(blockHeightByChain?: Record): Promise< - Record - > { + public async pullBalancesAtBlockHeight( + blockHeightByChain?: Record, + ): Promise> { const balances: Record = {}; if (blockHeightByChain) { // Validate blockHeightByChain @@ -389,8 +394,12 @@ export class WalletManager { await mapConcurrent( Object.entries(this.managers), async ([chainName, manager]) => { - const blockHeight = blockHeightByChain?.[chainName as ChainName] ?? await manager.getBlockHeight(); - const balancesByChain = await manager.pullBalancesAtBlockHeight(blockHeight) + const blockHeight = + blockHeightByChain?.[chainName as ChainName] ?? + (await manager.getBlockHeight()); + const balancesByChain = await manager.pullBalancesAtBlockHeight( + blockHeight, + ); balances[chainName] = balancesByChain; }, ); From ec1212b19a30803f9ed9da0f731e0fb06b1835ff Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 18:39:18 +0530 Subject: [PATCH 14/28] Fix Injecting native tokens logic --- examples/wallet-manager.ts | 153 ++++++++++-------- .../supported-tokens.config.ts | 46 ++++-- src/wallet-manager.ts | 36 +++-- 3 files changed, 139 insertions(+), 96 deletions(-) diff --git a/examples/wallet-manager.ts b/examples/wallet-manager.ts index d421393..c6229bd 100644 --- a/examples/wallet-manager.ts +++ b/examples/wallet-manager.ts @@ -31,89 +31,100 @@ const allChainWallets: WalletManagerFullConfig['config'] = { }, supportedTokens: [{ chainId: 2, + chainName: "ethereum", tokenContract: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", coingeckoId: "usd-coin", symbol: "USDC" }, { chainId: 2, + chainName: "ethereum", tokenContract: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", coingeckoId: "wrapped-bitcoin", symbol: "WBTC" }], } }, - // solana: { - // wallets: [ - // { address: "6VnfVsLdLwNuuCmooLTziQ99PFXZ5vc3yyqyb9tMDhhw", tokens: ['usdc'] }, - // ], - // walletBalanceConfig: { - // enabled: true, - // scheduled: { - // enabled: false, - // } - // }, - // }, - // sui: { - // rebalance: { - // enabled: false, - // strategy: 'default', - // interval: 10000, - // minBalanceThreshold: 0.1, - // }, - // wallets: [ - // { privateKey: 'ODV9VYi3eSljEWWmpWh8s9m/P2BNNxU/Vp8jwADeNuw=' }, - // { address: '0x934042d46762fadf9f61ef07aa265fc14d28525c7051224a5f1cba2409aef307' }, - // { address: '0x341ab296bbe653b426e996b17af33718db62af3c250c04fe186c9124db5bd8b7' }, - // { address: '0x7d20dcdb2bca4f508ea9613994683eb4e76e9c4ed371169677c1be02aaf0b58e' }, - // { address: '0x18337b4c5b964b7645506542589c5ed496e794af82f98b3789fed96f61a94c96' }, - // { address: '0x9ae846f88db3476d7c9f2d8fc49722f7085a3b46aad998120dd11ebeab83e021' }, - // { address: '0xcb39d897bf0561af7531d37db9781e54528269fed4761275931ce32f20352977' }, - // { - // address: '0x8f11fe7121be742f46e2b3bc2eba081efdc3027697c317a917a2d16fd9b59ab1', - // tokens: ['USDC', 'USDT'] - // }, - // ], - // walletBalanceConfig: { - // enabled: true, - // scheduled: { - // enabled: false, - // } - // }, - // priceFeedConfig: { - // supportedTokens: [{ - // chainId: 21, - // tokenContract: "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf", - // coingeckoId: "usd-coin", - // symbol: "USDC" - // }], - // enabled: false, - // } - // }, - // klatyn: { - // rebalance: { - // enabled: false, - // strategy: 'default', - // interval: 10000, - // minBalanceThreshold: 0.1, - // }, - // wallets: [ - // { - // address: "0x80C67432656d59144cEFf962E8fAF8926599bCF8", - // tokens: ["USDC", "DAI"] - // }, - // { - // address: "0x8d0d970225597085A59ADCcd7032113226C0419d", - // tokens: [] - // } - // ], - // walletBalanceConfig: { - // enabled: true, - // scheduled: { - // enabled: false, - // } - // }, - // } + solana: { + wallets: [ + { address: "6VnfVsLdLwNuuCmooLTziQ99PFXZ5vc3yyqyb9tMDhhw", tokens: ['usdc'] }, + ], + walletBalanceConfig: { + enabled: true, + scheduled: { + enabled: false, + } + }, + priceFeedConfig: { + enabled: true, + supportedTokens: [] + } + }, + sui: { + rebalance: { + enabled: false, + strategy: 'default', + interval: 10000, + minBalanceThreshold: 0.1, + }, + wallets: [ + { privateKey: 'ODV9VYi3eSljEWWmpWh8s9m/P2BNNxU/Vp8jwADeNuw=' }, + { address: '0x934042d46762fadf9f61ef07aa265fc14d28525c7051224a5f1cba2409aef307' }, + { address: '0x341ab296bbe653b426e996b17af33718db62af3c250c04fe186c9124db5bd8b7' }, + { address: '0x7d20dcdb2bca4f508ea9613994683eb4e76e9c4ed371169677c1be02aaf0b58e' }, + { address: '0x18337b4c5b964b7645506542589c5ed496e794af82f98b3789fed96f61a94c96' }, + { address: '0x9ae846f88db3476d7c9f2d8fc49722f7085a3b46aad998120dd11ebeab83e021' }, + { address: '0xcb39d897bf0561af7531d37db9781e54528269fed4761275931ce32f20352977' }, + { + address: '0x8f11fe7121be742f46e2b3bc2eba081efdc3027697c317a917a2d16fd9b59ab1', + tokens: ['USDC', 'USDT'] + }, + ], + walletBalanceConfig: { + enabled: true, + scheduled: { + enabled: false, + } + }, + priceFeedConfig: { + supportedTokens: [{ + chainId: 21, + chainName: "sui", + tokenContract: "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf", + coingeckoId: "usd-coin", + symbol: "USDC" + }], + enabled: true, + } + }, + klatyn: { + rebalance: { + enabled: false, + strategy: 'default', + interval: 10000, + minBalanceThreshold: 0.1, + }, + wallets: [ + { + address: "0x80C67432656d59144cEFf962E8fAF8926599bCF8", + tokens: ["USDC", "DAI"] + }, + { + address: "0x8d0d970225597085A59ADCcd7032113226C0419d", + tokens: [] + } + ], + walletBalanceConfig: { + enabled: true, + scheduled: { + enabled: false, + } + }, + priceFeedConfig: { + enabled: true, + supportedTokens: [] + } + } } export const manager = buildWalletManager({ diff --git a/src/price-assistant/supported-tokens.config.ts b/src/price-assistant/supported-tokens.config.ts index 6588b94..3c03620 100644 --- a/src/price-assistant/supported-tokens.config.ts +++ b/src/price-assistant/supported-tokens.config.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { ChainName, Environment } from "../wallets"; +import { TokenInfo } from "../wallet-manager"; export const CoinGeckoIdsSchema = z .union([ @@ -25,13 +26,6 @@ export const CoinGeckoIdsSchema = z export type CoinGeckoIds = z.infer; -export interface TokenInfo { - chainId: number; - coingeckoId: CoinGeckoIds; - symbol: string; - tokenContract: string; -} - export const coinGeckoIdByChainName = { "solana": "solana", "ethereum": "ethereum", @@ -49,182 +43,210 @@ export const coinGeckoIdByChainName = { "pythnet": "pyth-network" } as const satisfies Record; -const mainnetTokens = [ +const mainnetNativeTokens = [ { chainId: 1, + chainName: "solana", coingeckoId: "solana", symbol: "WSOL", tokenContract: "069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f00000000001", }, { chainId: 2, + chainName: "ethereum", coingeckoId: "ethereum", symbol: "WETH", tokenContract: "000000000000000000000000C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", }, { chainId: 4, + chainName: "bsc", coingeckoId: "binancecoin", symbol: "WBNB", tokenContract: "000000000000000000000000bb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", }, { chainId: 5, + chainName: "polygon", coingeckoId: "matic-network", symbol: "WMATIC", tokenContract: "0000000000000000000000000d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", }, { chainId: 6, + chainName: "avalanche", coingeckoId: "avalanche-2", symbol: "WAVAX", tokenContract: "000000000000000000000000B31f66AA3C1e785363F0875A1B74E27b85FD66c7", }, { chainId: 10, + chainName: "fantom", coingeckoId: "fantom", symbol: "WFTM", tokenContract: "00000000000000000000000021be370D5312f44cB42ce377BC9b8a0cEF1A4C83", }, { chainId: 13, + chainName: "klaytn", coingeckoId: "klay-token", symbol: "WKLAY", tokenContract: "", }, { chainId: 14, + chainName: "celo", coingeckoId: "celo", symbol: "WCELO", tokenContract: "000000000000000000000000471EcE3750Da237f93B8E339c536989b8978a438", }, { chainId: 16, + chainName: "moonbeam", coingeckoId: "moonbeam", symbol: "WGLMR", tokenContract: "000000000000000000000000Acc15dC74880C9944775448304B263D191c6077F", }, { chainId: 21, + chainName: "sui", coingeckoId: "sui", symbol: "WSUI", tokenContract: "9258181f5ceac8dbffb7030890243caed69a9599d2886d957a9cb7656af3bdb3", }, { chainId: 23, + chainName: "arbitrum", coingeckoId: "arbitrum", symbol: "WARB", tokenContract: "0x912CE59144191C1204E64559FE8253a0e49E6548", }, { chainId: 24, + chainName: "optimism", coingeckoId: "optimism", symbol: "WOP", tokenContract: "0x4200000000000000000000000000000000000042", }, { chainId: 26, + chainName: "pythnet", coingeckoId: "pyth-network", symbol: "WPYTH", tokenContract: "", }, { chainId: 30, + chainName: "base", coingeckoId: "base", symbol: "WBASE", tokenContract: "0x07150e919B4De5fD6a63DE1F9384828396f25fDC", }, ] satisfies TokenInfo[]; -const testnetTokens = [ +const testnetNativeTokens = [ { chainId: 1, + chainName: "solana", coingeckoId: "solana", symbol: "SOL", tokenContract: "", }, { chainId: 2, + chainName: "ethereum", coingeckoId: "ethereum", symbol: "ETH", tokenContract: "000000000000000000000000B4FBF271143F4FBf7B91A5ded31805e42b2208d6", }, { chainId: 4, + chainName: "bsc", coingeckoId: "binancecoin", symbol: "BNB", tokenContract: "000000000000000000000000ae13d989daC2f0dEbFf460aC112a837C89BAa7cd", }, { chainId: 5, + chainName: "polygon", coingeckoId: "matic-network", symbol: "MATIC", tokenContract: "0000000000000000000000009c3C9283D3e44854697Cd22D3Faa240Cfb032889", }, { chainId: 6, + chainName: "avalanche", coingeckoId: "avalanche-2", symbol: "AVAX", tokenContract: "000000000000000000000000d00ae08403B9bbb9124bB305C09058E32C39A48c", }, { chainId: 10, + chainName: "fantom", coingeckoId: "fantom", symbol: "FTM", tokenContract: "000000000000000000000000f1277d1Ed8AD466beddF92ef448A132661956621", }, { chainId: 13, + chainName: "klaytn", coingeckoId: "klay-token", symbol: "KLAY", tokenContract: "", }, { chainId: 14, + chainName: "celo", coingeckoId: "celo", symbol: "CELO", tokenContract: "000000000000000000000000F194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9", }, { chainId: 16, + chainName: "moonbeam", coingeckoId: "moonbeam", symbol: "GLMR", tokenContract: "000000000000000000000000D909178CC99d318e4D46e7E66a972955859670E1", }, { chainId: 21, + chainName: "sui", coingeckoId: "sui", symbol: "SUI", tokenContract: "587c29de216efd4219573e08a1f6964d4fa7cb714518c2c8a0f29abfa264327d", }, { chainId: 23, + chainName: "arbitrum", coingeckoId: "arbitrum", symbol: "ARB", tokenContract: "0xF861378B543525ae0C47d33C90C954Dc774Ac1F9", }, { chainId: 24, + chainName: "optimism", coingeckoId: "optimism", symbol: "OP", tokenContract: "0x4200000000000000000000000000000000000042", }, { chainId: 26, + chainName: "pythnet", coingeckoId: "pyth-network", symbol: "PYTH", tokenContract: "", }, { chainId: 30, + chainName: "base", coingeckoId: "base", symbol: "BASE", tokenContract: "", }, ] satisfies TokenInfo[]; -export const supportedTokensByEnv: Record = { - [Environment.MAINNET]: mainnetTokens, - [Environment.TESTNET]: testnetTokens, +export const supportedNativeTokensByEnv: Record = { + [Environment.MAINNET]: mainnetNativeTokens, + [Environment.TESTNET]: testnetNativeTokens, [Environment.DEVNET]: [], }; \ No newline at end of file diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index dc9749f..1ac4869 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -23,7 +23,7 @@ import { TransferRecepit } from "./wallets/base-wallet"; import { RebalanceInstruction } from "./rebalance-strategies"; import { CoinGeckoIdsSchema, - supportedTokensByEnv, + supportedNativeTokensByEnv, } from "./price-assistant/supported-tokens.config"; import { ScheduledPriceFeed } from "./price-assistant/scheduled-price-feed"; import { OnDemandPriceFeed } from "./price-assistant/ondemand-price-feed"; @@ -40,6 +40,7 @@ export const WalletRebalancingConfigSchema = z.object({ const TokenInfoSchema = z.object({ tokenContract: z.string(), chainId: z.number(), + chainName: z.string(), coingeckoId: CoinGeckoIdsSchema, symbol: z.string().optional(), }); @@ -173,21 +174,30 @@ export class WalletManager { continue; } } - const network = chainConfig.network || getDefaultNetwork(chainName); + + let network = chainConfig.network || ""; + // Using below flag to get nativeTokens by key mainnet or testnet only not like mainnet-beta, + // if network is not passed in the wallet config + // TODO: We can use a mapper that can map the custom network string to Environment enum + let isDefaultNetworkUsed = false; + + if (!network) { + network = getDefaultNetwork(chainName); + isDefaultNetworkUsed = true; + } // Inject native token into price feed config, if enabled if (chainConfig.priceFeedConfig?.enabled) { - const { supportedTokens } = chainConfig.priceFeedConfig; - const uniqueChainIds = [ - ...new Set(supportedTokens.map(token => token.chainId)), - ]; - const nativeTokens = supportedTokensByEnv[network as Environment]; - for (const chainId of uniqueChainIds) { - const nativeTokensByChainId = nativeTokens.filter( - token => token.chainId === chainId, - ); - if (nativeTokensByChainId.length > 0) { - supportedTokens.push(...nativeTokensByChainId); + const environment: Environment = isDefaultNetworkUsed ? Environment.MAINNET : (network as Environment) ?? Environment.TESTNET; + const nativeTokens = supportedNativeTokensByEnv[environment]; + const nativeTokensByChainId = nativeTokens.filter( + (token: TokenInfo) => token.chainName === chainName, + ); + + for (const nativeToken of nativeTokensByChainId) { + const isNativeTokenDefined = chainConfig.priceFeedConfig?.supportedTokens.find(token => token.coingeckoId === nativeToken.coingeckoId); + if (!isNativeTokenDefined) { + chainConfig.priceFeedConfig.supportedTokens.push(nativeToken); } } } From 607b30893d8b2b39fc181f91ca8029b7d7bcdd46 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 20:20:12 +0530 Subject: [PATCH 15/28] Lift priceFeedOptions as walletOptions --- examples/wallet-manager.ts | 13 +++--- src/chain-wallet-manager.ts | 15 ++----- src/price-assistant/scheduled-price-feed.ts | 4 +- src/wallet-manager.ts | 44 ++++++++++++++------- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/examples/wallet-manager.ts b/examples/wallet-manager.ts index c6229bd..24e7918 100644 --- a/examples/wallet-manager.ts +++ b/examples/wallet-manager.ts @@ -25,10 +25,6 @@ const allChainWallets: WalletManagerFullConfig['config'] = { } }, priceFeedConfig: { - enabled: true, - scheduled: { - enabled: false - }, supportedTokens: [{ chainId: 2, chainName: "ethereum", @@ -56,7 +52,6 @@ const allChainWallets: WalletManagerFullConfig['config'] = { } }, priceFeedConfig: { - enabled: true, supportedTokens: [] } }, @@ -94,7 +89,6 @@ const allChainWallets: WalletManagerFullConfig['config'] = { coingeckoId: "usd-coin", symbol: "USDC" }], - enabled: true, } }, klatyn: { @@ -121,7 +115,6 @@ const allChainWallets: WalletManagerFullConfig['config'] = { } }, priceFeedConfig: { - enabled: true, supportedTokens: [] } } @@ -138,6 +131,12 @@ export const manager = buildWalletManager({ enabled: true, serve: true, port: 9091, + }, + priceFeedOptions: { + enabled: true, + scheduled: { + enabled: false, + } } } }); diff --git a/src/chain-wallet-manager.ts b/src/chain-wallet-manager.ts index ff7eede..3aaaf54 100644 --- a/src/chain-wallet-manager.ts +++ b/src/chain-wallet-manager.ts @@ -17,8 +17,6 @@ import { EVMProvider, EVMWallet } from "./wallets/evm"; import { SolanaProvider, SolanaWallet } from "./wallets/solana"; import { SuiProvider, SuiWallet } from "./wallets/sui"; import { PriceFeed, WalletBalanceConfig, WalletPriceFeedConfig, WalletRebalancingConfig } from "./wallet-manager"; -import { ScheduledPriceFeed } from "./price-assistant/scheduled-price-feed"; -import { OnDemandPriceFeed } from "./price-assistant/ondemand-price-feed"; const DEFAULT_POLL_INTERVAL = 60 * 1000; const DEFAULT_REBALANCE_INTERVAL = 60 * 1000; @@ -79,7 +77,8 @@ export class ChainWalletManager { constructor( options: any, - private wallets: WalletConfig[] + private wallets: WalletConfig[], + priceFeedInstance?: PriceFeed, ) { this.validateOptions(options); this.options = this.parseOptions(options); @@ -89,15 +88,7 @@ export class ChainWalletManager { } this.logger = createLogger(this.options.logger); - const {priceFeedConfig} = this.options; - - if (priceFeedConfig?.enabled) { - if (priceFeedConfig?.scheduled?.enabled) { - this.priceFeed = new ScheduledPriceFeed(priceFeedConfig, this.logger); - } else { - this.priceFeed = new OnDemandPriceFeed(priceFeedConfig, this.logger); - } - } + this.priceFeed = priceFeedInstance this.walletToolbox = createWalletToolbox( options.network, diff --git a/src/price-assistant/scheduled-price-feed.ts b/src/price-assistant/scheduled-price-feed.ts index 4f982c3..ea8c195 100644 --- a/src/price-assistant/scheduled-price-feed.ts +++ b/src/price-assistant/scheduled-price-feed.ts @@ -1,7 +1,7 @@ import { Gauge, Registry } from "prom-client"; import { Logger } from "winston"; import { PriceFeed, TokenPriceData } from "./price-feed"; -import { TokenInfo, WalletPriceFeedConfig } from "../wallet-manager"; +import { TokenInfo, WalletPriceFeedConfig, WalletPriceFeedOptions } from "../wallet-manager"; import { getCoingeckoPrices } from "./helper"; import { inspect } from "util"; import { CoinGeckoIds } from "./supported-tokens.config"; @@ -15,7 +15,7 @@ export class ScheduledPriceFeed extends PriceFeed { tokenPriceGauge?: Gauge; private tokenContractToCoingeckoId: Record = {}; - constructor(priceFeedConfig: WalletPriceFeedConfig, logger: Logger, registry?: Registry) { + constructor(priceFeedConfig: WalletPriceFeedConfig & WalletPriceFeedOptions, logger: Logger, registry?: Registry) { const {scheduled, supportedTokens} = priceFeedConfig; super("SCHEDULED_TOKEN_PRICE", logger, registry, scheduled?.interval); this.supportedTokens = supportedTokens; diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index 1ac4869..3973eee 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -48,8 +48,11 @@ const TokenInfoSchema = z.object({ export type TokenInfo = z.infer; export const WalletPriceFeedConfigSchema = z.object({ - enabled: z.boolean(), supportedTokens: z.array(TokenInfoSchema), +}); + +export const WalletPriceFeedOptionsSchema = z.object({ + enabled: z.boolean(), scheduled: z .object({ enabled: z.boolean().default(false), @@ -74,6 +77,8 @@ export type WalletBalanceConfig = z.infer; export type WalletPriceFeedConfig = z.infer; +export type WalletPriceFeedOptions = z.infer; + export type WalletRebalancingConfig = z.infer< typeof WalletRebalancingConfigSchema >; @@ -118,6 +123,7 @@ export const WalletManagerOptionsSchema = z.object({ .optional(), failOnInvalidChain: z.boolean().default(true), failOnInvalidTokens: z.boolean().default(true).optional(), + priceFeedOptions: WalletPriceFeedOptionsSchema.optional(), }); export type WalletManagerOptions = z.infer; @@ -175,29 +181,29 @@ export class WalletManager { } } - let network = chainConfig.network || ""; - // Using below flag to get nativeTokens by key mainnet or testnet only not like mainnet-beta, - // if network is not passed in the wallet config - // TODO: We can use a mapper that can map the custom network string to Environment enum - let isDefaultNetworkUsed = false; + const network = chainConfig.network || getDefaultNetwork(chainName); - if (!network) { - network = getDefaultNetwork(chainName); - isDefaultNetworkUsed = true; - } + const isPriceFeedEnabled = options?.priceFeedOptions?.enabled; // Inject native token into price feed config, if enabled - if (chainConfig.priceFeedConfig?.enabled) { - const environment: Environment = isDefaultNetworkUsed ? Environment.MAINNET : (network as Environment) ?? Environment.TESTNET; + if (isPriceFeedEnabled) { + // Note: It is safe to use "mainnet" fallback here, because we are only using native token's coingeckoId + const environment: Environment = chainConfig.network ? network as Environment : Environment.MAINNET; const nativeTokens = supportedNativeTokensByEnv[environment]; const nativeTokensByChainId = nativeTokens.filter( (token: TokenInfo) => token.chainName === chainName, ); + if (!chainConfig.priceFeedConfig?.supportedTokens) { + chainConfig.priceFeedConfig = { + supportedTokens: [] + } + } + for (const nativeToken of nativeTokensByChainId) { const isNativeTokenDefined = chainConfig.priceFeedConfig?.supportedTokens.find(token => token.coingeckoId === nativeToken.coingeckoId); if (!isNativeTokenDefined) { - chainConfig.priceFeedConfig.supportedTokens.push(nativeToken); + chainConfig.priceFeedConfig?.supportedTokens.push(nativeToken); } } } @@ -208,15 +214,25 @@ export class WalletManager { logger: this.logger, rebalance: chainConfig.rebalance, walletOptions: chainConfig.chainConfig, - priceFeedConfig: chainConfig.priceFeedConfig, walletBalanceConfig: chainConfig.walletBalanceConfig, balancePollInterval: options?.balancePollInterval, failOnInvalidTokens: options?.failOnInvalidTokens ?? true, }; + let priceFeedInstance + if (isPriceFeedEnabled && chainConfig.priceFeedConfig) { + // TODO: + if (options?.priceFeedOptions?.scheduled?.enabled) { + priceFeedInstance = new ScheduledPriceFeed({...chainConfig.priceFeedConfig, ...options.priceFeedOptions}, this.logger); + } else { + priceFeedInstance = new OnDemandPriceFeed(chainConfig.priceFeedConfig, this.logger); + } + } + const chainManager = new ChainWalletManager( chainManagerConfig, chainConfig.wallets, + priceFeedInstance ); chainManager.on("error", error => { From 9020fe0077c36acb65de9627af83f614085245d4 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 20:42:08 +0530 Subject: [PATCH 16/28] Optimize price feed instance instantiation --- src/price-assistant/helper.ts | 24 ++++++++++++- src/wallet-manager.ts | 66 +++++++++++++---------------------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/src/price-assistant/helper.ts b/src/price-assistant/helper.ts index 9cc9e36..7011668 100644 --- a/src/price-assistant/helper.ts +++ b/src/price-assistant/helper.ts @@ -1,7 +1,9 @@ import axios from "axios"; import { inspect } from "util"; import { Logger } from "winston"; -import { CoinGeckoIds } from "./supported-tokens.config"; +import { CoinGeckoIds, supportedNativeTokensByEnv } from "./supported-tokens.config"; +import { Environment } from "../wallets"; +import { TokenInfo, WalletManagerConfig } from "../wallet-manager"; export type CoinGeckoPriceDict = Partial<{ [k in CoinGeckoIds]: { @@ -36,3 +38,23 @@ export async function getCoingeckoPrices( return response.data; } + +export function preparePriceFeedConfig (walletConfig: WalletManagerConfig, network?: string) { + const priceFeedSupportedTokens = Object.values(walletConfig).reduce((acc, chainConfig) => { + if (!chainConfig.priceFeedConfig?.supportedTokens) return acc; + return [...acc, ...chainConfig.priceFeedConfig.supportedTokens]; + }, [] as TokenInfo[]); + + // Inject native token into price feed config, if enabled + // Note: It is safe to use "mainnet" fallback here, because we are only using native token's coingeckoId + const environment: Environment = network ? network as Environment : Environment.MAINNET; + const nativeTokens = supportedNativeTokensByEnv[environment]; + + for (const nativeToken of nativeTokens) { + const isNativeTokenDefined = priceFeedSupportedTokens.find(token => token.coingeckoId === nativeToken.coingeckoId); + if (!isNativeTokenDefined) { + priceFeedSupportedTokens.push(nativeToken); + } + } + return priceFeedSupportedTokens; +} \ No newline at end of file diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index 3973eee..289847e 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -13,7 +13,6 @@ import { } from "./chain-wallet-manager"; import { ChainName, - Environment, isChain, KNOWN_CHAINS, WalletBalance, @@ -21,12 +20,10 @@ import { } from "./wallets"; import { TransferRecepit } from "./wallets/base-wallet"; import { RebalanceInstruction } from "./rebalance-strategies"; -import { - CoinGeckoIdsSchema, - supportedNativeTokensByEnv, -} from "./price-assistant/supported-tokens.config"; +import { CoinGeckoIdsSchema } from "./price-assistant/supported-tokens.config"; import { ScheduledPriceFeed } from "./price-assistant/scheduled-price-feed"; import { OnDemandPriceFeed } from "./price-assistant/ondemand-price-feed"; +import { preparePriceFeedConfig } from "./price-assistant/helper"; export const WalletRebalancingConfigSchema = z.object({ enabled: z.boolean(), @@ -77,7 +74,9 @@ export type WalletBalanceConfig = z.infer; export type WalletPriceFeedConfig = z.infer; -export type WalletPriceFeedOptions = z.infer; +export type WalletPriceFeedOptions = z.infer< + typeof WalletPriceFeedOptionsSchema +>; export type WalletRebalancingConfig = z.infer< typeof WalletRebalancingConfigSchema @@ -171,6 +170,24 @@ export class WalletManager { } } + const isPriceFeedEnabled = options?.priceFeedOptions?.enabled; + // Create PriceFeed instance once for all chains + let priceFeedInstance; + if (isPriceFeedEnabled) { + const allSupportedTokens = preparePriceFeedConfig(config); + if (options?.priceFeedOptions?.scheduled?.enabled) { + priceFeedInstance = new ScheduledPriceFeed( + { supportedTokens: allSupportedTokens, ...options.priceFeedOptions }, + this.logger, + ); + } else { + priceFeedInstance = new OnDemandPriceFeed( + { supportedTokens: allSupportedTokens }, + this.logger, + ); + } + } + for (const [chainName, chainConfig] of Object.entries(config)) { if (!isChain(chainName)) { if (options?.failOnInvalidChain) { @@ -183,31 +200,6 @@ export class WalletManager { const network = chainConfig.network || getDefaultNetwork(chainName); - const isPriceFeedEnabled = options?.priceFeedOptions?.enabled; - - // Inject native token into price feed config, if enabled - if (isPriceFeedEnabled) { - // Note: It is safe to use "mainnet" fallback here, because we are only using native token's coingeckoId - const environment: Environment = chainConfig.network ? network as Environment : Environment.MAINNET; - const nativeTokens = supportedNativeTokensByEnv[environment]; - const nativeTokensByChainId = nativeTokens.filter( - (token: TokenInfo) => token.chainName === chainName, - ); - - if (!chainConfig.priceFeedConfig?.supportedTokens) { - chainConfig.priceFeedConfig = { - supportedTokens: [] - } - } - - for (const nativeToken of nativeTokensByChainId) { - const isNativeTokenDefined = chainConfig.priceFeedConfig?.supportedTokens.find(token => token.coingeckoId === nativeToken.coingeckoId); - if (!isNativeTokenDefined) { - chainConfig.priceFeedConfig?.supportedTokens.push(nativeToken); - } - } - } - const chainManagerConfig = { network, chainName, @@ -219,20 +211,10 @@ export class WalletManager { failOnInvalidTokens: options?.failOnInvalidTokens ?? true, }; - let priceFeedInstance - if (isPriceFeedEnabled && chainConfig.priceFeedConfig) { - // TODO: - if (options?.priceFeedOptions?.scheduled?.enabled) { - priceFeedInstance = new ScheduledPriceFeed({...chainConfig.priceFeedConfig, ...options.priceFeedOptions}, this.logger); - } else { - priceFeedInstance = new OnDemandPriceFeed(chainConfig.priceFeedConfig, this.logger); - } - } - const chainManager = new ChainWalletManager( chainManagerConfig, chainConfig.wallets, - priceFeedInstance + priceFeedInstance, ); chainManager.on("error", error => { From e21a2e17feb8bddd260aa27724dab7d12ccc0bdd Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 20:43:31 +0530 Subject: [PATCH 17/28] Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11b41f4..325efc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.22-beta.0", + "version": "0.2.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.22-beta.0", + "version": "0.2.22", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index 217e50f..00bc4b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.22-beta.0", + "version": "0.2.22", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts", From 85af2acaefca9c75c8c1af6dff1a58373f993992 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Fri, 24 Nov 2023 20:46:41 +0530 Subject: [PATCH 18/28] Add comment for wallet balance config --- src/wallet-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index 289847e..c5fdf6e 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -87,6 +87,7 @@ export const WalletManagerChainConfigSchema = z.object({ // FIXME: This should be a zod schema chainConfig: z.any().optional(), rebalance: WalletRebalancingConfigSchema.optional(), + // This config can be used to control refresh balances behaviour walletBalanceConfig: WalletBalanceConfigSchema.optional(), wallets: z.array(WalletConfigSchema), priceFeedConfig: WalletPriceFeedConfigSchema.optional(), From 7c90d2e5d18a1eede78f43f3a67436f6bc12ed87 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Tue, 28 Nov 2023 13:28:52 +0530 Subject: [PATCH 19/28] Add chainName in balances schema --- src/wallets/evm/index.ts | 2 ++ src/wallets/index.ts | 1 + src/wallets/solana/index.ts | 2 ++ src/wallets/sui/index.ts | 3 +++ 4 files changed, 8 insertions(+) diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index c5a2601..986bba6 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -240,6 +240,7 @@ export class EvmWalletToolbox extends WalletToolbox { ...balance, address, formattedBalance, + chainName: this.chainName, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, ...(tokenUsdPrice && { @@ -277,6 +278,7 @@ export class EvmWalletToolbox extends WalletToolbox { address, tokenAddress, formattedBalance, + chainName: this.chainName, symbol: tokenData.symbol, ...(tokenUsdPrice && { balanceUsd: Number(formattedBalance) * tokenUsdPrice, diff --git a/src/wallets/index.ts b/src/wallets/index.ts index 9879da8..601a310 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -63,6 +63,7 @@ export type Balance = { address: string; isNative: boolean; rawBalance: string; + chainName: ChainName; formattedBalance: string; blockHeight?: number; tokenUsdPrice?: number; diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index ad64b38..3a2dba6 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -129,6 +129,7 @@ export class SolanaWalletToolbox extends WalletToolbox { return { ...balance, address, + chainName: this.chainName, formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, @@ -184,6 +185,7 @@ export class SolanaWalletToolbox extends WalletToolbox { isNative: false, rawBalance: tokenBalance.toString(), address, + chainName: this.chainName, formattedBalance: formattedBalance.toString(), symbol: tokenKnownSymbol ?? "unknown", ...(tokenUsdPrice && { diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index 4b30b28..3b776a4 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -175,6 +175,7 @@ export class SuiWalletToolbox extends WalletToolbox { return { ...balance, address, + chainName: this.chainName, formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, @@ -203,6 +204,7 @@ export class SuiWalletToolbox extends WalletToolbox { return { tokenAddress, address, + chainName: this.chainName, isNative: false, rawBalance: "0", formattedBalance: "0", @@ -222,6 +224,7 @@ export class SuiWalletToolbox extends WalletToolbox { return { tokenAddress, address, + chainName: this.chainName, isNative: false, rawBalance: balance.totalBalance, formattedBalance, From afa0653bee823e11227f714891891eb2cdc78740 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Tue, 28 Nov 2023 13:29:27 +0530 Subject: [PATCH 20/28] Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 325efc8..094801c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.22", + "version": "0.2.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.22", + "version": "0.2.23", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index 00bc4b2..d2cd944 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.22", + "version": "0.2.23", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts", From 92e4ce5880e0efe6b5152c9fe345c8c9e91a7c56 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Tue, 28 Nov 2023 16:43:01 +0530 Subject: [PATCH 21/28] minor update --- src/i-wallet-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i-wallet-manager.ts b/src/i-wallet-manager.ts index b88ba5d..5899108 100644 --- a/src/i-wallet-manager.ts +++ b/src/i-wallet-manager.ts @@ -18,7 +18,7 @@ import { ChainName } from "./wallets"; interface IWMContextManagedLocks { withWallet

(chainName: ChainName, fn: WithWalletExecutor, opts?: WalletExecuteOptions): Promise; pullBalances: () => Promise>; - pullBalancesAtBlockHeight: () => Promise>; + pullBalancesAtBlockHeight: (blockHeightByChain?: Record) => Promise>; getBlockHeight: (chainName: ChainName) => Promise; } interface IWMBareLocks { From 2386304b85582ddc814c709ec5b2ac32e43844b5 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Tue, 28 Nov 2023 16:45:51 +0530 Subject: [PATCH 22/28] publish beta version for integration --- package-lock.json | 4 ++-- package.json | 2 +- src/wallets/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 094801c..ff3794f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23", + "version": "0.2.23-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23", + "version": "0.2.23-beta", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index d2cd944..1d5e3cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23", + "version": "0.2.23-beta", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/wallets/index.ts b/src/wallets/index.ts index 601a310..9efd54f 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -65,7 +65,7 @@ export type Balance = { rawBalance: string; chainName: ChainName; formattedBalance: string; - blockHeight?: number; + blockHeight: number; tokenUsdPrice?: number; balanceUsd?: number; }; From ed0112a81d5fecdb0c95d5faca02301f07da8be4 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Tue, 28 Nov 2023 16:50:57 +0530 Subject: [PATCH 23/28] Revert mandatory blockHeight --- package-lock.json | 4 ++-- package.json | 2 +- src/wallets/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff3794f..397c9a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta", + "version": "0.2.23-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta", + "version": "0.2.23-beta.0", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index 1d5e3cf..e25e0f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta", + "version": "0.2.23-beta.0", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/wallets/index.ts b/src/wallets/index.ts index 9efd54f..601a310 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -65,7 +65,7 @@ export type Balance = { rawBalance: string; chainName: ChainName; formattedBalance: string; - blockHeight: number; + blockHeight?: number; tokenUsdPrice?: number; balanceUsd?: number; }; From fe3b028f9bc947768ec28ada3cb05cead0b46a32 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Wed, 29 Nov 2023 14:03:31 +0530 Subject: [PATCH 24/28] Expose blockHeightPerChain method|remove chainName from balance --- package-lock.json | 4 +-- package.json | 2 +- src/grpc/client.ts | 49 ++++++++++++++++++++++++++++++------- src/i-wallet-manager.ts | 1 + src/wallet-manager.ts | 48 +++++++++++++++++++++++++++++------- src/wallets/evm/index.ts | 3 +-- src/wallets/index.ts | 1 - src/wallets/solana/index.ts | 2 -- src/wallets/sui/index.ts | 4 +-- 9 files changed, 85 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 397c9a6..cef3250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta.0", + "version": "0.2.23-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta.0", + "version": "0.2.23-beta.1", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index e25e0f8..cc6093f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta.0", + "version": "0.2.23-beta.1", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/grpc/client.ts b/src/grpc/client.ts index 9858a74..f3eccb2 100644 --- a/src/grpc/client.ts +++ b/src/grpc/client.ts @@ -138,25 +138,56 @@ export class ClientWalletManager implements IClientWalletManager { return await this.balanceHandlerMapper("pullBalances"); } + private validateBlockHeightByChain( + blockHeightByChain: Record, + ) { + for (const chain in blockHeightByChain) { + const manager = this.managers[chain as ChainName]; + if (!manager) + throw new Error(`No wallets configured for chain: ${chain}`); + } + } + + public async getBlockHeightForAllSupportedChains(): Promise< + Record + > { + // Required concurrency is the number of chains as we want to fetch the block height for all chains in parallel + // to be precise about the block height at the time of fetching balances + let blockHeightPerChain = {} as Record; + const requiredConcurrency = Object.keys(this.managers).length; + await mapConcurrent( + Object.entries(this.managers), + async ([chainName, manager]) => { + try { + const blockHeight = await manager.getBlockHeight(); + blockHeightPerChain = { + ...blockHeightPerChain, + [chainName]: blockHeight, + } as Record; + } catch (err) { + throw new Error(`No block height found for chain: ${chainName}, error: ${err}`); + } + }, + requiredConcurrency, + ); + return blockHeightPerChain; + } + + // pullBalancesAtBlockHeight doesn't need balances to be refreshed in the background public async pullBalancesAtBlockHeight( blockHeightByChain?: Record, ): Promise> { const balances: Record = {}; if (blockHeightByChain) { - // Validate blockHeightByChain - for (const chain in blockHeightByChain) { - const manager = this.managers[chain as ChainName]; - if (!manager) - throw new Error(`No wallets configured for chain: ${chain}`); - } + this.validateBlockHeightByChain(blockHeightByChain); } + const blockHeightPerChain = blockHeightByChain ?? await this.getBlockHeightForAllSupportedChains(); + await mapConcurrent( Object.entries(this.managers), async ([chainName, manager]) => { - const blockHeight = - blockHeightByChain?.[chainName as ChainName] ?? - (await manager.getBlockHeight()); + const blockHeight = blockHeightPerChain[chainName as ChainName]; const balancesByChain = await manager.pullBalancesAtBlockHeight( blockHeight, ); diff --git a/src/i-wallet-manager.ts b/src/i-wallet-manager.ts index 5899108..09e26db 100644 --- a/src/i-wallet-manager.ts +++ b/src/i-wallet-manager.ts @@ -20,6 +20,7 @@ interface IWMContextManagedLocks { pullBalances: () => Promise>; pullBalancesAtBlockHeight: (blockHeightByChain?: Record) => Promise>; getBlockHeight: (chainName: ChainName) => Promise; + getBlockHeightForAllSupportedChains: () => Promise>; } interface IWMBareLocks { acquireLock(chainName: ChainName, opts?: WalletExecuteOptions): Promise diff --git a/src/wallet-manager.ts b/src/wallet-manager.ts index c5fdf6e..e290bc0 100644 --- a/src/wallet-manager.ts +++ b/src/wallet-manager.ts @@ -386,26 +386,56 @@ export class WalletManager { return await this.balanceHandlerMapper("pullBalances"); } + private validateBlockHeightByChain( + blockHeightByChain: Record, + ) { + for (const chain in blockHeightByChain) { + const manager = this.managers[chain as ChainName]; + if (!manager) + throw new Error(`No wallets configured for chain: ${chain}`); + } + } + + public async getBlockHeightForAllSupportedChains(): Promise< + Record + > { + // Required concurrency is the number of chains as we want to fetch the block height for all chains in parallel + // to be precise about the block height at the time of fetching balances + let blockHeightPerChain = {} as Record; + const requiredConcurrency = Object.keys(this.managers).length; + await mapConcurrent( + Object.entries(this.managers), + async ([chainName, manager]) => { + try { + const blockHeight = await manager.getBlockHeight(); + blockHeightPerChain = { + ...blockHeightPerChain, + [chainName]: blockHeight, + } as Record; + } catch (err) { + throw new Error(`No block height found for chain: ${chainName}, error: ${err}`); + } + }, + requiredConcurrency, + ); + return blockHeightPerChain; + } + // pullBalancesAtBlockHeight doesn't need balances to be refreshed in the background public async pullBalancesAtBlockHeight( blockHeightByChain?: Record, ): Promise> { const balances: Record = {}; if (blockHeightByChain) { - // Validate blockHeightByChain - for (const chain in blockHeightByChain) { - const manager = this.managers[chain as ChainName]; - if (!manager) - throw new Error(`No wallets configured for chain: ${chain}`); - } + this.validateBlockHeightByChain(blockHeightByChain); } + const blockHeightPerChain = blockHeightByChain ?? await this.getBlockHeightForAllSupportedChains(); + await mapConcurrent( Object.entries(this.managers), async ([chainName, manager]) => { - const blockHeight = - blockHeightByChain?.[chainName as ChainName] ?? - (await manager.getBlockHeight()); + const blockHeight = blockHeightPerChain[chainName as ChainName]; const balancesByChain = await manager.pullBalancesAtBlockHeight( blockHeight, ); diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 986bba6..547cd21 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -240,7 +240,6 @@ export class EvmWalletToolbox extends WalletToolbox { ...balance, address, formattedBalance, - chainName: this.chainName, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, ...(tokenUsdPrice && { @@ -278,7 +277,7 @@ export class EvmWalletToolbox extends WalletToolbox { address, tokenAddress, formattedBalance, - chainName: this.chainName, + symbol: tokenData.symbol, ...(tokenUsdPrice && { balanceUsd: Number(formattedBalance) * tokenUsdPrice, diff --git a/src/wallets/index.ts b/src/wallets/index.ts index 601a310..9879da8 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -63,7 +63,6 @@ export type Balance = { address: string; isNative: boolean; rawBalance: string; - chainName: ChainName; formattedBalance: string; blockHeight?: number; tokenUsdPrice?: number; diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index 3a2dba6..ad64b38 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -129,7 +129,6 @@ export class SolanaWalletToolbox extends WalletToolbox { return { ...balance, address, - chainName: this.chainName, formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, @@ -185,7 +184,6 @@ export class SolanaWalletToolbox extends WalletToolbox { isNative: false, rawBalance: tokenBalance.toString(), address, - chainName: this.chainName, formattedBalance: formattedBalance.toString(), symbol: tokenKnownSymbol ?? "unknown", ...(tokenUsdPrice && { diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index 3b776a4..57eedd5 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -175,7 +175,6 @@ export class SuiWalletToolbox extends WalletToolbox { return { ...balance, address, - chainName: this.chainName, formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, @@ -204,7 +203,7 @@ export class SuiWalletToolbox extends WalletToolbox { return { tokenAddress, address, - chainName: this.chainName, + isNative: false, rawBalance: "0", formattedBalance: "0", @@ -224,7 +223,6 @@ export class SuiWalletToolbox extends WalletToolbox { return { tokenAddress, address, - chainName: this.chainName, isNative: false, rawBalance: balance.totalBalance, formattedBalance, From ba555ff363497d1cad720556c8568b17d0d3c46e Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Wed, 29 Nov 2023 14:27:38 +0530 Subject: [PATCH 25/28] Bump package to latest version w.r.t main branch --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cef3250..73d428d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta.1", + "version": "0.2.25-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta.1", + "version": "0.2.25-beta.0", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index cc6093f..d11dac9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.23-beta.1", + "version": "0.2.25-beta.0", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts", From b9c8262917df44281acc24b1c9057e56fa5e6fee Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Mon, 4 Dec 2023 18:22:18 +0530 Subject: [PATCH 26/28] Resolve PR review comment --- src/wallets/base-wallet.ts | 2 +- src/wallets/cosmos/index.ts | 1 + src/wallets/evm/index.ts | 1 + src/wallets/solana/index.ts | 3 ++- src/wallets/sui/index.ts | 3 ++- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/wallets/base-wallet.ts b/src/wallets/base-wallet.ts index e4fca3e..d0f21e6 100644 --- a/src/wallets/base-wallet.ts +++ b/src/wallets/base-wallet.ts @@ -146,7 +146,7 @@ export abstract class WalletToolbox { minBalanceThreshold ?? 0, ); - balances.push({ ...nativeBalance, blockHeight }); + balances.push(nativeBalance); this.logger.debug( `Balances for ${address} pulled: ${JSON.stringify(nativeBalance)}`, diff --git a/src/wallets/cosmos/index.ts b/src/wallets/cosmos/index.ts index 353bc36..9a492c7 100644 --- a/src/wallets/cosmos/index.ts +++ b/src/wallets/cosmos/index.ts @@ -170,6 +170,7 @@ export class CosmosWalletToolbox extends WalletToolbox { this.provider = await StargateClient.connect(this.options.nodeUrl!); } + // TODO: Implement getNativeBalance by blockHeight if possible public async pullNativeBalance(address: string): Promise { const { nativeDenom, defaultDecimals } = this.chainConfig.defaultConfigs[this.network]; diff --git a/src/wallets/evm/index.ts b/src/wallets/evm/index.ts index 547cd21..8af5fb8 100644 --- a/src/wallets/evm/index.ts +++ b/src/wallets/evm/index.ts @@ -242,6 +242,7 @@ export class EvmWalletToolbox extends WalletToolbox { formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, + blockHeight, ...(tokenUsdPrice && { balanceUsd: Number(formattedBalance) * tokenUsdPrice, tokenUsdPrice diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index ad64b38..f3f9db7 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -115,7 +115,7 @@ export class SolanaWalletToolbox extends WalletToolbox { return validTokens; } - public async pullNativeBalance(address: string): Promise { + public async pullNativeBalance(address: string, blockHeight?: number): Promise { const balance = await pullSolanaNativeBalance(this.connection, address); const formattedBalance = ( Number(balance.rawBalance) / LAMPORTS_PER_SOL @@ -132,6 +132,7 @@ export class SolanaWalletToolbox extends WalletToolbox { formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, + blockHeight, ...(tokenUsdPrice && { balanceUsd: Number(formattedBalance) * tokenUsdPrice, tokenUsdPrice diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index 57eedd5..830cf7a 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -163,7 +163,7 @@ export class SuiWalletToolbox extends WalletToolbox { }, [] as string[]); } - public async pullNativeBalance(address: string): Promise { + public async pullNativeBalance(address: string, blockHeight?: number): Promise { const balance = await pullSuiNativeBalance(this.connection, address); const formattedBalance = String(+balance.rawBalance / 10 ** 9); @@ -178,6 +178,7 @@ export class SuiWalletToolbox extends WalletToolbox { formattedBalance, tokens: [], symbol: this.chainConfig.nativeCurrencySymbol, + blockHeight, ...(tokenUsdPrice && { balanceUsd: Number(formattedBalance) * tokenUsdPrice, tokenUsdPrice From 77fd30c8d2a4efdca5e53d6b97aca994cd131575 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Mon, 4 Dec 2023 19:23:57 +0530 Subject: [PATCH 27/28] Add coingeckoIds for cosm-wasm chains --- src/price-assistant/supported-tokens.config.ts | 17 +++++++++++++++-- src/wallets/solana/index.ts | 5 +++++ src/wallets/sui/index.ts | 4 ++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/price-assistant/supported-tokens.config.ts b/src/price-assistant/supported-tokens.config.ts index 3c03620..298c0f5 100644 --- a/src/price-assistant/supported-tokens.config.ts +++ b/src/price-assistant/supported-tokens.config.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { ChainName, Environment } from "../wallets"; import { TokenInfo } from "../wallet-manager"; +// get all coingeckoIds from here: https://api.coingecko.com/api/v3/coins/list export const CoinGeckoIdsSchema = z .union([ z.literal("solana"), @@ -21,7 +22,13 @@ export const CoinGeckoIdsSchema = z z.literal("optimism"), z.literal("klay-token"), z.literal("base"), - z.literal("pyth-network") + z.literal("pyth-network"), + z.literal("sepolia"), + z.literal("osmosis"), + z.literal("cosmos"), + z.literal("evmos"), + z.literal("kujira"), + z.literal("gateway"), ]); export type CoinGeckoIds = z.infer; @@ -40,7 +47,13 @@ export const coinGeckoIdByChainName = { "optimism": "optimism", "base": "base", "klaytn": "klay-token", - "pythnet": "pyth-network" + "pythnet": "pyth-network", + "sepolia": "ethereum", + "osmosis": "osmosis", + "cosmoshub": "cosmos", + "evmos": "evmos", + "kujira": "kujira", + "gateway": "gateway" } as const satisfies Record; const mainnetNativeTokens = [ diff --git a/src/wallets/solana/index.ts b/src/wallets/solana/index.ts index f3f9db7..bc83312 100644 --- a/src/wallets/solana/index.ts +++ b/src/wallets/solana/index.ts @@ -121,6 +121,11 @@ export class SolanaWalletToolbox extends WalletToolbox { Number(balance.rawBalance) / LAMPORTS_PER_SOL ).toString(); + + if (blockHeight) { + this.logger.warn(`Solana does not support pulling balances by block height, ignoring blockHeight: ${blockHeight}`); + } + // Pull prices in USD for all the native tokens in single network call await this.priceFeed?.pullTokenPrices(); const coingeckoId = coinGeckoIdByChainName[this.chainName]; diff --git a/src/wallets/sui/index.ts b/src/wallets/sui/index.ts index 7e6d70b..d21af83 100644 --- a/src/wallets/sui/index.ts +++ b/src/wallets/sui/index.ts @@ -166,6 +166,10 @@ export class SuiWalletToolbox extends WalletToolbox { public async pullNativeBalance(address: string, blockHeight?: number): Promise { const balance = await pullSuiNativeBalance(this.connection, address); const formattedBalance = String(+balance.rawBalance / 10 ** 9); + + if (blockHeight) { + this.logger.warn(`Sui does not support pulling balances by block height, ignoring blockHeight: ${blockHeight}`); + } // Pull prices in USD for all the native tokens in single network call await this.priceFeed?.pullTokenPrices(); From 8ca519b225bf08c16d96de7d52cddc4c2774ea58 Mon Sep 17 00:00:00 2001 From: Abhishek Rajput Date: Mon, 4 Dec 2023 19:34:49 +0530 Subject: [PATCH 28/28] Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fc590b..cdf3558 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.25", + "version": "0.2.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.25", + "version": "0.2.26", "license": "MIT", "dependencies": { "@cosmjs/proto-signing": "^0.31.1", diff --git a/package.json b/package.json index d86e70d..edcbe39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xlabs-xyz/wallet-monitor", - "version": "0.2.25", + "version": "0.2.26", "description": "A set of utilities to monitor blockchain wallets and react to them", "main": "lib/index.js", "types": "lib/index.d.ts",