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/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/src/aptos/command.ts b/apps/price_pusher/src/aptos/command.ts index abc000671b..20e70a0035 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"; @@ -20,7 +18,7 @@ import { AptosPricePusher, APTOS_ACCOUNT_HD_PATH, } from "./aptos.js"; -import { filterInvalidPriceItems } from "../utils.js"; +import { filterInvalidPriceItems, readMnemonic } from "../utils.js"; import { createAptosBalanceTracker } from "./balance-tracker.js"; export default { @@ -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 85985e1979..5d98d2b520 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"; @@ -18,7 +16,11 @@ import { EvmPriceListener, EvmPricePusher } from "./evm.js"; import { createPythContract } from "./pyth-contract.js"; import { createClient } from "./super-wallet.js"; import { PricePusherMetrics } from "../metrics.js"; -import { isWsEndpoint, filterInvalidPriceItems } from "../utils.js"; +import { + isWsEndpoint, + filterInvalidPriceItems, + readMnemonic, +} from "../utils.js"; import { createEvmBalanceTracker } from "./balance-tracker.js"; export default { @@ -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 }) => ({ id, alias })); diff --git a/apps/price_pusher/src/injective/command.ts b/apps/price_pusher/src/injective/command.ts index ae93365cdb..0adb8865db 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"; @@ -13,7 +11,7 @@ import { readPriceConfigFile } from "../price-config.js"; import { InjectivePriceListener, InjectivePricePusher } from "./injective.js"; import { Controller } from "../controller.js"; import { PythPriceListener } from "../pyth-price-listener.js"; -import { filterInvalidPriceItems } from "../utils.js"; +import { filterInvalidPriceItems, readMnemonic } from "../utils.js"; export default { command: "injective", describe: "run price pusher for injective", @@ -84,7 +82,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 }) => ({ id, alias })); diff --git a/apps/price_pusher/src/options.ts b/apps/price_pusher/src/options.ts index 0547a977ce..da17d4a471 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.", + description: + "Path to payer mnemonic (private key) file. " + + "If omitted, the mnemonic is read from the `MNEMONIC` environment variable.", type: "string", - required: true, + required: false, } as Options, }; diff --git a/apps/price_pusher/src/sui/command.ts b/apps/price_pusher/src/sui/command.ts index 59b1ed70af..65a76934c7 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"; @@ -18,7 +16,7 @@ import { readPriceConfigFile } from "../price-config"; import { PythPriceListener } from "../pyth-price-listener.js"; import { createSuiBalanceTracker } from "./balance-tracker.js"; import { SuiPriceListener, SuiPricePusher } from "./sui.js"; -import { filterInvalidPriceItems } from "../utils.js"; +import { filterInvalidPriceItems, readMnemonic } from "../utils.js"; export default { command: "sui", @@ -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 9097bb463e..1d278810a5 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 { HexString } from "@pythnetwork/hermes-client"; import { HermesClient } from "@pythnetwork/hermes-client"; @@ -61,6 +63,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[], 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/, + ); + }); +});