Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@faker-js/faker": "^10.5.0",
"@stylistic/eslint-plugin": "^5.8.0",
"@types/node": "^24.0.0",
"@vitest/coverage-v8": "^4.0.18",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import EventEmitter from "node:events";
import { describe, it, expect, vi } from "vitest";
import { mock } from "vitest-mock-extended";
import type { HermesClient } from "../hermes-client.ts";
import type { ContractClientService } from "../services/contract-client/contract-client.service.ts";
import type { CommandConfig } from "./command-config.ts";
import { adminRefreshParams } from "./admin-refresh-params.ts";

function setup() {
const client = mock<HermesClient>();
const client = mock<ContractClientService>();
const logger = mock<Console>();
const config: CommandConfig = {
const config = {
rpcEndpoint: "https://rpc.akashnet.net:443",
contractAddress: "akash1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5lzv7xu",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
walletSecret: { type: "mnemonic", value: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" },
logger,
process: new EventEmitter(),
createHermesClient: vi.fn(() => Promise.resolve(client)),
};
createContractClient: vi.fn(() => client),
} as unknown as CommandConfig;
return { config, client, logger };
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { CommandConfig } from "./command-config.ts";
import type { CommandConfig } from "../command-config.ts";

export async function adminRefreshParams(config: CommandConfig): Promise<void> {
config.logger?.log("Refreshing oracle parameters...\n");

const client = await config.createHermesClient(config);
const client = config.createContractClient(config);
const txHash = await client.refreshOracleParams();
config.logger?.log("Oracle params refreshed successfully!");
config.logger?.log(`TX: ${txHash}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import EventEmitter from "node:events";
import { describe, it, expect, vi } from "vitest";
import { mock } from "vitest-mock-extended";
import type { HermesClient } from "../hermes-client.ts";
import type { ContractClientService } from "../services/contract-client/contract-client.service.ts";
import type { CommandConfig } from "./command-config.ts";
import { adminTransfer } from "./admin-transfer.ts";

function setup() {
const client = mock<HermesClient>();
const client = mock<ContractClientService>();
const logger = mock<Console>();
const config: CommandConfig = {
const config = {
rpcEndpoint: "https://rpc.akashnet.net:443",
contractAddress: "akash1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5lzv7xu",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
walletSecret: { type: "mnemonic", value: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" },
logger,
process: new EventEmitter(),
createHermesClient: vi.fn(() => Promise.resolve(client)),
};
createContractClient: vi.fn(() => client),
} as unknown as CommandConfig;
return { config, client, logger };
}

Expand Down Expand Up @@ -46,14 +44,14 @@ describe("adminTransfer", () => {
const { config } = setup();

await expect(adminTransfer(config, "not-valid")).rejects.toThrow("Invalid address format");
expect(config.createHermesClient).not.toHaveBeenCalled();
expect(config.createContractClient).not.toHaveBeenCalled();
});

it("rejects empty address before connecting", async () => {
const { config } = setup();

await expect(adminTransfer(config, "")).rejects.toThrow("Invalid address format");
expect(config.createHermesClient).not.toHaveBeenCalled();
expect(config.createContractClient).not.toHaveBeenCalled();
});

it("propagates errors from transferAdmin", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { validateAkashAddress } from "../validation.ts";
import type { CommandConfig } from "./command-config.ts";
import { validateAkashAddress } from "../../validation.ts";
import type { CommandConfig } from "../command-config.ts";

export async function adminTransfer(config: CommandConfig, newAdmin: string): Promise<void> {
// SEC-05: Validate address format at CLI boundary
validateAkashAddress(newAdmin);
config.logger?.log(`Transferring admin to ${newAdmin}...\n`);

const client = await config.createHermesClient(config);
const client = config.createContractClient(config);
const txHash = await client.transferAdmin(newAdmin);
config.logger?.log("Admin transferred successfully!");
config.logger?.log(`TX: ${txHash}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import EventEmitter from "node:events";
import { describe, it, expect, vi } from "vitest";
import { mock } from "vitest-mock-extended";
import type { HermesClient } from "../hermes-client.ts";
import type { ContractClientService } from "../services/contract-client/contract-client.service.ts";
import type { CommandConfig } from "./command-config.ts";
import { adminUpdateFee } from "./admin-update-fee.ts";

function setup() {
const client = mock<HermesClient>();
const client = mock<ContractClientService>();
const logger = mock<Console>();
const config: CommandConfig = {
const config = {
rpcEndpoint: "https://rpc.akashnet.net:443",
contractAddress: "akash1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5lzv7xu",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
walletSecret: { type: "mnemonic", value: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" },
logger,
process: new EventEmitter(),
createHermesClient: vi.fn(() => Promise.resolve(client)),
};
createContractClient: vi.fn(() => client),
} as unknown as CommandConfig;
return { config, client, logger };
}

Expand Down Expand Up @@ -44,20 +42,20 @@ describe("adminUpdateFee", () => {
const { config } = setup();

await expect(adminUpdateFee(config, "abc")).rejects.toThrow("Invalid fee");
expect(config.createHermesClient).not.toHaveBeenCalled();
expect(config.createContractClient).not.toHaveBeenCalled();
});

it("rejects negative fee before connecting", async () => {
const { config } = setup();

await expect(adminUpdateFee(config, "-100")).rejects.toThrow("Invalid fee");
expect(config.createHermesClient).not.toHaveBeenCalled();
expect(config.createContractClient).not.toHaveBeenCalled();
});

it("rejects decimal fee before connecting", async () => {
const { config } = setup();

await expect(adminUpdateFee(config, "100.5")).rejects.toThrow("Invalid fee");
expect(config.createHermesClient).not.toHaveBeenCalled();
expect(config.createContractClient).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { validateFeeAmount } from "../validation.ts";
import type { CommandConfig } from "./command-config.ts";
import { validateFeeAmount } from "../../validation.ts";
import type { CommandConfig } from "../command-config.ts";

export async function adminUpdateFee(config: CommandConfig, newFee: string): Promise<void> {
// SEC-06: Validate fee format at CLI boundary
validateFeeAmount(newFee);
config.logger?.log(`Updating fee to ${newFee}...\n`);

const client = await config.createHermesClient(config);
const client = config.createContractClient(config);
const txHash = await client.updateFee(newFee);
config.logger?.log("Fee updated successfully!");
config.logger?.log(`TX: ${txHash}`);
Expand Down
22 changes: 11 additions & 11 deletions src/cli-commands/command-config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { z } from "zod";
import { HermesClient, type HermesConfig } from "../hermes-client.ts";
import { HermesClient, type HermesConfig } from "../services/hermes-client/hermes-client.ts";
import { validateContractAddress, validateWalletSecret } from "../validation.ts";
import type { PriceProducerFactoryOptions } from "../types.ts";
import { pollPriceStream } from "../price-stream/polling-price-stream/polling-price-stream.ts";
import { priceSSEStream } from "../price-stream/price-sse-stream/price-sse-stream.ts";
import { PriceUpdateOrdered } from "../price-update/price-update-ordered/price-update-ordered.ts";
import { PriceUpdateUnordered } from "../price-update/price-update-unordered/price-update-unordered.ts";
import { ContractClientService, type SigningClientServiceConfig } from "../services/contract-client/contract-client.service.ts";

export interface CommandConfig extends HermesConfig {
createHermesClient: (config: HermesConfig) => Promise<HermesClient>;
createHermesClient: (config: HermesConfig) => HermesClient;
createContractClient: (config: SigningClientServiceConfig) => ContractClientService;
signal: AbortSignal;
healthcheckPort: number;
insufficientBalanceRetryDelayMs: number;
Expand Down Expand Up @@ -48,9 +48,11 @@ const configSchema = z.object({
}).optional(),
PRICE_FETCHING_METHOD: z.enum(["polling", "sse"]).default("polling"),
PRICE_UPDATE_TX_METHOD: z.enum(["ordered", "unordered"]).default("ordered"),
UNORDERED_TX_TTL_MS: z.coerce.number().int().min(1000).positive().default(180_000),
UPDATE_INTERVAL_MS: z.coerce.number().int().nonnegative().default(5 * 1000), // Default to 5 seconds
HEALTHCHECK_PORT: z.coerce.number().int().min(1).max(65535).default(3000),
GAS_PRICE: z.string().regex(/^(\d+)(\.\d+)?uakt$/, { message: 'GAS_PRICE must be a valid number with unit (e.g., "0.025uakt")' }).default("0.025uakt"),
GAS_MULTIPLIER: z.coerce.number().min(1).default(1.5),
DENOM: z.string().default("uakt"),
NODE_ENV: z.enum(["development", "production"]).optional(),
SMART_CONTRACT_CONFIG_CACHE_TTL_MS: z.coerce.number().int().min(1000).positive().default(60 * 60 * 1000),
Expand All @@ -75,10 +77,13 @@ export function parseConfig(config: Record<string, string | undefined>): ParseCo
walletSecret: result.data.WALLET_SECRET,
healthcheckPort: result.data.HEALTHCHECK_PORT,
gasPrice: result.data.GAS_PRICE,
gasMultiplier: result.data.GAS_MULTIPLIER,
denom: result.data.DENOM,
priceDeviationTolerance: result.data.PRICE_DEVIATION_TOLERANCE,
smartContractConfigCacheTTLMs: result.data.SMART_CONTRACT_CONFIG_CACHE_TTL_MS,
insufficientBalanceRetryDelayMs: result.data.INSUFFICIENT_BALANCE_RETRY_DELAY_MS,
priceUpdateTxMethod: result.data.PRICE_UPDATE_TX_METHOD,
unorderedTxTtlMs: result.data.UNORDERED_TX_TTL_MS,
priceProducerFactory(options: PriceProducerFactoryOptions) {
if (result.data.PRICE_FETCHING_METHOD === "sse") {
return priceSSEStream({
Expand All @@ -96,13 +101,8 @@ export function parseConfig(config: Record<string, string | undefined>): ParseCo
authenticationToken: result.data.HERMES_API_KEY,
});
},
priceUpdaterFactory(client, signer) {
if (result.data.PRICE_UPDATE_TX_METHOD === "unordered") {
return new PriceUpdateUnordered(client, signer);
}
return new PriceUpdateOrdered(client);
},
createHermesClient: (cfg: HermesConfig) => HermesClient.connect(cfg),
createHermesClient: (cfg: HermesConfig) => new HermesClient(cfg),
createContractClient: (cfg: SigningClientServiceConfig) => new ContractClientService(cfg),
};

return { ok: true, value: parsedConfig };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { mock } from "vitest-mock-extended";
import type { HermesClient } from "../hermes-client.ts";
import type { CommandConfig } from "./command-config.ts";
import type { HermesClient } from "../../services/hermes-client/hermes-client.ts";
import type { CommandConfig } from "../command-config.ts";
import { daemonCommand } from "./daemon-command.ts";

describe("daemonCommand", () => {
Expand Down Expand Up @@ -105,18 +105,18 @@ describe("daemonCommand", () => {
const logger = mock<Console>();
const abortController = new AbortController();
testAbortController = abortController;
const config: CommandConfig = {
const config = {
rpcEndpoint: "https://rpc.akashnet.net:443",
contractAddress: "akash1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5lzv7xu",
walletSecret: { type: "mnemonic", value: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" },
priceProducerFactory: vi.fn(async function* () {}) as unknown as CommandConfig["priceProducerFactory"],
logger,
signal: abortController.signal,
healthcheckPort: 0,
createHermesClient: vi.fn(() => Promise.resolve(client)),
createHermesClient: vi.fn(() => client),
smartContractConfigCacheTTLMs: 0,
rawConfig: {} as CommandConfig["rawConfig"],
};
} as unknown as CommandConfig;
return { config, client, logger, abortController };
}
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import http from "node:http";
import type { AddressInfo } from "node:net";
import { prometheusExporter } from "../instrumentation/prometheus-exporter.ts";
import type { CommandConfig } from "./command-config.ts";
import { prometheusExporter } from "../../instrumentation/prometheus-exporter.ts";
import type { CommandConfig } from "../command-config.ts";

export async function daemonCommand(config: CommandConfig): Promise<void> {
if (config.signal.aborted) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import EventEmitter from "node:events";
import { describe, it, expect, vi } from "vitest";
import { mock } from "vitest-mock-extended";
import type { HermesClient } from "../hermes-client.ts";
import type { ContractClientService } from "../services/contract-client/contract-client.service.ts";
import type { CommandConfig } from "./command-config.ts";
import { queryCommand } from "./query-command.ts";

function setup() {
const client = mock<HermesClient>();
const client = mock<ContractClientService>();
const logger = mock<Console>();
const config: CommandConfig = {
const config = {
rpcEndpoint: "https://rpc.akashnet.net:443",
contractAddress: "akash1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5lzv7xu",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
walletSecret: { type: "mnemonic", value: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" },
logger,
process: new EventEmitter(),
createHermesClient: vi.fn(() => Promise.resolve(client)),
};
createContractClient: vi.fn(() => client),
} as unknown as CommandConfig;
return { config, client, logger };
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CommandConfig } from "./command-config.ts";
import type { CommandConfig } from "../command-config.ts";

export interface QueryOptions {
feed?: boolean;
Expand All @@ -7,7 +7,7 @@ export interface QueryOptions {
}

export async function queryCommand(config: CommandConfig, options: QueryOptions): Promise<void> {
const client = await config.createHermesClient(config);
const client = config.createContractClient(config);

if (options.config) {
config.logger?.log("Contract Configuration:\n");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { mock } from "vitest-mock-extended";
import type { HermesClient } from "../hermes-client.ts";
import type { CommandConfig } from "./command-config.ts";
import type { HermesClient } from "../../services/hermes-client/hermes-client.ts";
import type { CommandConfig } from "../command-config.ts";
import { statusCommand } from "./status-command.ts";

function setup() {
Expand All @@ -14,7 +14,7 @@ function setup() {
HERMES_ENDPOINT: "https://hermes.pyth.network",
},
logger,
createHermesClient: vi.fn(() => Promise.resolve(client)),
createHermesClient: vi.fn(() => client),
} as unknown as CommandConfig;
return { config, client, logger };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { CommandConfig } from "./command-config.ts";
import type { CommandConfig } from "../command-config.ts";

export async function statusCommand(config: CommandConfig): Promise<void> {
config.logger?.log("Contract Status...\n");

const client = await config.createHermesClient(config);
const client = config.createHermesClient(config);
const status = await client.getStatus();

config.logger?.log("Client Status:");
Expand Down
Loading
Loading