@gearbox-protocol/sdk 13.5.3 → 13.6.0-apy-plugin.2

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 (43) hide show
  1. package/dist/cjs/plugins/adapters/abi/actionAbi.js +1 -1
  2. package/dist/cjs/plugins/adapters/abi/conctructorAbi.js +1 -1
  3. package/dist/cjs/plugins/adapters/createAdapter.js +1 -1
  4. package/dist/cjs/plugins/adapters/types.js +1 -1
  5. package/dist/cjs/plugins/pools-history/ApyPlugin.js +265 -0
  6. package/dist/cjs/plugins/pools-history/apy-cache.js +120 -0
  7. package/dist/cjs/plugins/pools-history/apy-parser.js +169 -0
  8. package/dist/cjs/plugins/pools-history/constants.js +31 -0
  9. package/dist/cjs/plugins/pools-history/index.js +12 -2
  10. package/dist/cjs/plugins/pools-history/pool-apy-types.js +16 -0
  11. package/dist/cjs/plugins/pools-history/pool-apy-utils.js +141 -0
  12. package/dist/cjs/rewards/rewards/extra-apy.js +10 -8
  13. package/dist/cjs/sdk/utils/viem/simulateWithPriceUpdates.js +2 -39
  14. package/dist/esm/plugins/adapters/abi/actionAbi.js +1 -1
  15. package/dist/esm/plugins/adapters/abi/conctructorAbi.js +1 -1
  16. package/dist/esm/plugins/adapters/createAdapter.js +1 -1
  17. package/dist/esm/plugins/adapters/types.js +1 -1
  18. package/dist/esm/plugins/pools-history/ApyPlugin.js +254 -0
  19. package/dist/esm/plugins/pools-history/apy-cache.js +86 -0
  20. package/dist/esm/plugins/pools-history/apy-parser.js +143 -0
  21. package/dist/esm/plugins/pools-history/constants.js +6 -0
  22. package/dist/esm/plugins/pools-history/index.js +6 -1
  23. package/dist/esm/plugins/pools-history/pool-apy-types.js +0 -0
  24. package/dist/esm/plugins/pools-history/pool-apy-utils.js +113 -0
  25. package/dist/esm/rewards/rewards/extra-apy.js +10 -8
  26. package/dist/esm/sdk/utils/viem/simulateWithPriceUpdates.js +2 -41
  27. package/dist/types/plugins/adapters/types.d.ts +2 -2
  28. package/dist/types/plugins/pools-history/ApyPlugin.d.ts +46 -0
  29. package/dist/types/plugins/pools-history/apy-cache.d.ts +28 -0
  30. package/dist/types/plugins/pools-history/apy-parser.d.ts +5 -0
  31. package/dist/types/plugins/pools-history/constants.d.ts +2 -0
  32. package/dist/types/plugins/pools-history/index.d.ts +6 -1
  33. package/dist/types/plugins/pools-history/pool-apy-types.d.ts +39 -0
  34. package/dist/types/plugins/pools-history/pool-apy-utils.d.ts +71 -0
  35. package/dist/types/plugins/pools-history/types.d.ts +28 -0
  36. package/dist/types/rewards/rewards/api.d.ts +10 -1
  37. package/dist/types/rewards/rewards/common.d.ts +0 -10
  38. package/dist/types/rewards/rewards/extra-apy.d.ts +4 -6
  39. package/dist/types/sdk/base/types.d.ts +0 -9
  40. package/package.json +1 -1
  41. package/dist/cjs/plugins/pools-history/Pools7DAgoPlugin.js +0 -108
  42. package/dist/esm/plugins/pools-history/Pools7DAgoPlugin.js +0 -90
  43. package/dist/types/plugins/pools-history/Pools7DAgoPlugin.d.ts +0 -20
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var pool_apy_utils_exports = {};
20
+ __export(pool_apy_utils_exports, {
21
+ calculatePoolFullAPY: () => calculatePoolFullAPY,
22
+ calculatePoolFullAPY7DAgo: () => calculatePoolFullAPY7DAgo,
23
+ calculatePoolPoints: () => calculatePoolPoints,
24
+ calculateSupplyApy7d: () => calculateSupplyApy7d,
25
+ getPoolExtraAPY: () => getPoolExtraAPY
26
+ });
27
+ module.exports = __toCommonJS(pool_apy_utils_exports);
28
+ var import_constants = require("../../sdk/constants/index.js");
29
+ var import_formatter = require("../../sdk/utils/formatter.js");
30
+ const SCALE = 1000000n;
31
+ const WEEKS_PER_YEAR = 54n;
32
+ function getPoolExtraAPY(lookupAddresses, poolExtraAPYList) {
33
+ if (!poolExtraAPYList) return [];
34
+ const result = [];
35
+ for (const addr of lookupAddresses) {
36
+ const extra = poolExtraAPYList[addr.toLowerCase()];
37
+ if (extra) {
38
+ result.push(...extra);
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+ function calculateSupplyApy7d(currentDieselRate, currentSupplyRate, dieselRate7DAgo) {
44
+ if (dieselRate7DAgo > currentDieselRate) {
45
+ return (0, import_formatter.rayToNumber)(currentSupplyRate * SCALE) / Number(import_constants.PERCENTAGE_FACTOR);
46
+ }
47
+ const apy = (currentDieselRate * import_constants.RAY / dieselRate7DAgo - import_constants.RAY) * WEEKS_PER_YEAR;
48
+ return (0, import_formatter.rayToNumber)(apy * SCALE) / Number(import_constants.PERCENTAGE_FACTOR);
49
+ }
50
+ function calculatePoolFullAPY({
51
+ depositAPY,
52
+ underlyingAPY,
53
+ extraAPY,
54
+ currentExternalList
55
+ }) {
56
+ const baseAPY = [
57
+ { type: "supplyAPY", apy: depositAPY },
58
+ ...underlyingAPY > 0 ? [
59
+ {
60
+ type: "tokenYield",
61
+ apy: Number(underlyingAPY) / Number(import_constants.PERCENTAGE_FACTOR)
62
+ }
63
+ ] : []
64
+ ];
65
+ const filteredExtra = [...extraAPY ?? []].filter((r) => r.apy > 0);
66
+ const baseAPYTotal = baseAPY.reduce((s, r) => s + (r.apy || 0), 0);
67
+ const extraAPYTotal = filteredExtra.reduce((s, r) => s + (r.apy || 0), 0);
68
+ const total = baseAPYTotal + extraAPYTotal;
69
+ const externalAPY = resolveExternalAPY(currentExternalList, total);
70
+ return {
71
+ totalAPY: total,
72
+ baseAPY,
73
+ extraAPY: filteredExtra,
74
+ extraAPYTotal,
75
+ externalAPY
76
+ };
77
+ }
78
+ function resolveExternalAPY(list, baseTotal) {
79
+ const first = list?.[0];
80
+ if (!first) return void 0;
81
+ return {
82
+ ...first,
83
+ totalValue: baseTotal + first.value
84
+ };
85
+ }
86
+ function calculatePoolFullAPY7DAgo({
87
+ supplyAPY7DAgo,
88
+ depositAPY,
89
+ poolAPY
90
+ }) {
91
+ const {
92
+ baseAPY = [],
93
+ extraAPYTotal = 0,
94
+ extraAPY = [],
95
+ externalAPY
96
+ } = poolAPY ?? {};
97
+ const base = [
98
+ { apy: supplyAPY7DAgo || depositAPY, type: "supplyAPY" },
99
+ ...baseAPY.filter((r) => r.type !== "supplyAPY")
100
+ ];
101
+ const baseTotal = base.reduce((acc, r) => acc + r.apy, 0);
102
+ const total = baseTotal + extraAPYTotal;
103
+ return {
104
+ totalAPY: total,
105
+ extraAPYTotal,
106
+ baseAPY: base,
107
+ extraAPY,
108
+ externalAPY,
109
+ loading7DAgo: supplyAPY7DAgo === void 0
110
+ };
111
+ }
112
+ function calculatePoolPoints({
113
+ poolTokenSymbol,
114
+ points,
115
+ tokensList
116
+ }) {
117
+ return points?.map(({ info, points: pts }) => {
118
+ const { decimals = 18 } = tokensList.get(info.token) || {};
119
+ const amount = (0, import_formatter.formatBN)(pts, decimals);
120
+ const { name = "Points", duration } = info ?? {};
121
+ return {
122
+ reward: info,
123
+ name,
124
+ amount,
125
+ tokenTitle: poolTokenSymbol,
126
+ fullTip: [
127
+ `${amount} ${name}`,
128
+ ...duration ? [duration] : [],
129
+ ...poolTokenSymbol ? [poolTokenSymbol] : []
130
+ ].join(" per ")
131
+ };
132
+ });
133
+ }
134
+ // Annotate the CommonJS export names for ESM import in node:
135
+ 0 && (module.exports = {
136
+ calculatePoolFullAPY,
137
+ calculatePoolFullAPY7DAgo,
138
+ calculatePoolPoints,
139
+ calculateSupplyApy7d,
140
+ getPoolExtraAPY
141
+ });
@@ -82,11 +82,13 @@ class PoolPointsAPI {
82
82
  tokensList
83
83
  }) {
84
84
  const r = pools.reduce((acc, p) => {
85
- const pointsInfo = poolRewards[p.address] || [];
85
+ const poolAddress = p.pool.pool.address.toLowerCase();
86
+ const pointsInfo = poolRewards[poolAddress] || [];
86
87
  const poolPointsList = pointsInfo.reduce(
87
88
  (acc2, pointsInfo2) => {
88
- const { address: tokenAddress } = tokensList[pointsInfo2.token] || {};
89
- const tokenBalance = totalTokenBalances[tokenAddress || ""];
89
+ const { addr: tokenAddress } = tokensList.get(pointsInfo2.token) || {};
90
+ const tokenAddressLower = (tokenAddress || "").toLowerCase();
91
+ const tokenBalance = totalTokenBalances[tokenAddressLower];
90
92
  const points = PoolPointsAPI.getPoolTokenPoints(
91
93
  tokenBalance,
92
94
  p,
@@ -104,22 +106,22 @@ class PoolPointsAPI {
104
106
  },
105
107
  []
106
108
  );
107
- acc[p.address] = poolPointsList;
109
+ acc[poolAddress] = poolPointsList;
108
110
  return acc;
109
111
  }, {});
110
112
  return r;
111
113
  }
112
114
  static getPoolTokenPoints(tokenBalanceInPool, pool, tokensList, pointsInfo) {
113
- if (pool.expectedLiquidity <= 0) return 0n;
115
+ if (pool.pool.pool.expectedLiquidity <= 0) return 0n;
114
116
  if (pointsInfo.estimation === "relative" && !tokenBalanceInPool)
115
117
  return null;
116
- const { decimals = 18 } = tokensList[pointsInfo.token] || {};
118
+ const { decimals = 18 } = tokensList.get(pointsInfo.token) || {};
117
119
  const targetFactor = 10n ** BigInt(decimals);
118
120
  const defaultPoints = pointsInfo.amount * targetFactor / import_sdk.PERCENTAGE_FACTOR;
119
121
  if (pointsInfo.estimation === "absolute") return defaultPoints;
120
- const { decimals: underlyingDecimals = 18 } = tokensList[pool.underlyingToken] || {};
122
+ const { decimals: underlyingDecimals = 18 } = tokensList.get(pool.pool.pool.underlying) || {};
121
123
  const underlyingFactor = 10n ** BigInt(underlyingDecimals);
122
- const points = (tokenBalanceInPool?.balance || 0n) * defaultPoints / (pool.expectedLiquidity * targetFactor / underlyingFactor);
124
+ const points = (tokenBalanceInPool?.balance || 0n) * defaultPoints / (pool.pool.pool.expectedLiquidity * targetFactor / underlyingFactor);
123
125
  return import_common_utils.BigIntMath.min(points, defaultPoints);
124
126
  }
125
127
  }
@@ -24,8 +24,6 @@ __export(simulateWithPriceUpdates_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(simulateWithPriceUpdates_exports);
26
26
  var import_viem = require("viem");
27
- var import_actions = require("viem/actions");
28
- var import_utils = require("viem/utils");
29
27
  var import_errors = require("../../../abi/errors.js");
30
28
  var import_iUpdatablePriceFeed = require("../../../abi/iUpdatablePriceFeed.js");
31
29
  var import_cast = require("./cast.js");
@@ -55,41 +53,6 @@ async function simulateWithPriceUpdates(client, parameters) {
55
53
  "client chain not configured. multicallAddress is required."
56
54
  );
57
55
  }
58
- if (priceUpdates.length === 0 && restContracts.length === 1) {
59
- const contract = restContracts[0];
60
- const { abi, address, args, functionName } = contract;
61
- const callData = (0, import_viem.encodeFunctionData)({ abi, args, functionName });
62
- const request2 = {
63
- batch: false,
64
- data: callData,
65
- to: address,
66
- blockNumber: rest.blockNumber,
67
- blockTag: rest.blockTag,
68
- gas: rest.gas,
69
- account: rest.account ? (0, import_utils.parseAccount)(rest.account) : client.account,
70
- value: rest.value
71
- };
72
- try {
73
- const { data } = await (0, import_utils.getAction)(client, import_actions.call, "call")(request2);
74
- const result = (0, import_viem.decodeFunctionResult)({
75
- abi,
76
- args,
77
- data: data || "0x",
78
- functionName
79
- });
80
- return [
81
- result
82
- ];
83
- } catch (e) {
84
- throw getSimulateWithPriceUpdatesError(
85
- e,
86
- priceUpdates,
87
- restContracts,
88
- void 0,
89
- request2
90
- );
91
- }
92
- }
93
56
  let request;
94
57
  try {
95
58
  const contracts = [
@@ -210,8 +173,8 @@ function getSimulateWithPriceUpdatesError(cause, priceUpdates, calls, results, r
210
173
  return [extractCallError(result), p.pretty, tsValid].filter(Boolean).join(" ");
211
174
  });
212
175
  const prettyCalls = callsResults.map((c, i) => {
213
- const call2 = calls[i];
214
- return [extractCallError(c), `${call2.address}.${call2.functionName}`].filter(Boolean).join(" ");
176
+ const call = calls[i];
177
+ return [extractCallError(c), `${call.address}.${call.functionName}`].filter(Boolean).join(" ");
215
178
  });
216
179
  if (results[0]?.status === "failure") {
217
180
  prettyCalls.unshift(
@@ -48,7 +48,7 @@ const adapterActionSignatures = {
48
48
  310: "function setPairStatusBatch((address,address,address,uint8)[])",
49
49
  311: "function setPairStatusBatch((address,address,address,uint8,uint8)[])"
50
50
  },
51
- [AdapterType.TRADER_JOE_ROUTER]: {
51
+ [AdapterType.TRADERJOE_ROUTER]: {
52
52
  310: "function setPoolStatusBatch((address,address,uint256,uint8,bool)[])"
53
53
  },
54
54
  [AdapterType.UNISWAP_V2_ROUTER]: {
@@ -63,7 +63,7 @@ const adapterConstructorAbi = {
63
63
  [AdapterType.DAI_USDS_EXCHANGE]: {
64
64
  310: BASIC_ADAPTER_ABI
65
65
  },
66
- [AdapterType.TRADER_JOE_ROUTER]: {
66
+ [AdapterType.TRADERJOE_ROUTER]: {
67
67
  310: BASIC_ADAPTER_ABI
68
68
  },
69
69
  [AdapterType.UNISWAP_V2_ROUTER]: {
@@ -109,7 +109,7 @@ function createAdapter(options, data, strict) {
109
109
  return new PendleRouterAdapterContract(options, data);
110
110
  case "ADAPTER::STAKING_REWARDS":
111
111
  return new StakingRewardsAdapterContract(options, data);
112
- case "ADAPTER::TRADER_JOE_ROUTER":
112
+ case "ADAPTER::TRADERJOE_ROUTER":
113
113
  return new TraderJoeRouterAdapterContract(options, data);
114
114
  case "ADAPTER::UNISWAP_V2_ROUTER":
115
115
  return new UniswapV2AdapterContract(options, data);
@@ -36,7 +36,7 @@ var AdapterType = /* @__PURE__ */ ((AdapterType2) => {
36
36
  AdapterType2["MIDAS_REDEMPTION_VAULT"] = "MIDAS_REDEMPTION_VAULT";
37
37
  AdapterType2["PENDLE_ROUTER"] = "PENDLE_ROUTER";
38
38
  AdapterType2["STAKING_REWARDS"] = "STAKING_REWARDS";
39
- AdapterType2["TRADER_JOE_ROUTER"] = "TRADER_JOE_ROUTER";
39
+ AdapterType2["TRADERJOE_ROUTER"] = "TRADERJOE_ROUTER";
40
40
  AdapterType2["UNISWAP_V2_ROUTER"] = "UNISWAP_V2_ROUTER";
41
41
  AdapterType2["UNISWAP_V3_ROUTER"] = "UNISWAP_V3_ROUTER";
42
42
  AdapterType2["UNISWAP_V4_GATEWAY"] = "UNISWAP_V4_GATEWAY";
@@ -0,0 +1,254 @@
1
+ import { marketCompressorAbi } from "../../abi/compressors/marketCompressor.js";
2
+ import { PoolPointsAPI } from "../../rewards/rewards/extra-apy.js";
3
+ import {
4
+ AddressMap,
5
+ AP_MARKET_COMPRESSOR,
6
+ BasePlugin,
7
+ BLOCKS_PER_WEEK_BY_NETWORK,
8
+ PERCENTAGE_DECIMALS,
9
+ VERSION_RANGE_310
10
+ } from "../../sdk/index.js";
11
+ import { rayToNumber } from "../../sdk/utils/formatter.js";
12
+ import { hexEq } from "../../sdk/utils/hex.js";
13
+ import { ApyOutputCache } from "./apy-cache.js";
14
+ import { parseGearStats, parseNetworkApy } from "./apy-parser.js";
15
+ import { APY_STATE_CACHE_URL, DEFAULT_APY_INTERVAL_MS } from "./constants.js";
16
+ import {
17
+ calculatePoolFullAPY,
18
+ calculatePoolFullAPY7DAgo,
19
+ calculatePoolPoints,
20
+ calculateSupplyApy7d,
21
+ getPoolExtraAPY
22
+ } from "./pool-apy-utils.js";
23
+ const MAP_LABEL = "pools7DAgo";
24
+ class ApyPlugin extends BasePlugin {
25
+ #apyUrl;
26
+ #cacheTtlMs;
27
+ #pools7DAgo;
28
+ #apySnapshot;
29
+ constructor(loadOnAttach = false, options) {
30
+ super(loadOnAttach);
31
+ this.#apyUrl = options?.apyUrl ?? APY_STATE_CACHE_URL;
32
+ this.#cacheTtlMs = options?.cacheTtlMs ?? DEFAULT_APY_INTERVAL_MS;
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Load — single entry point for all data (on-chain + state-cache)
36
+ // ---------------------------------------------------------------------------
37
+ async load(force, loadOptions) {
38
+ if (!force && this.loaded) {
39
+ return this.state;
40
+ }
41
+ const targetBlock = this.sdk.currentBlock - BLOCKS_PER_WEEK_BY_NETWORK[this.sdk.networkType];
42
+ const [marketCompressorAddress] = this.sdk.addressProvider.mustGetLatest(
43
+ AP_MARKET_COMPRESSOR,
44
+ VERSION_RANGE_310
45
+ );
46
+ this.#logger?.debug(
47
+ `loading pools 7d ago with market compressor ${marketCompressorAddress}`
48
+ );
49
+ const markets = this.sdk.marketRegister.markets;
50
+ const shouldLoadPools7DAgo = !this.#pools7DAgo || !!loadOptions?.loadPools7DAgo;
51
+ const [multicallResp, apySnapshot] = await Promise.all([
52
+ shouldLoadPools7DAgo ? this.client.multicall({
53
+ allowFailure: true,
54
+ contracts: markets.map(
55
+ (m) => ({
56
+ address: marketCompressorAddress,
57
+ abi: marketCompressorAbi,
58
+ functionName: "getPoolState",
59
+ args: [m.pool.pool.address]
60
+ })
61
+ ),
62
+ blockNumber: targetBlock > 0n ? targetBlock : void 0,
63
+ batchSize: 0
64
+ }) : null,
65
+ this.#fetchApy()
66
+ ]);
67
+ if (multicallResp !== null) {
68
+ this.#pools7DAgo = new AddressMap(void 0, MAP_LABEL);
69
+ multicallResp.forEach((r, index) => {
70
+ const m = markets[index];
71
+ const cfg = m.configurator.address;
72
+ const pool = m.pool.pool.address;
73
+ if (r.status === "success") {
74
+ this.#pools7DAgo?.upsert(m.pool.pool.address, {
75
+ dieselRate: r.result.dieselRate,
76
+ pool
77
+ });
78
+ } else {
79
+ this.#logger?.error(
80
+ `failed to load pools 7d ago for market configurator ${this.labelAddress(cfg)} and pool ${this.labelAddress(pool)}: ${r.error}`
81
+ );
82
+ }
83
+ });
84
+ }
85
+ if (apySnapshot) {
86
+ this.#apySnapshot = apySnapshot;
87
+ }
88
+ return this.state;
89
+ }
90
+ get loaded() {
91
+ return !!this.#pools7DAgo && !!this.#apySnapshot;
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // Accessors
95
+ // ---------------------------------------------------------------------------
96
+ /**
97
+ * @throws if plugin is not loaded
98
+ */
99
+ get pools7DAgo() {
100
+ if (!this.#pools7DAgo) {
101
+ throw new Error("apy plugin not loaded");
102
+ }
103
+ return this.#pools7DAgo;
104
+ }
105
+ /**
106
+ * @throws if plugin is not loaded
107
+ */
108
+ get apySnapshot() {
109
+ if (!this.#apySnapshot) {
110
+ throw new Error("apy plugin not loaded");
111
+ }
112
+ return this.#apySnapshot;
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Aggregated APY / points
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Computes per-pool APY (current + 7d-ago) and points for all markets.
119
+ *
120
+ * @throws if plugin is not loaded
121
+ */
122
+ getPoolsAPY() {
123
+ if (!this.loaded) {
124
+ throw new Error("apy plugin not loaded");
125
+ }
126
+ const markets = this.sdk.marketRegister.markets;
127
+ const { apy } = this.apySnapshot;
128
+ const totalTokenBalances = {};
129
+ const poolPointsBase = apy.poolRewardsList && Object.keys(apy.poolRewardsList).length > 0 ? PoolPointsAPI.getPointsByPool({
130
+ poolRewards: apy.poolRewardsList,
131
+ totalTokenBalances,
132
+ pools: markets,
133
+ tokensList: this.sdk.tokensMeta
134
+ }) : {};
135
+ const data = {};
136
+ const data7DAgo = {};
137
+ const points = {};
138
+ for (const market of markets) {
139
+ const pool = market.pool.pool;
140
+ const poolAddr = pool.address.toLowerCase();
141
+ const depositAPY = rayToNumber(pool.supplyRate) * Number(PERCENTAGE_DECIMALS);
142
+ const underlyingAPY = apy.apyList?.[pool.underlying] ?? 0;
143
+ const lookupAddresses = this.#getExtraAPYLookupAddresses(poolAddr);
144
+ const extraAPY = getPoolExtraAPY(lookupAddresses, apy.poolExtraAPYList);
145
+ const currentExternalList = apy.poolExternalAPYList?.[poolAddr];
146
+ const poolAPY = calculatePoolFullAPY({
147
+ depositAPY,
148
+ underlyingAPY,
149
+ extraAPY,
150
+ currentExternalList
151
+ });
152
+ data[poolAddr] = poolAPY;
153
+ const pool7DAgo = this.#pools7DAgo?.get(poolAddr);
154
+ const supplyAPY7DAgo = pool7DAgo ? calculateSupplyApy7d(
155
+ pool.dieselRate,
156
+ pool.supplyRate,
157
+ pool7DAgo.dieselRate
158
+ ) : void 0;
159
+ data7DAgo[poolAddr] = calculatePoolFullAPY7DAgo({
160
+ supplyAPY7DAgo,
161
+ depositAPY,
162
+ poolAPY
163
+ });
164
+ const poolTokenMeta = this.sdk.tokensMeta.get(pool.underlying);
165
+ points[poolAddr] = calculatePoolPoints({
166
+ poolTokenSymbol: poolTokenMeta?.symbol,
167
+ points: poolPointsBase[poolAddr],
168
+ tokensList: this.sdk.tokensMeta
169
+ });
170
+ }
171
+ return { data, data7DAgo, points };
172
+ }
173
+ // ---------------------------------------------------------------------------
174
+ // Plugin lifecycle
175
+ // ---------------------------------------------------------------------------
176
+ async syncState() {
177
+ await this.load();
178
+ }
179
+ stateHuman(_) {
180
+ return this.pools7DAgo.values().flatMap((p) => ({
181
+ address: p.pool,
182
+ version: this.version,
183
+ dieselRate: p.dieselRate
184
+ }));
185
+ }
186
+ get state() {
187
+ return {
188
+ pools7DAgo: this.pools7DAgo.asRecord(),
189
+ apySnapshot: this.apySnapshot
190
+ };
191
+ }
192
+ hydrate(state) {
193
+ this.#pools7DAgo = new AddressMap(
194
+ Object.entries(state.pools7DAgo),
195
+ MAP_LABEL
196
+ );
197
+ this.#apySnapshot = state.apySnapshot;
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Internal
201
+ // ---------------------------------------------------------------------------
202
+ async #fetchApy() {
203
+ try {
204
+ const cache = ApyOutputCache.get(
205
+ this.#apyUrl,
206
+ this.#cacheTtlMs,
207
+ this.#logger
208
+ );
209
+ const output = await cache.fetch();
210
+ if (!output) return void 0;
211
+ const chainData = output.chains[this.sdk.chainId];
212
+ if (!chainData) {
213
+ this.#logger?.debug(
214
+ `apy state-cache: no data for chainId ${this.sdk.chainId}`
215
+ );
216
+ return void 0;
217
+ }
218
+ const apy = parseNetworkApy(chainData.tokens, chainData.pools);
219
+ if (!apy) return void 0;
220
+ return {
221
+ apy,
222
+ gearStats: parseGearStats(output),
223
+ timestamp: output.timestamp
224
+ };
225
+ } catch (e) {
226
+ this.#logger?.error(e, "failed to fetch apy state-cache");
227
+ return void 0;
228
+ }
229
+ }
230
+ /**
231
+ * Collects addresses to look up in poolExtraAPYList for a given pool.
232
+ * Includes pool address (= diesel token) + any staked diesel token
233
+ * outputs discovered from zappers.
234
+ */
235
+ #getExtraAPYLookupAddresses(poolAddr) {
236
+ const addresses = [poolAddr];
237
+ try {
238
+ const zappers = this.sdk.marketRegister.poolZappers(poolAddr);
239
+ for (const z of zappers) {
240
+ if (!hexEq(z.tokenOut.addr, poolAddr)) {
241
+ addresses.push(z.tokenOut.addr);
242
+ }
243
+ }
244
+ } catch {
245
+ }
246
+ return addresses;
247
+ }
248
+ get #logger() {
249
+ return this.logger;
250
+ }
251
+ }
252
+ export {
253
+ ApyPlugin
254
+ };
@@ -0,0 +1,86 @@
1
+ import axios from "axios";
2
+ class ApyOutputCache {
3
+ static #instances = /* @__PURE__ */ new Map();
4
+ #url;
5
+ #ttlMs;
6
+ #cache;
7
+ #pending;
8
+ #logger;
9
+ constructor(url, ttlMs, logger) {
10
+ this.#url = url;
11
+ this.#ttlMs = ttlMs;
12
+ this.#logger = logger;
13
+ }
14
+ /**
15
+ * Returns a shared cache instance for the given URL.
16
+ * The same instance is reused across all callers with identical URL.
17
+ */
18
+ static get(url, ttlMs, logger) {
19
+ let instance = ApyOutputCache.#instances.get(url);
20
+ if (!instance) {
21
+ instance = new ApyOutputCache(url, ttlMs, logger);
22
+ ApyOutputCache.#instances.set(url, instance);
23
+ }
24
+ if (logger) {
25
+ instance.#logger = logger;
26
+ }
27
+ return instance;
28
+ }
29
+ /**
30
+ * Returns cached Output if fresh, otherwise fetches from the network.
31
+ * Concurrent calls are de-duplicated.
32
+ */
33
+ async fetch() {
34
+ if (this.#cache && Date.now() - this.#cache.fetchedAt < this.#ttlMs) {
35
+ this.#logger?.debug("apy cache: TTL still valid, returning cached data");
36
+ return this.#cache.data;
37
+ }
38
+ if (this.#pending) {
39
+ this.#logger?.debug("apy cache: request in flight, waiting");
40
+ return this.#pending;
41
+ }
42
+ this.#pending = this.#doFetch();
43
+ try {
44
+ return await this.#pending;
45
+ } finally {
46
+ this.#pending = void 0;
47
+ }
48
+ }
49
+ async #doFetch() {
50
+ try {
51
+ const headers = {};
52
+ if (this.#cache?.etag) {
53
+ headers["If-None-Match"] = this.#cache.etag;
54
+ }
55
+ const response = await axios.get(this.#url, {
56
+ headers,
57
+ validateStatus: (status) => status === 200 || status === 304
58
+ });
59
+ if (response.status === 304 && this.#cache) {
60
+ this.#cache.fetchedAt = Date.now();
61
+ this.#logger?.debug("apy cache: 304 Not Modified, extended TTL");
62
+ return this.#cache.data;
63
+ }
64
+ const etag = response.headers["etag"];
65
+ this.#cache = {
66
+ data: response.data,
67
+ etag,
68
+ fetchedAt: Date.now()
69
+ };
70
+ this.#logger?.debug(
71
+ `apy cache: fetched fresh data (timestamp: ${response.data.timestamp})`
72
+ );
73
+ return response.data;
74
+ } catch (e) {
75
+ this.#logger?.error(e, "apy cache: fetch failed");
76
+ return this.#cache?.data;
77
+ }
78
+ }
79
+ /** Evicts all cached entries. Mainly useful for tests. */
80
+ static clearAll() {
81
+ ApyOutputCache.#instances.clear();
82
+ }
83
+ }
84
+ export {
85
+ ApyOutputCache
86
+ };