@pythnetwork/price-pusher 10.2.0 → 10.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/{lib/aptos/aptos.js → dist/aptos/aptos.cjs} +80 -76
- package/{lib → dist}/aptos/aptos.d.ts +5 -5
- package/{lib/aptos/balance-tracker.js → dist/aptos/balance-tracker.cjs} +37 -25
- package/{lib → dist}/aptos/balance-tracker.d.ts +9 -9
- package/dist/aptos/command.cjs +161 -0
- package/{lib → dist}/aptos/command.d.ts +1 -2
- package/dist/common.cjs +4 -0
- package/{lib → dist}/common.d.ts +0 -1
- package/{lib/controller.js → dist/controller.cjs} +35 -33
- package/{lib → dist}/controller.d.ts +5 -6
- package/dist/evm/balance-tracker.cjs +58 -0
- package/{lib → dist}/evm/balance-tracker.d.ts +10 -10
- package/dist/evm/command.cjs +205 -0
- package/{lib → dist}/evm/command.d.ts +1 -2
- package/dist/evm/custom-gas-station.cjs +54 -0
- package/{lib → dist}/evm/custom-gas-station.d.ts +1 -2
- package/dist/evm/evm.cjs +287 -0
- package/{lib → dist}/evm/evm.d.ts +8 -7
- package/{lib/evm/pyth-abi.js → dist/evm/pyth-abi.cjs} +181 -160
- package/{lib → dist}/evm/pyth-abi.d.ts +0 -1
- package/dist/evm/pyth-contract.cjs +17 -0
- package/{lib → dist}/evm/pyth-contract.d.ts +3 -4
- package/dist/evm/super-wallet.cjs +90 -0
- package/{lib → dist}/evm/super-wallet.d.ts +1 -2
- package/dist/fuel/command.cjs +135 -0
- package/{lib → dist}/fuel/command.d.ts +1 -2
- package/dist/fuel/fuel.cjs +108 -0
- package/{lib → dist}/fuel/fuel.d.ts +5 -5
- package/dist/index.cjs +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/injective/command.cjs +150 -0
- package/{lib → dist}/injective/command.d.ts +1 -2
- package/{lib/injective/injective.js → dist/injective/injective.cjs} +100 -98
- package/{lib → dist}/injective/injective.d.ts +7 -6
- package/dist/interface.cjs +142 -0
- package/{lib → dist}/interface.d.ts +12 -13
- package/dist/metrics.cjs +218 -0
- package/{lib → dist}/metrics.d.ts +10 -11
- package/dist/near/command.cjs +129 -0
- package/{lib → dist}/near/command.d.ts +1 -2
- package/dist/near/near.cjs +183 -0
- package/{lib → dist}/near/near.d.ts +5 -5
- package/dist/options.cjs +132 -0
- package/{lib → dist}/options.d.ts +1 -2
- package/dist/package.json +1 -0
- package/dist/price-config.cjs +104 -0
- package/{lib → dist}/price-config.d.ts +5 -6
- package/{lib/pyth-price-listener.js → dist/pyth-price-listener.cjs} +30 -24
- package/{lib → dist}/pyth-price-listener.d.ts +4 -4
- package/dist/solana/balance-tracker.cjs +60 -0
- package/{lib → dist}/solana/balance-tracker.d.ts +9 -9
- package/dist/solana/command.cjs +259 -0
- package/{lib → dist}/solana/command.d.ts +2 -3
- package/{lib/solana/solana.js → dist/solana/solana.cjs} +90 -78
- package/{lib → dist}/solana/solana.d.ts +6 -6
- package/dist/sui/balance-tracker.cjs +58 -0
- package/{lib → dist}/sui/balance-tracker.d.ts +9 -9
- package/dist/sui/command.cjs +190 -0
- package/{lib → dist}/sui/command.d.ts +1 -2
- package/{lib/sui/sui.js → dist/sui/sui.cjs} +145 -133
- package/{lib → dist}/sui/sui.d.ts +7 -8
- package/dist/ton/command.cjs +137 -0
- package/{lib → dist}/ton/command.d.ts +1 -2
- package/dist/ton/ton.cjs +103 -0
- package/{lib → dist}/ton/ton.d.ts +7 -6
- package/dist/utils.cjs +102 -0
- package/{lib → dist}/utils.d.ts +4 -4
- package/package.json +161 -20
- package/lib/aptos/aptos.d.ts.map +0 -1
- package/lib/aptos/balance-tracker.d.ts.map +0 -1
- package/lib/aptos/command.d.ts.map +0 -1
- package/lib/aptos/command.js +0 -126
- package/lib/common.d.ts.map +0 -1
- package/lib/common.js +0 -2
- package/lib/controller.d.ts.map +0 -1
- package/lib/evm/balance-tracker.d.ts.map +0 -1
- package/lib/evm/balance-tracker.js +0 -49
- package/lib/evm/command.d.ts.map +0 -1
- package/lib/evm/command.js +0 -178
- package/lib/evm/custom-gas-station.d.ts.map +0 -1
- package/lib/evm/custom-gas-station.js +0 -40
- package/lib/evm/evm.d.ts.map +0 -1
- package/lib/evm/evm.js +0 -270
- package/lib/evm/pyth-abi.d.ts.map +0 -1
- package/lib/evm/pyth-contract.d.ts.map +0 -1
- package/lib/evm/pyth-contract.js +0 -11
- package/lib/evm/super-wallet.d.ts.map +0 -1
- package/lib/evm/super-wallet.js +0 -73
- package/lib/fuel/command.d.ts.map +0 -1
- package/lib/fuel/command.js +0 -98
- package/lib/fuel/fuel.d.ts.map +0 -1
- package/lib/fuel/fuel.js +0 -101
- package/lib/index.d.ts +0 -3
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -34
- package/lib/injective/command.d.ts.map +0 -1
- package/lib/injective/command.js +0 -119
- package/lib/injective/injective.d.ts.map +0 -1
- package/lib/interface.d.ts.map +0 -1
- package/lib/interface.js +0 -122
- package/lib/metrics.d.ts.map +0 -1
- package/lib/metrics.js +0 -152
- package/lib/near/command.d.ts.map +0 -1
- package/lib/near/command.js +0 -103
- package/lib/near/near.d.ts.map +0 -1
- package/lib/near/near.js +0 -168
- package/lib/options.d.ts.map +0 -1
- package/lib/options.js +0 -84
- package/lib/price-config.d.ts.map +0 -1
- package/lib/price-config.js +0 -114
- package/lib/pyth-price-listener.d.ts.map +0 -1
- package/lib/solana/balance-tracker.d.ts.map +0 -1
- package/lib/solana/balance-tracker.js +0 -51
- package/lib/solana/command.d.ts.map +0 -1
- package/lib/solana/command.js +0 -223
- package/lib/solana/solana.d.ts.map +0 -1
- package/lib/sui/balance-tracker.d.ts.map +0 -1
- package/lib/sui/balance-tracker.js +0 -49
- package/lib/sui/command.d.ts.map +0 -1
- package/lib/sui/command.js +0 -160
- package/lib/sui/sui.d.ts.map +0 -1
- package/lib/ton/command.d.ts.map +0 -1
- package/lib/ton/command.js +0 -99
- package/lib/ton/ton.d.ts.map +0 -1
- package/lib/ton/ton.js +0 -97
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -61
|
@@ -0,0 +1,190 @@
|
|
|
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";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "default", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return _default;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _nodefs = /*#__PURE__*/ _interop_require_default(require("node:fs"));
|
|
12
|
+
const _client = require("@mysten/sui/client");
|
|
13
|
+
const _ed25519 = require("@mysten/sui/keypairs/ed25519");
|
|
14
|
+
const _hermesclient = require("@pythnetwork/hermes-client");
|
|
15
|
+
const _pino = /*#__PURE__*/ _interop_require_default(require("pino"));
|
|
16
|
+
const _controller = require("../controller.cjs");
|
|
17
|
+
const _metrics = require("../metrics.cjs");
|
|
18
|
+
const _options = /*#__PURE__*/ _interop_require_wildcard(require("../options.cjs"));
|
|
19
|
+
const _priceconfig = require("../price-config.cjs");
|
|
20
|
+
const _pythpricelistener = require("../pyth-price-listener.cjs");
|
|
21
|
+
const _balancetracker = require("./balance-tracker.cjs");
|
|
22
|
+
const _sui = require("./sui.cjs");
|
|
23
|
+
const _utils = require("../utils.cjs");
|
|
24
|
+
function _interop_require_default(obj) {
|
|
25
|
+
return obj && obj.__esModule ? obj : {
|
|
26
|
+
default: obj
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
30
|
+
if (typeof WeakMap !== "function") return null;
|
|
31
|
+
var cacheBabelInterop = new WeakMap();
|
|
32
|
+
var cacheNodeInterop = new WeakMap();
|
|
33
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
34
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
35
|
+
})(nodeInterop);
|
|
36
|
+
}
|
|
37
|
+
function _interop_require_wildcard(obj, nodeInterop) {
|
|
38
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
39
|
+
return obj;
|
|
40
|
+
}
|
|
41
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
42
|
+
return {
|
|
43
|
+
default: obj
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
47
|
+
if (cache && cache.has(obj)) {
|
|
48
|
+
return cache.get(obj);
|
|
49
|
+
}
|
|
50
|
+
var newObj = {
|
|
51
|
+
__proto__: null
|
|
52
|
+
};
|
|
53
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
54
|
+
for(var key in obj){
|
|
55
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
56
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
57
|
+
if (desc && (desc.get || desc.set)) {
|
|
58
|
+
Object.defineProperty(newObj, key, desc);
|
|
59
|
+
} else {
|
|
60
|
+
newObj[key] = obj[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
newObj.default = obj;
|
|
65
|
+
if (cache) {
|
|
66
|
+
cache.set(obj, newObj);
|
|
67
|
+
}
|
|
68
|
+
return newObj;
|
|
69
|
+
}
|
|
70
|
+
const _default = {
|
|
71
|
+
command: "sui",
|
|
72
|
+
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. ",
|
|
73
|
+
builder: {
|
|
74
|
+
endpoint: {
|
|
75
|
+
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.",
|
|
76
|
+
type: "string",
|
|
77
|
+
required: true
|
|
78
|
+
},
|
|
79
|
+
"pyth-state-id": {
|
|
80
|
+
description: "Pyth State Id. Can be found here" + "https://docs.pyth.network/documentation/pythnet-price-feeds/sui",
|
|
81
|
+
type: "string",
|
|
82
|
+
required: true
|
|
83
|
+
},
|
|
84
|
+
"wormhole-state-id": {
|
|
85
|
+
description: "Wormhole State Id. Can be found here" + "https://docs.pyth.network/documentation/pythnet-price-feeds/sui",
|
|
86
|
+
type: "string",
|
|
87
|
+
required: true
|
|
88
|
+
},
|
|
89
|
+
"num-gas-objects": {
|
|
90
|
+
description: "Number of gas objects in the pool.",
|
|
91
|
+
type: "number",
|
|
92
|
+
required: true,
|
|
93
|
+
default: 30
|
|
94
|
+
},
|
|
95
|
+
"ignore-gas-objects": {
|
|
96
|
+
description: "Gas objects to ignore when merging gas objects on startup -- use this for locked objects.",
|
|
97
|
+
type: "array",
|
|
98
|
+
required: false,
|
|
99
|
+
default: []
|
|
100
|
+
},
|
|
101
|
+
"gas-budget": {
|
|
102
|
+
description: "Gas budget for each price update",
|
|
103
|
+
type: "number",
|
|
104
|
+
required: true,
|
|
105
|
+
default: 500_000_000
|
|
106
|
+
},
|
|
107
|
+
"account-index": {
|
|
108
|
+
description: "Index of the account to use derived by the mnemonic",
|
|
109
|
+
type: "number",
|
|
110
|
+
required: true,
|
|
111
|
+
default: 0
|
|
112
|
+
},
|
|
113
|
+
..._options.priceConfigFile,
|
|
114
|
+
..._options.priceServiceEndpoint,
|
|
115
|
+
..._options.mnemonicFile,
|
|
116
|
+
..._options.pollingFrequency,
|
|
117
|
+
..._options.pushingFrequency,
|
|
118
|
+
..._options.logLevel,
|
|
119
|
+
..._options.controllerLogLevel,
|
|
120
|
+
..._options.enableMetrics,
|
|
121
|
+
..._options.metricsPort
|
|
122
|
+
},
|
|
123
|
+
handler: async function(argv) {
|
|
124
|
+
const { endpoint, priceConfigFile, priceServiceEndpoint, mnemonicFile, pushingFrequency, pollingFrequency, pythStateId, wormholeStateId, numGasObjects, ignoreGasObjects, gasBudget, accountIndex, logLevel, controllerLogLevel, enableMetrics, metricsPort } = argv;
|
|
125
|
+
const logger = (0, _pino.default)({
|
|
126
|
+
level: logLevel
|
|
127
|
+
});
|
|
128
|
+
const priceConfigs = (0, _priceconfig.readPriceConfigFile)(priceConfigFile);
|
|
129
|
+
const hermesClient = new _hermesclient.HermesClient(priceServiceEndpoint);
|
|
130
|
+
const mnemonic = _nodefs.default.readFileSync(mnemonicFile, "utf8").trim();
|
|
131
|
+
const keypair = _ed25519.Ed25519Keypair.deriveKeypair(mnemonic, `m/44'/784'/${accountIndex}'/0'/0'`);
|
|
132
|
+
const suiAddress = keypair.getPublicKey().toSuiAddress();
|
|
133
|
+
logger.info(`Pushing updates from wallet address: ${suiAddress}`);
|
|
134
|
+
let priceItems = priceConfigs.map(({ id, alias })=>({
|
|
135
|
+
id,
|
|
136
|
+
alias
|
|
137
|
+
}));
|
|
138
|
+
// Better to filter out invalid price items before creating the pyth listener
|
|
139
|
+
const { existingPriceItems, invalidPriceItems } = await (0, _utils.filterInvalidPriceItems)(hermesClient, priceItems);
|
|
140
|
+
if (invalidPriceItems.length > 0) {
|
|
141
|
+
logger.error(`Invalid price id submitted for: ${invalidPriceItems.map(({ alias })=>alias).join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
priceItems = existingPriceItems;
|
|
144
|
+
// Initialize metrics if enabled
|
|
145
|
+
let metrics;
|
|
146
|
+
if (enableMetrics) {
|
|
147
|
+
metrics = new _metrics.PricePusherMetrics(logger.child({
|
|
148
|
+
module: "Metrics"
|
|
149
|
+
}));
|
|
150
|
+
metrics.start(metricsPort);
|
|
151
|
+
logger.info(`Metrics server started on port ${metricsPort}`);
|
|
152
|
+
}
|
|
153
|
+
const pythListener = new _pythpricelistener.PythPriceListener(hermesClient, priceItems, logger.child({
|
|
154
|
+
module: "PythPriceListener"
|
|
155
|
+
}));
|
|
156
|
+
const suiClient = new _client.SuiClient({
|
|
157
|
+
url: endpoint
|
|
158
|
+
});
|
|
159
|
+
const suiListener = new _sui.SuiPriceListener(pythStateId, wormholeStateId, endpoint, priceItems, logger.child({
|
|
160
|
+
module: "SuiPriceListener"
|
|
161
|
+
}), {
|
|
162
|
+
pollingFrequency
|
|
163
|
+
});
|
|
164
|
+
const suiPusher = await _sui.SuiPricePusher.createWithAutomaticGasPool(hermesClient, logger.child({
|
|
165
|
+
module: "SuiPricePusher"
|
|
166
|
+
}), pythStateId, wormholeStateId, endpoint, keypair, gasBudget, numGasObjects, ignoreGasObjects);
|
|
167
|
+
const controller = new _controller.Controller(priceConfigs, pythListener, suiListener, suiPusher, logger.child({
|
|
168
|
+
module: "Controller"
|
|
169
|
+
}, {
|
|
170
|
+
level: controllerLogLevel
|
|
171
|
+
}), {
|
|
172
|
+
pushingFrequency,
|
|
173
|
+
metrics: metrics
|
|
174
|
+
});
|
|
175
|
+
// Create and start the balance tracker if metrics are enabled
|
|
176
|
+
if (metrics) {
|
|
177
|
+
const balanceTracker = (0, _balancetracker.createSuiBalanceTracker)({
|
|
178
|
+
client: suiClient,
|
|
179
|
+
address: suiAddress,
|
|
180
|
+
network: "sui",
|
|
181
|
+
updateInterval: pushingFrequency,
|
|
182
|
+
metrics,
|
|
183
|
+
logger
|
|
184
|
+
});
|
|
185
|
+
// Start the balance tracker
|
|
186
|
+
await balanceTracker.start();
|
|
187
|
+
}
|
|
188
|
+
void controller.start();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Options } from "yargs";
|
|
1
|
+
import type { Options } from "yargs";
|
|
2
2
|
declare const _default: {
|
|
3
3
|
command: string;
|
|
4
4
|
describe: string;
|
|
@@ -23,4 +23,3 @@ declare const _default: {
|
|
|
23
23
|
handler: (argv: any) => Promise<void>;
|
|
24
24
|
};
|
|
25
25
|
export default _default;
|
|
26
|
-
//# sourceMappingURL=command.d.ts.map
|
|
@@ -1,22 +1,39 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ "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 SuiPriceListener () {
|
|
13
|
+
return SuiPriceListener;
|
|
14
|
+
},
|
|
15
|
+
get SuiPricePusher () {
|
|
16
|
+
return SuiPricePusher;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const _client = require("@mysten/sui/client");
|
|
20
|
+
const _transactions = require("@mysten/sui/transactions");
|
|
21
|
+
const _pythsuijs = require("@pythnetwork/pyth-sui-js");
|
|
22
|
+
const _interface = require("../interface.cjs");
|
|
8
23
|
const GAS_FEE_FOR_SPLIT = 2_000_000_000;
|
|
9
24
|
// TODO: read this from on chain config
|
|
10
25
|
const MAX_NUM_GAS_OBJECTS_IN_PTB = 256;
|
|
11
26
|
const MAX_NUM_OBJECTS_IN_ARGUMENT = 510;
|
|
12
|
-
class SuiPriceListener extends
|
|
27
|
+
class SuiPriceListener extends _interface.ChainPriceListener {
|
|
13
28
|
pythClient;
|
|
14
29
|
provider;
|
|
15
30
|
logger;
|
|
16
|
-
constructor(pythStateId, wormholeStateId, endpoint, priceItems, logger, config)
|
|
31
|
+
constructor(pythStateId, wormholeStateId, endpoint, priceItems, logger, config){
|
|
17
32
|
super(config.pollingFrequency, priceItems);
|
|
18
|
-
this.provider = new
|
|
19
|
-
|
|
33
|
+
this.provider = new _client.SuiClient({
|
|
34
|
+
url: endpoint
|
|
35
|
+
});
|
|
36
|
+
this.pythClient = new _pythsuijs.SuiPythClient(this.provider, pythStateId, wormholeStateId);
|
|
20
37
|
this.logger = logger;
|
|
21
38
|
}
|
|
22
39
|
async getOnChainPriceInfo(priceId) {
|
|
@@ -28,47 +45,29 @@ class SuiPriceListener extends interface_1.ChainPriceListener {
|
|
|
28
45
|
// Fetching the price info object for the above priceInfoObjectId
|
|
29
46
|
const priceInfoObject = await this.provider.getObject({
|
|
30
47
|
id: priceInfoObjectId,
|
|
31
|
-
options: {
|
|
48
|
+
options: {
|
|
49
|
+
showContent: true
|
|
50
|
+
}
|
|
32
51
|
});
|
|
33
|
-
if (!priceInfoObject.data
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
throw new Error("fetched object datatype should be moveObject");
|
|
37
|
-
const priceInfo =
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
52
|
+
if (!priceInfoObject.data?.content) throw new Error("Price not found on chain for price id " + priceId);
|
|
53
|
+
if (priceInfoObject.data.content.dataType !== "moveObject") throw new Error("fetched object datatype should be moveObject");
|
|
54
|
+
const priceInfo = // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
39
55
|
// @ts-ignore
|
|
40
|
-
priceInfoObject.data.content.fields.price_info.fields.price_feed.fields
|
|
41
|
-
.price.fields;
|
|
56
|
+
priceInfoObject.data.content.fields.price_info.fields.price_feed.fields.price.fields;
|
|
42
57
|
const { magnitude, negative } = priceInfo.price.fields;
|
|
43
58
|
const conf = priceInfo.conf;
|
|
44
59
|
const timestamp = priceInfo.timestamp;
|
|
45
60
|
return {
|
|
46
|
-
price: negative ?
|
|
61
|
+
price: negative ? `-${magnitude}` : magnitude,
|
|
47
62
|
conf,
|
|
48
|
-
publishTime: Number(timestamp)
|
|
63
|
+
publishTime: Number(timestamp)
|
|
49
64
|
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.logger.error(err, `Polling Sui on-chain price for ${priceId} failed.`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.logger.error(error, `Polling Sui on-chain price for ${priceId} failed.`);
|
|
53
67
|
return undefined;
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
|
-
exports.SuiPriceListener = SuiPriceListener;
|
|
58
|
-
/**
|
|
59
|
-
* The `SuiPricePusher` is designed for high-throughput of price updates.
|
|
60
|
-
* Achieving this property requires sacrificing some nice-to-have features of other
|
|
61
|
-
* pusher implementations that can reduce cost when running multiple pushers. It also requires
|
|
62
|
-
* jumping through some Sui-specific hoops in order to maximize parallelism.
|
|
63
|
-
*
|
|
64
|
-
* The two main design features are:
|
|
65
|
-
* 1. This implementation does not use `update_price_feeds_if_necssary` and simulate the transaction
|
|
66
|
-
* before submission. If multiple instances of this pusher are running in parallel, all of them will
|
|
67
|
-
* land all of their pushed updates on-chain.
|
|
68
|
-
* 2. The pusher will split the Coin balance in the provided account into a pool of different Coin objects.
|
|
69
|
-
* Each transaction will be allocated a Coin object from this pool as needed. This process enables the
|
|
70
|
-
* transactions to avoid referencing the same owned objects, which allows them to be processed in parallel.
|
|
71
|
-
*/
|
|
72
71
|
class SuiPricePusher {
|
|
73
72
|
signer;
|
|
74
73
|
provider;
|
|
@@ -77,7 +76,7 @@ class SuiPricePusher {
|
|
|
77
76
|
gasBudget;
|
|
78
77
|
gasPool;
|
|
79
78
|
pythClient;
|
|
80
|
-
constructor(signer, provider, logger, hermesClient, gasBudget, gasPool, pythClient)
|
|
79
|
+
constructor(signer, provider, logger, hermesClient, gasBudget, gasPool, pythClient){
|
|
81
80
|
this.signer = signer;
|
|
82
81
|
this.provider = provider;
|
|
83
82
|
this.logger = logger;
|
|
@@ -87,21 +86,16 @@ class SuiPricePusher {
|
|
|
87
86
|
this.pythClient = pythClient;
|
|
88
87
|
}
|
|
89
88
|
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
*/
|
|
96
|
-
static async getPackageId(provider, objectId) {
|
|
97
|
-
const state = await provider
|
|
98
|
-
.getObject({
|
|
89
|
+
* getPackageId returns the latest package id that the object belongs to. Use this to
|
|
90
|
+
* fetch the latest package id for a given object id and handle package upgrades automatically.
|
|
91
|
+
* @returns package id
|
|
92
|
+
*/ static async getPackageId(provider, objectId) {
|
|
93
|
+
const state = await provider.getObject({
|
|
99
94
|
id: objectId,
|
|
100
95
|
options: {
|
|
101
|
-
showContent: true
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
.then((result) => {
|
|
96
|
+
showContent: true
|
|
97
|
+
}
|
|
98
|
+
}).then((result)=>{
|
|
105
99
|
if (result.data?.content?.dataType == "moveObject") {
|
|
106
100
|
return result.data.content.fields;
|
|
107
101
|
}
|
|
@@ -115,24 +109,24 @@ class SuiPricePusher {
|
|
|
115
109
|
throw new Error("upgrade_cap not found");
|
|
116
110
|
}
|
|
117
111
|
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
static async createWithAutomaticGasPool(hermesClient, logger, pythStateId, wormholeStateId, endpoint, keypair, gasBudget, numGasObjects, ignoreGasObjects) {
|
|
112
|
+
* Create a price pusher with a pool of `numGasObjects` gas coins that will be used to send transactions.
|
|
113
|
+
* The gas coins of the wallet for the provided keypair will be merged and then evenly split into `numGasObjects`.
|
|
114
|
+
*/ static async createWithAutomaticGasPool(hermesClient, logger, pythStateId, wormholeStateId, endpoint, keypair, gasBudget, numGasObjects, ignoreGasObjects) {
|
|
122
115
|
if (numGasObjects > MAX_NUM_OBJECTS_IN_ARGUMENT) {
|
|
123
116
|
throw new Error(`numGasObjects cannot be greater than ${MAX_NUM_OBJECTS_IN_ARGUMENT} until we implement split chunking`);
|
|
124
117
|
}
|
|
125
|
-
const provider = new
|
|
118
|
+
const provider = new _client.SuiClient({
|
|
119
|
+
url: endpoint
|
|
120
|
+
});
|
|
126
121
|
const gasPool = await SuiPricePusher.initializeGasPool(keypair, provider, numGasObjects, ignoreGasObjects, logger);
|
|
127
|
-
const pythClient = new
|
|
122
|
+
const pythClient = new _pythsuijs.SuiPythClient(provider, pythStateId, wormholeStateId);
|
|
128
123
|
return new SuiPricePusher(keypair, provider, logger, hermesClient, gasBudget, gasPool, pythClient);
|
|
129
124
|
}
|
|
130
125
|
async updatePriceFeed(priceIds, pubTimesToPush) {
|
|
131
126
|
if (priceIds.length === 0) {
|
|
132
127
|
return;
|
|
133
128
|
}
|
|
134
|
-
if (priceIds.length !== pubTimesToPush.length)
|
|
135
|
-
throw new Error("Invalid arguments");
|
|
129
|
+
if (priceIds.length !== pubTimesToPush.length) throw new Error("Invalid arguments");
|
|
136
130
|
if (this.gasPool.length === 0) {
|
|
137
131
|
this.logger.warn("Skipping update: no available gas coin.");
|
|
138
132
|
return;
|
|
@@ -140,27 +134,27 @@ class SuiPricePusher {
|
|
|
140
134
|
// 3 price feeds per transaction is the optimal number for gas cost.
|
|
141
135
|
const priceIdChunks = chunkArray(priceIds, 3);
|
|
142
136
|
const txBlocks = [];
|
|
143
|
-
await Promise.all(priceIdChunks.map(async (priceIdChunk)
|
|
137
|
+
await Promise.all(priceIdChunks.map(async (priceIdChunk)=>{
|
|
144
138
|
const response = await this.hermesClient.getLatestPriceUpdates(priceIdChunk, {
|
|
145
139
|
encoding: "base64",
|
|
146
|
-
ignoreInvalidPriceIds: true
|
|
140
|
+
ignoreInvalidPriceIds: true
|
|
147
141
|
});
|
|
148
142
|
if (response.binary.data.length !== 1) {
|
|
149
143
|
throw new Error(`Expected a single VAA for all priceIds ${priceIdChunk} but received ${response.binary.data.length} VAAs: ${response.binary.data}`);
|
|
150
144
|
}
|
|
151
145
|
const vaa = response.binary.data[0];
|
|
152
|
-
const tx = new
|
|
153
|
-
await this.pythClient.updatePriceFeeds(tx, [
|
|
146
|
+
const tx = new _transactions.Transaction();
|
|
147
|
+
await this.pythClient.updatePriceFeeds(tx, [
|
|
148
|
+
Buffer.from(vaa ?? "", "base64")
|
|
149
|
+
], priceIdChunk);
|
|
154
150
|
txBlocks.push(tx);
|
|
155
151
|
}));
|
|
156
152
|
await this.sendTransactionBlocks(txBlocks);
|
|
157
153
|
}
|
|
158
|
-
/** Send every transaction in txs in parallel, returning when all transactions have completed. */
|
|
159
|
-
|
|
160
|
-
return Promise.all(txs.map((tx) => this.sendTransactionBlock(tx)));
|
|
154
|
+
/** Send every transaction in txs in parallel, returning when all transactions have completed. */ async sendTransactionBlocks(txs) {
|
|
155
|
+
return Promise.all(txs.map((tx)=>this.sendTransactionBlock(tx)));
|
|
161
156
|
}
|
|
162
|
-
/** Send a single transaction block using a gas coin from the pool. */
|
|
163
|
-
async sendTransactionBlock(tx) {
|
|
157
|
+
/** Send a single transaction block using a gas coin from the pool. */ async sendTransactionBlock(tx) {
|
|
164
158
|
const gasObject = this.gasPool.shift();
|
|
165
159
|
if (gasObject === undefined) {
|
|
166
160
|
this.logger.warn("No available gas coin. Skipping push.");
|
|
@@ -168,29 +162,28 @@ class SuiPricePusher {
|
|
|
168
162
|
}
|
|
169
163
|
let nextGasObject = undefined;
|
|
170
164
|
try {
|
|
171
|
-
tx.setGasPayment([
|
|
165
|
+
tx.setGasPayment([
|
|
166
|
+
gasObject
|
|
167
|
+
]);
|
|
172
168
|
tx.setGasBudget(this.gasBudget);
|
|
173
169
|
const result = await this.provider.signAndExecuteTransaction({
|
|
174
170
|
signer: this.signer,
|
|
175
171
|
transaction: tx,
|
|
176
172
|
options: {
|
|
177
|
-
showEffects: true
|
|
178
|
-
}
|
|
173
|
+
showEffects: true
|
|
174
|
+
}
|
|
179
175
|
});
|
|
180
|
-
nextGasObject = result.effects?.mutated
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
String(err).includes("GasBalanceTooLow")) {
|
|
188
|
-
this.logger.error(err, "Insufficient gas balance");
|
|
176
|
+
nextGasObject = result.effects?.mutated?.map((obj)=>obj.reference).find((ref)=>ref.objectId === gasObject.objectId);
|
|
177
|
+
this.logger.info({
|
|
178
|
+
hash: result.digest
|
|
179
|
+
}, "Successfully updated price with transaction digest");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (String(error).includes("Balance of gas object") || String(error).includes("GasBalanceTooLow")) {
|
|
182
|
+
this.logger.error(error, "Insufficient gas balance");
|
|
189
183
|
// If the error is caused by insufficient gas, we should panic
|
|
190
|
-
throw
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
this.logger.error(err, "Failed to update price. Trying to refresh gas object references.");
|
|
184
|
+
throw error;
|
|
185
|
+
} else {
|
|
186
|
+
this.logger.error(error, "Failed to update price. Trying to refresh gas object references.");
|
|
194
187
|
// Refresh the coin object here in case the error is caused by an object version mismatch.
|
|
195
188
|
nextGasObject = await SuiPricePusher.tryRefreshObjectReference(this.provider, gasObject);
|
|
196
189
|
}
|
|
@@ -206,85 +199,98 @@ class SuiPricePusher {
|
|
|
206
199
|
static async initializeGasPool(signer, provider, numGasObjects, ignoreGasObjects, logger) {
|
|
207
200
|
const signerAddress = signer.toSuiAddress();
|
|
208
201
|
if (ignoreGasObjects.length > 0) {
|
|
209
|
-
logger.info({
|
|
202
|
+
logger.info({
|
|
203
|
+
ignoreGasObjects
|
|
204
|
+
}, "Ignoring some gas objects for coin merging");
|
|
210
205
|
}
|
|
211
206
|
const consolidatedCoin = await SuiPricePusher.mergeGasCoinsIntoOne(signer, provider, signerAddress, ignoreGasObjects, logger);
|
|
212
207
|
const coinResult = await provider.getObject({
|
|
213
208
|
id: consolidatedCoin.objectId,
|
|
214
|
-
options: {
|
|
209
|
+
options: {
|
|
210
|
+
showContent: true
|
|
211
|
+
}
|
|
215
212
|
});
|
|
216
213
|
let balance;
|
|
217
|
-
if (coinResult.data &&
|
|
218
|
-
coinResult.data.content &&
|
|
219
|
-
coinResult.data.content.dataType == "moveObject") {
|
|
214
|
+
if (coinResult.data?.content && coinResult.data.content.dataType == "moveObject") {
|
|
220
215
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
221
216
|
// @ts-ignore
|
|
222
217
|
balance = coinResult.data.content.fields.balance;
|
|
223
|
-
}
|
|
224
|
-
else
|
|
225
|
-
throw new Error("Bad coin object");
|
|
218
|
+
} else throw new Error("Bad coin object");
|
|
226
219
|
const splitAmount = (BigInt(balance) - BigInt(GAS_FEE_FOR_SPLIT)) / BigInt(numGasObjects);
|
|
227
220
|
const gasPool = await SuiPricePusher.splitGasCoinEqually(signer, provider, signerAddress, Number(splitAmount), numGasObjects, consolidatedCoin);
|
|
228
|
-
logger.info({
|
|
221
|
+
logger.info({
|
|
222
|
+
gasPool
|
|
223
|
+
}, "Gas pool is filled with coins");
|
|
229
224
|
return gasPool;
|
|
230
225
|
}
|
|
231
226
|
// Attempt to refresh the version of the provided object reference to point to the current version
|
|
232
227
|
// of the object. Throws an error if the object cannot be refreshed.
|
|
233
228
|
static async tryRefreshObjectReference(provider, ref) {
|
|
234
|
-
const objectResponse = await provider.getObject({
|
|
235
|
-
|
|
229
|
+
const objectResponse = await provider.getObject({
|
|
230
|
+
id: ref.objectId
|
|
231
|
+
});
|
|
232
|
+
if (objectResponse.data === undefined) {
|
|
233
|
+
throw new Error("Failed to refresh object reference");
|
|
234
|
+
} else {
|
|
236
235
|
return {
|
|
237
236
|
digest: objectResponse.data.digest,
|
|
238
237
|
objectId: objectResponse.data.objectId,
|
|
239
|
-
version: objectResponse.data.version
|
|
238
|
+
version: objectResponse.data.version
|
|
240
239
|
};
|
|
241
240
|
}
|
|
242
|
-
else {
|
|
243
|
-
throw new Error("Failed to refresh object reference");
|
|
244
|
-
}
|
|
245
241
|
}
|
|
246
242
|
static async getAllGasCoins(provider, owner) {
|
|
247
243
|
let hasNextPage = true;
|
|
248
244
|
let cursor;
|
|
249
245
|
const coins = new Set([]);
|
|
250
246
|
let numCoins = 0;
|
|
251
|
-
while
|
|
247
|
+
while(hasNextPage){
|
|
252
248
|
const paginatedCoins = await provider.getCoins({
|
|
253
249
|
owner,
|
|
254
|
-
cursor
|
|
250
|
+
cursor
|
|
255
251
|
});
|
|
256
252
|
numCoins += paginatedCoins.data.length;
|
|
257
|
-
paginatedCoins.data
|
|
253
|
+
for (const c of paginatedCoins.data)coins.add(JSON.stringify({
|
|
258
254
|
objectId: c.coinObjectId,
|
|
259
255
|
version: c.version,
|
|
260
|
-
digest: c.digest
|
|
261
|
-
}))
|
|
256
|
+
digest: c.digest
|
|
257
|
+
}));
|
|
262
258
|
hasNextPage = paginatedCoins.hasNextPage;
|
|
263
259
|
cursor = paginatedCoins.nextCursor;
|
|
264
260
|
}
|
|
265
261
|
if (numCoins !== coins.size) {
|
|
266
262
|
throw new Error("Unexpected getCoins result: duplicate coins found");
|
|
267
263
|
}
|
|
268
|
-
return [
|
|
264
|
+
return [
|
|
265
|
+
...coins
|
|
266
|
+
].map((item)=>JSON.parse(item));
|
|
269
267
|
}
|
|
270
268
|
static async splitGasCoinEqually(signer, provider, signerAddress, splitAmount, numGasObjects, gasCoin) {
|
|
271
269
|
// TODO: implement chunking if numGasObjects exceeds MAX_NUM_CREATED_OBJECTS
|
|
272
|
-
const tx = new
|
|
273
|
-
const coins = tx.splitCoins(tx.gas, Array.from({
|
|
274
|
-
|
|
275
|
-
tx.
|
|
270
|
+
const tx = new _transactions.Transaction();
|
|
271
|
+
const coins = tx.splitCoins(tx.gas, Array.from({
|
|
272
|
+
length: numGasObjects
|
|
273
|
+
}, ()=>tx.pure.u64(splitAmount)));
|
|
274
|
+
tx.transferObjects(Array.from({
|
|
275
|
+
length: numGasObjects
|
|
276
|
+
}, (_, i)=>coins[i]), tx.pure.address(signerAddress));
|
|
277
|
+
tx.setGasPayment([
|
|
278
|
+
gasCoin
|
|
279
|
+
]);
|
|
276
280
|
const result = await provider.signAndExecuteTransaction({
|
|
277
281
|
signer,
|
|
278
282
|
transaction: tx,
|
|
279
|
-
options: {
|
|
283
|
+
options: {
|
|
284
|
+
showEffects: true
|
|
285
|
+
}
|
|
280
286
|
});
|
|
281
|
-
const error = result
|
|
287
|
+
const error = result.effects?.status.error;
|
|
282
288
|
if (error) {
|
|
283
289
|
throw new Error(`Failed to initialize gas pool: ${error}. Try re-running the script`);
|
|
284
290
|
}
|
|
285
|
-
const newCoins = result.effects.created.map((obj)
|
|
291
|
+
const newCoins = result.effects.created.map((obj)=>obj.reference);
|
|
286
292
|
if (newCoins.length !== numGasObjects) {
|
|
287
|
-
throw new Error(`Failed to initialize gas pool. Expected ${numGasObjects}, got: ${newCoins}`);
|
|
293
|
+
throw new Error(`Failed to initialize gas pool. Expected ${numGasObjects}, got: ${JSON.stringify(newCoins)}`);
|
|
288
294
|
}
|
|
289
295
|
return newCoins;
|
|
290
296
|
}
|
|
@@ -297,13 +303,16 @@ class SuiPricePusher {
|
|
|
297
303
|
const gasCoinsChunks = chunkArray(gasCoins, MAX_NUM_GAS_OBJECTS_IN_PTB - 2);
|
|
298
304
|
let finalCoin;
|
|
299
305
|
const lockedAddresses = new Set();
|
|
300
|
-
|
|
301
|
-
for
|
|
302
|
-
const mergeTx = new
|
|
306
|
+
for (const value of initialLockedAddresses)lockedAddresses.add(value);
|
|
307
|
+
for(let i = 0; i < gasCoinsChunks.length; i++){
|
|
308
|
+
const mergeTx = new _transactions.Transaction();
|
|
303
309
|
let coins = gasCoinsChunks[i];
|
|
304
|
-
coins = coins
|
|
310
|
+
coins = coins?.filter((coin)=>!lockedAddresses.has(coin.objectId)) ?? [];
|
|
305
311
|
if (finalCoin) {
|
|
306
|
-
coins = [
|
|
312
|
+
coins = [
|
|
313
|
+
finalCoin,
|
|
314
|
+
...coins
|
|
315
|
+
];
|
|
307
316
|
}
|
|
308
317
|
mergeTx.setGasPayment(coins);
|
|
309
318
|
let mergeResult;
|
|
@@ -311,14 +320,17 @@ class SuiPricePusher {
|
|
|
311
320
|
mergeResult = await provider.signAndExecuteTransaction({
|
|
312
321
|
signer,
|
|
313
322
|
transaction: mergeTx,
|
|
314
|
-
options: {
|
|
323
|
+
options: {
|
|
324
|
+
showEffects: true
|
|
325
|
+
}
|
|
315
326
|
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
Object.values(
|
|
321
|
-
|
|
327
|
+
} catch (error_) {
|
|
328
|
+
logger.error(error_, "Merge transaction failed with error");
|
|
329
|
+
if (String(error_).includes("quorum of validators because of locked objects. Retried a conflicting transaction")) {
|
|
330
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
331
|
+
Object.values(error_.data).forEach((lockedObjects)=>{
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, unicorn/no-array-for-each
|
|
333
|
+
lockedObjects.forEach((lockedObject)=>{
|
|
322
334
|
lockedAddresses.add(lockedObject[0]);
|
|
323
335
|
});
|
|
324
336
|
});
|
|
@@ -326,22 +338,22 @@ class SuiPricePusher {
|
|
|
326
338
|
i--;
|
|
327
339
|
continue;
|
|
328
340
|
}
|
|
329
|
-
throw
|
|
341
|
+
throw error_;
|
|
330
342
|
}
|
|
331
|
-
const error = mergeResult
|
|
343
|
+
const error = mergeResult.effects?.status.error;
|
|
332
344
|
if (error) {
|
|
333
345
|
throw new Error(`Failed to merge coins when initializing gas pool: ${error}. Try re-running the script`);
|
|
334
346
|
}
|
|
335
|
-
finalCoin = mergeResult.effects.mutated.map((obj)
|
|
347
|
+
finalCoin = mergeResult.effects.mutated.map((obj)=>obj.reference)[0];
|
|
336
348
|
}
|
|
349
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
337
350
|
return finalCoin;
|
|
338
351
|
}
|
|
339
352
|
}
|
|
340
|
-
exports.SuiPricePusher = SuiPricePusher;
|
|
341
353
|
function chunkArray(array, size) {
|
|
342
354
|
const chunked = [];
|
|
343
355
|
let index = 0;
|
|
344
|
-
while
|
|
356
|
+
while(index < array.length){
|
|
345
357
|
chunked.push(array.slice(index, size + index));
|
|
346
358
|
index += size;
|
|
347
359
|
}
|