@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.
- package/README.md +25 -6
- package/dist/common.cjs +0 -3
- package/dist/common.d.ts +0 -4
- package/dist/controller.cjs +10 -5
- package/dist/evm/command.cjs +49 -3
- package/dist/evm/command.d.ts +5 -0
- package/dist/evm/evm.cjs +155 -51
- package/dist/evm/evm.d.ts +11 -5
- package/dist/evm/gas-price.cjs +114 -0
- package/dist/evm/gas-price.d.ts +33 -0
- package/dist/metrics.cjs +46 -24
- package/dist/metrics.d.ts +10 -5
- package/dist/price-config.cjs +2 -2
- package/dist/pyth-price-listener.cjs +3 -1
- package/dist/solana/solana.cjs +5 -5
- package/dist/sui/balance-tracker.cjs +2 -2
- package/dist/sui/balance-tracker.d.ts +4 -4
- package/dist/sui/command.cjs +44 -10
- package/dist/sui/command.d.ts +3 -0
- package/dist/sui/sui.cjs +149 -96
- package/dist/sui/sui.d.ts +29 -12
- package/package.json +14 -7
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
19
|
-
recordUpdateCondition(
|
|
20
|
-
recordPriceUpdateError(
|
|
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
|
-
|
|
23
|
-
|
|
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
|
}
|
package/dist/price-config.cjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
package/dist/solana/solana.cjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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:
|
|
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:
|
|
29
|
+
client: ClientWithCoreApi;
|
|
30
30
|
address: string;
|
|
31
31
|
network: string;
|
|
32
32
|
updateInterval: DurationInSeconds;
|
package/dist/sui/command.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
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 =
|
|
160
|
-
|
|
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
|
}, {
|
package/dist/sui/command.d.ts
CHANGED
|
@@ -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;
|