From 5243016a37373b54526f88804cec6ba6d9b13191 Mon Sep 17 00:00:00 2001 From: 0xghost42 Date: Wed, 13 May 2026 15:55:21 +0530 Subject: [PATCH 1/2] feat(price-pusher): allow mnemonic from MNEMONIC env var (closes #1015) Previously the price pusher required `--mnemonic-file` pointing at a file on disk containing the signing mnemonic. On platforms that provide encrypted env vars (DigitalOcean, Fly.io, etc.) this forces operators to first write the secret to disk before launching the process. Make `--mnemonic-file` optional and fall back to the `MNEMONIC` environment variable when the flag is not supplied. If both are supplied, the file takes precedence (explicit beats implicit). If neither is supplied, the existing required-arg error is replaced with a clearer message naming both sources. The helper lives in `utils.ts` so all four chain commands (evm, sui, aptos, injective) share identical resolution behaviour. --- apps/price_pusher/README.md | 2 ++ apps/price_pusher/src/aptos/command.ts | 6 ++---- apps/price_pusher/src/evm/command.ts | 10 ++++++---- apps/price_pusher/src/injective/command.ts | 6 ++---- apps/price_pusher/src/options.ts | 6 ++++-- apps/price_pusher/src/sui/command.ts | 6 ++---- apps/price_pusher/src/utils.ts | 17 +++++++++++++++++ 7 files changed, 35 insertions(+), 18 deletions(-) diff --git a/apps/price_pusher/README.md b/apps/price_pusher/README.md index 4df9dadaf8..401f379560 100644 --- a/apps/price_pusher/README.md +++ b/apps/price_pusher/README.md @@ -83,6 +83,8 @@ Pyth hosts [public endpoints](https://docs.pyth.network/price-feeds/api-instance Hermes RPC providers for more reliability. Please refer to [this document](https://docs.pyth.network/documentation/pythnet-price-feeds/hermes) for more information. +The signing mnemonic can be supplied via either the `--mnemonic-file` flag (path to a file containing the mnemonic) or the `MNEMONIC` environment variable. The environment variable is convenient for platforms that inject secrets as encrypted env vars (e.g. DigitalOcean, Fly.io). If both are supplied, `--mnemonic-file` takes precedence. + To run the price pusher, please run the following commands, replacing the command line arguments as necessary: ```sh diff --git a/apps/price_pusher/src/aptos/command.ts b/apps/price_pusher/src/aptos/command.ts index ab6928b873..2b384e6a21 100644 --- a/apps/price_pusher/src/aptos/command.ts +++ b/apps/price_pusher/src/aptos/command.ts @@ -3,8 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import fs from "node:fs"; - import { HermesClient } from "@pythnetwork/hermes-client"; import { AptosAccount } from "aptos"; import pino from "pino"; @@ -15,7 +13,7 @@ import { PricePusherMetrics } from "../metrics.js"; import * as options from "../options.js"; import { readPriceConfigFile } from "../price-config.js"; import { PythPriceListener } from "../pyth-price-listener.js"; -import { filterInvalidPriceItems } from "../utils.js"; +import { filterInvalidPriceItems, readMnemonic } from "../utils.js"; import { APTOS_ACCOUNT_HD_PATH, AptosPriceListener, @@ -87,7 +85,7 @@ export default { logger.info(`Metrics server started on port ${metricsPort}`); } - const mnemonic = fs.readFileSync(mnemonicFile, "utf8").trim(); + const mnemonic = readMnemonic(mnemonicFile); const account = AptosAccount.fromDerivePath( APTOS_ACCOUNT_HD_PATH, mnemonic, diff --git a/apps/price_pusher/src/evm/command.ts b/apps/price_pusher/src/evm/command.ts index 281c9d9ab0..8370985f4d 100644 --- a/apps/price_pusher/src/evm/command.ts +++ b/apps/price_pusher/src/evm/command.ts @@ -3,8 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import fs from "node:fs"; - import { HermesClient } from "@pythnetwork/hermes-client"; import pino from "pino"; import type { Options } from "yargs"; @@ -14,7 +12,11 @@ import { PricePusherMetrics } from "../metrics.js"; import * as options from "../options.js"; import { readPriceConfigFile } from "../price-config.js"; import { PythPriceListener } from "../pyth-price-listener.js"; -import { filterInvalidPriceItems, isWsEndpoint } from "../utils.js"; +import { + filterInvalidPriceItems, + isWsEndpoint, + readMnemonic, +} from "../utils.js"; import { createEvmBalanceTracker } from "./balance-tracker.js"; import { getCustomGasStation } from "./custom-gas-station.js"; import { EvmPriceListener, EvmPricePusher } from "./evm.js"; @@ -129,7 +131,7 @@ export default { accessToken: hermesAccessToken, }); - const mnemonic = fs.readFileSync(mnemonicFile, "utf8").trim(); + const mnemonic = readMnemonic(mnemonicFile); let priceItems = priceConfigs.map(({ id, alias }) => ({ alias, id })); diff --git a/apps/price_pusher/src/injective/command.ts b/apps/price_pusher/src/injective/command.ts index d9b5a03b9c..206000e4ac 100644 --- a/apps/price_pusher/src/injective/command.ts +++ b/apps/price_pusher/src/injective/command.ts @@ -1,8 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import fs from "node:fs"; - import { getNetworkInfo } from "@injectivelabs/networks"; import { HermesClient } from "@pythnetwork/hermes-client"; import { pino } from "pino"; @@ -11,7 +9,7 @@ import { Controller } from "../controller.js"; import * as options from "../options.js"; import { readPriceConfigFile } from "../price-config.js"; import { PythPriceListener } from "../pyth-price-listener.js"; -import { filterInvalidPriceItems } from "../utils.js"; +import { filterInvalidPriceItems, readMnemonic } from "../utils.js"; import { InjectivePriceListener, InjectivePricePusher } from "./injective.js"; export default { builder: { @@ -83,7 +81,7 @@ export default { const hermesClient = new HermesClient(priceServiceEndpoint, { accessToken: hermesAccessToken, }); - const mnemonic = fs.readFileSync(mnemonicFile, "utf8").trim(); + const mnemonic = readMnemonic(mnemonicFile); let priceItems = priceConfigs.map(({ id, alias }) => ({ alias, id })); diff --git a/apps/price_pusher/src/options.ts b/apps/price_pusher/src/options.ts index 294dbd8eca..19989b7bc9 100644 --- a/apps/price_pusher/src/options.ts +++ b/apps/price_pusher/src/options.ts @@ -60,9 +60,11 @@ export const pushingFrequency = { export const mnemonicFile = { "mnemonic-file": { - description: "Path to payer mnemonic (private key) file.", - required: true, + description: + "Path to payer mnemonic (private key) file. " + + "If omitted, the mnemonic is read from the `MNEMONIC` environment variable.", type: "string", + required: false, } as Options, }; diff --git a/apps/price_pusher/src/sui/command.ts b/apps/price_pusher/src/sui/command.ts index 270a1b936e..2441f0edf1 100644 --- a/apps/price_pusher/src/sui/command.ts +++ b/apps/price_pusher/src/sui/command.ts @@ -3,8 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import fs from "node:fs"; - import { SuiClient } from "@mysten/sui/client"; import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; import { HermesClient } from "@pythnetwork/hermes-client"; @@ -16,7 +14,7 @@ import { PricePusherMetrics } from "../metrics.js"; import * as options from "../options.js"; import { readPriceConfigFile } from "../price-config"; import { PythPriceListener } from "../pyth-price-listener.js"; -import { filterInvalidPriceItems } from "../utils.js"; +import { filterInvalidPriceItems, readMnemonic } from "../utils.js"; import { createSuiBalanceTracker } from "./balance-tracker.js"; import { SuiPriceListener, SuiPricePusher } from "./sui.js"; @@ -114,7 +112,7 @@ export default { accessToken: hermesAccessToken, }); - const mnemonic = fs.readFileSync(mnemonicFile, "utf8").trim(); + const mnemonic = readMnemonic(mnemonicFile); const keypair = Ed25519Keypair.deriveKeypair( mnemonic, `m/44'/784'/${accountIndex}'/0'/0'`, diff --git a/apps/price_pusher/src/utils.ts b/apps/price_pusher/src/utils.ts index 5844d0eb81..48b2ec532b 100644 --- a/apps/price_pusher/src/utils.ts +++ b/apps/price_pusher/src/utils.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters */ /* eslint-disable @typescript-eslint/no-explicit-any */ +import fs from "node:fs"; + import type { HermesClient, HexString } from "@pythnetwork/hermes-client"; import type { PriceItem } from "./interface.js"; @@ -60,6 +62,21 @@ export const assertDefined = (value: T | undefined): T => { } }; +export function readMnemonic(mnemonicFile: string | undefined): string { + if (mnemonicFile !== undefined && mnemonicFile !== "") { + return fs.readFileSync(mnemonicFile, "utf8").trim(); + } + + const envMnemonic = process.env.MNEMONIC; + if (envMnemonic !== undefined && envMnemonic !== "") { + return envMnemonic.trim(); + } + + throw new Error( + "No mnemonic provided. Pass --mnemonic-file or set the MNEMONIC environment variable.", + ); +} + export async function filterInvalidPriceItems( hermesClient: HermesClient, priceItems: PriceItem[], From 8c59265c024361ce6d59ee20184a27a6205c67db Mon Sep 17 00:00:00 2001 From: 0xghost42 Date: Thu, 14 May 2026 15:53:34 +0530 Subject: [PATCH 2/2] test(price-pusher): add unit tests for readMnemonic + bump minor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback: - Bump @pythnetwork/price-pusher to 10.5.0 (minor) — backward-compatible feature addition (MNEMONIC env var fallback). 10.4.0 was consumed by #3687 hermes-access-token. - Add tests/utils.test.ts covering all three readMnemonic paths plus the file-precedence-over-env behaviour: file source, env fallback, empty-string path (treated as not supplied), file > env precedence, missing-both error, empty-env error. - Wire `test:unit` script into apps/price_pusher/package.json so the workspace runner picks it up. `pnpm --filter @pythnetwork/price-pusher exec test-unit` — 6 passing. --- apps/price_pusher/package.json | 5 ++- apps/price_pusher/tests/utils.test.ts | 62 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 apps/price_pusher/tests/utils.test.ts diff --git a/apps/price_pusher/package.json b/apps/price_pusher/package.json index 79abf56c0e..3bc5476643 100644 --- a/apps/price_pusher/package.json +++ b/apps/price_pusher/package.json @@ -215,9 +215,10 @@ "dev": "ts-node src/index.ts", "prepublishOnly": "pnpm run build", "start": "node dist/index.cjs", - "test:types": "tsc" + "test:types": "tsc", + "test:unit": "test-unit" }, "type": "module", "types": "./dist/index.d.ts", - "version": "10.4.0" + "version": "10.5.0" } diff --git a/apps/price_pusher/tests/utils.test.ts b/apps/price_pusher/tests/utils.test.ts new file mode 100644 index 0000000000..18dbbb4211 --- /dev/null +++ b/apps/price_pusher/tests/utils.test.ts @@ -0,0 +1,62 @@ +// biome-ignore-all lint/style/noProcessEnv: test file manipulates env vars + +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { readMnemonic } from "../src/utils.js"; + +describe("readMnemonic", () => { + let tmpDir: string; + const originalMnemonic = process.env.MNEMONIC; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), "price-pusher-test-")); + delete process.env.MNEMONIC; + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + if (originalMnemonic === undefined) { + delete process.env.MNEMONIC; + } else { + process.env.MNEMONIC = originalMnemonic; + } + }); + + it("reads mnemonic from file when path supplied", () => { + const path = join(tmpDir, "mnemonic"); + writeFileSync(path, "from file mnemonic\n"); + expect(readMnemonic(path)).toBe("from file mnemonic"); + }); + + it("falls back to MNEMONIC env var when no file path supplied", () => { + process.env.MNEMONIC = "from env mnemonic"; + expect(readMnemonic(undefined)).toBe("from env mnemonic"); + }); + + it("treats empty-string file path as not supplied", () => { + process.env.MNEMONIC = "from env mnemonic"; + expect(readMnemonic("")).toBe("from env mnemonic"); + }); + + it("file source takes precedence over env var", () => { + const path = join(tmpDir, "mnemonic"); + writeFileSync(path, "from file\n"); + process.env.MNEMONIC = "from env"; + expect(readMnemonic(path)).toBe("from file"); + }); + + it("throws when neither file nor env var supplied", () => { + expect(() => readMnemonic(undefined)).toThrow( + /No mnemonic provided/, + ); + }); + + it("throws when env var is empty string", () => { + process.env.MNEMONIC = ""; + expect(() => readMnemonic(undefined)).toThrow( + /No mnemonic provided/, + ); + }); +});