This repository was archived by the owner on Apr 3, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathpusher.ts
234 lines (204 loc) · 7.26 KB
/
pusher.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import {
EvmPriceServiceConnection,
UnixTimestamp,
} from "@pythnetwork/pyth-evm-js";
import { addLeading0x, DurationInSeconds, sleep } from "./utils";
import { PriceInfo, PriceListener } from "./price-listener";
import { Contract } from "web3-eth-contract";
import { PriceConfig } from "./price-config";
import { TransactionReceipt } from "ethereum-protocol";
import { PythContractFactory } from "./pyth-contract-factory";
import { CustomGasStation } from "./custom-gas-station";
export class Pusher {
private connection: EvmPriceServiceConnection;
private pythContract: Contract;
private pythContractFactory: PythContractFactory;
private targetPriceListener: PriceListener;
private sourcePriceListener: PriceListener;
private priceConfigs: PriceConfig[];
private cooldownDuration: DurationInSeconds;
private customGasStation?: CustomGasStation;
constructor(
connection: EvmPriceServiceConnection,
pythContractFactory: PythContractFactory,
targetPriceListener: PriceListener,
sourcePriceListener: PriceListener,
priceConfigs: PriceConfig[],
config: {
cooldownDuration: DurationInSeconds;
},
customGasStation?: CustomGasStation
) {
this.connection = connection;
this.targetPriceListener = targetPriceListener;
this.sourcePriceListener = sourcePriceListener;
this.priceConfigs = priceConfigs;
this.cooldownDuration = config.cooldownDuration;
this.pythContractFactory = pythContractFactory;
this.pythContract = this.pythContractFactory.createPythContractWithPayer();
this.customGasStation = customGasStation;
}
async start() {
for (;;) {
const pricesToPush: PriceConfig[] = [];
const pubTimesToPush: UnixTimestamp[] = [];
for (const priceConfig of this.priceConfigs) {
const priceId = priceConfig.id;
const targetLatestPrice =
this.targetPriceListener.getLatestPriceInfo(priceId);
const sourceLatestPrice =
this.sourcePriceListener.getLatestPriceInfo(priceId);
if (
this.shouldUpdate(priceConfig, sourceLatestPrice, targetLatestPrice)
) {
pricesToPush.push(priceConfig);
pubTimesToPush.push((targetLatestPrice?.publishTime || 0) + 1);
}
}
this.pushUpdates(pricesToPush, pubTimesToPush);
await sleep(this.cooldownDuration * 1000);
}
}
// 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.
async pushUpdates(
pricesToPush: PriceConfig[],
pubTimesToPush: UnixTimestamp[]
) {
if (pricesToPush.length === 0) {
return;
}
const priceIds = pricesToPush.map((priceConfig) =>
addLeading0x(priceConfig.id)
);
const priceFeedUpdateData = await this.connection.getPriceFeedsUpdateData(
priceIds
);
console.log(
"Pushing ",
pricesToPush.map(
(priceConfig) => `${priceConfig.alias} (${priceConfig.id})`
)
);
const updateFee = await this.pythContract.methods
.getUpdateFee(priceFeedUpdateData)
.call();
console.log(`Update fee: ${updateFee}`);
const gasPrice = await this.customGasStation?.getCustomGasPrice();
this.pythContract.methods
.updatePriceFeedsIfNecessary(
priceFeedUpdateData,
priceIds,
pubTimesToPush
)
.send({ value: updateFee, gasPrice })
.on("transactionHash", (hash: string) => {
console.log(`Successful. Tx hash: ${hash}`);
})
.on("error", (err: Error, receipt?: TransactionReceipt) => {
if (
err.message.includes(
"VM Exception while processing transaction: revert"
)
) {
// Since we are using custom error structs on solidity the rejection
// doesn't return any information why the call has reverted. Assuming that
// the update data is valid there is no possible rejection cause other than
// the target chain price being already updated.
console.log(
"Execution reverted. With high probablity, the target chain price " +
"has already updated, Skipping this push."
);
return;
}
if (
err.message.includes("the tx doesn't have the correct nonce.") ||
err.message.includes("nonce too low")
) {
console.log(
"Multiple users are using the same accounts and nonce is incorrect. Skipping this push."
);
return;
}
if (
err.message.includes("sender doesn't have enough funds to send tx.")
) {
console.error("Payer is out of balance, please top it up.");
throw err;
}
console.error("An unidentified error has occured:");
console.error(receipt);
throw err;
});
}
/**
* Checks whether on-chain price needs to be updated with the latest pyth price information.
*
* @param priceConfig Config of the price feed to check
* @returns True if the on-chain price needs to be updated.
*/
shouldUpdate(
priceConfig: PriceConfig,
sourceLatestPrice: PriceInfo | undefined,
targetLatestPrice: PriceInfo | undefined
): boolean {
const priceId = priceConfig.id;
// There is no price to update the target with.
if (sourceLatestPrice === undefined) {
return false;
}
// It means that price never existed there. So we should push the latest price feed.
if (targetLatestPrice === undefined) {
console.log(
`${priceConfig.alias} (${priceId}) is not available on the target network. Pushing the price.`
);
return true;
}
// The current price is not newer than the price onchain
if (sourceLatestPrice.publishTime < targetLatestPrice.publishTime) {
return false;
}
const timeDifference =
sourceLatestPrice.publishTime - targetLatestPrice.publishTime;
const priceDeviationPct =
(Math.abs(
Number(sourceLatestPrice.price) - Number(targetLatestPrice.price)
) /
Number(targetLatestPrice.price)) *
100;
const confidenceRatioPct = Math.abs(
(Number(sourceLatestPrice.conf) / Number(sourceLatestPrice.price)) * 100
);
console.log(`Analyzing price ${priceConfig.alias} (${priceId})`);
console.log("Source latest price: ", sourceLatestPrice);
console.log("Target latest price: ", targetLatestPrice);
console.log(
`Time difference: ${timeDifference} (< ${priceConfig.timeDifference}?)`
);
console.log(
`Price deviation: ${priceDeviationPct.toFixed(5)}% (< ${
priceConfig.priceDeviation
}%?)`
);
console.log(
`Confidence ratio: ${confidenceRatioPct.toFixed(5)}% (< ${
priceConfig.confidenceRatio
}%?)`
);
const result =
timeDifference >= priceConfig.timeDifference ||
priceDeviationPct >= priceConfig.priceDeviation ||
confidenceRatioPct >= priceConfig.confidenceRatio;
if (result == true) {
console.log(
"Some of the above values passed the threshold. Will push the price."
);
} else {
console.log(
"None of the above values passed the threshold. No push needed."
);
}
return result;
}
}