diff --git a/apps/price_pusher/grafana-dashboard.sample.json b/apps/price_pusher/grafana-dashboard.sample.json index ab10dbf8de..137b69a746 100644 --- a/apps/price_pusher/grafana-dashboard.sample.json +++ b/apps/price_pusher/grafana-dashboard.sample.json @@ -18,7 +18,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 1, + "id": 86, "links": [], "panels": [ { @@ -60,7 +60,7 @@ }, "gridPos": { "h": 6, - "w": 6, + "w": 12, "x": 0, "y": 1 }, @@ -82,7 +82,7 @@ "textMode": "value", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.1.0", "repeat": "chain", "repeatDirection": "h", "targets": [ @@ -141,7 +141,7 @@ }, "gridPos": { "h": 6, - "w": 6, + "w": 12, "x": 0, "y": 8 }, @@ -163,7 +163,7 @@ "textMode": "value", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.1.0", "repeat": "chain", "repeatDirection": "h", "targets": [ @@ -226,7 +226,7 @@ }, "gridPos": { "h": 6, - "w": 8, + "w": 12, "x": 0, "y": 15 }, @@ -248,7 +248,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.1.0", "repeat": "chain", "repeatDirection": "h", "targets": [ @@ -381,7 +381,7 @@ } ] }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.1.0", "repeat": "chain", "repeatDirection": "v", "targets": [ @@ -440,6 +440,7 @@ "excludeByName": { "Time": true, "Value #A": true, + "Value #C": false, "__name__": true, "__name__#B": true, "__name__#C": true, @@ -458,14 +459,13 @@ "job#C": true, "price_id": false, "price_id#B": true, - "price_id#C": true, - "Value #C": false + "price_id#C": true }, "includeByName": { - "alias 1": true, - "price_id": true, "Value #B": true, - "Value #C": true + "Value #C": true, + "alias 1": true, + "price_id": true }, "indexByName": { "Time 1": 4, @@ -504,7 +504,7 @@ "h": 1, "w": 24, "x": 0, - "y": 30 + "y": 38 }, "id": 19, "panels": [], @@ -528,7 +528,6 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, - "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -589,7 +588,7 @@ "h": 8, "w": 12, "x": 0, - "y": 31 + "y": 39 }, "id": 3, "options": { @@ -630,7 +629,7 @@ "h": 1, "w": 24, "x": 0, - "y": 39 + "y": 47 }, "id": 14, "panels": [], @@ -706,9 +705,9 @@ }, "gridPos": { "h": 8, - "w": 8, + "w": 12, "x": 0, - "y": 40 + "y": 48 }, "id": 13, "options": { @@ -765,7 +764,7 @@ "h": 1, "w": 24, "x": 0, - "y": 48 + "y": 56 }, "id": 20, "panels": [], @@ -806,9 +805,9 @@ }, "gridPos": { "h": 6, - "w": 8, + "w": 12, "x": 0, - "y": 49 + "y": 57 }, "id": 10, "options": { @@ -828,7 +827,7 @@ "textMode": "value_and_name", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.1.0", "repeat": "chain", "repeatDirection": "h", "targets": [ @@ -865,7 +864,6 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, - "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -906,10 +904,10 @@ "overrides": [] }, "gridPos": { - "h": 8, + "h": 6, "w": 12, "x": 0, - "y": 55 + "y": 63 }, "id": 11, "options": { @@ -950,7 +948,7 @@ "h": 1, "w": 24, "x": 0, - "y": 63 + "y": 69 }, "id": 21, "panels": [], @@ -974,7 +972,6 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, - "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -1019,7 +1016,7 @@ "h": 8, "w": 12, "x": 0, - "y": 64 + "y": 70 }, "id": 6, "options": { @@ -1053,11 +1050,149 @@ ], "title": "Failed Updates: ${chain}", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 78 + }, + "id": 188, + "panels": [], + "title": "Tx Hash", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "ads9ouz3jh4hsa" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 79 + }, + "id": 187, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "repeat": "chain", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "ads9ouz3jh4hsa" + }, + "editorMode": "code", + "expr": "{namespace=~\"$chain\"} | logfmt | json | msg =~ `.*(Price update successful|Transaction confirmed|Successfully updated price).*` | line_format `Tx Hash: {{.hash}}`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Tx Hash: ${chain}", + "type": "logs" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 87 + }, + "id": 31, + "panels": [], + "title": "Logs", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "ads9ouz3jh4hsa" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 88 + }, + "id": 41, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "ads9ouz3jh4hsa" + }, + "editorMode": "code", + "expr": "{namespace=~\"$chain\"} | logfmt", + "queryType": "range", + "refId": "A" + } + ], + "title": "All Logs", + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "ads9ouz3jh4hsa" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 88 + }, + "id": 42, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "ads9ouz3jh4hsa" + }, + "editorMode": "builder", + "expr": "{namespace=~\"$chain\"} | logfmt | detected_level = `error`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Error Logs", + "type": "logs" } ], - "preload": false, "refresh": "5s", - "schemaVersion": 40, + "schemaVersion": 39, "tags": [], "templating": { "list": [ @@ -1065,10 +1200,12 @@ "current": { "selected": true, "text": [ - "All" + "optimism-sepolia-price-pusher-testnet", + "sui-price-pusher-mainnet" ], "value": [ - "$__all" + "optimism-sepolia-price-pusher-testnet", + "sui-price-pusher-mainnet" ] }, "datasource": { @@ -1097,13 +1234,13 @@ ] }, "time": { - "from": "now-1h", + "from": "now-3h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Pyth Price Pusher Dashboard", "uid": "pyth-price-pusher", - "version": 44, + "version": 12, "weekStart": "" } diff --git a/apps/price_pusher/package.json b/apps/price_pusher/package.json index fc76bc44d1..a2817e9443 100644 --- a/apps/price_pusher/package.json +++ b/apps/price_pusher/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/price-pusher", - "version": "9.1.2", + "version": "9.1.3", "description": "Pyth Price Pusher", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/apps/price_pusher/src/aptos/balance-tracker.ts b/apps/price_pusher/src/aptos/balance-tracker.ts new file mode 100644 index 0000000000..86c4059d48 --- /dev/null +++ b/apps/price_pusher/src/aptos/balance-tracker.ts @@ -0,0 +1,111 @@ +import { AptosClient } from "aptos"; +import { + BaseBalanceTracker, + BaseBalanceTrackerConfig, + IBalanceTracker, +} from "../interface"; +import { DurationInSeconds } from "../utils"; +import { PricePusherMetrics } from "../metrics"; +import { Logger } from "pino"; + +/** + * Aptos-specific configuration for balance tracker + */ +export interface AptosBalanceTrackerConfig extends BaseBalanceTrackerConfig { + /** Aptos node endpoint URL */ + endpoint: string; + /** Aptos account address */ + address: string; + /** Optional decimal places for APT token (default: 8) */ + decimals?: number; +} + +/** + * Aptos-specific implementation of the balance tracker + */ +export class AptosBalanceTracker extends BaseBalanceTracker { + private client: AptosClient; + private aptosAddress: string; + private decimals: number; + + constructor(config: AptosBalanceTrackerConfig) { + super({ + ...config, + logger: config.logger.child({ module: "AptosBalanceTracker" }), + }); + + this.client = new AptosClient(config.endpoint); + this.aptosAddress = config.address; + // APT has 8 decimal places by default + this.decimals = config.decimals ?? 8; + } + + /** + * Aptos-specific implementation of balance update + * Fetches the native APT balance for the configured address + */ + protected async updateBalance(): Promise { + try { + // Get account resource to check the balance + const accountResource = await this.client.getAccountResource( + this.aptosAddress, + "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", + ); + + // Extract the balance value from the account resource + const rawBalance = (accountResource.data as any).coin.value; + + // Convert the balance to a bigint + const balance = BigInt(rawBalance); + + // Calculate the normalized balance for display + const normalizedBalance = Number(balance) / Math.pow(10, this.decimals); + + // Update metrics with the new balance + this.metrics.updateWalletBalance( + this.address, + this.network, + normalizedBalance, + ); + + this.logger.debug( + `Updated Aptos wallet balance: ${this.address} = ${normalizedBalance.toString()} APT (raw: ${balance.toString()})`, + ); + } catch (error) { + this.logger.error( + { error }, + "Error fetching Aptos wallet balance for metrics", + ); + } + } +} + +/** + * Parameters for creating an Aptos balance tracker + */ +export interface CreateAptosBalanceTrackerParams { + endpoint: string; + address: string; + network: string; + updateInterval: DurationInSeconds; + metrics: PricePusherMetrics; + logger: Logger; + decimals?: number; +} + +/** + * Factory function to create a balance tracker for Aptos chain + */ +export function createAptosBalanceTracker( + params: CreateAptosBalanceTrackerParams, +): IBalanceTracker { + return new AptosBalanceTracker({ + endpoint: params.endpoint, + address: params.address, + network: params.network, + updateInterval: params.updateInterval, + metrics: params.metrics, + logger: params.logger, + decimals: params.decimals, + }); +} diff --git a/apps/price_pusher/src/aptos/command.ts b/apps/price_pusher/src/aptos/command.ts index 675b451d05..69ddf523ba 100644 --- a/apps/price_pusher/src/aptos/command.ts +++ b/apps/price_pusher/src/aptos/command.ts @@ -13,6 +13,9 @@ import { import { AptosAccount } from "aptos"; import pino from "pino"; import { filterInvalidPriceItems } from "../utils"; +import { PricePusherMetrics } from "../metrics"; +import { createAptosBalanceTracker } from "./balance-tracker"; + export default { command: "aptos", describe: "run price pusher for aptos", @@ -40,6 +43,8 @@ export default { ...options.pushingFrequency, ...options.logLevel, ...options.controllerLogLevel, + ...options.enableMetrics, + ...options.metricsPort, }, handler: async function (argv: any) { // FIXME: type checks for this @@ -54,6 +59,8 @@ export default { overrideGasPriceMultiplier, logLevel, controllerLogLevel, + enableMetrics, + metricsPort, } = argv; const logger = pino({ level: logLevel }); @@ -61,6 +68,14 @@ export default { const priceConfigs = readPriceConfigFile(priceConfigFile); 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}`); + } + const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim(); const account = AptosAccount.fromDerivePath( APTOS_ACCOUNT_HD_PATH, @@ -113,9 +128,27 @@ export default { aptosListener, aptosPusher, logger.child({ module: "Controller" }, { level: controllerLogLevel }), - { pushingFrequency }, + { + pushingFrequency, + metrics, + }, ); + // Create and start the balance tracker if metrics are enabled + if (metrics) { + const balanceTracker = createAptosBalanceTracker({ + address: account.address().toString(), + endpoint, + network: "aptos", + updateInterval: pushingFrequency, + metrics, + logger: logger.child({ module: "AptosBalanceTracker" }), + }); + + // Start the balance tracker + await balanceTracker.start(); + } + controller.start(); }, }; diff --git a/apps/price_pusher/src/balance-tracker.ts b/apps/price_pusher/src/balance-tracker.ts deleted file mode 100644 index f11b9f0290..0000000000 --- a/apps/price_pusher/src/balance-tracker.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PricePusherMetrics } from "./metrics"; -import { Logger } from "pino"; -import { DurationInSeconds } from "./utils"; -import { IBalanceTracker } from "./interface"; -import { EvmBalanceTracker } from "./evm/balance-tracker"; -import { SuperWalletClient } from "./evm/super-wallet"; - -/** - * Parameters for creating an EVM balance tracker - */ -export interface CreateEvmBalanceTrackerParams { - client: SuperWalletClient; - address: `0x${string}`; - network: string; - updateInterval: DurationInSeconds; - metrics: PricePusherMetrics; - logger: Logger; -} - -/** - * Factory function to create a balance tracker for EVM chains - */ -export function createEvmBalanceTracker( - params: CreateEvmBalanceTrackerParams, -): IBalanceTracker { - return new EvmBalanceTracker({ - client: params.client, - address: params.address, - network: params.network, - updateInterval: params.updateInterval, - metrics: params.metrics, - logger: params.logger, - }); -} - -// Additional factory functions for other chains would follow the same pattern: -// export function createSuiBalanceTracker(params: CreateSuiBalanceTrackerParams): IBalanceTracker { ... } -// export function createSolanaBalanceTracker(params: CreateSolanaBalanceTrackerParams): IBalanceTracker { ... } diff --git a/apps/price_pusher/src/evm/balance-tracker.ts b/apps/price_pusher/src/evm/balance-tracker.ts index 2170c4c7f4..01023ab55a 100644 --- a/apps/price_pusher/src/evm/balance-tracker.ts +++ b/apps/price_pusher/src/evm/balance-tracker.ts @@ -1,5 +1,12 @@ import { SuperWalletClient } from "./super-wallet"; -import { BaseBalanceTracker, BaseBalanceTrackerConfig } from "../interface"; +import { + BaseBalanceTracker, + BaseBalanceTrackerConfig, + IBalanceTracker, +} from "../interface"; +import { DurationInSeconds } from "../utils"; +import { PricePusherMetrics } from "../metrics"; +import { Logger } from "pino"; /** * EVM-specific configuration for balance tracker @@ -49,3 +56,31 @@ export class EvmBalanceTracker extends BaseBalanceTracker { } } } + +/** + * Parameters for creating an EVM balance tracker + */ +export interface CreateEvmBalanceTrackerParams { + client: SuperWalletClient; + address: `0x${string}`; + network: string; + updateInterval: DurationInSeconds; + metrics: PricePusherMetrics; + logger: Logger; +} + +/** + * Factory function to create a balance tracker for EVM chains + */ +export function createEvmBalanceTracker( + params: CreateEvmBalanceTrackerParams, +): IBalanceTracker { + return new EvmBalanceTracker({ + client: params.client, + address: params.address, + network: params.network, + updateInterval: params.updateInterval, + metrics: params.metrics, + logger: params.logger, + }); +} diff --git a/apps/price_pusher/src/evm/command.ts b/apps/price_pusher/src/evm/command.ts index 14ad68b448..044b274fcf 100644 --- a/apps/price_pusher/src/evm/command.ts +++ b/apps/price_pusher/src/evm/command.ts @@ -12,7 +12,7 @@ import { createClient } from "./super-wallet"; import { createPythContract } from "./pyth-contract"; import { isWsEndpoint, filterInvalidPriceItems } from "../utils"; import { PricePusherMetrics } from "../metrics"; -import { createEvmBalanceTracker } from "../balance-tracker"; +import { createEvmBalanceTracker } from "./balance-tracker"; export default { command: "evm", diff --git a/apps/price_pusher/src/sui/balance-tracker.ts b/apps/price_pusher/src/sui/balance-tracker.ts new file mode 100644 index 0000000000..2b2dde2e60 --- /dev/null +++ b/apps/price_pusher/src/sui/balance-tracker.ts @@ -0,0 +1,96 @@ +import { SuiClient } from "@mysten/sui/client"; +import { + BaseBalanceTracker, + BaseBalanceTrackerConfig, + IBalanceTracker, +} from "../interface"; +import { DurationInSeconds } from "../utils"; +import { PricePusherMetrics } from "../metrics"; +import { Logger } from "pino"; + +/** + * Sui-specific configuration for balance tracker + */ +export interface SuiBalanceTrackerConfig extends BaseBalanceTrackerConfig { + /** Sui client instance */ + client: SuiClient; +} + +/** + * Sui-specific implementation of the balance tracker + */ +export class SuiBalanceTracker extends BaseBalanceTracker { + private client: SuiClient; + + constructor(config: SuiBalanceTrackerConfig) { + super({ + ...config, + logger: config.logger.child({ module: "SuiBalanceTracker" }), + }); + + this.client = config.client; + } + + /** + * Sui-specific implementation of balance update + */ + protected async updateBalance(): Promise { + try { + // Get all coins owned by the address + const { data: coins } = await this.client.getCoins({ + owner: this.address, + }); + + // Sum up all coin balances + const totalBalance = coins.reduce((acc, coin) => { + return acc + BigInt(coin.balance); + }, BigInt(0)); + + // Convert to a normalized number for reporting (SUI has 9 decimals) + const normalizedBalance = Number(totalBalance) / 1e9; + + this.metrics.updateWalletBalance( + this.address, + this.network, + normalizedBalance, + ); + + this.logger.debug( + `Updated Sui wallet balance: ${this.address} = ${normalizedBalance} SUI`, + ); + } catch (error) { + this.logger.error( + { error }, + "Error fetching Sui wallet balance for metrics", + ); + } + } +} + +/** + * Parameters for creating a Sui balance tracker + */ +export interface CreateSuiBalanceTrackerParams { + client: SuiClient; + address: string; + network: string; + updateInterval: DurationInSeconds; + metrics: PricePusherMetrics; + logger: Logger; +} + +/** + * Factory function to create a balance tracker for Sui chain + */ +export function createSuiBalanceTracker( + params: CreateSuiBalanceTrackerParams, +): IBalanceTracker { + return new SuiBalanceTracker({ + client: params.client, + address: params.address, + network: params.network, + updateInterval: params.updateInterval, + metrics: params.metrics, + logger: params.logger, + }); +} diff --git a/apps/price_pusher/src/sui/command.ts b/apps/price_pusher/src/sui/command.ts index 800e185384..e743b6d6c2 100644 --- a/apps/price_pusher/src/sui/command.ts +++ b/apps/price_pusher/src/sui/command.ts @@ -9,6 +9,9 @@ import { SuiPriceListener, SuiPricePusher } from "./sui"; import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; import pino from "pino"; import { filterInvalidPriceItems } from "../utils"; +import { PricePusherMetrics } from "../metrics"; +import { createSuiBalanceTracker } from "./balance-tracker"; +import { SuiClient } from "@mysten/sui/client"; export default { command: "sui", @@ -72,6 +75,8 @@ export default { ...options.pushingFrequency, ...options.logLevel, ...options.controllerLogLevel, + ...options.enableMetrics, + ...options.metricsPort, }, handler: async function (argv: any) { const { @@ -89,6 +94,8 @@ export default { accountIndex, logLevel, controllerLogLevel, + enableMetrics, + metricsPort, } = argv; const logger = pino({ level: logLevel }); @@ -101,11 +108,8 @@ export default { mnemonic, `m/44'/784'/${accountIndex}'/0'/0'`, ); - logger.info( - `Pushing updates from wallet address: ${keypair - .getPublicKey() - .toSuiAddress()}`, - ); + const suiAddress = keypair.getPublicKey().toSuiAddress(); + logger.info(`Pushing updates from wallet address: ${suiAddress}`); let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias })); @@ -123,12 +127,22 @@ export default { priceItems = existingPriceItems; + // 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}`); + } + const pythListener = new PythPriceListener( hermesClient, priceItems, logger.child({ module: "PythPriceListener" }), ); + const suiClient = new SuiClient({ url: endpoint }); + const suiListener = new SuiPriceListener( pythStateId, wormholeStateId, @@ -137,6 +151,7 @@ export default { logger.child({ module: "SuiPriceListener" }), { pollingFrequency }, ); + const suiPusher = await SuiPricePusher.createWithAutomaticGasPool( hermesClient, logger.child({ module: "SuiPricePusher" }), @@ -155,9 +170,27 @@ export default { suiListener, suiPusher, logger.child({ module: "Controller" }, { level: controllerLogLevel }), - { pushingFrequency }, + { + pushingFrequency, + metrics, + }, ); + // Create and start the balance tracker if metrics are enabled + if (metrics) { + const balanceTracker = createSuiBalanceTracker({ + client: suiClient, + address: suiAddress, + network: "sui", + updateInterval: pushingFrequency, + metrics, + logger, + }); + + // Start the balance tracker + await balanceTracker.start(); + } + controller.start(); }, };