@pythnetwork/price-pusher 10.4.0 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get describeGasPrice () {
13
+ return describeGasPrice;
14
+ },
15
+ get escalateGasPrice () {
16
+ return escalateGasPrice;
17
+ },
18
+ get gasPriceStrategies () {
19
+ return gasPriceStrategies;
20
+ },
21
+ get gasPriceToTxParams () {
22
+ return gasPriceToTxParams;
23
+ },
24
+ get getGasPrice () {
25
+ return getGasPrice;
26
+ },
27
+ get scaleGasPrice () {
28
+ return scaleGasPrice;
29
+ }
30
+ });
31
+ const gasPriceStrategies = [
32
+ "eip1559",
33
+ "legacy"
34
+ ];
35
+ const getGasPrice = async (client, config, logger)=>{
36
+ if (config.strategy === "legacy") {
37
+ const gasPrice = config.gasPrice !== undefined ? BigInt(Math.ceil(config.gasPrice)) : await config.customGasStation?.getCustomGasPrice() || await client.getGasPrice();
38
+ return {
39
+ gasPrice,
40
+ strategy: "legacy"
41
+ };
42
+ }
43
+ // Source the fee from the chain base fee and an estimated priority fee
44
+ // instead of the deprecated eth_gasPrice RPC (which some RPCs mishandle).
45
+ // We pad the base fee to leave headroom for increases between blocks and
46
+ // apply a multiplier to the priority fee, mirroring the gas model in Fortuna.
47
+ const block = await client.getBlock({
48
+ blockTag: "latest"
49
+ });
50
+ if (block.baseFeePerGas == null) {
51
+ throw new Error("The chain does not report a base fee per gas, so it does not support " + "eip1559 transactions. Re-run with `--gas-pricing-strategy legacy`.");
52
+ }
53
+ const priorityFee = scaleBigInt(await client.estimateMaxPriorityFeePerGas(), config.priorityFeeMultiplier);
54
+ const maxFeePerGas = scaleBigInt(block.baseFeePerGas, config.baseFeeMultiplier) + priorityFee;
55
+ logger.debug(`Estimated baseFeePerGas=${block.baseFeePerGas} priorityFee=${priorityFee}`);
56
+ return {
57
+ maxFeePerGas,
58
+ maxPriorityFeePerGas: priorityFee,
59
+ strategy: "eip1559"
60
+ };
61
+ };
62
+ const scaleGasPrice = (gasPrice, factor)=>{
63
+ if (gasPrice.strategy === "legacy") {
64
+ return {
65
+ gasPrice: scaleBigInt(gasPrice.gasPrice, factor),
66
+ strategy: "legacy"
67
+ };
68
+ }
69
+ return {
70
+ maxFeePerGas: scaleBigInt(gasPrice.maxFeePerGas, factor),
71
+ maxPriorityFeePerGas: scaleBigInt(gasPrice.maxPriorityFeePerGas, factor),
72
+ strategy: "eip1559"
73
+ };
74
+ };
75
+ const escalateGasPrice = (fresh, escalated, cap)=>{
76
+ // The strategy is fixed for the lifetime of the pusher, so fresh and escalated
77
+ // always share it. The guards keep the discriminated union narrow.
78
+ if (fresh.strategy === "legacy" && escalated.strategy === "legacy") {
79
+ return {
80
+ gasPrice: capComponent(fresh.gasPrice, escalated.gasPrice, cap),
81
+ strategy: "legacy"
82
+ };
83
+ }
84
+ if (fresh.strategy === "eip1559" && escalated.strategy === "eip1559") {
85
+ // Cap the total spend via maxFeePerGas. The priority fee (tip) only needs
86
+ // to be raised enough to outbid the stuck tx; it is bounded by the capped
87
+ // maxFeePerGas (the EIP-1559 invariant priority <= maxFee), NOT by the fresh
88
+ // tip scaled by `cap` — when the market tip drops, that would stop the tip
89
+ // from escalating and leave the replacement underpriced.
90
+ const maxFeePerGas = capComponent(fresh.maxFeePerGas, escalated.maxFeePerGas, cap);
91
+ const bumpedPriority = maxBigInt(escalated.maxPriorityFeePerGas, fresh.maxPriorityFeePerGas);
92
+ return {
93
+ maxFeePerGas,
94
+ maxPriorityFeePerGas: minBigInt(bumpedPriority, maxFeePerGas),
95
+ strategy: "eip1559"
96
+ };
97
+ }
98
+ return fresh;
99
+ };
100
+ // Raise `fresh` to at least `escalated`, but never above `fresh * cap`.
101
+ const capComponent = (fresh, escalated, cap)=>minBigInt(maxBigInt(escalated, fresh), scaleBigInt(fresh, cap));
102
+ const maxBigInt = (a, b)=>a > b ? a : b;
103
+ const minBigInt = (a, b)=>a < b ? a : b;
104
+ const gasPriceToTxParams = (gasPrice)=>gasPrice.strategy === "legacy" ? {
105
+ gasPrice: gasPrice.gasPrice
106
+ } : {
107
+ maxFeePerGas: gasPrice.maxFeePerGas,
108
+ maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas
109
+ };
110
+ const describeGasPrice = (gasPrice)=>gasPrice.strategy === "legacy" ? `gasPrice=${gasPrice.gasPrice}` : `maxFeePerGas=${gasPrice.maxFeePerGas} maxPriorityFeePerGas=${gasPrice.maxPriorityFeePerGas}`;
111
+ // Scale a bigint by a floating point factor using fixed-point arithmetic to
112
+ // avoid precision loss on large wei values.
113
+ const SCALE_PRECISION = 1_000_000_000n;
114
+ const scaleBigInt = (value, factor)=>value * BigInt(Math.round(factor * Number(SCALE_PRECISION))) / SCALE_PRECISION;
@@ -0,0 +1,33 @@
1
+ import type { Logger } from "pino";
2
+ import type { CustomGasStation } from "./custom-gas-station.js";
3
+ import type { SuperWalletClient } from "./super-wallet.js";
4
+ export declare const gasPriceStrategies: readonly ["eip1559", "legacy"];
5
+ export type GasPriceStrategy = (typeof gasPriceStrategies)[number];
6
+ export type GasPrice = {
7
+ strategy: "legacy";
8
+ gasPrice: bigint;
9
+ } | {
10
+ strategy: "eip1559";
11
+ maxFeePerGas: bigint;
12
+ maxPriorityFeePerGas: bigint;
13
+ };
14
+ export type GasPriceConfig = {
15
+ strategy: GasPriceStrategy;
16
+ baseFeeMultiplier: number;
17
+ priorityFeeMultiplier: number;
18
+ gasPrice?: number | undefined;
19
+ customGasStation?: CustomGasStation | undefined;
20
+ };
21
+ export declare const getGasPrice: (client: SuperWalletClient, config: GasPriceConfig, logger: Logger) => Promise<GasPrice>;
22
+ export declare const scaleGasPrice: (gasPrice: GasPrice, factor: number) => GasPrice;
23
+ export declare const escalateGasPrice: (fresh: GasPrice, escalated: GasPrice, cap: number) => GasPrice;
24
+ export declare const gasPriceToTxParams: (gasPrice: GasPrice) => {
25
+ gasPrice: bigint;
26
+ maxFeePerGas?: never;
27
+ maxPriorityFeePerGas?: never;
28
+ } | {
29
+ maxFeePerGas: bigint;
30
+ maxPriorityFeePerGas: bigint;
31
+ gasPrice?: never;
32
+ };
33
+ export declare const describeGasPrice: (gasPrice: GasPrice) => string;
package/dist/metrics.cjs CHANGED
@@ -30,6 +30,11 @@ class PricePusherMetrics {
30
30
  targetPriceValue;
31
31
  // Wallet metrics
32
32
  walletBalance;
33
+ // Liveness heartbeat
34
+ loopIterations;
35
+ // Liveness state for the /live endpoint (threshold set by the controller)
36
+ lastLoopIterationAt;
37
+ livenessThresholdMs = 60_000;
33
38
  constructor(logger){
34
39
  this.logger = logger;
35
40
  this.registry = new _promclient.Registry();
@@ -42,7 +47,6 @@ class PricePusherMetrics {
42
47
  this.lastPublishedTime = new _promclient.Gauge({
43
48
  help: "The last published time of a price feed in unix timestamp",
44
49
  labelNames: [
45
- "price_id",
46
50
  "alias"
47
51
  ],
48
52
  name: "pyth_price_last_published_time",
@@ -53,7 +57,6 @@ class PricePusherMetrics {
53
57
  this.priceUpdateAttempts = new _promclient.Counter({
54
58
  help: "Total number of price update attempts with their trigger condition and status",
55
59
  labelNames: [
56
- "price_id",
57
60
  "alias",
58
61
  "trigger",
59
62
  "status"
@@ -73,7 +76,6 @@ class PricePusherMetrics {
73
76
  this.sourceTimestamp = new _promclient.Gauge({
74
77
  help: "Latest source chain price publish timestamp",
75
78
  labelNames: [
76
- "price_id",
77
79
  "alias"
78
80
  ],
79
81
  name: "pyth_source_timestamp",
@@ -84,7 +86,6 @@ class PricePusherMetrics {
84
86
  this.configuredTimeDifference = new _promclient.Gauge({
85
87
  help: "Configured time difference threshold between source and target chains",
86
88
  labelNames: [
87
- "price_id",
88
89
  "alias"
89
90
  ],
90
91
  name: "pyth_configured_time_difference",
@@ -95,7 +96,6 @@ class PricePusherMetrics {
95
96
  this.sourcePriceValue = new _promclient.Gauge({
96
97
  help: "Latest price value from Pyth source",
97
98
  labelNames: [
98
- "price_id",
99
99
  "alias"
100
100
  ],
101
101
  name: "pyth_source_price",
@@ -106,7 +106,6 @@ class PricePusherMetrics {
106
106
  this.targetPriceValue = new _promclient.Gauge({
107
107
  help: "Latest price value from target chain",
108
108
  labelNames: [
109
- "price_id",
110
109
  "alias"
111
110
  ],
112
111
  name: "pyth_target_price",
@@ -126,11 +125,32 @@ class PricePusherMetrics {
126
125
  this.registry
127
126
  ]
128
127
  });
128
+ // Liveness heartbeat: increments once per controller loop iteration so a
129
+ // hung (but not crashed) process is detectable via the metric going flat.
130
+ this.loopIterations = new _promclient.Counter({
131
+ help: "Total number of controller loop iterations completed (liveness heartbeat)",
132
+ name: "pyth_pusher_loop_iterations_total",
133
+ registers: [
134
+ this.registry
135
+ ]
136
+ });
129
137
  // Setup the metrics endpoint
130
138
  this.server.get("/metrics", async (_, res)=>{
131
139
  res.set("Content-Type", this.registry.contentType);
132
140
  res.end(await this.registry.metrics());
133
141
  });
142
+ // Liveness endpoint for the k8s livenessProbe: 200 while the control loop is
143
+ // progressing, 503 once no iteration has completed within the threshold (hung),
144
+ // so Kubernetes restarts a hung-but-not-crashed pod.
145
+ this.server.get("/live", (_, res)=>{
146
+ const last = this.lastLoopIterationAt;
147
+ const healthy = last !== undefined && Date.now() - last <= this.livenessThresholdMs;
148
+ res.status(healthy ? 200 : 503).json({
149
+ lastLoopIterationAt: last ?? null,
150
+ status: healthy ? "ok" : "stale",
151
+ thresholdMs: this.livenessThresholdMs
152
+ });
153
+ });
134
154
  }
135
155
  // Start the metrics server
136
156
  start(port) {
@@ -139,22 +159,20 @@ class PricePusherMetrics {
139
159
  });
140
160
  }
141
161
  // Record a successful price update
142
- recordPriceUpdate(priceId, alias, trigger = "yes") {
162
+ recordPriceUpdate(alias, trigger = "yes") {
143
163
  this.priceUpdateAttempts.inc({
144
164
  alias,
145
- price_id: priceId,
146
165
  status: "success",
147
166
  trigger: trigger.toLowerCase()
148
167
  });
149
168
  }
150
169
  // Record update condition status (YES/NO/EARLY)
151
- recordUpdateCondition(priceId, alias, condition) {
170
+ recordUpdateCondition(alias, condition) {
152
171
  const triggerLabel = _priceconfig.UpdateCondition[condition].toLowerCase();
153
172
  // Only record as 'skipped' when the condition is NO
154
173
  if (condition === _priceconfig.UpdateCondition.NO) {
155
174
  this.priceUpdateAttempts.inc({
156
175
  alias,
157
- price_id: priceId,
158
176
  status: "skipped",
159
177
  trigger: triggerLabel
160
178
  });
@@ -163,10 +181,9 @@ class PricePusherMetrics {
163
181
  // when recordPriceUpdate or recordPriceUpdateError is called
164
182
  }
165
183
  // Record a price update error
166
- recordPriceUpdateError(priceId, alias, trigger = "yes") {
184
+ recordPriceUpdateError(alias, trigger = "yes") {
167
185
  this.priceUpdateAttempts.inc({
168
186
  alias,
169
- price_id: priceId,
170
187
  status: "error",
171
188
  trigger: trigger.toLowerCase()
172
189
  });
@@ -175,33 +192,38 @@ class PricePusherMetrics {
175
192
  setPriceFeedsTotal(count) {
176
193
  this.priceFeedsTotal.set(count);
177
194
  }
195
+ // Record a completed controller loop iteration (liveness heartbeat)
196
+ recordLoopIteration() {
197
+ this.lastLoopIterationAt = Date.now();
198
+ this.loopIterations.inc();
199
+ }
200
+ // Configure how long without a completed loop iteration before /live reports
201
+ // unhealthy (used by the k8s livenessProbe to restart a hung pod).
202
+ setLivenessThresholdSeconds(seconds) {
203
+ this.livenessThresholdMs = seconds * 1000;
204
+ }
178
205
  // Update source, target and configured time difference timestamps
179
- updateTimestamps(priceId, alias, targetLatestPricePublishTime, sourceLatestPricePublishTime, priceConfigTimeDifference) {
206
+ updateTimestamps(alias, targetLatestPricePublishTime, sourceLatestPricePublishTime, priceConfigTimeDifference) {
180
207
  this.sourceTimestamp.set({
181
- alias,
182
- price_id: priceId
208
+ alias
183
209
  }, sourceLatestPricePublishTime);
184
210
  this.lastPublishedTime.set({
185
- alias,
186
- price_id: priceId
211
+ alias
187
212
  }, targetLatestPricePublishTime);
188
213
  this.configuredTimeDifference.set({
189
- alias,
190
- price_id: priceId
214
+ alias
191
215
  }, priceConfigTimeDifference);
192
216
  }
193
217
  // Update price values
194
- updatePriceValues(priceId, alias, sourcePrice, targetPrice) {
218
+ updatePriceValues(alias, sourcePrice, targetPrice) {
195
219
  if (sourcePrice !== undefined) {
196
220
  this.sourcePriceValue.set({
197
- alias,
198
- price_id: priceId
221
+ alias
199
222
  }, Number(sourcePrice));
200
223
  }
201
224
  if (targetPrice !== undefined) {
202
225
  this.targetPriceValue.set({
203
- alias,
204
- price_id: priceId
226
+ alias
205
227
  }, Number(targetPrice));
206
228
  }
207
229
  }
package/dist/metrics.d.ts CHANGED
@@ -13,13 +13,18 @@ export declare class PricePusherMetrics {
13
13
  sourcePriceValue: Gauge;
14
14
  targetPriceValue: Gauge;
15
15
  walletBalance: Gauge;
16
+ loopIterations: Counter;
17
+ private lastLoopIterationAt;
18
+ private livenessThresholdMs;
16
19
  constructor(logger: Logger);
17
20
  start(port: number): void;
18
- recordPriceUpdate(priceId: string, alias: string, trigger?: string): void;
19
- recordUpdateCondition(priceId: string, alias: string, condition: UpdateCondition): void;
20
- recordPriceUpdateError(priceId: string, alias: string, trigger?: string): void;
21
+ recordPriceUpdate(alias: string, trigger?: string): void;
22
+ recordUpdateCondition(alias: string, condition: UpdateCondition): void;
23
+ recordPriceUpdateError(alias: string, trigger?: string): void;
21
24
  setPriceFeedsTotal(count: number): void;
22
- updateTimestamps(priceId: string, alias: string, targetLatestPricePublishTime: number, sourceLatestPricePublishTime: number, priceConfigTimeDifference: number): void;
23
- updatePriceValues(priceId: string, alias: string, sourcePrice: string | undefined, targetPrice: string | undefined): void;
25
+ recordLoopIteration(): void;
26
+ setLivenessThresholdSeconds(seconds: number): void;
27
+ updateTimestamps(alias: string, targetLatestPricePublishTime: number, sourceLatestPricePublishTime: number, priceConfigTimeDifference: number): void;
28
+ updatePriceValues(alias: string, sourcePrice: string | undefined, targetPrice: string | undefined): void;
24
29
  updateWalletBalance(walletAddress: string, network: string, balance: bigint | number): void;
25
30
  }
@@ -74,12 +74,12 @@ function shouldUpdate(priceConfig, sourceLatestPrice, targetLatestPrice, logger)
74
74
  const priceId = priceConfig.id;
75
75
  // There is no price to update the target with. So we should not update it.
76
76
  if (sourceLatestPrice === undefined) {
77
- logger.info(`${priceConfig.alias} (${priceId}) is not available on the source network. Ignoring it.`);
77
+ logger.warn(`${priceConfig.alias} (${priceId}) is not available on the source network. Ignoring it.`);
78
78
  return 2;
79
79
  }
80
80
  // It means that price never existed there. So we should push the latest price feed.
81
81
  if (targetLatestPrice === undefined) {
82
- logger.info(`${priceConfig.alias} (${priceId}) is not available on the target network. Pushing the price.`);
82
+ logger.debug(`${priceConfig.alias} (${priceId}) is not available on the target network. Pushing the price.`);
83
83
  return 0;
84
84
  }
85
85
  // The current price is not newer than the price onchain
@@ -64,7 +64,9 @@ class PythPriceListener {
64
64
  }
65
65
  };
66
66
  eventSource.onerror = async (error)=>{
67
- console.error("Error receiving updates from Hermes:", error);
67
+ this.logger.error({
68
+ err: error
69
+ }, "Error receiving updates from Hermes");
68
70
  eventSource.close();
69
71
  await (0, _utils.sleep)(5000); // Wait a bit before trying to reconnect
70
72
  void this.startListening(); // Attempt to restart the listener
@@ -38,7 +38,7 @@ class SolanaPriceListener extends _interface.ChainPriceListener {
38
38
  try {
39
39
  const blockTime = await this.pythSolanaReceiver.connection.getBlockTime(slot);
40
40
  if ((blockTime === null || blockTime < Date.now() / 1000 - HEALTH_CHECK_TIMEOUT_SECONDS) && blockTime !== null) {
41
- this.logger.info(`Solana connection is behind by ${(Date.now() / 1000 - blockTime).toString()} seconds`);
41
+ this.logger.warn(`Solana connection is behind by ${(Date.now() / 1000 - blockTime).toString()} seconds`);
42
42
  }
43
43
  } catch (error) {
44
44
  this.logger.error({
@@ -115,7 +115,7 @@ class SolanaPricePusher {
115
115
  });
116
116
  try {
117
117
  const signatures = await (0, _solanautils.sendTransactions)(transactions, this.pythSolanaReceiver.connection, this.pythSolanaReceiver.wallet);
118
- this.logger.info({
118
+ this.logger.debug({
119
119
  signatures
120
120
  }, "updatePriceFeed successful");
121
121
  } catch (error) {
@@ -153,7 +153,7 @@ class SolanaPricePusherJito {
153
153
  try {
154
154
  const response = await fetch("https://bundles.jito.wtf/api/v1/bundles/tip_floor");
155
155
  if (!response.ok) {
156
- this.logger.error({
156
+ this.logger.warn({
157
157
  status: response.status,
158
158
  statusText: response.statusText
159
159
  }, "getRecentJitoTips http request failed");
@@ -164,7 +164,7 @@ class SolanaPricePusherJito {
164
164
  return Math.floor(Number(data[0].landed_tips_50th_percentile) * _web3.LAMPORTS_PER_SOL);
165
165
  } catch (error) {
166
166
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
167
- this.logger.error({
167
+ this.logger.warn({
168
168
  err: error
169
169
  }, "getRecentJitoTips failed");
170
170
  return undefined;
@@ -174,7 +174,7 @@ class SolanaPricePusherJito {
174
174
  const recentJitoTip = await this.getRecentJitoTipLamports();
175
175
  const jitoTip = this.dynamicJitoTips && recentJitoTip !== undefined ? Math.max(this.defaultJitoTipLamports, recentJitoTip) : this.defaultJitoTipLamports;
176
176
  const cappedJitoTip = Math.min(jitoTip, this.maxJitoTipLamports);
177
- this.logger.info({
177
+ this.logger.debug({
178
178
  cappedJitoTip
179
179
  }, "using jito tip of");
180
180
  let priceFeedUpdateData;
@@ -32,11 +32,11 @@ class SuiBalanceTracker extends _interface.BaseBalanceTracker {
32
32
  * Sui-specific implementation of balance update
33
33
  */ async updateBalance() {
34
34
  try {
35
- const balance = await this.client.getBalance({
35
+ const { balance } = await this.client.core.getBalance({
36
36
  owner: this.address
37
37
  });
38
38
  // Convert to a normalized number for reporting (SUI has 9 decimals)
39
- const normalizedBalance = Number(balance.totalBalance) / 1e9;
39
+ const normalizedBalance = Number(balance.balance) / 1e9;
40
40
  this.metrics.updateWalletBalance(this.address, this.network, normalizedBalance);
41
41
  this.logger.debug(`Updated Sui wallet balance: ${this.address} = ${normalizedBalance.toString()} SUI`);
42
42
  } catch (error) {
@@ -1,4 +1,4 @@
1
- import type { SuiClient } from "@mysten/sui/client";
1
+ import type { ClientWithCoreApi } from "@mysten/sui/client";
2
2
  import type { Logger } from "pino";
3
3
  import type { BaseBalanceTrackerConfig, IBalanceTracker } from "../interface.js";
4
4
  import { BaseBalanceTracker } from "../interface.js";
@@ -8,8 +8,8 @@ import type { DurationInSeconds } from "../utils.js";
8
8
  * Sui-specific configuration for balance tracker
9
9
  */
10
10
  export type SuiBalanceTrackerConfig = {
11
- /** Sui client instance */
12
- client: SuiClient;
11
+ /** Sui client instance (JSON-RPC or gRPC) */
12
+ client: ClientWithCoreApi;
13
13
  } & BaseBalanceTrackerConfig;
14
14
  /**
15
15
  * Sui-specific implementation of the balance tracker
@@ -26,7 +26,7 @@ export declare class SuiBalanceTracker extends BaseBalanceTracker {
26
26
  * Parameters for creating a Sui balance tracker
27
27
  */
28
28
  export type CreateSuiBalanceTrackerParams = {
29
- client: SuiClient;
29
+ client: ClientWithCoreApi;
30
30
  address: string;
31
31
  network: string;
32
32
  updateInterval: DurationInSeconds;
@@ -1,4 +1,4 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ "use strict";
1
+ /* biome-ignore-all lint/style/noNonNullAssertion: pre-existing; metrics wiring asserts on optional config */ /* biome-ignore-all lint/suspicious/noExplicitAny: pre-existing; yargs argv is untyped */ "use strict";
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
@@ -9,7 +9,6 @@ Object.defineProperty(exports, "default", {
9
9
  }
10
10
  });
11
11
  const _nodefs = /*#__PURE__*/ _interop_require_default(require("node:fs"));
12
- const _client = require("@mysten/sui/client");
13
12
  const _ed25519 = require("@mysten/sui/keypairs/ed25519");
14
13
  const _hermesclient = require("@pythnetwork/hermes-client");
15
14
  const _pino = /*#__PURE__*/ _interop_require_default(require("pino"));
@@ -76,22 +75,49 @@ const _default = {
76
75
  type: "number"
77
76
  },
78
77
  endpoint: {
79
- description: "RPC endpoint URL for sui. The pusher will periodically" + "poll for updates. The polling interval is configurable via the " + "`polling-frequency` command-line argument.",
78
+ description: "RPC endpoint URL for sui. The pusher will periodically" + "poll for updates. The polling interval is configurable via the " + "`polling-frequency` command-line argument. For `--endpoint-type grpc` " + "this is the gRPC-web base URL.",
80
79
  required: true,
81
80
  type: "string"
82
81
  },
82
+ "endpoint-type": {
83
+ choices: [
84
+ "json-rpc",
85
+ "grpc"
86
+ ],
87
+ default: "json-rpc",
88
+ description: "Transport to use for the Sui RPC endpoint. Sui Foundation is deprecating " + "JSON-RPC (public endpoints off July 2026, removed by mid-Oct 2026); use " + "`grpc` to migrate. `grpc` relies on @mysten/sui's experimental SuiGrpcClient.",
89
+ required: false,
90
+ type: "string"
91
+ },
83
92
  "gas-budget": {
84
93
  default: 500_000_000,
85
94
  description: "Gas budget for each price update",
86
95
  required: true,
87
96
  type: "number"
88
97
  },
98
+ "grpc-metadata": {
99
+ default: [],
100
+ description: "Metadata header(s) sent with every gRPC request, formatted as " + "`key=value`. Repeat for multiple headers. Use this to authenticate " + "native-gRPC providers: `x-token=<secret>` for QuikNode/Ankr, " + "`x-api-key=<secret>` for BlockVision. Only used with `--endpoint-type grpc`.",
101
+ required: false,
102
+ type: "array"
103
+ },
89
104
  "ignore-gas-objects": {
90
105
  default: [],
91
106
  description: "Gas objects to ignore when merging gas objects on startup -- use this for locked objects.",
92
107
  required: false,
93
108
  type: "array"
94
109
  },
110
+ network: {
111
+ choices: [
112
+ "mainnet",
113
+ "testnet",
114
+ "devnet",
115
+ "localnet"
116
+ ],
117
+ description: "Sui network label for the RPC client. This selects the network the " + "endpoint serves; it does not change which `--endpoint` URL is used.",
118
+ required: true,
119
+ type: "string"
120
+ },
95
121
  "num-gas-objects": {
96
122
  default: 30,
97
123
  description: "Number of gas objects in the pool.",
@@ -122,10 +148,20 @@ const _default = {
122
148
  command: "sui",
123
149
  describe: "Run price pusher for sui. Most of the arguments below are" + "network specific, so there's one set of values for mainnet and" + " another for testnet. See config.sui.mainnet.sample.json for the " + "appropriate values for your network. ",
124
150
  handler: async function(argv) {
125
- const { endpoint, priceConfigFile, priceServiceEndpoint, hermesAccessToken, mnemonicFile, pushingFrequency, pollingFrequency, pythStateId, wormholeStateId, numGasObjects, ignoreGasObjects, gasBudget, accountIndex, logLevel, controllerLogLevel, enableMetrics, metricsPort } = argv;
151
+ const { endpoint, endpointType, network, priceConfigFile, priceServiceEndpoint, hermesAccessToken, mnemonicFile, pushingFrequency, pollingFrequency, pythStateId, wormholeStateId, numGasObjects, ignoreGasObjects, gasBudget, accountIndex, grpcMetadata: grpcMetadataArgs, logLevel, controllerLogLevel, enableMetrics, metricsPort } = argv;
126
152
  const logger = (0, _pino.default)({
127
153
  level: logLevel
128
154
  });
155
+ // Parse `--grpc-metadata key=value` pairs into the metadata map forwarded
156
+ // to the gRPC transport (e.g. the `x-token` auth header).
157
+ const grpcMetadata = {};
158
+ for (const entry of grpcMetadataArgs){
159
+ const eq = entry.indexOf("=");
160
+ if (eq === -1) {
161
+ throw new Error(`Invalid --grpc-metadata "${entry}": expected key=value`);
162
+ }
163
+ grpcMetadata[entry.slice(0, eq)] = entry.slice(eq + 1);
164
+ }
129
165
  const priceConfigs = (0, _priceconfig.readPriceConfigFile)(priceConfigFile);
130
166
  const hermesClient = new _hermesclient.HermesClient(priceServiceEndpoint, {
131
167
  accessToken: hermesAccessToken
@@ -156,17 +192,15 @@ const _default = {
156
192
  const pythListener = new _pythpricelistener.PythPriceListener(hermesClient, priceItems, logger.child({
157
193
  module: "PythPriceListener"
158
194
  }));
159
- const suiClient = new _client.SuiClient({
160
- url: endpoint
161
- });
162
- const suiListener = new _sui.SuiPriceListener(pythStateId, wormholeStateId, endpoint, priceItems, logger.child({
195
+ const suiClient = (0, _sui.createSuiProvider)(endpointType, network, endpoint, grpcMetadata);
196
+ const suiListener = new _sui.SuiPriceListener(pythStateId, wormholeStateId, endpoint, endpointType, network, priceItems, logger.child({
163
197
  module: "SuiPriceListener"
164
198
  }), {
165
199
  pollingFrequency
166
- });
200
+ }, grpcMetadata);
167
201
  const suiPusher = await _sui.SuiPricePusher.createWithAutomaticGasPool(hermesClient, logger.child({
168
202
  module: "SuiPricePusher"
169
- }), pythStateId, wormholeStateId, endpoint, keypair, gasBudget, numGasObjects, ignoreGasObjects);
203
+ }), pythStateId, wormholeStateId, endpoint, endpointType, network, keypair, gasBudget, numGasObjects, ignoreGasObjects, grpcMetadata);
170
204
  const controller = new _controller.Controller(priceConfigs, pythListener, suiListener, suiPusher, logger.child({
171
205
  module: "Controller"
172
206
  }, {
@@ -13,8 +13,11 @@ declare const _default: {
13
13
  "price-config-file": Options;
14
14
  "account-index": Options;
15
15
  endpoint: Options;
16
+ "endpoint-type": Options;
16
17
  "gas-budget": Options;
18
+ "grpc-metadata": Options;
17
19
  "ignore-gas-objects": Options;
20
+ network: Options;
18
21
  "num-gas-objects": Options;
19
22
  "pyth-state-id": Options;
20
23
  "wormhole-state-id": Options;