[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:
parent
5aa38d2ba1
commit
3d8215edc2
|
@ -15,3 +15,4 @@ bigtable-writer.json
|
|||
.aptos
|
||||
tsconfig.tsbuildinfo
|
||||
*~
|
||||
*mnemonic*
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue