[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:
parent
47470860bf
commit
c8acfc5660
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue