price-config.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { HexString } from "@pythnetwork/hermes-client";
  2. import Joi from "joi";
  3. import YAML from "yaml";
  4. import fs from "fs";
  5. import { Logger } from "pino";
  6. import { DurationInSeconds, PctNumber, removeLeading0x } from "./utils";
  7. import { PriceInfo } from "./interface";
  8. const PriceConfigFileSchema: Joi.Schema = Joi.array()
  9. .items(
  10. Joi.object({
  11. alias: Joi.string().required(),
  12. id: Joi.string()
  13. .regex(/^(0x)?[a-f0-9]{64}$/)
  14. .required(),
  15. time_difference: Joi.number().required(),
  16. price_deviation: Joi.number().required(),
  17. confidence_ratio: Joi.number().required(),
  18. early_update: Joi.object({
  19. time_difference: Joi.number().optional(),
  20. price_deviation: Joi.number().optional(),
  21. confidence_ratio: Joi.number().optional(),
  22. }).optional(),
  23. }),
  24. )
  25. .unique("id")
  26. .unique("alias")
  27. .required();
  28. export type PriceConfig = {
  29. alias: string;
  30. id: HexString;
  31. timeDifference: DurationInSeconds;
  32. priceDeviation: PctNumber;
  33. confidenceRatio: PctNumber;
  34. // An early update happens when another price has met the conditions to be pushed, so this
  35. // price can be included in a batch update for minimal gas cost.
  36. // By default, every price feed will be early updated in a batch if any other price update triggers
  37. // the conditions. This configuration will typically minimize gas usage.
  38. //
  39. // However, if you would like to customize this behavior, set `customEarlyUpdate: true` in your config
  40. // for the price feed, then set the specific conditions (time / price / confidence) under which you would
  41. // like the early update to trigger.
  42. customEarlyUpdate: boolean | undefined;
  43. earlyUpdateTimeDifference: DurationInSeconds | undefined;
  44. earlyUpdatePriceDeviation: PctNumber | undefined;
  45. earlyUpdateConfidenceRatio: PctNumber | undefined;
  46. };
  47. export function readPriceConfigFile(path: string): PriceConfig[] {
  48. const priceConfigs = YAML.parse(fs.readFileSync(path, "utf-8"));
  49. const validationResult = PriceConfigFileSchema.validate(priceConfigs);
  50. if (validationResult.error !== undefined) {
  51. throw validationResult.error;
  52. }
  53. return (priceConfigs as any[]).map((priceConfigRaw) => {
  54. const priceConfig: PriceConfig = {
  55. alias: priceConfigRaw.alias,
  56. id: removeLeading0x(priceConfigRaw.id),
  57. timeDifference: priceConfigRaw.time_difference,
  58. priceDeviation: priceConfigRaw.price_deviation,
  59. confidenceRatio: priceConfigRaw.confidence_ratio,
  60. customEarlyUpdate: priceConfigRaw.early_update !== undefined,
  61. earlyUpdateTimeDifference: priceConfigRaw.early_update?.time_difference,
  62. earlyUpdatePriceDeviation: priceConfigRaw.early_update?.price_deviation,
  63. earlyUpdateConfidenceRatio: priceConfigRaw.early_update?.confidence_ratio,
  64. };
  65. return priceConfig;
  66. });
  67. }
  68. export enum UpdateCondition {
  69. // This price feed must be updated
  70. YES,
  71. // This price feed may be updated as part of a larger batch
  72. EARLY,
  73. // This price feed shouldn't be updated
  74. NO,
  75. }
  76. /**
  77. * Checks whether on-chain price needs to be updated with the latest pyth price information.
  78. *
  79. * @param priceConfig Config of the price feed to check
  80. * @returns True if the on-chain price needs to be updated.
  81. */
  82. export function shouldUpdate(
  83. priceConfig: PriceConfig,
  84. sourceLatestPrice: PriceInfo | undefined,
  85. targetLatestPrice: PriceInfo | undefined,
  86. logger: Logger,
  87. ): UpdateCondition {
  88. const priceId = priceConfig.id;
  89. // There is no price to update the target with.
  90. if (sourceLatestPrice === undefined) {
  91. return UpdateCondition.YES;
  92. }
  93. // It means that price never existed there. So we should push the latest price feed.
  94. if (targetLatestPrice === undefined) {
  95. logger.info(
  96. `${priceConfig.alias} (${priceId}) is not available on the target network. Pushing the price.`,
  97. );
  98. return UpdateCondition.YES;
  99. }
  100. // The current price is not newer than the price onchain
  101. if (sourceLatestPrice.publishTime < targetLatestPrice.publishTime) {
  102. return UpdateCondition.NO;
  103. }
  104. const timeDifference =
  105. sourceLatestPrice.publishTime - targetLatestPrice.publishTime;
  106. const priceDeviationPct =
  107. (Math.abs(
  108. Number(sourceLatestPrice.price) - Number(targetLatestPrice.price),
  109. ) /
  110. Number(targetLatestPrice.price)) *
  111. 100;
  112. const confidenceRatioPct = Math.abs(
  113. (Number(sourceLatestPrice.conf) / Number(sourceLatestPrice.price)) * 100,
  114. );
  115. logger.info(
  116. {
  117. sourcePrice: sourceLatestPrice,
  118. targetPrice: targetLatestPrice,
  119. symbol: priceConfig.alias,
  120. },
  121. `Analyzing price ${priceConfig.alias} (${priceId}). ` +
  122. `Time difference: ${timeDifference} (< ${priceConfig.timeDifference}? / early: < ${priceConfig.earlyUpdateTimeDifference}) OR ` +
  123. `Price deviation: ${priceDeviationPct.toFixed(5)}% (< ${
  124. priceConfig.priceDeviation
  125. }%? / early: < ${priceConfig.earlyUpdatePriceDeviation}%?) OR ` +
  126. `Confidence ratio: ${confidenceRatioPct.toFixed(5)}% (< ${
  127. priceConfig.confidenceRatio
  128. }%? / early: < ${priceConfig.earlyUpdatePriceDeviation}%?)`,
  129. );
  130. if (
  131. timeDifference >= priceConfig.timeDifference ||
  132. priceDeviationPct >= priceConfig.priceDeviation ||
  133. confidenceRatioPct >= priceConfig.confidenceRatio
  134. ) {
  135. return UpdateCondition.YES;
  136. } else if (
  137. priceConfig.customEarlyUpdate === undefined ||
  138. !priceConfig.customEarlyUpdate ||
  139. (priceConfig.earlyUpdateTimeDifference !== undefined &&
  140. timeDifference >= priceConfig.earlyUpdateTimeDifference) ||
  141. (priceConfig.earlyUpdatePriceDeviation !== undefined &&
  142. priceDeviationPct >= priceConfig.earlyUpdatePriceDeviation) ||
  143. (priceConfig.earlyUpdateConfidenceRatio !== undefined &&
  144. confidenceRatioPct >= priceConfig.earlyUpdateConfidenceRatio)
  145. ) {
  146. return UpdateCondition.EARLY;
  147. } else {
  148. return UpdateCondition.NO;
  149. }
  150. }