Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
68 changes: 19 additions & 49 deletions src/hermes-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet, DirectSecp256k1Wallet, type OfflineDirectSigner } from "@cosmjs/proto-signing";
import { GasPrice } from "@cosmjs/stargate";
import { priceUpdateCounter } from "./metrics.ts";
import { latestValue } from "./price-stream/latest-value/latest-value.ts";
import { PriceUpdateConfirmed } from "./price-update/price-update-confirmed/price-update-confirmed.ts";
import type { Logger, PriceProducerFactory, PriceUpdate, PriceUpdater, PythPriceData } from "./types.ts";
import {
validateEndpointUrl,
sanitizeErrorMessage,
validateAkashAddress,
validateContractAddress,
validateEndpointUrl,
validateFeeAmount,
sanitizeErrorMessage,
validateWalletSecret,
} from "./validation.ts";
import { priceUpdateCounter } from "./metrics.ts";
import type { PriceUpdate, PythPriceData, PriceProducerFactory, Logger } from "./types.ts";
import { latestValue } from "./price-stream/latest-value/latest-value.ts";

export interface HermesConfig {
/**
Expand Down Expand Up @@ -81,17 +82,6 @@ export interface HermesConfig {
// Matches pyth contract msg.rs
// =====================

interface UpdatePriceFeedMsg {
update_price_feed: {
// VAA data from Pyth Hermes API (base64 encoded Binary)
// The Pyth contract will:
// 1. Verify VAA via Wormhole contract
// 2. Parse Pyth price attestation from payload
// 3. Relay to x/oracle module
vaa: string;
};
}

interface UpdateFeeMsg {
update_fee: {
new_fee: string; // Uint256 serializes as string in JSON
Expand Down Expand Up @@ -176,6 +166,7 @@ export class HermesClient {
await client.initialize();
return client;
}
#priceUpdater?: PriceUpdater;

constructor(config: HermesConfig) {
const unsafeAllowInsecureEndpoints = config.unsafeAllowInsecureEndpoints ?? false;
Expand Down Expand Up @@ -215,6 +206,7 @@ export class HermesClient {
gasPrice: GasPrice.fromString(this.#config.gasPrice),
},
);

this.#logger.log("Connected to chain successfully");

this.#logger.log("Fetching smart contract configuration...");
Expand Down Expand Up @@ -397,43 +389,30 @@ export class HermesClient {
const startTime = performance.now();

try {
const { priceData, vaa } = priceUpdate;
const currentPrice = await this.queryCurrentPrice();

if (this.#canIgnorePriceUpdate(priceData, currentPrice)) {
if (this.#canIgnorePriceUpdate(priceUpdate.priceData, currentPrice)) {
priceUpdateCounter.add(1, { result: "skipped" });
return;
}

// Prepare execute message with VAA
// The contract will:
// 1. Verify VAA via Wormhole contract
// 2. Parse Pyth price attestation from VAA payload
// 3. Validate price feed ID matches expected
// 4. Relay validated price to x/oracle module
const msg: UpdatePriceFeedMsg = {
update_price_feed: {
vaa: vaa,
},
};

const config = await this.queryConfig();

// Execute update
this.#logger.log("Submitting VAA to Pyth contract...");
this.#logger.log(` Wormhole contract: ${config.wormhole_contract}`);
const result = await this.#getCosmClient().execute(
this.#senderAddress,
this.#config.contractAddress,
msg,
"auto",
undefined,
[{ denom: this.#config.denom, amount: config.update_fee }],
);
this.#priceUpdater ??= new PriceUpdateConfirmed(this.#getCosmClient());
const result = await this.#priceUpdater.updatePrice(priceUpdate, {
Comment thread
stalniy marked this conversation as resolved.
senderAddress: this.#senderAddress,
contractAddress: this.#config.contractAddress,
denom: this.#config.denom,
updateFee: config.update_fee,
});

const price = priceUpdate.priceData.price;
this.#logger.log(`Price updated successfully! TX: ${result.transactionHash}`);
this.#logger.log(` Gas used: ${result.gasUsed}`);
Comment thread
stalniy marked this conversation as resolved.
Outdated
this.#logger.log(` New price: ${priceData.price.price} (expo: ${priceData.price.expo})`);
this.#logger.log(` New price: ${price.price} (expo: ${price.expo})`);
priceUpdateCounter.add(1, { result: "success" });
} catch (error) {
// SEC-04: Sanitize error messages to prevent information leakage
Expand Down Expand Up @@ -574,14 +553,5 @@ export class HermesClient {

// Export types for external use
export type {
DataSourceResponse,
UpdatePriceFeedMsg,
UpdateFeeMsg,
TransferAdminMsg,
RefreshOracleParamsMsg,
ConfigResponse,
PriceResponse,
PriceFeedResponse,
PriceFeedIdResponse,
OracleParamsResponse,
ConfigResponse, DataSourceResponse, OracleParamsResponse, PriceFeedIdResponse, PriceFeedResponse, PriceResponse, RefreshOracleParamsMsg, TransferAdminMsg, UpdateFeeMsg,
Comment thread
stalniy marked this conversation as resolved.
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect } from "vitest";
import { mock } from "vitest-mock-extended";
import type { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { PriceUpdateConfirmed } from "./price-update-confirmed";
import type { PriceUpdate, PriceUpdateOptions } from "../../types";

describe(PriceUpdateConfirmed.name, () => {
it("executes contract with correct message and funds", async () => {
const { signingClient, updater } = setup();
signingClient.execute.mockResolvedValue({
transactionHash: "tx123",
gasUsed: 200_000n,
gasWanted: 300_000n,
height: 1,
logs: [],
events: [],
});

await updater.updatePrice(priceUpdate, options);

expect(signingClient.execute).toHaveBeenCalledWith(
"akash1sender",
"akash1contract",
{ update_price_feed: { vaa: "base64-encoded-vaa" } },
"auto",
undefined,
[{ denom: "uakt", amount: "1000" }],
);
});

it("returns transactionHash and gasUsed from result", async () => {
const { signingClient, updater } = setup();
signingClient.execute.mockResolvedValue({
transactionHash: "abc",
gasUsed: 150_000n,
gasWanted: 200_000n,
height: 1,
logs: [],
events: [],
});

const result = await updater.updatePrice(priceUpdate, options);

expect(result).toEqual({
transactionHash: "abc",
gasUsed: 150_000n,
});
});

it("propagates execution errors", async () => {
const { signingClient, updater } = setup();
signingClient.execute.mockRejectedValue(new Error("out of gas"));

await expect(updater.updatePrice(priceUpdate, options)).rejects.toThrow("out of gas");
});

const options: PriceUpdateOptions = {
senderAddress: "akash1sender",
contractAddress: "akash1contract",
denom: "uakt",
updateFee: "1000",
};

const priceUpdate: PriceUpdate = {
priceData: {
id: "price-feed-id",
price: { price: "100", conf: "1", expo: -8, publish_time: 1000 },
ema_price: { price: "99", conf: "2", expo: -8, publish_time: 1000 },
},
vaa: "base64-encoded-vaa",
};

function setup() {
const signingClient = mock<SigningCosmWasmClient>();
const updater = new PriceUpdateConfirmed(signingClient);
return { signingClient, updater };
}

});
36 changes: 36 additions & 0 deletions src/price-update/price-update-confirmed/price-update-confirmed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import type { PriceUpdate, PriceUpdateOptions, PriceUpdater, UpdatePriceFeedMsg } from "../../types";
Comment thread
stalniy marked this conversation as resolved.
Outdated

export class PriceUpdateConfirmed implements PriceUpdater {
readonly #signingClient: SigningCosmWasmClient;

constructor(signingClient: SigningCosmWasmClient) {
this.#signingClient = signingClient;
}

async updatePrice(priceUpdate: PriceUpdate, options: PriceUpdateOptions): Promise<{ transactionHash: string; gasUsed: bigint }> {
// Prepare execute message with VAA
// The contract will:
// 1. Verify VAA via Wormhole contract
// 2. Parse Pyth price attestation from VAA payload
// 3. Validate price feed ID matches expected
// 4. Relay validated price to x/oracle module
const msg: UpdatePriceFeedMsg = {
update_price_feed: {
vaa: priceUpdate.vaa,
},
};
const result = await this.#signingClient.execute(
options.senderAddress,
options.contractAddress,
msg,
"auto",
undefined,
[{ denom: options.denom, amount: options.updateFee }],
);
return {
transactionHash: result.transactionHash,
gasUsed: result.gasUsed,
};
}
}
25 changes: 25 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,28 @@ export interface HermesResponse {
};
parsed: PythPriceData[];
}

export interface PriceUpdater {
updatePrice: (priceUpdate: PriceUpdate, options: PriceUpdateOptions) => Promise<{
transactionHash: string;
Comment thread
stalniy marked this conversation as resolved.
gasUsed?: bigint;
}>;
}

export interface PriceUpdateOptions {
senderAddress: string;
contractAddress: string;
denom: string;
updateFee: string;
}

export interface UpdatePriceFeedMsg {
update_price_feed: {
// VAA data from Pyth Hermes API (base64 encoded Binary)
// The Pyth contract will:
// 1. Verify VAA via Wormhole contract
// 2. Parse Pyth price attestation from payload
// 3. Relay to x/oracle module
vaa: string;
};
}
Loading