[price-pusher] Add nonce for evm + refactor (#679)

* [price-pusher] Add nonce for evm + refactor

* Rename cooldown-duration to pushing-frequency

* Update readme
This commit is contained in:
Ali Behjati 2023-03-09 19:08:30 +03:30 committed by GitHub
parent 5aa38d2ba1
commit 3d8215edc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 177 additions and 48 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ bigtable-writer.json
.aptos
tsconfig.tsbuildinfo
*~
*mnemonic*

2
package-lock.json generated
View File

@ -47819,7 +47819,7 @@
},
"price_pusher": {
"name": "@pythnetwork/pyth-price-pusher",
"version": "3.0.0",
"version": "4.0.0",
"license": "Apache-2.0",
"dependencies": {
"@injectivelabs/sdk-ts": "^1.0.457",

View File

@ -54,16 +54,18 @@ npm run start -- evm --endpoint wss://example-rpc.com \
--price-service-endpoint https://example-pyth-price.com \
--price-config-file "path/to/price-config-file.yaml.testnet.sample.yaml" \
--mnemonic-file "path/to/mnemonic.txt" \
[--cooldown-duration 10] \
[--polling-frequency 5]
[--pushing-frequency 10] \
[--polling-frequency 5] \
[--override-gas-price-multiplier 1.1]
# For Injective
npm run start -- injective --grpc-endpoint https://grpc-endpoint.com \
--pyth-contract-address inj1z60tg0... --price-service-endpoint "https://example-pyth-price.com" \
--price-config-file "path/to/price-config-file.yaml.testnet.sample.yaml" \
--mnemonic-file "path/to/mnemonic.txt" \
[--cooldown-duration 10] \
[--polling-frequency 5]
[--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>
@ -82,11 +84,11 @@ npm run start -- {network} --help
### Example
For example, to push `BTC/USD` and `BNB/USD` prices on BNB testnet, run the following command:
For example, to push `BTC/USD` and `BNB/USD` prices on Fantom testnet, run the following command:
```sh
npm run dev -- evm --endpoint https://endpoints.omniatech.io/v1/fantom/testnet/public \
--pyth-contract-address 0xd7308b14BF4008e7C7196eC35610B1427C5702EA --price-service-endpoint https://xc-testnet.pyth.network \
--pyth-contract-address 0xff1a0f4744e8582DF1aE09D5611b887B6a12925C --price-service-endpoint https://xc-testnet.pyth.network \
--mnemonic-file "./mnemonic" --price-config-file "./price-config.testnet.sample.yaml"
```
@ -125,3 +127,13 @@ docker-compose -f docker-compose.testnet.sample.yaml up
It will take a few minutes until all the services are up and running.
[pyth price service]: https://github.com/pyth-network/pyth-crosschain/tree/main/price_service/server
## Reliability
You can run multiple instances of the price pusher to increase the reliability. It is better to use
difference RPCs to get better reliability in case an RPC goes down. **If you use the same payer account
in different pushers, then due to blockchains nonce or sequence for accounts, a transaction won't be
pushed twiced and you won't pay additional costs most of the time.** However, there might be some race
condiitons in the RPCs because they are often behind a load balancer than can sometimes cause rejected
transactions land on-chain. You can reduce the chances of additional cost overhead by reducing the
pushing frequency.

View File

@ -0,0 +1,7 @@
{
"endpoint": "https://endpoints.omniatech.io/v1/fantom/testnet/public",
"pyth-contract-address": "0xff1a0f4744e8582DF1aE09D5611b887B6a12925CZ",
"price-service-endpoint": "https://xc-testnet.pyth.network",
"mnemonic-file": "./mnemonic",
"price-config-file": "./price-config.testnet.sample.yaml"
}

View File

@ -65,9 +65,17 @@ services:
- "/command_config"
configs:
- command_config
- mnemonic
- price_config
depends_on:
price-service:
condition: service_healthy
configs:
command_config:
file: ./config.injective.testnet.sample.json # Replace this with the path to the configuration file
# Replace this with the path to the configuration file. You need to update the paths defined in
# the config file
file: ./config.injective.testnet.sample.json
mnemonic:
file: ./mnemonic # Replace this with the path to the mnemonic file
price_config:
file: ./price-config.testnet.sample.yaml # Replace this with the path to the price configuration file

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/pyth-price-pusher",
"version": "3.0.0",
"version": "4.0.0",
"description": "Pyth Price Pusher",
"homepage": "https://pyth.network",
"main": "lib/index.js",

View File

@ -4,17 +4,17 @@ import { IPricePusher, IPriceListener } from "./interface";
import { PriceConfig, shouldUpdate } from "./price-config";
export class Controller {
private cooldownDuration: DurationInSeconds;
private pushingFrequency: DurationInSeconds;
constructor(
private priceConfigs: PriceConfig[],
private sourcePriceListener: IPriceListener,
private targetPriceListener: IPriceListener,
private targetChainPricePusher: IPricePusher,
config: {
cooldownDuration: DurationInSeconds;
pushingFrequency: DurationInSeconds;
}
) {
this.cooldownDuration = config.cooldownDuration;
this.pushingFrequency = config.pushingFrequency;
}
async start() {
@ -25,7 +25,7 @@ export class Controller {
// wait for the listeners to get updated. There could be a restart
// before this run and we need to respect the cooldown duration as
// their might be a message sent before.
await sleep(this.cooldownDuration * 1000);
await sleep(this.pushingFrequency * 1000);
for (;;) {
const pricesToPush: PriceConfig[] = [];
@ -58,7 +58,7 @@ export class Controller {
);
}
await sleep(this.cooldownDuration * 1000);
await sleep(this.pushingFrequency * 1000);
}
}
}

View File

@ -34,12 +34,19 @@ export default {
choices: ["slow", "standard", "fast"],
required: false,
} as Options,
"override-gas-price-multiplier": {
description:
"Multiply the gas price by this number if the transaction is not landing to override it. Default to 1.1",
type: "number",
required: false,
default: 1.1,
} as Options,
...options.priceConfigFile,
...options.priceServiceEndpoint,
...options.mnemonicFile,
...options.pythContractAddress,
...options.pollingFrequency,
...options.cooldownDuration,
...options.pushingFrequency,
},
handler: function (argv: any) {
// FIXME: type checks for this
@ -49,17 +56,25 @@ export default {
priceServiceEndpoint,
mnemonicFile,
pythContractAddress,
cooldownDuration,
pushingFrequency,
pollingFrequency,
customGasStation,
txSpeed,
overrideGasPriceMultiplier,
} = argv;
const priceConfigs = readPriceConfigFile(priceConfigFile);
const priceServiceConnection = new PriceServiceConnection(
priceServiceEndpoint,
{
logger: console,
logger: {
// Log only warnings and errors from the price service client
info: () => undefined,
warn: console.warn,
error: console.error,
debug: () => undefined,
trace: () => undefined,
},
}
);
const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
@ -84,7 +99,8 @@ export default {
const gasStation = getCustomGasStation(customGasStation, txSpeed);
const evmPusher = new EvmPricePusher(
priceServiceConnection,
pythContractFactory.createPythContractWithPayer(),
pythContractFactory,
overrideGasPriceMultiplier,
gasStation
);
@ -93,7 +109,7 @@ export default {
pythListener,
evmListener,
evmPusher,
{ cooldownDuration }
{ pushingFrequency }
);
controller.start();

View File

@ -7,7 +7,7 @@ import {
customGasChainIds,
} from "../utils";
type chainMethods = Record<CustomGasChainId, () => Promise<string>>;
type chainMethods = Record<CustomGasChainId, () => Promise<string | undefined>>;
export class CustomGasStation {
private chain: CustomGasChainId;
@ -25,11 +25,19 @@ export class CustomGasStation {
}
private async fetchMaticMainnetGasPrice() {
const res = await fetch("https://gasstation-mainnet.matic.network/v2");
const jsonRes = await res.json();
const gasPrice = jsonRes[this.speed].maxFee;
const gweiGasPrice = Web3.utils.toWei(gasPrice.toFixed(2), "Gwei");
return gweiGasPrice.toString();
try {
const res = await fetch("https://gasstation-mainnet.matic.network/v2");
const jsonRes = await res.json();
const gasPrice = jsonRes[this.speed].maxFee;
const gweiGasPrice = Web3.utils.toWei(gasPrice.toFixed(2), "Gwei");
return gweiGasPrice.toString();
} catch (e) {
console.error(
"Failed to fetch gas price from Matic mainnet. Returning undefined"
);
console.error(e);
return undefined;
}
}
}

View File

@ -9,7 +9,6 @@ import { TransactionReceipt } from "ethereum-protocol";
import { addLeading0x, DurationInSeconds, removeLeading0x } from "../utils";
import AbstractPythAbi from "@pythnetwork/pyth-sdk-solidity/abis/AbstractPyth.json";
import HDWalletProvider from "@truffle/hdwallet-provider";
import { Provider } from "web3/providers";
import Web3 from "web3";
import { isWsEndpoint } from "../utils";
import {
@ -18,6 +17,7 @@ import {
UnixTimestamp,
} from "@pythnetwork/price-service-client";
import { CustomGasStation } from "./custom-gas-station";
import { Provider } from "web3/providers";
export class EvmPriceListener extends ChainPriceListener {
private pythContractFactory: PythContractFactory;
@ -117,18 +117,35 @@ export class EvmPriceListener extends ChainPriceListener {
}
}
type PushAttempt = {
nonce: number;
gasPrice: number;
};
export class EvmPricePusher implements IPricePusher {
private customGasStation?: CustomGasStation;
private pythContract: Contract;
private web3: Web3;
private pusherAddress: string | undefined;
private lastPushAttempt: PushAttempt | undefined;
constructor(
private connection: PriceServiceConnection,
private pythContract: Contract,
pythContractFactory: PythContractFactory,
private overrideGasPriceMultiplier: number,
customGasStation?: CustomGasStation
) {
this.customGasStation = customGasStation;
this.pythContract = pythContractFactory.createPythContractWithPayer();
this.web3 = new Web3(pythContractFactory.createWeb3PayerProvider() as any);
}
// The pubTimes are passed here to use the values that triggered the push.
// This is an optimization to avoid getting a newer value (as an update comes)
// and will help multiple price pushers to have consistent behaviour.
// To ensure that we transactions are landing and we are not pushing the prices twice
// we will re-use the same nonce (with a higher gas price) if the previous transaction
// is not landed yet.
async updatePriceFeed(
priceIds: string[],
pubTimesToPush: UnixTimestamp[]
@ -146,10 +163,7 @@ export class EvmPricePusher implements IPricePusher {
priceIdsWith0x
);
console.log(
"Pushing ",
priceIdsWith0x.map((priceIdWith0x) => `${priceIdWith0x}`)
);
console.log("Pushing ", priceIdsWith0x);
let updateFee;
@ -165,7 +179,37 @@ export class EvmPricePusher implements IPricePusher {
throw e;
}
const gasPrice = await this.customGasStation?.getCustomGasPrice();
let gasPrice = Number(
(await this.customGasStation?.getCustomGasPrice()) ||
(await this.web3.eth.getGasPrice())
);
// Try to re-use the same nonce and increase the gas if the last tx is not landed yet.
if (this.pusherAddress === undefined) {
this.pusherAddress = (await this.web3.eth.getAccounts())[0];
}
const lastExecutedNonce =
(await this.web3.eth.getTransactionCount(this.pusherAddress)) - 1;
let gasPriceToOverride = undefined;
if (this.lastPushAttempt !== undefined) {
if (this.lastPushAttempt.nonce <= lastExecutedNonce) {
this.lastPushAttempt = undefined;
} else {
gasPriceToOverride = Math.ceil(
this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier
);
}
}
if (gasPriceToOverride !== undefined && gasPriceToOverride > gasPrice) {
gasPrice = gasPriceToOverride;
}
const txNonce = lastExecutedNonce + 1;
console.log(`Using gas price: ${gasPrice} and nonce: ${txNonce}`);
this.pythContract.methods
.updatePriceFeedsIfNecessary(
@ -173,7 +217,7 @@ export class EvmPricePusher implements IPricePusher {
priceIdsWith0x,
pubTimesToPush
)
.send({ value: updateFee, gasPrice })
.send({ value: updateFee, gasPrice, nonce: txNonce })
.on("transactionHash", (hash: string) => {
console.log(`Successful. Tx hash: ${hash}`);
})
@ -207,10 +251,32 @@ export class EvmPricePusher implements IPricePusher {
throw err;
}
if (err.message.includes("transaction underpriced")) {
console.error(
"The gas price of the transaction is too low. Skipping this push. " +
"You might want to use a custom gas station or increase the override gas price " +
"multiplier to increase the likelihood of the transaction landing on-chain."
);
return;
}
if (err.message.includes("could not replace existing tx")) {
console.log(
"A transaction with the same nonce has been mined and this one is no longer needed."
);
return;
}
console.error("An unidentified error has occured:");
console.error(receipt);
throw err;
});
// Update lastAttempt
this.lastPushAttempt = {
nonce: txNonce,
gasPrice: gasPrice,
};
}
private async getPriceFeedsUpdateData(
@ -238,12 +304,7 @@ export class PythContractFactory {
* @returns Pyth contract
*/
createPythContractWithPayer(): Contract {
const provider = new HDWalletProvider({
mnemonic: {
phrase: this.mnemonic,
},
providerOrUrl: this.createWeb3Provider() as Provider,
});
const provider = this.createWeb3PayerProvider();
const web3 = new Web3(provider as any);
@ -275,7 +336,7 @@ export class PythContractFactory {
return isWsEndpoint(this.endpoint);
}
private createWeb3Provider() {
createWeb3Provider() {
if (isWsEndpoint(this.endpoint)) {
Web3.providers.WebsocketProvider.prototype.sendAsync =
Web3.providers.WebsocketProvider.prototype.send;
@ -300,4 +361,13 @@ export class PythContractFactory {
});
}
}
createWeb3PayerProvider() {
return new HDWalletProvider({
mnemonic: {
phrase: this.mnemonic,
},
providerOrUrl: this.createWeb3Provider() as Provider,
});
}
}

View File

@ -24,7 +24,7 @@ export default {
...options.mnemonicFile,
...options.pythContractAddress,
...options.pollingFrequency,
...options.cooldownDuration,
...options.pushingFrequency,
},
handler: function (argv: any) {
// FIXME: type checks for this
@ -34,7 +34,7 @@ export default {
priceServiceEndpoint,
mnemonicFile,
pythContractAddress,
cooldownDuration,
pushingFrequency,
pollingFrequency,
} = argv;
@ -42,7 +42,14 @@ export default {
const priceServiceConnection = new PriceServiceConnection(
priceServiceEndpoint,
{
logger: console,
logger: {
// Log only warnings and errors from the price service client
info: () => undefined,
warn: console.warn,
error: console.error,
debug: () => undefined,
trace: () => undefined,
},
}
);
const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
@ -74,7 +81,7 @@ export default {
pythListener,
injectiveListener,
injectivePusher,
{ cooldownDuration }
{ pushingFrequency }
);
controller.start();

View File

@ -37,11 +37,11 @@ export const pollingFrequency = {
} as Options,
};
export const cooldownDuration = {
"cooldown-duration": {
export const pushingFrequency = {
"pushing-duration": {
description:
"The amount of time (in seconds) to wait between pushing price updates. " +
"This value should be greater than the block time of the network, so this program confirms " +
"The frequency to push prices to the RPC. " +
"It is better that the value be greater than the block time of the network, so this program confirms " +
"it is updated and does not push it twice.",
type: "number",
required: false,