[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] \
|
[--polling-frequency 5] \
|
||||||
[--num-gas-objects 30]
|
[--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
|
# 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>
|
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",
|
"name": "@pythnetwork/price-pusher",
|
||||||
"version": "6.1.0",
|
"version": "6.2.0",
|
||||||
"description": "Pyth Price Pusher",
|
"description": "Pyth Price Pusher",
|
||||||
"homepage": "https://pyth.network",
|
"homepage": "https://pyth.network",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
@ -59,6 +59,7 @@
|
||||||
"@truffle/hdwallet-provider": "^2.1.3",
|
"@truffle/hdwallet-provider": "^2.1.3",
|
||||||
"aptos": "^1.8.5",
|
"aptos": "^1.8.5",
|
||||||
"joi": "^17.6.0",
|
"joi": "^17.6.0",
|
||||||
|
"near-api-js": "^3.0.2",
|
||||||
"web3": "^1.8.1",
|
"web3": "^1.8.1",
|
||||||
"web3-eth-contract": "^1.8.1",
|
"web3-eth-contract": "^1.8.1",
|
||||||
"yaml": "^2.1.1",
|
"yaml": "^2.1.1",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import injective from "./injective/command";
|
||||||
import evm from "./evm/command";
|
import evm from "./evm/command";
|
||||||
import aptos from "./aptos/command";
|
import aptos from "./aptos/command";
|
||||||
import sui from "./sui/command";
|
import sui from "./sui/command";
|
||||||
|
import near from "./near/command";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.config("config")
|
.config("config")
|
||||||
|
@ -13,4 +14,5 @@ yargs(hideBin(process.argv))
|
||||||
.command(injective)
|
.command(injective)
|
||||||
.command(aptos)
|
.command(aptos)
|
||||||
.command(sui)
|
.command(sui)
|
||||||
|
.command(near)
|
||||||
.help().argv;
|
.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