diff --git a/apps/price_pusher/config.solana.mainnet.sample.json b/apps/price_pusher/config.solana.mainnet.sample.json index 77b22187ee..34d5d6eeb0 100644 --- a/apps/price_pusher/config.solana.mainnet.sample.json +++ b/apps/price_pusher/config.solana.mainnet.sample.json @@ -8,7 +8,7 @@ "dynamic-jito-tips": true, "jito-bundle-size": "5", "updates-per-jito-bundle": "6", - "price-config-file": "./price-config.yaml", + "price-config-file": "./price-config.stable.sample.yaml", "price-service-endpoint": "https://hermes.pyth.network/", "pyth-contract-address": "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT", "pushing-frequency": "30" diff --git a/apps/price_pusher/config.solana.testnet.sample.json b/apps/price_pusher/config.solana.testnet.sample.json index 7a6fa8ff1b..a50c7010c5 100644 --- a/apps/price_pusher/config.solana.testnet.sample.json +++ b/apps/price_pusher/config.solana.testnet.sample.json @@ -2,7 +2,7 @@ "endpoint": "https://api.devnet.solana.com", "keypair-file": "./id.json", "shard-id": 1, - "price-config-file": "./price-config.yaml", + "price-config-file": "./price-config.stable.sample.yaml", "price-service-endpoint": "https://hermes.pyth.network/", "pyth-contract-address": "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT", "pushing-frequency": "30" diff --git a/apps/price_pusher/package.json b/apps/price_pusher/package.json index a2817e9443..04a4a3dab5 100644 --- a/apps/price_pusher/package.json +++ b/apps/price_pusher/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/price-pusher", - "version": "9.1.3", + "version": "9.1.4", "description": "Pyth Price Pusher", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/apps/price_pusher/src/solana/balance-tracker.ts b/apps/price_pusher/src/solana/balance-tracker.ts new file mode 100644 index 0000000000..3b22d15983 --- /dev/null +++ b/apps/price_pusher/src/solana/balance-tracker.ts @@ -0,0 +1,94 @@ +import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"; +import { + BaseBalanceTracker, + BaseBalanceTrackerConfig, + IBalanceTracker, +} from "../interface"; +import { DurationInSeconds } from "../utils"; +import { PricePusherMetrics } from "../metrics"; +import { Logger } from "pino"; + +/** + * Solana-specific configuration for balance tracker + */ +export interface SolanaBalanceTrackerConfig extends BaseBalanceTrackerConfig { + /** Solana connection instance */ + connection: Connection; + /** Solana public key */ + publicKey: PublicKey; +} + +/** + * Solana-specific implementation of the balance tracker + */ +export class SolanaBalanceTracker extends BaseBalanceTracker { + private connection: Connection; + private publicKey: PublicKey; + + constructor(config: SolanaBalanceTrackerConfig) { + super({ + ...config, + logger: config.logger.child({ module: "SolanaBalanceTracker" }), + }); + + this.connection = config.connection; + this.publicKey = config.publicKey; + } + + /** + * Solana-specific implementation of balance update + */ + protected async updateBalance(): Promise { + try { + const balanceInLamports = await this.connection.getBalance( + this.publicKey, + ); + + // Convert from lamports to SOL + const balanceInSol = balanceInLamports / LAMPORTS_PER_SOL; + + this.metrics.updateWalletBalance( + this.address, + this.network, + balanceInSol, + ); + this.logger.debug( + `Updated Solana wallet balance: ${this.address} = ${balanceInSol.toString()} SOL (${balanceInLamports} lamports)`, + ); + } catch (error) { + this.logger.error( + { error }, + "Error fetching Solana wallet balance for metrics", + ); + } + } +} + +/** + * Parameters for creating a Solana balance tracker + */ +export interface CreateSolanaBalanceTrackerParams { + connection: Connection; + publicKey: PublicKey; + network: string; + updateInterval: DurationInSeconds; + metrics: PricePusherMetrics; + logger: Logger; +} + +/** + * Factory function to create a balance tracker for Solana + */ +export function createSolanaBalanceTracker( + params: CreateSolanaBalanceTrackerParams, +): IBalanceTracker { + return new SolanaBalanceTracker({ + connection: params.connection, + publicKey: params.publicKey, + address: params.publicKey.toString(), + network: params.network, + updateInterval: params.updateInterval, + metrics: params.metrics, + logger: params.logger, + }); +} diff --git a/apps/price_pusher/src/solana/command.ts b/apps/price_pusher/src/solana/command.ts index 6070126e06..93420ef79d 100644 --- a/apps/price_pusher/src/solana/command.ts +++ b/apps/price_pusher/src/solana/command.ts @@ -21,6 +21,10 @@ import pino from "pino"; import { Logger } from "pino"; import { HermesClient } from "@pythnetwork/hermes-client"; import { filterInvalidPriceItems } from "../utils"; +import { PricePusherMetrics } from "../metrics"; +import { createSolanaBalanceTracker } from "./balance-tracker"; +import { IBalanceTracker } from "../interface"; + export default { command: "solana", describe: "run price pusher for solana", @@ -99,6 +103,8 @@ export default { ...options.pushingFrequency, ...options.logLevel, ...options.controllerLogLevel, + ...options.enableMetrics, + ...options.metricsPort, }, handler: async function (argv: any) { const { @@ -122,6 +128,8 @@ export default { treasuryId, logLevel, controllerLogLevel, + enableMetrics, + metricsPort, } = argv; const logger = pino({ level: logLevel }); @@ -130,6 +138,14 @@ export default { const hermesClient = new HermesClient(priceServiceEndpoint); + // Initialize metrics if enabled + let metrics: PricePusherMetrics | undefined; + if (enableMetrics) { + metrics = new PricePusherMetrics(logger.child({ module: "Metrics" })); + metrics.start(metricsPort); + logger.info(`Metrics server started on port ${metricsPort}`); + } + let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias })); // Better to filter out invalid price items before creating the pyth listener @@ -152,11 +168,10 @@ export default { logger.child({ module: "PythPriceListener" }), ); - const wallet = new NodeWallet( - Keypair.fromSecretKey( - Uint8Array.from(JSON.parse(fs.readFileSync(keypairFile, "ascii"))), - ), + const keypair = Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(fs.readFileSync(keypairFile, "ascii"))), ); + const wallet = new NodeWallet(keypair); const connection = new Connection(endpoint, "processed"); const pythSolanaReceiver = new PythSolanaReceiver({ @@ -166,6 +181,21 @@ export default { treasuryId: treasuryId, }); + // Create and start the balance tracker if metrics are enabled + if (metrics) { + const balanceTracker: IBalanceTracker = createSolanaBalanceTracker({ + connection, + publicKey: keypair.publicKey, + network: "solana", + updateInterval: 60, + metrics, + logger, + }); + + // Start the balance tracker + await balanceTracker.start(); + } + // Fetch the account lookup table if provided const lookupTableAccount = addressLookupTableAccount ? await connection @@ -220,7 +250,10 @@ export default { solanaPriceListener, solanaPricePusher, logger.child({ module: "Controller" }, { level: controllerLogLevel }), - { pushingFrequency }, + { + pushingFrequency, + metrics, + }, ); controller.start();