Skip to content

Commit 87aea6f

Browse files
authored
refactor(apps/price_pusher): crash on RPC failures (#1730)
* refactor(appts/price_pusher): fix warnings * refactor(apps/price_pusher): crash on rpc issues * refactor(apps/price_pusher): crash on stale hermes data * fix: run linter * chore: bump version * fix: do not crash on sendtx failure in solana * fix: address issues raised in review
1 parent cc114f7 commit 87aea6f

File tree

6 files changed

+73
-66
lines changed

6 files changed

+73
-66
lines changed

apps/price_pusher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-pusher",
3-
"version": "7.0.0-alpha",
3+
"version": "7.0.0",
44
"description": "Pyth Price Pusher",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

apps/price_pusher/src/aptos/aptos.ts

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ export class AptosPriceListener extends ChainPriceListener {
2323
}
2424

2525
async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
26-
try {
27-
const client = new AptosClient(this.endpoint);
26+
const client = new AptosClient(this.endpoint);
2827

29-
const res = await client.getAccountResource(
30-
this.pythModule,
31-
`${this.pythModule}::state::LatestPriceInfo`
32-
);
28+
const res = await client.getAccountResource(
29+
this.pythModule,
30+
`${this.pythModule}::state::LatestPriceInfo`
31+
);
3332

33+
try {
3434
// This depends upon the pyth contract storage on Aptos and should not be undefined.
3535
// If undefined, there has been some change and we would need to update accordingly.
3636
const handle = (res.data as any).info.handle;
@@ -134,29 +134,26 @@ export class AptosPricePusher implements IPricePusher {
134134
return;
135135
}
136136

137-
try {
138-
const account = AptosAccount.fromDerivePath(
139-
APTOS_ACCOUNT_HD_PATH,
140-
this.mnemonic
141-
);
142-
const client = new AptosClient(this.endpoint);
143-
144-
const sequenceNumber = await this.tryGetNextSequenceNumber(
145-
client,
146-
account
147-
);
148-
const rawTx = await client.generateTransaction(
149-
account.address(),
150-
{
151-
function: `${this.pythContractAddress}::pyth::update_price_feeds_with_funder`,
152-
type_arguments: [],
153-
arguments: [priceFeedUpdateData],
154-
},
155-
{
156-
sequence_number: sequenceNumber.toFixed(),
157-
}
158-
);
137+
const account = AptosAccount.fromDerivePath(
138+
APTOS_ACCOUNT_HD_PATH,
139+
this.mnemonic
140+
);
141+
const client = new AptosClient(this.endpoint);
142+
143+
const sequenceNumber = await this.tryGetNextSequenceNumber(client, account);
144+
const rawTx = await client.generateTransaction(
145+
account.address(),
146+
{
147+
function: `${this.pythContractAddress}::pyth::update_price_feeds_with_funder`,
148+
type_arguments: [],
149+
arguments: [priceFeedUpdateData],
150+
},
151+
{
152+
sequence_number: sequenceNumber.toFixed(),
153+
}
154+
);
159155

156+
try {
160157
const signedTx = await client.signTransaction(account, rawTx);
161158
const pendingTx = await client.submitTransaction(signedTx);
162159

apps/price_pusher/src/injective/injective.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ export class InjectivePricePusher implements IPricePusher {
232232
updateFeeQueryResponse = JSON.parse(json);
233233
} catch (err) {
234234
this.logger.error(err, "Error fetching update fee");
235-
return;
235+
// Throwing an error because it is likely an RPC issue
236+
throw err;
236237
}
237238

238239
try {

apps/price_pusher/src/pyth-price-listener.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import {
66
import { PriceInfo, IPriceListener, PriceItem } from "./interface";
77
import { Logger } from "pino";
88

9+
type TimestampInMs = number & { readonly _: unique symbol };
10+
911
export class PythPriceListener implements IPriceListener {
1012
private connection: PriceServiceConnection;
1113
private priceIds: HexString[];
1214
private priceIdToAlias: Map<HexString, string>;
1315
private latestPriceInfo: Map<HexString, PriceInfo>;
1416
private logger: Logger;
17+
private lastUpdated: TimestampInMs | undefined;
1518

1619
constructor(
1720
connection: PriceServiceConnection,
@@ -46,6 +49,17 @@ export class PythPriceListener implements IPriceListener {
4649
publishTime: latestAvailablePrice.publishTime,
4750
});
4851
});
52+
53+
// Check health of the price feeds 5 second. If the price feeds are not updating
54+
// for more than 30s, throw an error.
55+
setInterval(() => {
56+
if (
57+
this.lastUpdated === undefined ||
58+
this.lastUpdated < Date.now() - 30 * 1000
59+
) {
60+
throw new Error("Hermes Price feeds are not updating.");
61+
}
62+
}, 5000);
4963
}
5064

5165
private onNewPriceFeed(priceFeed: PriceFeed) {
@@ -68,6 +82,7 @@ export class PythPriceListener implements IPriceListener {
6882
};
6983

7084
this.latestPriceInfo.set(priceFeed.id, priceInfo);
85+
this.lastUpdated = Date.now() as TimestampInMs;
7186
}
7287

7388
getLatestPriceInfo(priceId: string): PriceInfo | undefined {

apps/price_pusher/src/solana/solana.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ export class SolanaPriceListener extends ChainPriceListener {
2828
super(config.pollingFrequency, priceItems);
2929
}
3030

31+
// Checking the health of the Solana connection by checking the last block time
32+
// and ensuring it is not older than 30 seconds.
33+
private async checkHealth() {
34+
const slot = await this.pythSolanaReceiver.connection.getSlot();
35+
const blockTime = await this.pythSolanaReceiver.connection.getBlockTime(
36+
slot
37+
);
38+
if (blockTime === null || blockTime < Date.now() / 1000 - 30) {
39+
throw new Error("Solana connection is unhealthy");
40+
}
41+
}
42+
43+
async start() {
44+
// Frequently check the RPC connection to ensure it is healthy
45+
setInterval(this.checkHealth.bind(this), 5000);
46+
47+
await super.start();
48+
}
49+
3150
async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
3251
try {
3352
const priceFeedAccount =

apps/price_pusher/src/sui/sui.ts

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,6 @@ export class SuiPricePusher implements IPricePusher {
112112
private readonly provider: SuiClient,
113113
private logger: Logger,
114114
private priceServiceConnection: PriceServiceConnection,
115-
private pythPackageId: string,
116-
private pythStateId: string,
117-
private wormholePackageId: string,
118-
private wormholeStateId: string,
119-
endpoint: string,
120-
keypair: Ed25519Keypair,
121115
private gasBudget: number,
122116
private gasPool: SuiObjectRef[],
123117
private pythClient: SuiPythClient
@@ -180,14 +174,6 @@ export class SuiPricePusher implements IPricePusher {
180174
}
181175

182176
const provider = new SuiClient({ url: endpoint });
183-
const pythPackageId = await SuiPricePusher.getPackageId(
184-
provider,
185-
pythStateId
186-
);
187-
const wormholePackageId = await SuiPricePusher.getPackageId(
188-
provider,
189-
wormholeStateId
190-
);
191177

192178
const gasPool = await SuiPricePusher.initializeGasPool(
193179
keypair,
@@ -208,12 +194,6 @@ export class SuiPricePusher implements IPricePusher {
208194
provider,
209195
logger,
210196
priceServiceConnection,
211-
pythPackageId,
212-
pythStateId,
213-
wormholePackageId,
214-
wormholeStateId,
215-
endpoint,
216-
keypair,
217197
gasBudget,
218198
gasPool,
219199
pythClient
@@ -337,7 +317,7 @@ export class SuiPricePusher implements IPricePusher {
337317
ignoreGasObjects: string[],
338318
logger: Logger
339319
): Promise<SuiObjectRef[]> {
340-
const signerAddress = await signer.toSuiAddress();
320+
const signerAddress = signer.toSuiAddress();
341321

342322
if (ignoreGasObjects.length > 0) {
343323
logger.info(
@@ -383,25 +363,20 @@ export class SuiPricePusher implements IPricePusher {
383363
}
384364

385365
// Attempt to refresh the version of the provided object reference to point to the current version
386-
// of the object. Return the provided object reference if an error occurs or the object could not
387-
// be retrieved.
366+
// of the object. Throws an error if the object cannot be refreshed.
388367
private static async tryRefreshObjectReference(
389368
provider: SuiClient,
390369
ref: SuiObjectRef
391370
): Promise<SuiObjectRef> {
392-
try {
393-
const objectResponse = await provider.getObject({ id: ref.objectId });
394-
if (objectResponse.data !== undefined) {
395-
return {
396-
digest: objectResponse.data!.digest,
397-
objectId: objectResponse.data!.objectId,
398-
version: objectResponse.data!.version,
399-
};
400-
} else {
401-
return ref;
402-
}
403-
} catch (error) {
404-
return ref;
371+
const objectResponse = await provider.getObject({ id: ref.objectId });
372+
if (objectResponse.data !== undefined) {
373+
return {
374+
digest: objectResponse.data!.digest,
375+
objectId: objectResponse.data!.objectId,
376+
version: objectResponse.data!.version,
377+
};
378+
} else {
379+
throw new Error("Failed to refresh object reference");
405380
}
406381
}
407382

0 commit comments

Comments
 (0)