Skip to content

Commit 51bf542

Browse files
authored
feat(price_pusher): add price_pusher for ton (#2127)
* add price_pusher for ton * add ton command * fix * address comments * bump * fix
1 parent da3af5d commit 51bf542

File tree

7 files changed

+1345
-115
lines changed

7 files changed

+1345
-115
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"endpoint": "https://toncenter.com/api/v2/jsonRPC",
3+
"pyth-contract-address": "EQBU6k8HH6yX4Jf3d18swWbnYr31D3PJI7PgjXT",
4+
"price-service-endpoint": "https://hermes.pyth.network",
5+
"private-key-file": "./mnemonic",
6+
"price-config-file": "./price-config.stable.sample.yaml"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"endpoint": "https://testnet.toncenter.com/api/v2/jsonRPC",
3+
"pyth-contract-address": "EQB4ZnrI5qsP_IUJgVJNwEGKLzZWsQOFhiaqDbD7pTt_f9oU",
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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-pusher",
3-
"version": "8.3.0",
3+
"version": "8.3.1",
44
"description": "Pyth Price Pusher",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",
@@ -62,12 +62,15 @@
6262
"@mysten/sui": "^1.3.0",
6363
"@pythnetwork/price-service-client": "workspace:*",
6464
"@pythnetwork/price-service-sdk": "workspace:^",
65+
"@pythnetwork/pyth-fuel-js": "workspace:*",
6566
"@pythnetwork/pyth-sdk-solidity": "workspace:*",
6667
"@pythnetwork/pyth-solana-receiver": "workspace:*",
6768
"@pythnetwork/pyth-sui-js": "workspace:*",
69+
"@pythnetwork/pyth-ton-js": "workspace:*",
6870
"@pythnetwork/solana-utils": "workspace:*",
69-
"@pythnetwork/pyth-fuel-js": "workspace:*",
7071
"@solana/web3.js": "^1.93.0",
72+
"@ton/crypto": "^3.3.0",
73+
"@ton/ton": "^15.1.0",
7174
"@types/pino": "^7.0.5",
7275
"aptos": "^1.8.5",
7376
"fuels": "^0.94.5",

apps/price_pusher/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import sui from "./sui/command";
88
import near from "./near/command";
99
import solana from "./solana/command";
1010
import fuel from "./fuel/command";
11+
import ton from "./ton/command";
1112

1213
yargs(hideBin(process.argv))
1314
.parserConfiguration({
@@ -22,4 +23,5 @@ yargs(hideBin(process.argv))
2223
.command(sui)
2324
.command(near)
2425
.command(solana)
26+
.command(ton)
2527
.help().argv;

apps/price_pusher/src/ton/command.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 { TonPriceListener, TonPricePusher } from "./ton";
7+
import { Controller } from "../controller";
8+
import { Address, TonClient } from "@ton/ton";
9+
import fs from "fs";
10+
import pino from "pino";
11+
12+
export default {
13+
command: "ton",
14+
describe: "run price pusher for TON",
15+
builder: {
16+
endpoint: {
17+
description: "TON 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 TON",
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 client = new TonClient({ endpoint });
76+
const privateKey = fs.readFileSync(privateKeyFile, "utf8").trim();
77+
const contractAddress = Address.parse(pythContractAddress);
78+
const provider = client.provider(contractAddress);
79+
80+
const tonPriceListener = new TonPriceListener(
81+
provider,
82+
contractAddress,
83+
priceItems,
84+
logger.child({ module: "TonPriceListener" }),
85+
{ pollingFrequency }
86+
);
87+
88+
const tonPricePusher = new TonPricePusher(
89+
client,
90+
privateKey,
91+
contractAddress,
92+
priceServiceConnection,
93+
logger.child({ module: "TonPricePusher" })
94+
);
95+
96+
const controller = new Controller(
97+
priceConfigs,
98+
pythListener,
99+
tonPriceListener,
100+
tonPricePusher,
101+
logger.child({ module: "Controller" }, { level: controllerLogLevel }),
102+
{ pushingFrequency }
103+
);
104+
105+
await controller.start();
106+
},
107+
};

apps/price_pusher/src/ton/ton.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 {
11+
Address,
12+
ContractProvider,
13+
OpenedContract,
14+
Sender,
15+
TonClient,
16+
WalletContractV4,
17+
} from "@ton/ton";
18+
import { keyPairFromSeed } from "@ton/crypto";
19+
import {
20+
PythContract,
21+
calculateUpdatePriceFeedsFee,
22+
} from "@pythnetwork/pyth-ton-js";
23+
24+
export class TonPriceListener extends ChainPriceListener {
25+
private contract: OpenedContract<PythContract>;
26+
27+
constructor(
28+
private provider: ContractProvider,
29+
private contractAddress: Address,
30+
priceItems: PriceItem[],
31+
private logger: Logger,
32+
config: {
33+
pollingFrequency: DurationInSeconds;
34+
}
35+
) {
36+
super(config.pollingFrequency, priceItems);
37+
this.contract = this.provider.open(
38+
PythContract.createFromAddress(this.contractAddress)
39+
);
40+
}
41+
42+
async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
43+
try {
44+
const formattedPriceId = addLeading0x(priceId);
45+
const priceInfo = await this.contract.getPriceUnsafe(formattedPriceId);
46+
47+
this.logger.debug(
48+
`Polled a TON on chain price for feed ${this.priceIdToAlias.get(
49+
priceId
50+
)} (${priceId}).`
51+
);
52+
53+
return {
54+
conf: priceInfo.conf.toString(),
55+
price: priceInfo.price.toString(),
56+
publishTime: priceInfo.publishTime,
57+
};
58+
} catch (err) {
59+
this.logger.error({ err, priceId }, `Polling on-chain price failed.`);
60+
return undefined;
61+
}
62+
}
63+
}
64+
65+
export class TonPricePusher implements IPricePusher {
66+
private contract: OpenedContract<PythContract>;
67+
private sender: Sender;
68+
69+
constructor(
70+
private client: TonClient,
71+
private privateKey: string,
72+
private contractAddress: Address,
73+
private priceServiceConnection: PriceServiceConnection,
74+
private logger: Logger
75+
) {
76+
this.contract = this.client
77+
.provider(this.contractAddress)
78+
.open(PythContract.createFromAddress(this.contractAddress));
79+
const keyPair = keyPairFromSeed(Buffer.from(this.privateKey, "hex"));
80+
const wallet = WalletContractV4.create({
81+
publicKey: keyPair.publicKey,
82+
workchain: 0, // workchain 0 is the masterchain
83+
});
84+
const provider = this.client.open(wallet);
85+
this.sender = provider.sender(keyPair.secretKey);
86+
}
87+
88+
async updatePriceFeed(
89+
priceIds: string[],
90+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
91+
pubTimesToPush: number[]
92+
): Promise<void> {
93+
if (priceIds.length === 0) {
94+
return;
95+
}
96+
97+
let priceFeedUpdateData: string[];
98+
try {
99+
priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
100+
priceIds
101+
);
102+
} catch (err: any) {
103+
this.logger.error(err, "getPriceFeedsUpdateData failed");
104+
return;
105+
}
106+
107+
try {
108+
for (const updateData of priceFeedUpdateData) {
109+
const updateDataBuffer = Buffer.from(updateData, "base64");
110+
const updateFee = await this.contract.getUpdateFee(updateDataBuffer);
111+
const totalFee =
112+
calculateUpdatePriceFeedsFee(BigInt(priceIds.length)) +
113+
BigInt(updateFee);
114+
115+
await this.contract.sendUpdatePriceFeeds(
116+
this.sender,
117+
updateDataBuffer,
118+
totalFee
119+
);
120+
}
121+
122+
this.logger.info("updatePriceFeed successful");
123+
} catch (err: any) {
124+
this.logger.error(err, "updatePriceFeed failed");
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)