Skip to content

Commit 9f746d9

Browse files
committed
refactor: extracts price updater
1 parent 280c823 commit 9f746d9

4 files changed

Lines changed: 159 additions & 49 deletions

File tree

src/hermes-client.ts

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@
1414
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
1515
import { DirectSecp256k1HdWallet, DirectSecp256k1Wallet, type OfflineDirectSigner } from "@cosmjs/proto-signing";
1616
import { GasPrice } from "@cosmjs/stargate";
17+
import { priceUpdateCounter } from "./metrics.ts";
18+
import { latestValue } from "./price-stream/latest-value/latest-value.ts";
19+
import { PriceUpdateConfirmed } from "./price-update/price-update-confirmed/price-update-confirmed.ts";
20+
import type { Logger, PriceProducerFactory, PriceUpdate, PriceUpdater, PythPriceData } from "./types.ts";
1721
import {
18-
validateEndpointUrl,
22+
sanitizeErrorMessage,
1923
validateAkashAddress,
2024
validateContractAddress,
25+
validateEndpointUrl,
2126
validateFeeAmount,
22-
sanitizeErrorMessage,
2327
validateWalletSecret,
2428
} from "./validation.ts";
25-
import { priceUpdateCounter } from "./metrics.ts";
26-
import type { PriceUpdate, PythPriceData, PriceProducerFactory, Logger } from "./types.ts";
27-
import { latestValue } from "./price-stream/latest-value/latest-value.ts";
2829

2930
export interface HermesConfig {
3031
/**
@@ -81,17 +82,6 @@ export interface HermesConfig {
8182
// Matches pyth contract msg.rs
8283
// =====================
8384

84-
interface UpdatePriceFeedMsg {
85-
update_price_feed: {
86-
// VAA data from Pyth Hermes API (base64 encoded Binary)
87-
// The Pyth contract will:
88-
// 1. Verify VAA via Wormhole contract
89-
// 2. Parse Pyth price attestation from payload
90-
// 3. Relay to x/oracle module
91-
vaa: string;
92-
};
93-
}
94-
9585
interface UpdateFeeMsg {
9686
update_fee: {
9787
new_fee: string; // Uint256 serializes as string in JSON
@@ -176,6 +166,7 @@ export class HermesClient {
176166
await client.initialize();
177167
return client;
178168
}
169+
#priceUpdater?: PriceUpdater;
179170

180171
constructor(config: HermesConfig) {
181172
const unsafeAllowInsecureEndpoints = config.unsafeAllowInsecureEndpoints ?? false;
@@ -215,6 +206,7 @@ export class HermesClient {
215206
gasPrice: GasPrice.fromString(this.#config.gasPrice),
216207
},
217208
);
209+
218210
this.#logger.log("Connected to chain successfully");
219211

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

399391
try {
400-
const { priceData, vaa } = priceUpdate;
401392
const currentPrice = await this.queryCurrentPrice();
402393

403-
if (this.#canIgnorePriceUpdate(priceData, currentPrice)) {
394+
if (this.#canIgnorePriceUpdate(priceUpdate.priceData, currentPrice)) {
404395
priceUpdateCounter.add(1, { result: "skipped" });
405396
return;
406397
}
407398

408-
// Prepare execute message with VAA
409-
// The contract will:
410-
// 1. Verify VAA via Wormhole contract
411-
// 2. Parse Pyth price attestation from VAA payload
412-
// 3. Validate price feed ID matches expected
413-
// 4. Relay validated price to x/oracle module
414-
const msg: UpdatePriceFeedMsg = {
415-
update_price_feed: {
416-
vaa: vaa,
417-
},
418-
};
419-
420399
const config = await this.queryConfig();
421400

422401
// Execute update
423402
this.#logger.log("Submitting VAA to Pyth contract...");
424403
this.#logger.log(` Wormhole contract: ${config.wormhole_contract}`);
425-
const result = await this.#getCosmClient().execute(
426-
this.#senderAddress,
427-
this.#config.contractAddress,
428-
msg,
429-
"auto",
430-
undefined,
431-
[{ denom: this.#config.denom, amount: config.update_fee }],
432-
);
404+
this.#priceUpdater ??= new PriceUpdateConfirmed(this.#getCosmClient());
405+
const result = await this.#priceUpdater.updatePrice(priceUpdate, {
406+
senderAddress: this.#senderAddress,
407+
contractAddress: this.#config.contractAddress,
408+
denom: this.#config.denom,
409+
updateFee: config.update_fee,
410+
});
433411

412+
const price = priceUpdate.priceData.price;
434413
this.#logger.log(`Price updated successfully! TX: ${result.transactionHash}`);
435414
this.#logger.log(` Gas used: ${result.gasUsed}`);
436-
this.#logger.log(` New price: ${priceData.price.price} (expo: ${priceData.price.expo})`);
415+
this.#logger.log(` New price: ${price.price} (expo: ${price.expo})`);
437416
priceUpdateCounter.add(1, { result: "success" });
438417
} catch (error) {
439418
// SEC-04: Sanitize error messages to prevent information leakage
@@ -574,14 +553,5 @@ export class HermesClient {
574553

575554
// Export types for external use
576555
export type {
577-
DataSourceResponse,
578-
UpdatePriceFeedMsg,
579-
UpdateFeeMsg,
580-
TransferAdminMsg,
581-
RefreshOracleParamsMsg,
582-
ConfigResponse,
583-
PriceResponse,
584-
PriceFeedResponse,
585-
PriceFeedIdResponse,
586-
OracleParamsResponse,
556+
ConfigResponse, DataSourceResponse, OracleParamsResponse, PriceFeedIdResponse, PriceFeedResponse, PriceResponse, RefreshOracleParamsMsg, TransferAdminMsg, UpdateFeeMsg,
587557
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it, expect } from "vitest";
2+
import { mock } from "vitest-mock-extended";
3+
import type { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
4+
import { PriceUpdateConfirmed } from "./price-update-confirmed";
5+
import type { PriceUpdate, PriceUpdateOptions } from "../../types";
6+
7+
describe(PriceUpdateConfirmed.name, () => {
8+
it("executes contract with correct message and funds", async () => {
9+
const { signingClient, updater } = setup();
10+
signingClient.execute.mockResolvedValue({
11+
transactionHash: "tx123",
12+
gasUsed: 200_000n,
13+
gasWanted: 300_000n,
14+
height: 1,
15+
logs: [],
16+
events: [],
17+
});
18+
19+
await updater.updatePrice(priceUpdate, options);
20+
21+
expect(signingClient.execute).toHaveBeenCalledWith(
22+
"akash1sender",
23+
"akash1contract",
24+
{ update_price_feed: { vaa: "base64-encoded-vaa" } },
25+
"auto",
26+
undefined,
27+
[{ denom: "uakt", amount: "1000" }],
28+
);
29+
});
30+
31+
it("returns transactionHash and gasUsed from result", async () => {
32+
const { signingClient, updater } = setup();
33+
signingClient.execute.mockResolvedValue({
34+
transactionHash: "abc",
35+
gasUsed: 150_000n,
36+
gasWanted: 200_000n,
37+
height: 1,
38+
logs: [],
39+
events: [],
40+
});
41+
42+
const result = await updater.updatePrice(priceUpdate, options);
43+
44+
expect(result).toEqual({
45+
transactionHash: "abc",
46+
gasUsed: 150_000n,
47+
});
48+
});
49+
50+
it("propagates execution errors", async () => {
51+
const { signingClient, updater } = setup();
52+
signingClient.execute.mockRejectedValue(new Error("out of gas"));
53+
54+
await expect(updater.updatePrice(priceUpdate, options)).rejects.toThrow("out of gas");
55+
});
56+
57+
const options: PriceUpdateOptions = {
58+
senderAddress: "akash1sender",
59+
contractAddress: "akash1contract",
60+
denom: "uakt",
61+
updateFee: "1000",
62+
};
63+
64+
const priceUpdate: PriceUpdate = {
65+
priceData: {
66+
id: "price-feed-id",
67+
price: { price: "100", conf: "1", expo: -8, publish_time: 1000 },
68+
ema_price: { price: "99", conf: "2", expo: -8, publish_time: 1000 },
69+
},
70+
vaa: "base64-encoded-vaa",
71+
};
72+
73+
function setup() {
74+
const signingClient = mock<SigningCosmWasmClient>();
75+
const updater = new PriceUpdateConfirmed(signingClient);
76+
return { signingClient, updater };
77+
}
78+
79+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
2+
import type { PriceUpdate, PriceUpdateOptions, PriceUpdater, UpdatePriceFeedMsg } from "../../types";
3+
4+
export class PriceUpdateConfirmed implements PriceUpdater {
5+
readonly #signingClient: SigningCosmWasmClient;
6+
7+
constructor(signingClient: SigningCosmWasmClient) {
8+
this.#signingClient = signingClient;
9+
}
10+
11+
async updatePrice(priceUpdate: PriceUpdate, options: PriceUpdateOptions): Promise<{ transactionHash: string; gasUsed: bigint }> {
12+
// Prepare execute message with VAA
13+
// The contract will:
14+
// 1. Verify VAA via Wormhole contract
15+
// 2. Parse Pyth price attestation from VAA payload
16+
// 3. Validate price feed ID matches expected
17+
// 4. Relay validated price to x/oracle module
18+
const msg: UpdatePriceFeedMsg = {
19+
update_price_feed: {
20+
vaa: priceUpdate.vaa,
21+
},
22+
};
23+
const result = await this.#signingClient.execute(
24+
options.senderAddress,
25+
options.contractAddress,
26+
msg,
27+
"auto",
28+
undefined,
29+
[{ denom: options.denom, amount: options.updateFee }],
30+
);
31+
return {
32+
transactionHash: result.transactionHash,
33+
gasUsed: result.gasUsed,
34+
};
35+
}
36+
}

src/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,28 @@ export interface HermesResponse {
3737
};
3838
parsed: PythPriceData[];
3939
}
40+
41+
export interface PriceUpdater {
42+
updatePrice: (priceUpdate: PriceUpdate, options: PriceUpdateOptions) => Promise<{
43+
transactionHash: string;
44+
gasUsed?: bigint;
45+
}>;
46+
}
47+
48+
export interface PriceUpdateOptions {
49+
senderAddress: string;
50+
contractAddress: string;
51+
denom: string;
52+
updateFee: string;
53+
}
54+
55+
export interface UpdatePriceFeedMsg {
56+
update_price_feed: {
57+
// VAA data from Pyth Hermes API (base64 encoded Binary)
58+
// The Pyth contract will:
59+
// 1. Verify VAA via Wormhole contract
60+
// 2. Parse Pyth price attestation from payload
61+
// 3. Relay to x/oracle module
62+
vaa: string;
63+
};
64+
}

0 commit comments

Comments
 (0)