Skip to content

Commit 853b777

Browse files
authored
feat(apps/price_pusher): add fuel price pusher (#1910)
* update .gitignore * add fuel * precommit * update fuel testnet contract address * add comment * bump
1 parent 5527782 commit 853b777

File tree

7 files changed

+294
-63
lines changed

7 files changed

+294
-63
lines changed

apps/price_pusher/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
docker-compose.yaml
22
price-config.yaml
33
lib
4+
mnemonic
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"endpoint": "https://testnet.fuel.network/v1/graphql",
3+
"pyth-contract-address": "0xe31e04946c67fb41923f93d50ee7fc1c6c99d6e07c02860c6bea5f4a13919277",
4+
"price-service-endpoint": "https://hermes.pyth.network",
5+
"private-key-file": "./mnemonic",
6+
"price-config-file": "./price-config.stable.sample.yaml"
7+
}

apps/price_pusher/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-pusher",
3-
"version": "8.0.0",
3+
"version": "8.0.1",
44
"description": "Pyth Price Pusher",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",
@@ -35,7 +35,11 @@
3535
"oracle",
3636
"evm",
3737
"ethereum",
38-
"injective"
38+
"injective",
39+
"fuel",
40+
"aptos",
41+
"sui",
42+
"near"
3943
],
4044
"license": "Apache-2.0",
4145
"devDependencies": {
@@ -63,9 +67,11 @@
6367
"@pythnetwork/pyth-solana-receiver": "workspace:*",
6468
"@pythnetwork/pyth-sui-js": "workspace:*",
6569
"@pythnetwork/solana-utils": "workspace:*",
70+
"@pythnetwork/pyth-fuel-js": "workspace:*",
6671
"@solana/web3.js": "^1.93.0",
6772
"@types/pino": "^7.0.5",
6873
"aptos": "^1.8.5",
74+
"fuels": "^0.94.5",
6975
"jito-ts": "^3.0.1",
7076
"joi": "^17.6.0",
7177
"near-api-js": "^3.0.2",

apps/price_pusher/src/fuel/command.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Options } from "yargs";
2+
import * as options from "../options";
3+
import { readPriceConfigFile } from "../price-config";
4+
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
5+
import { PythPriceListener } from "../pyth-price-listener";
6+
import { FuelPriceListener, FuelPricePusher } from "./fuel";
7+
import { Controller } from "../controller";
8+
import { Provider, Wallet } from "fuels";
9+
import fs from "fs";
10+
import pino from "pino";
11+
12+
export default {
13+
command: "fuel",
14+
describe: "run price pusher for Fuel",
15+
builder: {
16+
endpoint: {
17+
description: "Fuel RPC API endpoint",
18+
type: "string",
19+
required: true,
20+
} as Options,
21+
"private-key-file": {
22+
description: "Path to the private key file",
23+
type: "string",
24+
required: true,
25+
} as Options,
26+
"pyth-contract-address": {
27+
description: "Pyth contract address on Fuel",
28+
type: "string",
29+
required: true,
30+
} as Options,
31+
...options.priceConfigFile,
32+
...options.priceServiceEndpoint,
33+
...options.pushingFrequency,
34+
...options.pollingFrequency,
35+
...options.logLevel,
36+
...options.priceServiceConnectionLogLevel,
37+
...options.controllerLogLevel,
38+
},
39+
handler: async function (argv: any) {
40+
const {
41+
endpoint,
42+
privateKeyFile,
43+
pythContractAddress,
44+
priceConfigFile,
45+
priceServiceEndpoint,
46+
pushingFrequency,
47+
pollingFrequency,
48+
logLevel,
49+
priceServiceConnectionLogLevel,
50+
controllerLogLevel,
51+
} = argv;
52+
53+
const logger = pino({ level: logLevel });
54+
55+
const priceConfigs = readPriceConfigFile(priceConfigFile);
56+
57+
const priceServiceConnection = new PriceServiceConnection(
58+
priceServiceEndpoint,
59+
{
60+
logger: logger.child(
61+
{ module: "PriceServiceConnection" },
62+
{ level: priceServiceConnectionLogLevel }
63+
),
64+
}
65+
);
66+
67+
const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
68+
69+
const pythListener = new PythPriceListener(
70+
priceServiceConnection,
71+
priceItems,
72+
logger.child({ module: "PythPriceListener" })
73+
);
74+
75+
const provider = await Provider.create(endpoint);
76+
const privateKey = fs.readFileSync(privateKeyFile, "utf8").trim();
77+
const wallet = Wallet.fromPrivateKey(privateKey, provider);
78+
79+
const fuelPriceListener = new FuelPriceListener(
80+
provider,
81+
pythContractAddress,
82+
priceItems,
83+
logger.child({ module: "FuelPriceListener" }),
84+
{ pollingFrequency }
85+
);
86+
87+
const fuelPricePusher = new FuelPricePusher(
88+
wallet,
89+
pythContractAddress,
90+
priceServiceConnection,
91+
logger.child({ module: "FuelPricePusher" })
92+
);
93+
94+
const controller = new Controller(
95+
priceConfigs,
96+
pythListener,
97+
fuelPriceListener,
98+
fuelPricePusher,
99+
logger.child({ module: "Controller" }, { level: controllerLogLevel }),
100+
{ pushingFrequency }
101+
);
102+
103+
await controller.start();
104+
},
105+
};

apps/price_pusher/src/fuel/fuel.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
2+
import {
3+
ChainPriceListener,
4+
IPricePusher,
5+
PriceInfo,
6+
PriceItem,
7+
} from "../interface";
8+
import { addLeading0x, DurationInSeconds } from "../utils";
9+
import { Logger } from "pino";
10+
import { Provider, Contract, hexlify, arrayify, Wallet, BN } from "fuels";
11+
import {
12+
PYTH_CONTRACT_ABI,
13+
FUEL_ETH_ASSET_ID,
14+
} from "@pythnetwork/pyth-fuel-js";
15+
16+
// Convert TAI64 timestamp to Unix timestamp
17+
function tai64ToUnix(tai64: BN): number {
18+
// TAI64 is 2^62 seconds ahead of Unix epoch (1970-01-01)
19+
// Additional 10-second offset accounts for TAI being ahead of UTC at Unix epoch
20+
const result = BigInt(tai64.toString()) - BigInt(2n ** 62n) - 10n;
21+
return Number(result);
22+
}
23+
24+
export class FuelPriceListener extends ChainPriceListener {
25+
private contract: Contract;
26+
27+
constructor(
28+
private provider: Provider,
29+
private pythContractId: string,
30+
priceItems: PriceItem[],
31+
private logger: Logger,
32+
config: {
33+
pollingFrequency: DurationInSeconds;
34+
}
35+
) {
36+
super(config.pollingFrequency, priceItems);
37+
this.contract = new Contract(
38+
this.pythContractId,
39+
PYTH_CONTRACT_ABI,
40+
this.provider
41+
);
42+
}
43+
44+
async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
45+
try {
46+
const formattedPriceId = addLeading0x(priceId);
47+
const priceInfo = await this.contract.functions
48+
.price_unsafe(formattedPriceId)
49+
.get();
50+
51+
console.log({
52+
conf: priceInfo.value.confidence.toString(),
53+
price: priceInfo.value.price.toString(),
54+
publishTime: tai64ToUnix(priceInfo.value.publish_time),
55+
});
56+
57+
this.logger.debug(
58+
`Polled a Fuel on chain price for feed ${this.priceIdToAlias.get(
59+
priceId
60+
)} (${priceId}).`
61+
);
62+
63+
return {
64+
conf: priceInfo.value.confidence.toString(),
65+
price: priceInfo.value.price.toString(),
66+
publishTime: tai64ToUnix(priceInfo.value.publish_time),
67+
};
68+
} catch (err) {
69+
this.logger.error({ err, priceId }, `Polling on-chain price failed.`);
70+
return undefined;
71+
}
72+
}
73+
}
74+
75+
export class FuelPricePusher implements IPricePusher {
76+
private contract: Contract;
77+
78+
constructor(
79+
private wallet: Wallet,
80+
private pythContractId: string,
81+
private priceServiceConnection: PriceServiceConnection,
82+
private logger: Logger
83+
) {
84+
this.contract = new Contract(
85+
this.pythContractId,
86+
PYTH_CONTRACT_ABI,
87+
this.wallet as Provider
88+
);
89+
}
90+
91+
async updatePriceFeed(
92+
priceIds: string[],
93+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
94+
pubTimesToPush: number[]
95+
): Promise<void> {
96+
if (priceIds.length === 0) {
97+
return;
98+
}
99+
100+
let priceFeedUpdateData: string[];
101+
try {
102+
priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
103+
priceIds
104+
);
105+
} catch (err: any) {
106+
this.logger.error(err, "getPriceFeedsUpdateData failed");
107+
return;
108+
}
109+
110+
const updateData = priceFeedUpdateData.map((data) =>
111+
arrayify(Buffer.from(data, "base64"))
112+
);
113+
114+
try {
115+
const updateFee = await this.contract.functions
116+
.update_fee(updateData)
117+
.get();
118+
119+
const result = await this.contract.functions
120+
.update_price_feeds(updateData)
121+
.callParams({
122+
forward: [updateFee.value, hexlify(FUEL_ETH_ASSET_ID)],
123+
})
124+
.call();
125+
126+
this.logger.info(
127+
{ transactionId: result.transactionId },
128+
"updatePriceFeed successful"
129+
);
130+
} catch (err: any) {
131+
this.logger.error(err, "updatePriceFeed failed");
132+
}
133+
}
134+
}

apps/price_pusher/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import aptos from "./aptos/command";
77
import sui from "./sui/command";
88
import near from "./near/command";
99
import solana from "./solana/command";
10+
import fuel from "./fuel/command";
1011

1112
yargs(hideBin(process.argv))
1213
.parserConfiguration({
@@ -15,6 +16,7 @@ yargs(hideBin(process.argv))
1516
.config("config")
1617
.global("config")
1718
.command(evm)
19+
.command(fuel)
1820
.command(injective)
1921
.command(aptos)
2022
.command(sui)

0 commit comments

Comments
 (0)