@gearbox-protocol/sdk 18.3.1 → 18.4.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.
Files changed (179) hide show
  1. package/dist/cjs/abi/310/iKillSwitchProxy.js +130 -0
  2. package/dist/cjs/onchain/index.js +18 -0
  3. package/dist/cjs/onchain/market/adapters/abi/adapters/iTreehouseFastlaneAdapter.js +113 -0
  4. package/dist/cjs/onchain/market/adapters/abi/adapters/iTreehouseRedemptionGatewayAdapter.js +258 -0
  5. package/dist/cjs/onchain/market/adapters/abi/adapters/iTreehouseRouterAdapter.js +161 -0
  6. package/dist/cjs/onchain/market/adapters/abi/adapters/index.js +6 -0
  7. package/dist/cjs/onchain/market/adapters/abi/index.js +13 -0
  8. package/dist/cjs/onchain/market/adapters/abi/treehouse/iTreehouseFastlane.js +39 -0
  9. package/dist/cjs/onchain/market/adapters/abi/treehouse/iTreehouseRedemptionGateway.js +230 -0
  10. package/dist/cjs/onchain/market/adapters/abi/treehouse/iTreehouseRouter.js +29 -0
  11. package/dist/cjs/onchain/market/adapters/abi/treehouse/index.js +7 -0
  12. package/dist/cjs/onchain/market/adapters/contracts/TreehouseFastlaneAdapterContract.js +72 -0
  13. package/dist/cjs/onchain/market/adapters/contracts/TreehouseRedemptionGatewayAdapterContract.js +117 -0
  14. package/dist/cjs/onchain/market/adapters/contracts/TreehouseRouterAdapterContract.js +61 -0
  15. package/dist/cjs/onchain/market/adapters/contracts/index.js +6 -0
  16. package/dist/cjs/onchain/market/adapters/createAdapter.js +6 -0
  17. package/dist/cjs/onchain/market/adapters/index.js +18 -0
  18. package/dist/cjs/onchain/market/index.js +18 -0
  19. package/dist/cjs/permissionless/api/index.js +31 -0
  20. package/dist/cjs/permissionless/api/oracles/PermissionlessOracles.js +637 -65
  21. package/dist/cjs/permissionless/api/oracles/enrich.js +99 -0
  22. package/dist/cjs/permissionless/api/oracles/index.js +33 -0
  23. package/dist/cjs/permissionless/api/oracles/onchain-feed.js +25 -0
  24. package/dist/cjs/permissionless/api/oracles/schemas.js +84 -59
  25. package/dist/cjs/permissionless/api/oracles/score/formula.js +56 -0
  26. package/dist/cjs/permissionless/api/oracles/score/index.js +11 -0
  27. package/dist/cjs/permissionless/api/oracles/score/run.js +252 -0
  28. package/dist/cjs/permissionless/api/oracles/score/types.js +1 -0
  29. package/dist/cjs/permissionless/api/oracles/stress/classify.js +258 -0
  30. package/dist/cjs/permissionless/api/oracles/stress/index.js +24 -0
  31. package/dist/cjs/permissionless/api/oracles/stress/override.js +125 -0
  32. package/dist/cjs/permissionless/api/oracles/stress/run.js +485 -0
  33. package/dist/cjs/permissionless/api/oracles/stress/score.js +98 -0
  34. package/dist/cjs/permissionless/api/oracles/stress/types.js +1 -0
  35. package/dist/cjs/permissionless/api/schemas.js +0 -3
  36. package/dist/cjs/permissionless/api/transactions/InstanceOwnerTransactions.js +14 -3
  37. package/dist/cjs/permissionless/api/transactions/lp-price-feeds.js +10 -4
  38. package/dist/cjs/permissionless/bindings/bytecode-repository.js +8 -0
  39. package/dist/cjs/permissionless/bindings/governance/governor.js +1 -1
  40. package/dist/cjs/permissionless/bindings/index.js +8 -3
  41. package/dist/cjs/permissionless/bindings/kill-switch-proxy.js +188 -0
  42. package/dist/cjs/permissionless/bindings/market-configurator-factory.js +39 -1
  43. package/dist/cjs/permissionless/bindings/market-configurator.js +142 -0
  44. package/dist/cjs/permissionless/index.js +42 -3
  45. package/dist/cjs/permissionless/utils/index.js +3 -0
  46. package/dist/cjs/permissionless/utils/literals.js +2 -0
  47. package/dist/cjs/permissionless/utils/price-update/get-prices.js +64 -0
  48. package/dist/cjs/permissionless/utils/price-update/index.js +2 -0
  49. package/dist/cjs/testnets/TestnetsAPI.js +179 -0
  50. package/dist/cjs/testnets/errors/TestnetNotFoundError.js +15 -0
  51. package/dist/cjs/testnets/errors/TestnetRequestFailedError.js +24 -0
  52. package/dist/cjs/testnets/errors/TestnetStatusError.js +19 -0
  53. package/dist/cjs/testnets/errors/TestnetValidationError.js +24 -0
  54. package/dist/cjs/testnets/errors/TestnetWaitTimeoutError.js +30 -0
  55. package/dist/cjs/testnets/errors/TestnetsError.js +34 -0
  56. package/dist/cjs/testnets/errors/index.js +13 -0
  57. package/dist/cjs/testnets/index.js +32 -0
  58. package/dist/cjs/testnets/package.json +1 -0
  59. package/dist/cjs/testnets/schemas.js +242 -0
  60. package/dist/cjs/testnets/types.js +1 -0
  61. package/dist/esm/abi/310/iKillSwitchProxy.js +129 -0
  62. package/dist/esm/onchain/index.js +10 -1
  63. package/dist/esm/onchain/market/adapters/abi/adapters/iTreehouseFastlaneAdapter.js +112 -0
  64. package/dist/esm/onchain/market/adapters/abi/adapters/iTreehouseRedemptionGatewayAdapter.js +257 -0
  65. package/dist/esm/onchain/market/adapters/abi/adapters/iTreehouseRouterAdapter.js +160 -0
  66. package/dist/esm/onchain/market/adapters/abi/adapters/index.js +4 -1
  67. package/dist/esm/onchain/market/adapters/abi/index.js +8 -1
  68. package/dist/esm/onchain/market/adapters/abi/treehouse/iTreehouseFastlane.js +38 -0
  69. package/dist/esm/onchain/market/adapters/abi/treehouse/iTreehouseRedemptionGateway.js +229 -0
  70. package/dist/esm/onchain/market/adapters/abi/treehouse/iTreehouseRouter.js +28 -0
  71. package/dist/esm/onchain/market/adapters/abi/treehouse/index.js +4 -0
  72. package/dist/esm/onchain/market/adapters/contracts/TreehouseFastlaneAdapterContract.js +71 -0
  73. package/dist/esm/onchain/market/adapters/contracts/TreehouseRedemptionGatewayAdapterContract.js +116 -0
  74. package/dist/esm/onchain/market/adapters/contracts/TreehouseRouterAdapterContract.js +60 -0
  75. package/dist/esm/onchain/market/adapters/contracts/index.js +4 -1
  76. package/dist/esm/onchain/market/adapters/createAdapter.js +6 -0
  77. package/dist/esm/onchain/market/adapters/index.js +10 -1
  78. package/dist/esm/onchain/market/index.js +10 -1
  79. package/dist/esm/permissionless/api/index.js +7 -1
  80. package/dist/esm/permissionless/api/oracles/PermissionlessOracles.js +639 -67
  81. package/dist/esm/permissionless/api/oracles/enrich.js +94 -0
  82. package/dist/esm/permissionless/api/oracles/index.js +10 -1
  83. package/dist/esm/permissionless/api/oracles/onchain-feed.js +24 -0
  84. package/dist/esm/permissionless/api/oracles/schemas.js +80 -58
  85. package/dist/esm/permissionless/api/oracles/score/formula.js +51 -0
  86. package/dist/esm/permissionless/api/oracles/score/index.js +4 -0
  87. package/dist/esm/permissionless/api/oracles/score/run.js +250 -0
  88. package/dist/esm/permissionless/api/oracles/score/types.js +1 -0
  89. package/dist/esm/permissionless/api/oracles/stress/classify.js +256 -0
  90. package/dist/esm/permissionless/api/oracles/stress/index.js +6 -0
  91. package/dist/esm/permissionless/api/oracles/stress/override.js +119 -0
  92. package/dist/esm/permissionless/api/oracles/stress/run.js +478 -0
  93. package/dist/esm/permissionless/api/oracles/stress/score.js +95 -0
  94. package/dist/esm/permissionless/api/oracles/stress/types.js +1 -0
  95. package/dist/esm/permissionless/api/schemas.js +2 -4
  96. package/dist/esm/permissionless/api/transactions/InstanceOwnerTransactions.js +14 -3
  97. package/dist/esm/permissionless/api/transactions/lp-price-feeds.js +10 -4
  98. package/dist/esm/permissionless/bindings/bytecode-repository.js +8 -0
  99. package/dist/esm/permissionless/bindings/governance/governor.js +1 -1
  100. package/dist/esm/permissionless/bindings/index.js +5 -4
  101. package/dist/esm/permissionless/bindings/kill-switch-proxy.js +184 -0
  102. package/dist/esm/permissionless/bindings/market-configurator-factory.js +37 -0
  103. package/dist/esm/permissionless/bindings/market-configurator.js +143 -1
  104. package/dist/esm/permissionless/index.js +13 -6
  105. package/dist/esm/permissionless/utils/index.js +3 -3
  106. package/dist/esm/permissionless/utils/literals.js +2 -1
  107. package/dist/esm/permissionless/utils/price-update/get-prices.js +63 -1
  108. package/dist/esm/permissionless/utils/price-update/index.js +2 -2
  109. package/dist/esm/testnets/TestnetsAPI.js +178 -0
  110. package/dist/esm/testnets/errors/TestnetNotFoundError.js +14 -0
  111. package/dist/esm/testnets/errors/TestnetRequestFailedError.js +23 -0
  112. package/dist/esm/testnets/errors/TestnetStatusError.js +18 -0
  113. package/dist/esm/testnets/errors/TestnetValidationError.js +23 -0
  114. package/dist/esm/testnets/errors/TestnetWaitTimeoutError.js +29 -0
  115. package/dist/esm/testnets/errors/TestnetsError.js +33 -0
  116. package/dist/esm/testnets/errors/index.js +7 -0
  117. package/dist/esm/testnets/index.js +11 -0
  118. package/dist/esm/testnets/package.json +1 -0
  119. package/dist/esm/testnets/schemas.js +228 -0
  120. package/dist/esm/testnets/types.js +1 -0
  121. package/dist/types/abi/310/iKillSwitchProxy.d.ts +114 -0
  122. package/dist/types/onchain/index.d.ts +10 -1
  123. package/dist/types/onchain/market/adapters/abi/adapters/iTreehouseFastlaneAdapter.d.ts +101 -0
  124. package/dist/types/onchain/market/adapters/abi/adapters/iTreehouseRedemptionGatewayAdapter.d.ts +237 -0
  125. package/dist/types/onchain/market/adapters/abi/adapters/iTreehouseRouterAdapter.d.ts +146 -0
  126. package/dist/types/onchain/market/adapters/abi/adapters/index.d.ts +4 -1
  127. package/dist/types/onchain/market/adapters/abi/index.d.ts +8 -1
  128. package/dist/types/onchain/market/adapters/abi/treehouse/iTreehouseFastlane.d.ts +33 -0
  129. package/dist/types/onchain/market/adapters/abi/treehouse/iTreehouseRedemptionGateway.d.ts +207 -0
  130. package/dist/types/onchain/market/adapters/abi/treehouse/iTreehouseRouter.d.ts +27 -0
  131. package/dist/types/onchain/market/adapters/abi/treehouse/index.d.ts +4 -0
  132. package/dist/types/onchain/market/adapters/contracts/TreehouseFastlaneAdapterContract.d.ts +156 -0
  133. package/dist/types/onchain/market/adapters/contracts/TreehouseRedemptionGatewayAdapterContract.d.ts +481 -0
  134. package/dist/types/onchain/market/adapters/contracts/TreehouseRouterAdapterContract.d.ts +193 -0
  135. package/dist/types/onchain/market/adapters/contracts/index.d.ts +4 -1
  136. package/dist/types/onchain/market/adapters/index.d.ts +10 -1
  137. package/dist/types/onchain/market/index.d.ts +10 -1
  138. package/dist/types/permissionless/api/index.d.ts +10 -2
  139. package/dist/types/permissionless/api/oracles/PermissionlessOracles.d.ts +237 -47
  140. package/dist/types/permissionless/api/oracles/enrich.d.ts +59 -0
  141. package/dist/types/permissionless/api/oracles/index.d.ts +12 -2
  142. package/dist/types/permissionless/api/oracles/onchain-feed.d.ts +15 -0
  143. package/dist/types/permissionless/api/oracles/schemas.d.ts +147 -41
  144. package/dist/types/permissionless/api/oracles/score/formula.d.ts +36 -0
  145. package/dist/types/permissionless/api/oracles/score/index.d.ts +4 -0
  146. package/dist/types/permissionless/api/oracles/score/run.d.ts +42 -0
  147. package/dist/types/permissionless/api/oracles/score/types.d.ts +118 -0
  148. package/dist/types/permissionless/api/oracles/stress/classify.d.ts +57 -0
  149. package/dist/types/permissionless/api/oracles/stress/index.d.ts +6 -0
  150. package/dist/types/permissionless/api/oracles/stress/override.d.ts +59 -0
  151. package/dist/types/permissionless/api/oracles/stress/run.d.ts +100 -0
  152. package/dist/types/permissionless/api/oracles/stress/score.d.ts +57 -0
  153. package/dist/types/permissionless/api/oracles/stress/types.d.ts +272 -0
  154. package/dist/types/permissionless/api/oracles/types.d.ts +320 -125
  155. package/dist/types/permissionless/api/schemas.d.ts +0 -2
  156. package/dist/types/permissionless/api/transactions/lp-price-feeds.d.ts +9 -1
  157. package/dist/types/permissionless/bindings/bytecode-repository.d.ts +10 -1
  158. package/dist/types/permissionless/bindings/index.d.ts +4 -3
  159. package/dist/types/permissionless/bindings/kill-switch-proxy.d.ts +205 -0
  160. package/dist/types/permissionless/bindings/market-configurator-factory.d.ts +30 -0
  161. package/dist/types/permissionless/bindings/market-configurator.d.ts +30 -1
  162. package/dist/types/permissionless/bindings/types.d.ts +27 -0
  163. package/dist/types/permissionless/index.d.ts +15 -6
  164. package/dist/types/permissionless/utils/index.d.ts +3 -3
  165. package/dist/types/permissionless/utils/literals.d.ts +1 -0
  166. package/dist/types/permissionless/utils/price-update/get-prices.d.ts +33 -0
  167. package/dist/types/permissionless/utils/price-update/index.d.ts +2 -2
  168. package/dist/types/testnets/TestnetsAPI.d.ts +108 -0
  169. package/dist/types/testnets/errors/TestnetNotFoundError.d.ts +12 -0
  170. package/dist/types/testnets/errors/TestnetRequestFailedError.d.ts +11 -0
  171. package/dist/types/testnets/errors/TestnetStatusError.d.ts +15 -0
  172. package/dist/types/testnets/errors/TestnetValidationError.d.ts +16 -0
  173. package/dist/types/testnets/errors/TestnetWaitTimeoutError.d.ts +21 -0
  174. package/dist/types/testnets/errors/TestnetsError.d.ts +47 -0
  175. package/dist/types/testnets/errors/index.d.ts +7 -0
  176. package/dist/types/testnets/index.d.ts +11 -0
  177. package/dist/types/testnets/schemas.d.ts +554 -0
  178. package/dist/types/testnets/types.d.ts +717 -0
  179. package/package.json +6 -1
@@ -0,0 +1,485 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_abi_compressors_priceFeedCompressor = require("../../../../abi/compressors/priceFeedCompressor.js");
3
+ const require_permissionless_api_oracles_stress_classify = require("./classify.js");
4
+ const require_permissionless_api_oracles_stress_override = require("./override.js");
5
+ const require_permissionless_api_oracles_stress_score = require("./score.js");
6
+ let viem = require("viem");
7
+ //#region src/permissionless/api/oracles/stress/run.ts
8
+ /**
9
+ * The two ends the question is asked at, which is what "infinitely large and
10
+ * infinitely small" comes to once a type has to hold it.
11
+ */
12
+ const DEFAULT_SCENARIOS = [{
13
+ label: "unbounded up",
14
+ multiplier: require_permissionless_api_oracles_stress_override.UNBOUNDED_UP
15
+ }, {
16
+ label: "unbounded down",
17
+ multiplier: require_permissionless_api_oracles_stress_override.UNBOUNDED_DOWN
18
+ }];
19
+ const aggregatorAbi = (0, viem.parseAbi)(["function latestRoundData() view returns (uint80, int256, uint256, uint256, uint80)"]);
20
+ /**
21
+ * What a Gearbox LP feed exposes about the rate it reads.
22
+ *
23
+ * `lpContract` is the contract the rate comes off — the wstETH token, the
24
+ * vault, the Curve pool — and is what a rate source is perturbed at.
25
+ * The two bounds are the window the feed will accept a rate inside, which is
26
+ * both this test's baseline and, as it turns out, most of its answer.
27
+ */
28
+ const lpFeedAbi = (0, viem.parseAbi)([
29
+ "function lpContract() view returns (address)",
30
+ "function lowerBound() view returns (uint256)",
31
+ "function upperBound() view returns (uint256)"
32
+ ]);
33
+ /**
34
+ * The Gearbox LP feeds: a protocol's exchange rate, clamped to a window the
35
+ * feed holds, times the price of what the rate is denominated in.
36
+ */
37
+ const RATE_FEED_TYPE = /::(ERC4626|CURVE_\w+|WSTETH|YEARN|MELLOW\w*|BALANCER\w*)$/;
38
+ /** The compressor's `bytes32` contract type, as the string it encodes. */
39
+ function typeName(raw) {
40
+ return (0, viem.hexToString)(raw).replace(/\0+$/, "");
41
+ }
42
+ /** `price × amount` in USD, both of them as the chain reports them. */
43
+ function usd(amount, price) {
44
+ if (amount === null || price === null || price <= 0n) return null;
45
+ return amount * Number((0, viem.formatUnits)(price, 8));
46
+ }
47
+ /**
48
+ * The feed trees of every oracle the chain's markets use, flattened.
49
+ *
50
+ * One call per oracle and not per feed: the compressor answers an oracle's
51
+ * entire tree at once, with every node's type, description, children and
52
+ * current answer, which is the whole of what this test needs off the chain.
53
+ * A node carried by several oracles is one node — the trees overlap heavily,
54
+ * since the aggregators under them are the same handful.
55
+ */
56
+ async function readForest(client, compressor, oracles) {
57
+ const states = await client.multicall({
58
+ contracts: oracles.map((oracle) => ({
59
+ address: compressor,
60
+ abi: require_abi_compressors_priceFeedCompressor.priceFeedCompressorAbi,
61
+ functionName: "getPriceOracleState",
62
+ args: [oracle]
63
+ })),
64
+ allowFailure: true
65
+ });
66
+ const nodes = /* @__PURE__ */ new Map();
67
+ const answers = /* @__PURE__ */ new Map();
68
+ const unreadableOracles = [];
69
+ states.forEach((state, i) => {
70
+ if (state.status !== "success") {
71
+ unreadableOracles.push(oracles[i]);
72
+ return;
73
+ }
74
+ const { priceFeedTree } = state.result;
75
+ addTree(priceFeedTree, nodes, answers);
76
+ });
77
+ return {
78
+ nodes,
79
+ answers,
80
+ unreadableOracles
81
+ };
82
+ }
83
+ /**
84
+ * The trees under a handful of feeds, flattened, whether or not any oracle
85
+ * holds them.
86
+ *
87
+ * One call for all of them: the compressor walks the feeds it is given the
88
+ * way it walks an oracle's, and answers with the same nodes.
89
+ */
90
+ async function readFeedTrees(client, compressor, feeds) {
91
+ const tree = await client.readContract({
92
+ address: compressor,
93
+ abi: require_abi_compressors_priceFeedCompressor.priceFeedCompressorAbi,
94
+ functionName: "loadPriceFeedTree",
95
+ args: [feeds]
96
+ });
97
+ const nodes = /* @__PURE__ */ new Map();
98
+ const answers = /* @__PURE__ */ new Map();
99
+ addTree(tree, nodes, answers);
100
+ return {
101
+ nodes,
102
+ answers
103
+ };
104
+ }
105
+ /** A tree's nodes into the flattened forest, the first copy of each kept. */
106
+ function addTree(tree, nodes, answers) {
107
+ for (const node of tree) {
108
+ const address = node.baseParams.addr.toLowerCase();
109
+ if (nodes.has(address)) continue;
110
+ nodes.set(address, {
111
+ address,
112
+ contractType: typeName(node.baseParams.contractType),
113
+ version: Number(node.baseParams.version),
114
+ description: node.description,
115
+ underlyingFeeds: node.underlyingFeeds.map((feed) => feed.toLowerCase())
116
+ });
117
+ answers.set(address, node.answer.success ? node.answer.price : null);
118
+ }
119
+ }
120
+ /**
121
+ * Where each rate-bearing feed reads its rate, and what it will accept.
122
+ *
123
+ * The baseline a rate source is perturbed from is the feed's own `lowerBound`
124
+ * and not the rate itself. Reading the rate would mean knowing which getter
125
+ * each protocol publishes it under — `stEthPerToken`, `convertToAssets`,
126
+ * `get_virtual_price`, `pricePerShare` — and in exactly which units, which is
127
+ * a table that goes stale. The bound is within the feed's own accepted window
128
+ * of the live rate by construction, so it is the same figure to within that
129
+ * window, which is two percent or so. That is immaterial against the
130
+ * multipliers the ends of the sweep use, and at the fine end of the threshold
131
+ * search the bound *is* the answer: an LP feed clamps the rate it accepts, so
132
+ * where it breaks is where its bound is.
133
+ */
134
+ async function readRateTargets(client, feeds) {
135
+ const targets = /* @__PURE__ */ new Map();
136
+ const baselines = /* @__PURE__ */ new Map();
137
+ const bounds = /* @__PURE__ */ new Map();
138
+ if (feeds.length === 0) return {
139
+ targets,
140
+ baselines,
141
+ bounds
142
+ };
143
+ const results = await client.multicall({
144
+ contracts: feeds.flatMap((address) => [
145
+ {
146
+ address,
147
+ abi: lpFeedAbi,
148
+ functionName: "lpContract"
149
+ },
150
+ {
151
+ address,
152
+ abi: lpFeedAbi,
153
+ functionName: "lowerBound"
154
+ },
155
+ {
156
+ address,
157
+ abi: lpFeedAbi,
158
+ functionName: "upperBound"
159
+ }
160
+ ]),
161
+ allowFailure: true
162
+ });
163
+ feeds.forEach((feed, i) => {
164
+ const contract = results[3 * i];
165
+ const lower = results[3 * i + 1];
166
+ const upper = results[3 * i + 2];
167
+ if (contract?.status !== "success" || lower?.status !== "success") return;
168
+ const target = contract.result.toLowerCase();
169
+ targets.set(feed, target);
170
+ baselines.set(target, lower.result);
171
+ if (upper?.status === "success") bounds.set(feed, {
172
+ lower: lower.result,
173
+ upper: upper.result
174
+ });
175
+ });
176
+ return {
177
+ targets,
178
+ baselines,
179
+ bounds
180
+ };
181
+ }
182
+ /** Tries after the first before a refused read is given up on. */
183
+ const READ_RETRIES = 3;
184
+ /**
185
+ * A multicall failure that is the node's and not the feed's.
186
+ *
187
+ * A call that reverted inside the multicall is decoded from its own return
188
+ * data and carries no error of the `eth_call` around it; a chunk the node
189
+ * refused carries exactly that, for every call in the chunk.
190
+ */
191
+ function isRefusal(error) {
192
+ return error instanceof viem.BaseError && error.walk((cause) => cause instanceof viem.CallExecutionError) !== null;
193
+ }
194
+ /**
195
+ * What every feed answers with one group's sources overridden.
196
+ *
197
+ * One `eth_call` for the whole chain: the overrides go in once and the feeds
198
+ * are read through a multicall inside them, so a group costs a round trip per
199
+ * scenario rather than a round trip per feed. A feed that reverts comes back
200
+ * `null`, which is an outcome and not an error.
201
+ */
202
+ async function readUnder(client, feeds, sources, multiplier, updatedAt, freshen = []) {
203
+ const driven = require_permissionless_api_oracles_stress_override.groupOverride(sources, multiplier, updatedAt);
204
+ const taken = new Set(driven.map((override) => override.address.toLowerCase()));
205
+ const overrides = [...require_permissionless_api_oracles_stress_override.groupOverride(freshen, 1, updatedAt).filter((override) => !taken.has(override.address.toLowerCase())), ...driven];
206
+ const readOnce = () => client.multicall({
207
+ contracts: feeds.map((address) => ({
208
+ address,
209
+ abi: aggregatorAbi,
210
+ functionName: "latestRoundData"
211
+ })),
212
+ allowFailure: true,
213
+ stateOverride: overrides.length > 0 ? overrides : void 0
214
+ });
215
+ let results = [];
216
+ for (let attempt = 0;; attempt++) {
217
+ results = await readOnce();
218
+ const refused = results.find((result) => result.status === "failure" && isRefusal(result.error));
219
+ if (!refused) break;
220
+ if (attempt >= READ_RETRIES) {
221
+ const reason = refused.error instanceof viem.BaseError ? refused.error.walk().shortMessage : String(refused.error);
222
+ throw new Error(`the node would not answer a read of ${feeds.length} feeds after 4 tries: ${reason}`, { cause: refused.error });
223
+ }
224
+ await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
225
+ }
226
+ return new Map(feeds.map((feed, i) => {
227
+ const result = results[i];
228
+ return [feed, result?.status === "success" ? result.result[1] ?? null : null];
229
+ }));
230
+ }
231
+ /**
232
+ * A perturbation too small for any protection to be aimed at, which is what
233
+ * makes it a control.
234
+ *
235
+ * A tenth of a percent is inside every bound these feeds carry and under every
236
+ * deviation threshold, so a slot that does not budge under it is a slot the
237
+ * override never reached — and every other figure about it is then unknown
238
+ * rather than reassuring.
239
+ */
240
+ const CALIBRATION_MULTIPLIER = 1.001;
241
+ /**
242
+ * How many of a group's reachable slots actually responded to being driven.
243
+ *
244
+ * Exact inequality on the answer and not a factor: at a tenth of a percent the
245
+ * point is only whether the number moved at all.
246
+ */
247
+ async function countResponsive(client, roots, slots, members, base, updatedAt) {
248
+ const nudged = await readUnder(client, roots, members, CALIBRATION_MULTIPLIER, updatedAt);
249
+ return slots.filter((slot) => {
250
+ return (base.get(slot.priceFeed) ?? null) !== (nudged.get(slot.priceFeed) ?? null);
251
+ }).length;
252
+ }
253
+ /** `stressed / base`, or `null` where either end is missing. */
254
+ function factorOf(base, stressed) {
255
+ if (base === null || stressed === null || base === 0n) return null;
256
+ return Number(stressed) / Number(base);
257
+ }
258
+ /**
259
+ * What a group did to one chain, market slot by market slot.
260
+ *
261
+ * The ratio and not the price: a slot is scored by how the collateral moved
262
+ * against the market's own underlying, because that is what an account is
263
+ * valued by. A group that moves both legs equally has moved nothing, and this
264
+ * is where that shows up — the underlying's own feed is one of the feeds read
265
+ * under the same override, so both legs come from the same call.
266
+ */
267
+ function outcomeOf(scenario, slots, base, stressed, quotaUsd, chainTotalUsd) {
268
+ const movedPositions = /* @__PURE__ */ new Set();
269
+ const out = slots.map((slot) => {
270
+ const feed = {
271
+ feed: slot.priceFeed,
272
+ base: base.get(slot.priceFeed) ?? null,
273
+ stressed: stressed.get(slot.priceFeed) ?? null,
274
+ factor: factorOf(base.get(slot.priceFeed) ?? null, stressed.get(slot.priceFeed) ?? null)
275
+ };
276
+ const underlying = slot.underlyingPriceFeed;
277
+ const underlyingFactor = underlying === null ? 1 : factorOf(base.get(underlying) ?? null, stressed.get(underlying) ?? null) ?? null;
278
+ return {
279
+ market: slot.market,
280
+ marketName: slot.marketName,
281
+ curatorName: slot.curatorName,
282
+ token: slot.token,
283
+ tokenSymbol: slot.tokenSymbol,
284
+ reserve: slot.reserve,
285
+ isUnderlying: slot.isUnderlying,
286
+ feed,
287
+ ratioFactor: feed.factor === null || underlyingFactor === null || underlyingFactor === 0 ? null : feed.factor / underlyingFactor,
288
+ quotaUsedUsd: quotaUsd.get(slotKey(slot)) ?? null
289
+ };
290
+ });
291
+ const ratios = out.map((slot) => slot.ratioFactor).filter((r) => r !== null && Number.isFinite(r));
292
+ let movedUsd = 0;
293
+ slots.forEach((slot, i) => {
294
+ const outcome = out[i];
295
+ if (!(outcome !== void 0 && (outcome.ratioFactor !== null && outcome.ratioFactor !== 1 || outcome.feed.stressed === null && outcome.feed.base !== null)) || movedPositions.has(positionKey(slot))) return;
296
+ movedPositions.add(positionKey(slot));
297
+ movedUsd += outcome.quotaUsedUsd ?? 0;
298
+ });
299
+ return {
300
+ scenario,
301
+ slots: out,
302
+ worstInflation: ratios.length > 0 ? Math.max(...ratios) : null,
303
+ worstDeflation: ratios.length > 0 ? Math.min(...ratios) : null,
304
+ brokenSlots: out.filter((slot) => slot.feed.stressed === null && slot.feed.base !== null).length,
305
+ deadSlots: out.filter((slot) => slot.feed.base === null).length,
306
+ valueAtRisk: chainTotalUsd > 0 ? movedUsd / chainTotalUsd : 0
307
+ };
308
+ }
309
+ /** What identifies a slot across the reads that describe it. */
310
+ function slotKey(slot) {
311
+ return `${slot.market}:${slot.token}:${slot.reserve}`;
312
+ }
313
+ /**
314
+ * What identifies the *position* a slot prices, which is not the slot.
315
+ *
316
+ * A quota belongs to a `(market, token)` pair, and that pair has two slots:
317
+ * the main feed and the reserve one. Summing money over slots would count
318
+ * every such pair twice, so anything denominated in money is summed over this
319
+ * instead, and the slot figures stay per slot.
320
+ */
321
+ function positionKey(slot) {
322
+ return `${slot.market}:${slot.token}`;
323
+ }
324
+ /**
325
+ * The quota riding on a set of slots, counted once per position.
326
+ *
327
+ * The sum a reader takes to be money at risk, so the double counting a
328
+ * token's two slots would introduce is removed here rather than left for them
329
+ * to notice.
330
+ */
331
+ function quotaOver(slots, quotaUsd) {
332
+ const counted = /* @__PURE__ */ new Set();
333
+ let total = 0;
334
+ for (const slot of slots) {
335
+ const key = positionKey(slot);
336
+ if (counted.has(key)) continue;
337
+ counted.add(key);
338
+ total += quotaUsd.get(slotKey(slot)) ?? 0;
339
+ }
340
+ return total;
341
+ }
342
+ /**
343
+ * The smallest multiple of a group's sources that moves some market past a
344
+ * ratio of `target`, found by bisection.
345
+ *
346
+ * Eight steps over twelve orders of magnitude, which lands the answer inside a
347
+ * factor of about ten percent — enough to say whether a source has to be
348
+ * wrong by a percent or by a thousandfold before anything breaks, which is the
349
+ * question. `null` is a group that does not get there at the far end either.
350
+ */
351
+ async function bisect(read, from, to, target, steps = 8) {
352
+ if (!target(await read(to))) return null;
353
+ let lo = from;
354
+ let hi = to;
355
+ for (let i = 0; i < steps; i++) {
356
+ const mid = Math.sqrt(lo * hi);
357
+ if (target(await read(mid))) hi = mid;
358
+ else lo = mid;
359
+ }
360
+ return hi;
361
+ }
362
+ /**
363
+ * Every source behind one chain's markets, put through the sweep a group at a
364
+ * time.
365
+ *
366
+ * Takes the slots as read, so the caller decides whether that read came from
367
+ * the backend or from somewhere else, and takes a client rather than making
368
+ * one, so the whole thing is testable against a stub.
369
+ **/
370
+ async function runStressTest({ client, chainId, slots, compressor, scenarios = DEFAULT_SCENARIOS, thresholds = true, groups: only, logger }) {
371
+ const { nodes, answers, unreadableOracles } = await readForest(client, compressor, [...new Set(slots.map((slot) => slot.priceOracle.toLowerCase()))]);
372
+ for (const oracle of unreadableOracles) logger?.warn(`price oracle ${oracle} of chain ${chainId} would not report its feed tree; the markets on it are left out`);
373
+ const { targets, baselines: rateBaselines } = await readRateTargets(client, [...nodes.values()].filter((node) => RATE_FEED_TYPE.test(node.contractType)).map((node) => node.address));
374
+ const { sources, unresolved } = require_permissionless_api_oracles_stress_classify.classifySources(chainId, [...nodes.values()], targets, (target) => rateBaselines.get(target) ?? answers.get(target) ?? null);
375
+ for (const node of unresolved) logger?.warn(`feed ${node.address} of chain ${chainId} would not say what it reads its rate from; its protocol is left out of the partition`);
376
+ const roots = [.../* @__PURE__ */ new Set([...slots.map((slot) => slot.priceFeed), ...slots.map((slot) => slot.underlyingPriceFeed).filter((feed) => feed !== null)])];
377
+ const reach = require_permissionless_api_oracles_stress_classify.sourcesByFeed(roots, nodes, sources);
378
+ const quotaUsd = /* @__PURE__ */ new Map();
379
+ for (const slot of slots) {
380
+ const price = slot.underlyingPriceFeed === null ? null : answers.get(slot.underlyingPriceFeed) ?? null;
381
+ quotaUsd.set(slotKey(slot), usd(slot.quotaUsed, price));
382
+ }
383
+ const totalUsd = quotaOver(slots, quotaUsd);
384
+ const updatedAt = BigInt(Math.floor(Date.now() / 1e3));
385
+ const base = await readUnder(client, roots, [], 1, updatedAt);
386
+ const byGroup = /* @__PURE__ */ new Map();
387
+ for (const source of sources) byGroup.set(source.group, [...byGroup.get(source.group) ?? [], source]);
388
+ const unidentifiedUsd = quotaOver(slots.filter((slot) => {
389
+ const reached = reach.get(slot.priceFeed);
390
+ return sources.some((source) => !source.identified && reached?.has(source.target));
391
+ }), quotaUsd);
392
+ const reports = [];
393
+ for (const [group, members] of byGroup) {
394
+ if (only && !only.includes(group)) continue;
395
+ const targetSet = new Set(members.map((source) => source.target));
396
+ const reachable = slots.filter((slot) => [...reach.get(slot.priceFeed) ?? []].some((source) => targetSet.has(source)));
397
+ const reachableUsd = quotaOver(reachable, quotaUsd);
398
+ const roles = /* @__PURE__ */ new Map();
399
+ for (const slot of reachable) {
400
+ const key = positionKey(slot);
401
+ roles.set(key, (roles.get(key) ?? /* @__PURE__ */ new Set()).add(slot.reserve));
402
+ }
403
+ const undefendedPositions = [...roles.values()].filter((set) => set.has(true) && set.has(false)).length;
404
+ const silentSources = members.filter((source) => source.perturbable && source.baseline === null).length;
405
+ const drivable = members.filter((source) => source.perturbable && source.baseline !== null);
406
+ if (reachable.length === 0 || drivable.length === 0) {
407
+ reports.push({
408
+ chainId,
409
+ group,
410
+ sources: members,
411
+ identified: members.every((source) => source.identified),
412
+ reachableSlots: reachable.length,
413
+ reachableUsd,
414
+ undefendedPositions,
415
+ outcomes: [],
416
+ breakingPoints: [],
417
+ responsive: 0,
418
+ silentSources,
419
+ score: 10,
420
+ verdict: require_permissionless_api_oracles_stress_score.verdictOf(group, 10, [], reachable.length, reachableUsd, 0, silentSources, undefendedPositions, members.some((source) => !source.perturbable))
421
+ });
422
+ continue;
423
+ }
424
+ const outcomes = [];
425
+ for (const scenario of scenarios) {
426
+ const stressed = await readUnder(client, roots, members, scenario.multiplier, updatedAt);
427
+ outcomes.push(outcomeOf(scenario, reachable, base, stressed, quotaUsd, totalUsd));
428
+ }
429
+ const breakingPoints = [];
430
+ if (thresholds) {
431
+ const worstRatio = async (multiplier) => {
432
+ const stressed = await readUnder(client, roots, members, multiplier, updatedAt);
433
+ const outcome = outcomeOf({
434
+ label: "probe",
435
+ multiplier
436
+ }, reachable, base, stressed, quotaUsd, totalUsd);
437
+ return Math.max(outcome.worstInflation ?? 1, 1 / (outcome.worstDeflation ?? 1));
438
+ };
439
+ const up = await bisect(worstRatio, 1, require_permissionless_api_oracles_stress_override.UNBOUNDED_UP, (worst) => worst >= 2);
440
+ const down = await bisect(worstRatio, 1, require_permissionless_api_oracles_stress_override.UNBOUNDED_DOWN, (worst) => worst >= 2);
441
+ breakingPoints.push({
442
+ market: reachable[0]?.market ?? "0x",
443
+ token: reachable[0]?.token ?? "0x",
444
+ inflation: up,
445
+ deflation: down
446
+ });
447
+ }
448
+ const responsive = await countResponsive(client, roots, reachable, members, base, updatedAt);
449
+ const score = require_permissionless_api_oracles_stress_score.scoreGroup(outcomes);
450
+ reports.push({
451
+ chainId,
452
+ group,
453
+ sources: members,
454
+ identified: members.every((source) => source.identified),
455
+ reachableSlots: reachable.length,
456
+ reachableUsd,
457
+ undefendedPositions,
458
+ outcomes,
459
+ breakingPoints,
460
+ responsive,
461
+ silentSources,
462
+ score,
463
+ verdict: require_permissionless_api_oracles_stress_score.verdictOf(group, score, outcomes, reachable.length, reachableUsd, responsive, silentSources, undefendedPositions, false)
464
+ });
465
+ }
466
+ return {
467
+ chainId,
468
+ slots: slots.length,
469
+ roots: roots.length,
470
+ nodes: nodes.size,
471
+ deadSlots: slots.filter((slot) => (base.get(slot.priceFeed) ?? null) === null).length,
472
+ sources,
473
+ totalUsd,
474
+ unidentifiedUsd,
475
+ groups: reports.sort((a, b) => a.score - b.score || b.reachableUsd - a.reachableUsd)
476
+ };
477
+ }
478
+ //#endregion
479
+ exports.DEFAULT_SCENARIOS = DEFAULT_SCENARIOS;
480
+ exports.RATE_FEED_TYPE = RATE_FEED_TYPE;
481
+ exports.readFeedTrees = readFeedTrees;
482
+ exports.readForest = readForest;
483
+ exports.readRateTargets = readRateTargets;
484
+ exports.readUnder = readUnder;
485
+ exports.runStressTest = runStressTest;
@@ -0,0 +1,98 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/permissionless/api/oracles/stress/score.ts
3
+ /**
4
+ * Turning what a group did into a mark out of ten.
5
+ *
6
+ * Any single number over a thing this shaped is a judgement, so the one here
7
+ * is spelled out rather than tuned: what it weighs, and why each term is
8
+ * weighed that way, is in the comments below, and every figure it is computed
9
+ * from stays in the report so a reader who disagrees can reweigh them.
10
+ **/
11
+ /**
12
+ * How bad one slot's outcome is, on nought to one.
13
+ *
14
+ * Two kinds of harm, which are not the same kind of problem:
15
+ *
16
+ * - a wrong price. The collateral is valued against the market's underlying,
17
+ * so what matters is the ratio and not either leg: a market whose
18
+ * collateral and underlying both doubled has not moved. Overvalued
19
+ * collateral is borrowed against and the loan is never repaid; undervalued
20
+ * collateral is liquidated out from under an account that was solvent.
21
+ * Both are counted, because a reader asking what a source can do to them
22
+ * is exposed to each.
23
+ *
24
+ * - a price that stopped. The slot reverted where it used to answer, which
25
+ * freezes the market: nothing can be borrowed and, worse, nothing can be
26
+ * liquidated. Scored at half a full mispricing — it is a liveness failure
27
+ * rather than a licence to drain, and it is also loud, where a wrong number
28
+ * is silent.
29
+ *
30
+ * A hundredfold move in the ratio is full harm. Ten times is a half, and the
31
+ * curve is logarithmic between them because the damage is: a position is
32
+ * liquidatable somewhere around a tenth of a move, and everything past that is
33
+ * degrees of already-gone.
34
+ */
35
+ function slotHarm(slot) {
36
+ if (slot.feed.stressed === null && slot.feed.base !== null) return .5;
37
+ const factor = slot.ratioFactor;
38
+ if (factor === null || factor === 1 || !Number.isFinite(factor)) return factor !== null && !Number.isFinite(factor) ? 1 : 0;
39
+ return Math.min(1, Math.abs(Math.log10(factor)) / 2);
40
+ }
41
+ /**
42
+ * The group's mark, over every scenario it was put through.
43
+ *
44
+ * Two views of the same slots, because either alone misleads. Weighing harm by
45
+ * the quota riding on it is what a reader with money in the protocol wants —
46
+ * a source that can only move a market with nothing in it has not done much.
47
+ * But a market with nothing in it today is where tomorrow's deposit goes, so
48
+ * the worst slot regardless of size is carried too, at a smaller weight.
49
+ *
50
+ * Ten is a group that moved nothing. Nought is one that revalued everything
51
+ * the chain has out.
52
+ */
53
+ function scoreGroup(outcomes) {
54
+ let weighted = 0;
55
+ let worst = 0;
56
+ for (const outcome of outcomes) {
57
+ const total = outcome.slots.reduce((sum, slot) => sum + (slot.quotaUsedUsd ?? 0), 0);
58
+ let scenarioWeighted = 0;
59
+ for (const slot of outcome.slots) {
60
+ const harm = slotHarm(slot);
61
+ worst = Math.max(worst, harm);
62
+ if (total > 0) scenarioWeighted += (slot.quotaUsedUsd ?? 0) / total * harm;
63
+ }
64
+ weighted = Math.max(weighted, scenarioWeighted);
65
+ }
66
+ const damage = .7 * weighted + .3 * worst;
67
+ return Math.round((1 - damage) * 100) / 10;
68
+ }
69
+ /**
70
+ * The one line the score stands for.
71
+ *
72
+ * Says which of the two failures the group causes, because the remedies
73
+ * differ: a wrong price is answered with bounds and a second feed, a dead one
74
+ * with a reserve feed and a staleness period.
75
+ */
76
+ function verdictOf(group, score, outcomes, reachableSlots, reachableUsd, responsive, silentSources, undefendedPositions, selfContained) {
77
+ const money = () => `$${Math.round(reachableUsd).toLocaleString("en-US")} of quota`;
78
+ if (reachableSlots === 0) return `${group} prices nothing any market reads.`;
79
+ if (selfContained) return `${group} carries its price in its own code and is not published to, so there is no source to lie: ${money()} behind ${reachableSlots} slot(s), at the mercy of whoever can replace the contract rather than of whoever reports to it.`;
80
+ if (outcomes.length === 0) return `${group} is not answering as it stands — ${silentSources} silent source(s) — so nothing could be perturbed: ${money()} behind ${reachableSlots} slot(s) is priced through something already not reporting.`;
81
+ if (responsive === 0) return `${group}: not one of its ${reachableSlots} slot(s) moved under a perturbation of a tenth of a percent, so this test never drove it. Treat every figure for it as unknown, not as safe.`;
82
+ const broken = Math.max(...outcomes.map((o) => o.brokenSlots), 0);
83
+ const inflation = Math.max(...outcomes.map((o) => o.worstInflation ?? 1), 1);
84
+ const deflation = Math.min(...outcomes.map((o) => o.worstDeflation ?? 1), 1);
85
+ const ends = [inflation > 1.01 ? `${inflation.toExponential(1)}× too high` : null, deflation < .99 ? `${deflation.toExponential(1)}× of what it should be` : null].filter((end) => end !== null);
86
+ const dead = Math.max(...outcomes.map((o) => o.deadSlots), 0);
87
+ const live = reachableSlots - dead;
88
+ const moves = ends.length > 0 ? `drives the collateral-to-underlying ratio of up to ${live} live slot(s) to ${ends.join(" and ")}` : `moves no ratio by more than a percent, with ${responsive} slot(s) proven to respond at all`;
89
+ const freeze = broken > 0 ? `, and stops ${broken} of them answering` : "";
90
+ const undefended = undefendedPositions > 0 ? ` Its reach includes ${undefendedPositions} position(s) whose main and reserve feed both read it, so the fallback fails with it.` : "";
91
+ const standing = dead > 0 ? ` ${dead} of its slot(s) are priced through a feed that is not answering right now, before any of this.` : "";
92
+ const silent = silentSources > 0 ? ` (${silentSources} of its sources are already silent and were not perturbed)` : "";
93
+ return `${group} (${score}/10): ${money()} behind it; ${moves}${freeze}.${undefended}${standing}${silent}`;
94
+ }
95
+ //#endregion
96
+ exports.scoreGroup = scoreGroup;
97
+ exports.slotHarm = slotHarm;
98
+ exports.verdictOf = verdictOf;
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
@@ -12,8 +12,6 @@ let zod_v4 = require("zod/v4");
12
12
  **/
13
13
  /** An address, in whatever case the backend serves it. */
14
14
  const addressSchema = zod_v4.z.custom((value) => typeof value === "string" && (0, viem.isAddress)(value, { strict: false }), { message: "invalid address" });
15
- /** A `0x`-prefixed hex string, as viem's `Hex`. */
16
- const hexSchema = zod_v4.z.custom((value) => typeof value === "string" && (0, viem.isHex)(value), { message: "invalid hex string" });
17
15
  /**
18
16
  * A record keyed by address. Spelled out here because zod types a record key
19
17
  * by the schema's output, which the branded `Address` is not inferred as.
@@ -25,4 +23,3 @@ function addressRecord(value) {
25
23
  //#endregion
26
24
  exports.addressRecord = addressRecord;
27
25
  exports.addressSchema = addressSchema;
28
- exports.hexSchema = hexSchema;
@@ -137,10 +137,21 @@ var InstanceOwnerTransactions = class {
137
137
  const batch = await this.#batchContext(chainId);
138
138
  const pfCompressor = await this.#priceFeedCompressor(batch.client);
139
139
  const known = await batch.store.getKnownPriceFeeds();
140
+ const isKnown = new Set(known.map(lower));
140
141
  const feeds = await require_permissionless_api_transactions_lp_price_feeds.getFeedsToUpdate(batch.client, pfCompressor, known);
142
+ const addTxs = [];
143
+ const added = [];
141
144
  const configureTxs = [];
142
- for (const feed of feeds) configureTxs.push(...await require_permissionless_api_transactions_lp_price_feeds.updateBounds(batch.client, feed));
143
- const txs = [batch.store.configurePriceFeeds(configureTxs.map((tx) => ({
145
+ for (const feed of feeds) {
146
+ const boundTxs = await require_permissionless_api_transactions_lp_price_feeds.updateBounds(batch.client, feed.address);
147
+ if (boundTxs.length === 0) continue;
148
+ if (!isKnown.has(feed.address)) {
149
+ addTxs.push(batch.store.addPriceFeedTx(feed.address, 0, feed.description));
150
+ added.push(feed.address);
151
+ }
152
+ configureTxs.push(...boundTxs);
153
+ }
154
+ const txs = [...addTxs, batch.store.configurePriceFeeds(configureTxs.map((tx) => ({
144
155
  target: tx.to,
145
156
  callData: tx.callData
146
157
  })))];
@@ -149,7 +160,7 @@ var InstanceOwnerTransactions = class {
149
160
  author: owner,
150
161
  name: "update-bounds",
151
162
  txs,
152
- touchedFeeds: []
163
+ touchedFeeds: added
153
164
  });
154
165
  }
155
166
  /**
@@ -52,9 +52,12 @@ async function updateBounds(client, address) {
52
52
  /**
53
53
  * Of the whole feed tree behind `feeds`, the ones that are LP feeds: the
54
54
  * probe keeps whatever answers `getLPExchangeRate`.
55
+ *
56
+ * The trees reach past `feeds` themselves, so an LP feed read inside one of
57
+ * them is among the results whether or not it is one of `feeds`.
55
58
  **/
56
59
  async function getFeedsToUpdate(client, pfCompressor, feeds) {
57
- const updateableFeeds = /* @__PURE__ */ new Set();
60
+ const updateableFeeds = /* @__PURE__ */ new Map();
58
61
  const { data } = await client.call({
59
62
  to: pfCompressor,
60
63
  data: (0, viem.encodeFunctionData)({
@@ -69,8 +72,8 @@ async function getFeedsToUpdate(client, pfCompressor, feeds) {
69
72
  functionName: "loadPriceFeedTree",
70
73
  data: data ?? "0x"
71
74
  });
72
- for (const tree of trees) updateableFeeds.add(tree.baseParams.addr.toLowerCase());
73
- const candidates = Array.from(updateableFeeds);
75
+ for (const tree of trees) updateableFeeds.set(tree.baseParams.addr.toLowerCase(), tree.description);
76
+ const candidates = Array.from(updateableFeeds.keys());
74
77
  const { results } = await require_onchain_utils_viem_simulateMulticall.simulateMulticall(client, {
75
78
  contracts: candidates.map((feed) => ({
76
79
  address: feed,
@@ -79,7 +82,10 @@ async function getFeedsToUpdate(client, pfCompressor, feeds) {
79
82
  })),
80
83
  allowFailure: true
81
84
  });
82
- return candidates.filter((_, index) => results[index]?.status === "success");
85
+ return candidates.filter((_, index) => results[index]?.status === "success").map((address) => ({
86
+ address,
87
+ description: updateableFeeds.get(address) ?? ""
88
+ }));
83
89
  }
84
90
  //#endregion
85
91
  exports.getFeedsToUpdate = getFeedsToUpdate;