[price_pusher] Add near command (#1306)

* add near command

* add try catch to getPriceFeedsUpdateData and getUpdateFeeEstimate

* add private-key-path optional parameter

* chore: run pre-commit

* fix: make private key optional

* chore: bump version

---------

Co-authored-by: Ali Behjati <bahjatia@gmail.com>
This commit is contained in:
MagicGordon 2024-03-01 17:08:37 +08:00 committed by GitHub
parent 47470860bf
commit c8acfc5660
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1274 additions and 2 deletions

910
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -133,6 +133,18 @@ npm run start -- sui \
[--polling-frequency 5] \
[--num-gas-objects 30]
# For Near
npm run start -- near \
--node-url https://rpc.testnet.near.org \
--network testnet \
--account-id payer.testnet \
--pyth-contract-address pyth-oracle.testnet \
--price-service-endpoint "https://hermes-beta.pyth.network" \
--price-config-file ./price-config.beta.sample.yaml \
[--private-key-path ./payer.testnet.json] \
[--pushing-frequency 10] \
[--polling-frequency 5]
# Or, run the price pusher docker image instead of building from the source
docker run public.ecr.aws/pyth-network/xc-price-pusher:v<version> -- <above-arguments>

View File

@ -0,0 +1,8 @@
{
"node-url": "https://rpc.mainnet.near.org",
"network": "mainnet",
"account-id": "payer.near",
"pyth-contract-address": "pyth-oracle.near",
"price-service-endpoint": "https://hermes.pyth.network",
"price-config-file": "./price-config.stable.sample.yaml"
}

View File

@ -0,0 +1,8 @@
{
"node-url": "https://rpc.testnet.near.org",
"network": "testnet",
"account-id": "payer.testnet",
"pyth-contract-address": "pyth-oracle.testnet",
"price-service-endpoint": "https://hermes-beta.pyth.network",
"price-config-file": "./price-config.beta.sample.yaml"
}

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/price-pusher",
"version": "6.1.0",
"version": "6.2.0",
"description": "Pyth Price Pusher",
"homepage": "https://pyth.network",
"main": "lib/index.js",
@ -59,6 +59,7 @@
"@truffle/hdwallet-provider": "^2.1.3",
"aptos": "^1.8.5",
"joi": "^17.6.0",
"near-api-js": "^3.0.2",
"web3": "^1.8.1",
"web3-eth-contract": "^1.8.1",
"yaml": "^2.1.1",

View File

@ -5,6 +5,7 @@ import injective from "./injective/command";
import evm from "./evm/command";
import aptos from "./aptos/command";
import sui from "./sui/command";
import near from "./near/command";
yargs(hideBin(process.argv))
.config("config")
@ -13,4 +14,5 @@ yargs(hideBin(process.argv))
.command(injective)
.command(aptos)
.command(sui)
.command(near)
.help().argv;

View File

@ -0,0 +1,100 @@
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
import * as options from "../options";
import { readPriceConfigFile } from "../price-config";
import { PythPriceListener } from "../pyth-price-listener";
import { Controller } from "../controller";
import { Options } from "yargs";
import { NearAccount, NearPriceListener, NearPricePusher } from "./near";
export default {
command: "near",
describe: "run price pusher for near",
builder: {
"node-url": {
description:
"NEAR RPC API url. used to make JSON RPC calls to interact with NEAR.",
type: "string",
required: true,
} as Options,
network: {
description: "testnet or mainnet.",
type: "string",
required: true,
} as Options,
"account-id": {
description: "payer account identifier.",
type: "string",
required: true,
} as Options,
"private-key-path": {
description: "path to payer private key file.",
type: "string",
required: false,
} as Options,
...options.priceConfigFile,
...options.priceServiceEndpoint,
...options.pythContractAddress,
...options.pollingFrequency,
...options.pushingFrequency,
},
handler: function (argv: any) {
// FIXME: type checks for this
const {
nodeUrl,
network,
accountId,
privateKeyPath,
priceConfigFile,
priceServiceEndpoint,
pythContractAddress,
pushingFrequency,
pollingFrequency,
} = argv;
const priceConfigs = readPriceConfigFile(priceConfigFile);
const priceServiceConnection = new PriceServiceConnection(
priceServiceEndpoint,
{
logger: {
// Log only warnings and errors from the price service client
info: () => undefined,
warn: console.warn,
error: console.error,
debug: () => undefined,
trace: () => undefined,
},
}
);
const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
const pythListener = new PythPriceListener(
priceServiceConnection,
priceItems
);
const nearAccount = new NearAccount(
network,
accountId,
nodeUrl,
privateKeyPath,
pythContractAddress
);
const nearListener = new NearPriceListener(nearAccount, priceItems, {
pollingFrequency,
});
const nearPusher = new NearPricePusher(nearAccount, priceServiceConnection);
const controller = new Controller(
priceConfigs,
pythListener,
nearListener,
nearPusher,
{ pushingFrequency }
);
controller.start();
},
};

View File

@ -0,0 +1,233 @@
import os from "os";
import path from "path";
import fs from "fs";
import {
IPricePusher,
PriceInfo,
ChainPriceListener,
PriceItem,
} from "../interface";
import {
PriceServiceConnection,
HexString,
} from "@pythnetwork/price-service-client";
import { DurationInSeconds } from "../utils";
import { Account, Connection, KeyPair } from "near-api-js";
import {
ExecutionStatus,
ExecutionStatusBasic,
FinalExecutionOutcome,
} from "near-api-js/lib/providers/provider";
import { InMemoryKeyStore } from "near-api-js/lib/key_stores";
export class NearPriceListener extends ChainPriceListener {
constructor(
private account: NearAccount,
priceItems: PriceItem[],
config: {
pollingFrequency: DurationInSeconds;
}
) {
super("near", config.pollingFrequency, priceItems);
}
async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
try {
const priceRaw = await this.account.getPriceUnsafe(priceId);
console.log(
`Polled a NEAR on chain price for feed ${this.priceIdToAlias.get(
priceId
)} (${priceId}) ${JSON.stringify(priceRaw)}.`
);
if (priceRaw) {
return {
conf: priceRaw.conf,
price: priceRaw.price,
publishTime: priceRaw.publish_time,
};
} else {
return undefined;
}
} catch (e) {
console.error(`Polling on-chain price for ${priceId} failed. Error:`);
console.error(e);
return undefined;
}
}
}
export class NearPricePusher implements IPricePusher {
constructor(
private account: NearAccount,
private connection: PriceServiceConnection
) {}
async updatePriceFeed(
priceIds: string[],
pubTimesToPush: number[]
): Promise<void> {
if (priceIds.length === 0) {
return;
}
if (priceIds.length !== pubTimesToPush.length)
throw new Error("Invalid arguments");
let priceFeedUpdateData;
try {
priceFeedUpdateData = await this.getPriceFeedsUpdateData(priceIds);
} catch (e: any) {
console.error(new Date(), "getPriceFeedsUpdateData failed:", e);
return;
}
console.log("Pushing ", priceIds);
for (const data of priceFeedUpdateData) {
let updateFee;
try {
updateFee = await this.account.getUpdateFeeEstimate(data);
console.log(`Update fee: ${updateFee}`);
} catch (e: any) {
console.error(new Date(), "getUpdateFeeEstimate failed:", e);
continue;
}
try {
const outcome = await this.account.updatePriceFeeds(data, updateFee);
const failureMessages: (ExecutionStatus | ExecutionStatusBasic)[] = [];
const is_success = Object.values(outcome["receipts_outcome"]).reduce(
(is_success, receipt) => {
if (
Object.prototype.hasOwnProperty.call(
receipt["outcome"]["status"],
"Failure"
)
) {
failureMessages.push(receipt["outcome"]["status"]);
return false;
}
return is_success;
},
true
);
if (is_success) {
console.log(
new Date(),
"updatePriceFeeds successful. Tx hash: ",
outcome["transaction"]["hash"]
);
} else {
console.error(
new Date(),
"updatePriceFeeds failed:",
JSON.stringify(failureMessages, undefined, 2)
);
}
} catch (e: any) {
console.error(new Date(), "updatePriceFeeds failed:", e);
}
}
}
private async getPriceFeedsUpdateData(
priceIds: HexString[]
): Promise<string[]> {
const latestVaas = await this.connection.getLatestVaas(priceIds);
return latestVaas.map((vaa) => Buffer.from(vaa, "base64").toString("hex"));
}
}
export class NearAccount {
private account: Account;
constructor(
network: string,
accountId: string,
nodeUrl: string,
privateKeyPath: string | undefined,
private pythAccountId: string
) {
const connection = this.getConnection(
network,
accountId,
nodeUrl,
privateKeyPath
);
this.account = new Account(connection, accountId);
}
async getPriceUnsafe(priceId: string): Promise<any> {
return await this.account.viewFunction({
contractId: this.pythAccountId,
methodName: "get_price_unsafe",
args: {
price_identifier: priceId,
},
});
}
async getUpdateFeeEstimate(data: string): Promise<any> {
return await this.account.viewFunction({
contractId: this.pythAccountId,
methodName: "get_update_fee_estimate",
args: {
data,
},
});
}
async updatePriceFeeds(
data: string,
updateFee: any
): Promise<FinalExecutionOutcome> {
return await this.account.functionCall({
contractId: this.pythAccountId,
methodName: "update_price_feeds",
args: {
data,
},
gas: "300000000000000" as any,
attachedDeposit: updateFee,
});
}
private getConnection(
network: string,
accountId: string,
nodeUrl: string,
privateKeyPath: string | undefined
): Connection {
const content = fs.readFileSync(
privateKeyPath ||
path.join(
os.homedir(),
".near-credentials",
network,
accountId + ".json"
)
);
const accountInfo = JSON.parse(content.toString());
let privateKey = accountInfo.private_key;
if (!privateKey && accountInfo.secret_key) {
privateKey = accountInfo.secret_key;
}
if (accountInfo.account_id && privateKey) {
const keyPair = KeyPair.fromString(privateKey);
const keyStore = new InMemoryKeyStore();
keyStore.setKey(network, accountInfo.account_id, keyPair);
return Connection.fromConfig({
networkId: network,
provider: { type: "JsonRpcProvider", args: { url: nodeUrl } },
signer: { type: "InMemorySigner", keyStore },
jsvmAccountId: `jsvm.${network}`,
});
} else {
throw new Error("Invalid key file!");
}
}
}