@elmntl/jlpd-sdk 0.2.0 → 0.13.4

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 (115) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +185 -404
  3. package/dist/common/ata.d.ts +26 -0
  4. package/dist/common/ata.js +48 -0
  5. package/dist/common/buffer.d.ts +15 -0
  6. package/dist/common/buffer.js +118 -0
  7. package/dist/common/connection.d.ts +13 -0
  8. package/dist/common/connection.js +2 -0
  9. package/dist/common/constants.d.ts +14 -0
  10. package/dist/common/constants.js +31 -0
  11. package/dist/common/index.d.ts +6 -0
  12. package/dist/common/index.js +28 -0
  13. package/dist/common/strategy-interface.d.ts +59 -0
  14. package/dist/common/strategy-interface.js +42 -0
  15. package/dist/elemental-lend/accounts.d.ts +103 -0
  16. package/dist/elemental-lend/accounts.js +354 -0
  17. package/dist/elemental-lend/constants.d.ts +25 -0
  18. package/dist/elemental-lend/constants.js +48 -0
  19. package/dist/elemental-lend/index.d.ts +8 -0
  20. package/dist/elemental-lend/index.js +24 -0
  21. package/dist/elemental-lend/instructions.d.ts +104 -0
  22. package/dist/elemental-lend/instructions.js +266 -0
  23. package/dist/elemental-lend/jupiter-lend.d.ts +91 -0
  24. package/dist/elemental-lend/jupiter-lend.js +189 -0
  25. package/dist/elemental-lend/kamino-vault.d.ts +173 -0
  26. package/dist/elemental-lend/kamino-vault.js +483 -0
  27. package/dist/elemental-lend/pda.d.ts +12 -0
  28. package/dist/elemental-lend/pda.js +24 -0
  29. package/dist/elemental-lend/protocol-actions.d.ts +56 -0
  30. package/dist/elemental-lend/protocol-actions.js +244 -0
  31. package/dist/elemental-lend/types.d.ts +113 -0
  32. package/dist/elemental-lend/types.js +2 -0
  33. package/dist/elemental-lend-v2/accounts.d.ts +14 -0
  34. package/dist/elemental-lend-v2/accounts.js +136 -0
  35. package/dist/elemental-lend-v2/adapters.d.ts +22 -0
  36. package/dist/elemental-lend-v2/adapters.js +50 -0
  37. package/dist/elemental-lend-v2/constants.d.ts +48 -0
  38. package/dist/elemental-lend-v2/constants.js +104 -0
  39. package/dist/elemental-lend-v2/index.d.ts +8 -0
  40. package/dist/elemental-lend-v2/index.js +24 -0
  41. package/dist/elemental-lend-v2/instructions.d.ts +96 -0
  42. package/dist/elemental-lend-v2/instructions.js +190 -0
  43. package/dist/elemental-lend-v2/lut.d.ts +20 -0
  44. package/dist/elemental-lend-v2/lut.js +65 -0
  45. package/dist/elemental-lend-v2/pda.d.ts +9 -0
  46. package/dist/elemental-lend-v2/pda.js +27 -0
  47. package/dist/elemental-lend-v2/types.d.ts +105 -0
  48. package/dist/elemental-lend-v2/types.js +2 -0
  49. package/dist/elemental-lend-v2/update-aum.d.ts +29 -0
  50. package/dist/elemental-lend-v2/update-aum.js +82 -0
  51. package/dist/index.d.ts +5 -4199
  52. package/dist/index.js +37 -5187
  53. package/dist/jlpd-strategy/accounts.d.ts +83 -0
  54. package/dist/jlpd-strategy/accounts.js +216 -0
  55. package/dist/jlpd-strategy/adapter.d.ts +81 -0
  56. package/dist/jlpd-strategy/adapter.js +118 -0
  57. package/dist/jlpd-strategy/base-to-base-swap.d.ts +74 -0
  58. package/dist/jlpd-strategy/base-to-base-swap.js +205 -0
  59. package/dist/jlpd-strategy/constants.d.ts +127 -0
  60. package/dist/jlpd-strategy/constants.js +174 -0
  61. package/dist/jlpd-strategy/fluid-view.d.ts +199 -0
  62. package/dist/jlpd-strategy/fluid-view.js +799 -0
  63. package/dist/jlpd-strategy/hedge-derived.d.ts +135 -0
  64. package/dist/jlpd-strategy/hedge-derived.js +231 -0
  65. package/dist/jlpd-strategy/hedge-instructions.d.ts +210 -0
  66. package/dist/jlpd-strategy/hedge-instructions.js +300 -0
  67. package/dist/jlpd-strategy/hedge-state.d.ts +88 -0
  68. package/dist/jlpd-strategy/hedge-state.js +110 -0
  69. package/dist/jlpd-strategy/index.d.ts +17 -0
  70. package/dist/jlpd-strategy/index.js +33 -0
  71. package/dist/jlpd-strategy/instructions.d.ts +159 -0
  72. package/dist/jlpd-strategy/instructions.js +234 -0
  73. package/dist/jlpd-strategy/jlp-borrow.d.ts +63 -0
  74. package/dist/jlpd-strategy/jlp-borrow.js +87 -0
  75. package/dist/jlpd-strategy/jlp-data.d.ts +166 -0
  76. package/dist/jlpd-strategy/jlp-data.js +611 -0
  77. package/dist/jlpd-strategy/jupusd-earn.d.ts +90 -0
  78. package/dist/jlpd-strategy/jupusd-earn.js +166 -0
  79. package/dist/jlpd-strategy/live-jlp-price.d.ts +46 -0
  80. package/dist/jlpd-strategy/live-jlp-price.js +267 -0
  81. package/dist/jlpd-strategy/pda.d.ts +13 -0
  82. package/dist/jlpd-strategy/pda.js +28 -0
  83. package/dist/jlpd-strategy/settle-yield.d.ts +45 -0
  84. package/dist/jlpd-strategy/settle-yield.js +113 -0
  85. package/dist/jlpd-strategy/swap-jlp.d.ts +198 -0
  86. package/dist/jlpd-strategy/swap-jlp.js +586 -0
  87. package/dist/jlpd-strategy/types.d.ts +101 -0
  88. package/dist/jlpd-strategy/types.js +29 -0
  89. package/dist/p-stv-core/accounts.d.ts +152 -0
  90. package/dist/p-stv-core/accounts.js +407 -0
  91. package/dist/p-stv-core/constants.d.ts +138 -0
  92. package/dist/p-stv-core/constants.js +181 -0
  93. package/dist/p-stv-core/events.d.ts +7 -0
  94. package/dist/p-stv-core/events.js +246 -0
  95. package/dist/p-stv-core/index.d.ts +11 -0
  96. package/dist/p-stv-core/index.js +27 -0
  97. package/dist/p-stv-core/instructions.d.ts +637 -0
  98. package/dist/p-stv-core/instructions.js +670 -0
  99. package/dist/p-stv-core/lut.d.ts +41 -0
  100. package/dist/p-stv-core/lut.js +81 -0
  101. package/dist/p-stv-core/pda.d.ts +30 -0
  102. package/dist/p-stv-core/pda.js +65 -0
  103. package/dist/p-stv-core/prices.d.ts +20 -0
  104. package/dist/p-stv-core/prices.js +77 -0
  105. package/dist/p-stv-core/remaining-accounts.d.ts +107 -0
  106. package/dist/p-stv-core/remaining-accounts.js +326 -0
  107. package/dist/p-stv-core/send-tx.d.ts +72 -0
  108. package/dist/p-stv-core/send-tx.js +290 -0
  109. package/dist/p-stv-core/sol-wrap.d.ts +34 -0
  110. package/dist/p-stv-core/sol-wrap.js +74 -0
  111. package/dist/p-stv-core/types.d.ts +361 -0
  112. package/dist/p-stv-core/types.js +2 -0
  113. package/package.json +56 -15
  114. package/dist/index.d.mts +0 -4199
  115. package/dist/index.mjs +0 -5090
@@ -0,0 +1,799 @@
1
+ "use strict";
2
+ /**
3
+ * Jupiter Lend (Fluid Vaults + Fluid Earn) read-only valuation helpers — TS mirror of
4
+ * `jlpd-strategy/programs/jlpd-strategy/src/utils/jup_lend_view.rs` (program
5
+ * `GXqt4ZH2UUBsLWwMNJiZMXza3q7xEGChfW8XjVRjLxr5`, mainnet).
6
+ *
7
+ * Pure decode + math, ALL-bigint (matching Rust's u128/i128 exactly — truncating division
8
+ * for the hop-rate math, ceil-division for debt). Answers the same question the on-chain
9
+ * `hedge_borrow`/`hedge_withdraw_collateral` guard answers: what LTV does a position imply,
10
+ * valued on the SAME Fluid oracle chain the vault itself liquidates on (never this program's
11
+ * own JLP NAV mark, `computeLiveJlpPriceUsd` — see `live-jlp-price.ts` — which is a DIFFERENT
12
+ * feed that can diverge from Fluid's exactly when liquidation risk is highest).
13
+ *
14
+ * Ported/derived from `github.com/Instadapp/fluid-solana-programs` the same way `oracle.rs`
15
+ * itself was ported (fixed-byte-offset decode, no new on-chain-format dependency). Every
16
+ * offset below is cross-checked against `jup_lend_view.rs`'s own named constants.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.FLUID_ZERO_TICK_SCALED_RATIO = exports.JUP_LEND_EARN_MAX_SANE_RATE = exports.JUP_LEND_EARN_MIN_SANE_RATE = void 0;
20
+ exports.getRatioAtTick = getRatioAtTick;
21
+ exports.decodeFluidPosition = decodeFluidPosition;
22
+ exports.deriveFluidTickPda = deriveFluidTickPda;
23
+ exports.settleTickPdaForPosition = settleTickPdaForPosition;
24
+ exports.decodeFluidTick = decodeFluidTick;
25
+ exports.requireFluidPositionLiquidationCurrent = requireFluidPositionLiquidationCurrent;
26
+ exports.decodeFluidVaultState = decodeFluidVaultState;
27
+ exports.requireFluidVaultStateFresh = requireFluidVaultStateFresh;
28
+ exports.decodeFluidVaultConfig = decodeFluidVaultConfig;
29
+ exports.requireFluidVaultIdBind = requireFluidVaultIdBind;
30
+ exports.collateralJlpNormal = collateralJlpNormal;
31
+ exports.debtNativeCeil = debtNativeCeil;
32
+ exports.debtNativeCeilProgramView = debtNativeCeilProgramView;
33
+ exports.positionLtvBps = positionLtvBps;
34
+ exports.readOracleHopCount = readOracleHopCount;
35
+ exports.readFluidPrice1e15FromAccounts = readFluidPrice1e15FromAccounts;
36
+ exports.fetchFluidSlotLtv = fetchFluidSlotLtv;
37
+ exports.earnExchangeRate = earnExchangeRate;
38
+ const web3_js_1 = require("@solana/web3.js");
39
+ const constants_1 = require("./constants");
40
+ // ============================================================================
41
+ // Discriminators (sha256("account:<Type>")[..8], except Chainlink's own scheme)
42
+ // ============================================================================
43
+ const VAULT_CONFIG_DISCRIMINATOR = Buffer.from([99, 86, 43, 216, 184, 102, 119, 77]);
44
+ const VAULT_STATE_DISCRIMINATOR = Buffer.from([228, 196, 82, 165, 98, 210, 235, 152]);
45
+ const POSITION_DISCRIMINATOR = Buffer.from([170, 188, 143, 228, 122, 64, 247, 208]);
46
+ const ORACLE_CONFIG_DISCRIMINATOR = Buffer.from([139, 194, 131, 179, 140, 179, 229, 244]);
47
+ /** Chainlink's own discriminator (`data_feeds_store_v1::TRANSMISSIONS_DISCRIMINATOR`) — NOT sha256("account:X"). */
48
+ const CHAINLINK_TRANSMISSIONS_DISCRIMINATOR = Buffer.from([96, 179, 69, 66, 128, 129, 73, 117]);
49
+ /** `sha256("account:PriceUpdateV2")[..8]` — `pyth-solana-receiver-sdk` 1.0.1's own Anchor discriminator. */
50
+ const PYTH_PRICE_UPDATE_DISCRIMINATOR = Buffer.from([0x22, 0xf1, 0x23, 0x63, 0x9d, 0x7e, 0xf4, 0xcd]);
51
+ // ============================================================================
52
+ // Byte offsets — mirror jup_lend_view.rs's own named offset constants exactly.
53
+ // VaultConfig/VaultState/Position are zero-copy `#[repr(C, packed)]`: 8-byte disc then every
54
+ // field packed in declaration order, no padding.
55
+ // ============================================================================
56
+ const VAULT_CONFIG_OFFSET_VAULT_ID = 8;
57
+ const VAULT_CONFIG_OFFSET_ORACLE = 26;
58
+ const VAULT_CONFIG_MIN_LEN = 219;
59
+ const VAULT_STATE_OFFSET_VAULT_ID = 8;
60
+ const VAULT_STATE_OFFSET_SUPPLY_EX_PRICE = 99;
61
+ const VAULT_STATE_OFFSET_BORROW_EX_PRICE = 107;
62
+ const VAULT_STATE_OFFSET_LAST_UPDATE_TIMESTAMP = 119;
63
+ const VAULT_STATE_MIN_LEN = 127;
64
+ /** Fluid's liquidity-layer floor: exchange prices init at exactly 1e12 and only ever increase. */
65
+ const VAULT_STATE_MIN_EX_PRICE = 1000000000000n; // 1e12
66
+ /** `settle_yield`'s NAV-fold staleness bound (24h) — see `jup_lend_view.rs`'s doc comment. */
67
+ const FLUID_VAULT_STATE_MAX_STALENESS_SECS = 86400n;
68
+ const POSITION_OFFSET_VAULT_ID = 8;
69
+ const POSITION_OFFSET_IS_SUPPLY_ONLY = 46;
70
+ const POSITION_OFFSET_TICK = 47;
71
+ /** `Position.tick_id` u32 — the tick GENERATION this position was placed at (between `tick` @47
72
+ * and `supply_amount` @55). Mirrors `jup_lend_view.rs::POSITION_OFFSET_TICK_ID`. */
73
+ const POSITION_OFFSET_TICK_ID = 51;
74
+ const POSITION_OFFSET_COLLATERAL = 55;
75
+ const POSITION_OFFSET_DUST_DEBT = 63;
76
+ const POSITION_MIN_LEN = 71;
77
+ // --- Fluid `Tick` account (mirrors jup_lend_view.rs's TICK_* constants) ---
78
+ const TICK_DISCRIMINATOR = Buffer.from([176, 94, 67, 247, 133, 173, 7, 115]);
79
+ const TICK_OFFSET_VAULT_ID = 8; // u16
80
+ const TICK_OFFSET_TICK = 10; // i32
81
+ const TICK_OFFSET_IS_LIQUIDATED = 14; // u8 (canonical 0/1)
82
+ const TICK_OFFSET_TOTAL_IDS = 15; // u32
83
+ const TICK_MIN_LEN = 40;
84
+ const TICK_SEED = Buffer.from("tick");
85
+ const ORACLE_CFG_OFFSET_SOURCES_LEN = 10;
86
+ const ORACLE_CFG_OFFSET_SOURCES = 14;
87
+ const ORACLE_SOURCE_STRIDE = 66;
88
+ const ORACLE_SOURCE_OFF_PUBKEY = 0;
89
+ const ORACLE_SOURCE_OFF_INVERT = 32;
90
+ const ORACLE_SOURCE_OFF_TYPE = 65;
91
+ const ORACLE_SOURCE_TYPE_PYTH = 0;
92
+ const ORACLE_SOURCE_TYPE_CHAINLINK = 4;
93
+ const CHAINLINK_OFFSET_DECIMALS = 138;
94
+ const CHAINLINK_OFFSET_LATEST_ROUND_ID = 143;
95
+ const CHAINLINK_OFFSET_LIVE_LENGTH = 148;
96
+ const CHAINLINK_OFFSET_SLOT = 200;
97
+ const CHAINLINK_OFFSET_ANSWER = 216;
98
+ const CHAINLINK_MIN_LEN = CHAINLINK_OFFSET_ANSWER + 16; // 232
99
+ const CHAINLINK_MAX_STALENESS_SECS = 7200n;
100
+ const AVG_SLOT_TIME_MS = 400n;
101
+ const ORACLE_RATE_OUTPUT_DECIMALS = 15;
102
+ const PYTH_OFFSET_VERIFICATION_LEVEL = 40;
103
+ const PYTH_VERIFICATION_LEVEL_FULL = 1;
104
+ const PYTH_OFFSET_PRICE = 73;
105
+ const PYTH_OFFSET_CONF = 81;
106
+ const PYTH_OFFSET_EXPONENT = 89;
107
+ const PYTH_OFFSET_PUBLISH_TIME = 93;
108
+ const PYTH_MIN_LEN = PYTH_OFFSET_PUBLISH_TIME + 8; // 101
109
+ const PYTH_CONFIDENCE_SCALE_FACTOR_LIQUIDATE = 25n;
110
+ const PYTH_MAX_STALENESS_SECS = 7200n;
111
+ const PYTH_MAX_ABS_EXPONENT = 15;
112
+ /** Jup-Lend Earn `lending` account — `tokenExchangePrice: u64` at 1e12 precision. */
113
+ const LENDING_OFFSET_EXCHANGE_RATE = 115;
114
+ const LENDING_MIN_LEN = LENDING_OFFSET_EXCHANGE_RATE + 8;
115
+ const LENDING_DISCRIMINATOR = Buffer.from([135, 199, 82, 16, 249, 131, 182, 241]);
116
+ exports.JUP_LEND_EARN_MIN_SANE_RATE = 1000000000000n; // 1e12
117
+ exports.JUP_LEND_EARN_MAX_SANE_RATE = 1000n * 1000000000000n;
118
+ // ============================================================================
119
+ // Precision constants
120
+ // ============================================================================
121
+ const EXCHANGE_PRICES_PRECISION = 1000000000000n; // 1e12
122
+ const FLUID_MAX_TOKEN_DECIMALS = 9;
123
+ const TOKEN_DECIMALS_JLP = 6;
124
+ /** `10^(RATE_OUTPUT_DECIMALS)`; JLP's 6-dec collapses `collateral_value_in_debt_native`'s stage 1 to `/1e15`. */
125
+ const JLP_PRICE_1E15_PRECISION = 1000000000000000n;
126
+ /** Oracle chain's own accumulator base + per-hop combine divisor. */
127
+ const ORACLE_RATE_SCALE = 1000000000000000n; // 1e15
128
+ /** `10^(RATE_OUTPUT_DECIMALS * 2)` — invert numerator: `rate = 1e30 / rate`. */
129
+ const ORACLE_INVERT_NUMERATOR = 1000000000000000000000000000000n; // 1e30
130
+ /** Defense-in-depth clamp — applies ONLY to a single-hop (direct USD) oracle chain. */
131
+ const MIN_JLP_PRICE_1E15 = 500000000000000n; // $0.50
132
+ const MAX_JLP_PRICE_1E15 = 50000000000000000n; // $50.00
133
+ // ============================================================================
134
+ // Fluid tick math — faithful port of `library::math::tick::TickMath::get_ratio_at_tick`
135
+ // (fluid-solana-programs crates/library/src/math/tick.rs:9-117). Same FACTOR table, same
136
+ // bit-by-bit accumulation, same positive-tick reciprocal + round-up correction.
137
+ // ============================================================================
138
+ const FLUID_MIN_TICK = -16383;
139
+ const FLUID_MAX_TICK = 16383;
140
+ /** "No active tick" sentinel — i32::MIN. */
141
+ const FLUID_COLD_TICK = -2147483648;
142
+ /** `ratio_x48` at tick 0 (`1 << 48`). */
143
+ exports.FLUID_ZERO_TICK_SCALED_RATIO = 1n << 48n;
144
+ const U128_MAX = (1n << 128n) - 1n;
145
+ // FACTOR00..FACTOR14 = 2^64 / 1.0015^(2^i) — tick.rs:24-38, verbatim.
146
+ const FACTOR00 = 0x10000000000000000n;
147
+ const FACTOR01 = 0xff9dd7de423466c2n;
148
+ const FACTOR02 = 0xff3bd55f4488ad27n;
149
+ const FACTOR03 = 0xfe78410fd6498b74n;
150
+ const FACTOR04 = 0xfcf2d9987c9be179n;
151
+ const FACTOR05 = 0xf9ef02c4529258b0n;
152
+ const FACTOR06 = 0xf402d288133a85a1n;
153
+ const FACTOR07 = 0xe895615b5beb6386n;
154
+ const FACTOR08 = 0xd34f17a00ffa00a8n;
155
+ const FACTOR09 = 0xae6b7961714e2055n;
156
+ const FACTOR10 = 0x76d6461f27082d75n;
157
+ const FACTOR11 = 0x372a3bfe0745d8b7n;
158
+ const FACTOR12 = 0x0be32cbee4897976n;
159
+ const FACTOR13 = 0x008d4f70c9ff4925n;
160
+ const FACTOR14 = 0x00004e009ae55194n;
161
+ function mulShift64(n0, n1) {
162
+ return (n0 * n1) >> 64n;
163
+ }
164
+ /** `ratio_x48 = 1.0015^tick * 2^48`. */
165
+ function getRatioAtTick(tick) {
166
+ if (!Number.isInteger(tick) || tick < FLUID_MIN_TICK || tick > FLUID_MAX_TICK) {
167
+ throw new Error(`getRatioAtTick: tick ${tick} out of bounds [${FLUID_MIN_TICK}, ${FLUID_MAX_TICK}]`);
168
+ }
169
+ const absTick = Math.abs(tick);
170
+ let factor = FACTOR00;
171
+ if (absTick & 0x1)
172
+ factor = FACTOR01;
173
+ if (absTick & 0x2)
174
+ factor = mulShift64(factor, FACTOR02);
175
+ if (absTick & 0x4)
176
+ factor = mulShift64(factor, FACTOR03);
177
+ if (absTick & 0x8)
178
+ factor = mulShift64(factor, FACTOR04);
179
+ if (absTick & 0x10)
180
+ factor = mulShift64(factor, FACTOR05);
181
+ if (absTick & 0x20)
182
+ factor = mulShift64(factor, FACTOR06);
183
+ if (absTick & 0x40)
184
+ factor = mulShift64(factor, FACTOR07);
185
+ if (absTick & 0x80)
186
+ factor = mulShift64(factor, FACTOR08);
187
+ if (absTick & 0x100)
188
+ factor = mulShift64(factor, FACTOR09);
189
+ if (absTick & 0x200)
190
+ factor = mulShift64(factor, FACTOR10);
191
+ if (absTick & 0x400)
192
+ factor = mulShift64(factor, FACTOR11);
193
+ if (absTick & 0x800)
194
+ factor = mulShift64(factor, FACTOR12);
195
+ if (absTick & 0x1000)
196
+ factor = mulShift64(factor, FACTOR13);
197
+ if (absTick & 0x2000)
198
+ factor = mulShift64(factor, FACTOR14);
199
+ let precision = 0n;
200
+ if (tick >= 0) {
201
+ if (factor === 0n)
202
+ throw new Error("getRatioAtTick: math overflow (factor == 0)");
203
+ factor = U128_MAX / factor;
204
+ if (factor % 0x10000n !== 0n)
205
+ precision = 1n;
206
+ }
207
+ return (factor >> 16n) + precision;
208
+ }
209
+ /**
210
+ * Raw (pre-interest) debt implied by `tick` + `collateralRaw`, per Fluid's
211
+ * `Position::get_position_info`: when `tick > COLD_TICK`,
212
+ * `debt_raw = ((collateral_raw + 1) * ratio_x48 >> 48) + 1`; `tick == COLD_TICK` ⇒ 0.
213
+ */
214
+ function tickImpliedDebtRaw(tick, collateralRaw) {
215
+ if (tick <= FLUID_COLD_TICK)
216
+ return 0n;
217
+ const collateralForDebtCalc = collateralRaw + 1n;
218
+ const ratioX48 = getRatioAtTick(tick);
219
+ const scaled = ratioX48 * collateralForDebtCalc;
220
+ return (scaled >> 48n) + 1n;
221
+ }
222
+ function ceilDiv(numerator, denominator) {
223
+ if (denominator === 0n)
224
+ throw new Error("ceilDiv: division by zero");
225
+ const quotient = numerator / denominator;
226
+ const remainder = numerator % denominator;
227
+ return remainder === 0n ? quotient : quotient + 1n;
228
+ }
229
+ /** `raw * exPrice / 1e12`, FLOORED. */
230
+ function normalAmountFloor(raw, exPrice) {
231
+ return (raw * exPrice) / EXCHANGE_PRICES_PRECISION;
232
+ }
233
+ /** As `normalAmountFloor`, but CEILED — used for debt. */
234
+ function normalAmountCeil(raw, exPrice) {
235
+ return ceilDiv(raw * exPrice, EXCHANGE_PRICES_PRECISION);
236
+ }
237
+ /** Normalized-9-decimal amount -> native `decimals`, FLOORED. */
238
+ function unscaleFloor(scaled, decimals) {
239
+ if (decimals > FLUID_MAX_TOKEN_DECIMALS)
240
+ throw new Error("unscaleFloor: unsupported token decimals");
241
+ const divisor = 10n ** BigInt(FLUID_MAX_TOKEN_DECIMALS - decimals);
242
+ return scaled / divisor;
243
+ }
244
+ /** As `unscaleFloor`, but CEILED — used for debt. */
245
+ function unscaleCeil(scaled, decimals) {
246
+ if (decimals > FLUID_MAX_TOKEN_DECIMALS)
247
+ throw new Error("unscaleCeil: unsupported token decimals");
248
+ const divisor = 10n ** BigInt(FLUID_MAX_TOKEN_DECIMALS - decimals);
249
+ return ceilDiv(scaled, divisor);
250
+ }
251
+ /** The tick this position's tick-implied debt should be computed at (COLD_TICK when supply-only). */
252
+ function effectiveTick(position) {
253
+ return position.isSupplyOnly ? FLUID_COLD_TICK : position.tick;
254
+ }
255
+ /**
256
+ * Decode + validate a Fluid `Position` account. `expected` pins it to the caller's canonical
257
+ * pubkey (e.g. one `HedgePositionSlot.borrowPosition`) — mirrors `decode_fluid_position`.
258
+ */
259
+ function decodeFluidPosition(account, expected) {
260
+ if (!account.pubkey.equals(expected)) {
261
+ throw new Error("decodeFluidPosition: account pubkey does not match expected pin");
262
+ }
263
+ if (!account.owner.equals(constants_1.JUP_LEND_BORROW_PROGRAM_ID)) {
264
+ throw new Error("decodeFluidPosition: unexpected owner");
265
+ }
266
+ const data = account.data;
267
+ if (data.length < POSITION_MIN_LEN)
268
+ throw new Error("decodeFluidPosition: account data too short");
269
+ if (!data.subarray(0, 8).equals(POSITION_DISCRIMINATOR)) {
270
+ throw new Error("decodeFluidPosition: discriminator mismatch");
271
+ }
272
+ return {
273
+ vaultId: data.readUInt16LE(POSITION_OFFSET_VAULT_ID),
274
+ // Fluid's own `is_supply_only_position()` checks `== 1` exactly, not `!= 0`.
275
+ isSupplyOnly: data[POSITION_OFFSET_IS_SUPPLY_ONLY] === 1,
276
+ tick: data.readInt32LE(POSITION_OFFSET_TICK),
277
+ tickId: data.readUInt32LE(POSITION_OFFSET_TICK_ID),
278
+ collateralRaw: data.readBigUInt64LE(POSITION_OFFSET_COLLATERAL),
279
+ dustDebtRaw: data.readBigUInt64LE(POSITION_OFFSET_DUST_DEBT),
280
+ };
281
+ }
282
+ /**
283
+ * Canonical Tick PDA `[b"tick", vault_id_le2, (tick − MIN_TICK)_i32_le4]` under the Fluid vault
284
+ * program — byte-identical to the on-chain `derive_fluid_tick_pda` (golden-vector tested).
285
+ */
286
+ function deriveFluidTickPda(vaultId, tick) {
287
+ if (!Number.isInteger(tick) || tick < FLUID_MIN_TICK || tick > FLUID_MAX_TICK) {
288
+ throw new Error(`deriveFluidTickPda: tick ${tick} out of bounds [${FLUID_MIN_TICK}, ${FLUID_MAX_TICK}]`);
289
+ }
290
+ if (!Number.isInteger(vaultId) || vaultId < 0 || vaultId > 0xffff) {
291
+ throw new Error(`deriveFluidTickPda: vaultId ${vaultId} out of u16 range`);
292
+ }
293
+ const vaultIdLe = Buffer.alloc(2);
294
+ vaultIdLe.writeUInt16LE(vaultId);
295
+ const normalizedLe = Buffer.alloc(4);
296
+ normalizedLe.writeInt32LE(tick - FLUID_MIN_TICK);
297
+ return web3_js_1.PublicKey.findProgramAddressSync([TICK_SEED, vaultIdLe, normalizedLe], constants_1.JUP_LEND_BORROW_PROGRAM_ID)[0];
298
+ }
299
+ /**
300
+ * The Tick account settle_yield's per-slot triple must carry for `position`: the canonical
301
+ * `Tick(vaultId, position.tick)` for an ACTIVE debt position, or the `Tick(vaultId, MIN_TICK)`
302
+ * cold placeholder for a supply-only one (key-pinned by the program; the placeholder may not
303
+ * exist on chain — contents are ignored, only the key matters).
304
+ */
305
+ function settleTickPdaForPosition(position) {
306
+ return position.isSupplyOnly
307
+ ? deriveFluidTickPda(position.vaultId, FLUID_MIN_TICK)
308
+ : deriveFluidTickPda(position.vaultId, position.tick);
309
+ }
310
+ /**
311
+ * Decode + fully validate a Fluid `Tick` against `expected` (the canonical PDA) — mirrors the
312
+ * on-chain `decode_fluid_tick`: key equality, Fluid ownership, discriminator, min length, and a
313
+ * CANONICAL `is_liquidated` byte (any value other than 0/1 rejects).
314
+ */
315
+ function decodeFluidTick(account, expected) {
316
+ if (!account.pubkey.equals(expected)) {
317
+ throw new Error("decodeFluidTick: account pubkey does not match expected pin");
318
+ }
319
+ if (!account.owner.equals(constants_1.JUP_LEND_BORROW_PROGRAM_ID)) {
320
+ throw new Error("decodeFluidTick: unexpected owner");
321
+ }
322
+ const data = account.data;
323
+ if (data.length < TICK_MIN_LEN)
324
+ throw new Error("decodeFluidTick: account data too short");
325
+ if (!data.subarray(0, 8).equals(TICK_DISCRIMINATOR)) {
326
+ throw new Error("decodeFluidTick: discriminator mismatch");
327
+ }
328
+ const liqByte = data[TICK_OFFSET_IS_LIQUIDATED];
329
+ if (liqByte > 1)
330
+ throw new Error("decodeFluidTick: non-canonical is_liquidated byte");
331
+ return {
332
+ vaultId: data.readUInt16LE(TICK_OFFSET_VAULT_ID),
333
+ tick: data.readInt32LE(TICK_OFFSET_TICK),
334
+ isLiquidated: liqByte === 1,
335
+ totalIds: data.readUInt32LE(TICK_OFFSET_TOTAL_IDS),
336
+ };
337
+ }
338
+ /**
339
+ * Mirror of the on-chain liquidation-staleness gate (`require_fluid_position_liquidation_current`):
340
+ * throws unless `position`'s stored bytes are proven liquidation-current by its canonical Tick.
341
+ * For a supply-only position only the placeholder KEY is checked (`tickAccount` may be null —
342
+ * the placeholder need not exist). For an active position the Tick must decode cleanly and pass
343
+ * `!isLiquidated && totalIds === tickId` (strict generation equality — `<` is not a valid Fluid
344
+ * transition and also rejects).
345
+ */
346
+ function requireFluidPositionLiquidationCurrent(position, tickAccount) {
347
+ if (position.isSupplyOnly) {
348
+ const placeholder = deriveFluidTickPda(position.vaultId, FLUID_MIN_TICK);
349
+ if (tickAccount && !tickAccount.pubkey.equals(placeholder)) {
350
+ throw new Error("requireFluidPositionLiquidationCurrent: wrong cold placeholder Tick key");
351
+ }
352
+ return;
353
+ }
354
+ if (!tickAccount) {
355
+ throw new Error("requireFluidPositionLiquidationCurrent: active position requires its Tick account");
356
+ }
357
+ const expected = deriveFluidTickPda(position.vaultId, position.tick);
358
+ const tick = decodeFluidTick(tickAccount, expected);
359
+ if (tick.vaultId !== position.vaultId || tick.tick !== position.tick) {
360
+ throw new Error("requireFluidPositionLiquidationCurrent: Tick vault_id/tick mismatch vs position");
361
+ }
362
+ if (tick.isLiquidated) {
363
+ throw new Error("requireFluidPositionLiquidationCurrent: tick is liquidated -- position is liquidation-stale");
364
+ }
365
+ if (tick.totalIds !== position.tickId) {
366
+ throw new Error(`requireFluidPositionLiquidationCurrent: tick generation ${tick.totalIds} != position tick_id ${position.tickId} -- position is liquidation-stale`);
367
+ }
368
+ }
369
+ /**
370
+ * Decode + validate a Fluid `VaultState` account. `expected` pins it to the caller's canonical
371
+ * pubkey (e.g. one `HedgePositionSlot.vaultState`) — mirrors `decode_fluid_vault_state`.
372
+ */
373
+ function decodeFluidVaultState(account, expected) {
374
+ if (!account.pubkey.equals(expected)) {
375
+ throw new Error("decodeFluidVaultState: account pubkey does not match expected pin");
376
+ }
377
+ if (!account.owner.equals(constants_1.JUP_LEND_BORROW_PROGRAM_ID)) {
378
+ throw new Error("decodeFluidVaultState: unexpected owner");
379
+ }
380
+ const data = account.data;
381
+ if (data.length < VAULT_STATE_MIN_LEN)
382
+ throw new Error("decodeFluidVaultState: account data too short");
383
+ if (!data.subarray(0, 8).equals(VAULT_STATE_DISCRIMINATOR)) {
384
+ throw new Error("decodeFluidVaultState: discriminator mismatch");
385
+ }
386
+ const vaultId = data.readUInt16LE(VAULT_STATE_OFFSET_VAULT_ID);
387
+ const supplyExPrice = data.readBigUInt64LE(VAULT_STATE_OFFSET_SUPPLY_EX_PRICE);
388
+ const borrowExPrice = data.readBigUInt64LE(VAULT_STATE_OFFSET_BORROW_EX_PRICE);
389
+ const lastUpdateTimestamp = data.readBigUInt64LE(VAULT_STATE_OFFSET_LAST_UPDATE_TIMESTAMP);
390
+ if (supplyExPrice < VAULT_STATE_MIN_EX_PRICE || borrowExPrice < VAULT_STATE_MIN_EX_PRICE) {
391
+ throw new Error("decodeFluidVaultState: exchange price below the 1e12 sanity floor");
392
+ }
393
+ return { vaultId, supplyExPrice, borrowExPrice, lastUpdateTimestamp };
394
+ }
395
+ /**
396
+ * Require a Fluid `VaultState`'s `lastUpdateTimestamp` to be within
397
+ * `FLUID_VAULT_STATE_MAX_STALENESS_SECS` (24h) of `nowUnixSec` — mirrors
398
+ * `require_fluid_vault_state_fresh` — the check every risk-limit valuation applies:
399
+ * `settle_yield`'s NAV fold, the `hedge_supply_collateral` pledge cap, and the per-op LTV
400
+ * guards on `hedge_borrow`/`hedge_withdraw_collateral` (Fluid's `operate` CPI does not
401
+ * refresh `last_update_timestamp`, so a post-`operate` read is not automatically fresh).
402
+ */
403
+ function requireFluidVaultStateFresh(vaultState, nowUnixSec) {
404
+ const age = nowUnixSec - vaultState.lastUpdateTimestamp;
405
+ if (age < 0n)
406
+ throw new Error("requireFluidVaultStateFresh: negative age (clock skew?)");
407
+ if (age > FLUID_VAULT_STATE_MAX_STALENESS_SECS) {
408
+ throw new Error(`requireFluidVaultStateFresh: stale (${age}s > ${FLUID_VAULT_STATE_MAX_STALENESS_SECS}s)`);
409
+ }
410
+ }
411
+ /**
412
+ * Decode + validate a Fluid `VaultConfig` account. `expected` pins it to the caller's canonical
413
+ * pubkey (e.g. one `HedgePositionSlot.vaultConfig`) — mirrors the account-loading half of
414
+ * `read_fluid_price_1e15`.
415
+ */
416
+ function decodeFluidVaultConfig(account, expected) {
417
+ if (!account.pubkey.equals(expected)) {
418
+ throw new Error("decodeFluidVaultConfig: account pubkey does not match expected pin");
419
+ }
420
+ if (!account.owner.equals(constants_1.JUP_LEND_BORROW_PROGRAM_ID)) {
421
+ throw new Error("decodeFluidVaultConfig: unexpected owner");
422
+ }
423
+ const data = account.data;
424
+ if (data.length < VAULT_CONFIG_MIN_LEN)
425
+ throw new Error("decodeFluidVaultConfig: account data too short");
426
+ if (!data.subarray(0, 8).equals(VAULT_CONFIG_DISCRIMINATOR)) {
427
+ throw new Error("decodeFluidVaultConfig: discriminator mismatch");
428
+ }
429
+ const vaultId = data.readUInt16LE(VAULT_CONFIG_OFFSET_VAULT_ID);
430
+ const oracle = new web3_js_1.PublicKey(data.subarray(VAULT_CONFIG_OFFSET_ORACLE, VAULT_CONFIG_OFFSET_ORACLE + 32));
431
+ return { vaultId, oracle };
432
+ }
433
+ /**
434
+ * Bind a Fluid position's, vault state's, and vault config's `vaultId` together in one call —
435
+ * mirrors `require_fluid_vault_id_bind`.
436
+ */
437
+ function requireFluidVaultIdBind(positionVaultId, vaultStateVaultId, vaultConfigVaultId) {
438
+ if (positionVaultId !== vaultStateVaultId || vaultStateVaultId !== vaultConfigVaultId) {
439
+ throw new Error(`requireFluidVaultIdBind: vault_id mismatch (position=${positionVaultId}, vaultState=${vaultStateVaultId}, vaultConfig=${vaultConfigVaultId})`);
440
+ }
441
+ }
442
+ // ============================================================================
443
+ // Public valuation API — collateral / debt / LTV
444
+ // ============================================================================
445
+ /** `position.vaultId == vaultState.vaultId` — the 2-way bind `collateralJlpNormal`/`debtNativeCeil` require. */
446
+ function requireVaultIdMatch(positionVaultId, vaultStateVaultId) {
447
+ if (positionVaultId !== vaultStateVaultId) {
448
+ throw new Error(`requireVaultIdMatch: vault_id mismatch (position=${positionVaultId}, vaultState=${vaultStateVaultId})`);
449
+ }
450
+ }
451
+ /**
452
+ * Collateral (always JLP, 6 decimals) held by `position`, interest-adjusted and FLOORED —
453
+ * mirrors `collateral_jlp_normal` exactly.
454
+ */
455
+ function collateralJlpNormal(position, vaultState) {
456
+ requireVaultIdMatch(position.vaultId, vaultState.vaultId);
457
+ const scaled = normalAmountFloor(position.collateralRaw, vaultState.supplyExPrice);
458
+ return unscaleFloor(scaled, TOKEN_DECIMALS_JLP);
459
+ }
460
+ /**
461
+ * TRUE debt owed by `position` in `mintDecimals`-decimal native units of the debt mint,
462
+ * interest-adjusted and CEILED at both conversion stages.
463
+ *
464
+ * Fluid stores a position as (tick, collateral, dust): `operate` rounds the debt/col ratio UP one
465
+ * tick (1.0015 steps) and books the excess as `dust_debt`, so the true owed amount is
466
+ * `tick_implied − dust` (Fluid's own `get_net_debt_raw` = `debt_raw.safe_sub(dust_debt_raw)`,
467
+ * structs.rs). Adding dust — the sign this function shipped with — overstates debt by 2×dust,
468
+ * a random ≤~30bps of total debt that re-rolls on every operate.
469
+ */
470
+ function debtNativeCeil(position, vaultState, mintDecimals) {
471
+ requireVaultIdMatch(position.vaultId, vaultState.vaultId);
472
+ const tick = effectiveTick(position);
473
+ const tickDebtRaw = tickImpliedDebtRaw(tick, position.collateralRaw);
474
+ // Fluid semantics: dust ≥ registered debt ⇒ fully liquidated/absorbed — nothing owed.
475
+ const totalRaw = tickDebtRaw > position.dustDebtRaw ? tickDebtRaw - position.dustDebtRaw : 0n;
476
+ const scaled = normalAmountCeil(totalRaw, vaultState.borrowExPrice);
477
+ return unscaleCeil(scaled, mintDecimals);
478
+ }
479
+ /**
480
+ * The DEPLOYED jlpd-strategy program's debt view (`jup_lend_view.rs::debt_native_ceil`), which
481
+ * ADDS dust — overstating true debt by 2×dust. Wrong as economics, but the program's own LTV /
482
+ * repay guards are enforced against THIS number until the program-side sign fix ships, so any
483
+ * caller sizing an operation the PROGRAM must accept has to use this view, not the true one.
484
+ */
485
+ function debtNativeCeilProgramView(position, vaultState, mintDecimals) {
486
+ requireVaultIdMatch(position.vaultId, vaultState.vaultId);
487
+ const tick = effectiveTick(position);
488
+ const tickDebtRaw = tickImpliedDebtRaw(tick, position.collateralRaw);
489
+ const totalRaw = tickDebtRaw + position.dustDebtRaw;
490
+ const scaled = normalAmountCeil(totalRaw, vaultState.borrowExPrice);
491
+ return unscaleCeil(scaled, mintDecimals);
492
+ }
493
+ /**
494
+ * Value `collateralJlpNative` (JLP, 6-dec) in the debt mint's NATIVE units, using Fluid's own
495
+ * pair-native oracle price (`price1e15`) — mirrors `collateral_value_in_debt_native` exactly.
496
+ */
497
+ function collateralValueInDebtNative(collateralJlpNative, debtDecimals, price1e15) {
498
+ const naive6dec = (collateralJlpNative * price1e15) / JLP_PRICE_1E15_PRECISION; // floor, stage 1
499
+ if (debtDecimals >= TOKEN_DECIMALS_JLP) {
500
+ const scale = 10n ** BigInt(debtDecimals - TOKEN_DECIMALS_JLP);
501
+ return naive6dec * scale;
502
+ }
503
+ const scale = 10n ** BigInt(TOKEN_DECIMALS_JLP - debtDecimals);
504
+ return naive6dec / scale; // floor, stage 2
505
+ }
506
+ /**
507
+ * `ltvBps = ceil(debtNative * 10_000 / colInDebtNative)`, mirrors `position_ltv_bps` exactly —
508
+ * `debtNative == 0` short-circuits to 0; a non-zero debt against zero-valued collateral
509
+ * saturates to `0xFFFF` (u16::MAX), matching the on-chain guard's "maximally unsafe" reading.
510
+ */
511
+ function positionLtvBps(debtNative, debtDecimals, collateralJlpNative, price1e15) {
512
+ if (debtNative === 0n)
513
+ return 0;
514
+ const colInDebtNative = collateralValueInDebtNative(collateralJlpNative, debtDecimals, price1e15);
515
+ if (colInDebtNative === 0n)
516
+ return 0xffff;
517
+ const numerator = debtNative * 10000n;
518
+ const ltv = ceilDiv(numerator, colInDebtNative);
519
+ return ltv > 0xffffn ? 0xffff : Number(ltv);
520
+ }
521
+ /**
522
+ * Peek a Fluid oracle config account's own declared hop count (1 or 2) — mirrors
523
+ * `read_oracle_hop_count` (sizes how many feed accounts to fetch before decoding any price).
524
+ */
525
+ function readOracleHopCount(account, expected) {
526
+ if (!account.pubkey.equals(expected)) {
527
+ throw new Error("readOracleHopCount: account pubkey does not match expected pin");
528
+ }
529
+ if (!account.owner.equals(constants_1.FLUID_ORACLE_PROGRAM_ID)) {
530
+ throw new Error("readOracleHopCount: unexpected owner");
531
+ }
532
+ const data = account.data;
533
+ if (data.length < ORACLE_CFG_OFFSET_SOURCES_LEN + 4) {
534
+ throw new Error("readOracleHopCount: account data too short");
535
+ }
536
+ if (!data.subarray(0, 8).equals(ORACLE_CONFIG_DISCRIMINATOR)) {
537
+ throw new Error("readOracleHopCount: discriminator mismatch");
538
+ }
539
+ const rawLen = data.readUInt32LE(ORACLE_CFG_OFFSET_SOURCES_LEN);
540
+ if (rawLen !== 1 && rawLen !== 2) {
541
+ throw new Error(`readOracleHopCount: unsupported hop count ${rawLen} (only 1 or 2 supported)`);
542
+ }
543
+ return rawLen;
544
+ }
545
+ function readOracleSourceMeta(data, index) {
546
+ const base = ORACLE_CFG_OFFSET_SOURCES + index * ORACLE_SOURCE_STRIDE;
547
+ if (data.length < base + ORACLE_SOURCE_STRIDE) {
548
+ throw new Error("readOracleSourceMeta: account data too short");
549
+ }
550
+ return {
551
+ sourcePk: new web3_js_1.PublicKey(data.subarray(base + ORACLE_SOURCE_OFF_PUBKEY, base + ORACLE_SOURCE_OFF_PUBKEY + 32)),
552
+ invert: data[base + ORACLE_SOURCE_OFF_INVERT] !== 0,
553
+ sourceType: data[base + ORACLE_SOURCE_OFF_TYPE],
554
+ };
555
+ }
556
+ /** Read a signed 128-bit little-endian integer (two's complement) out of `data` at `offset`. */
557
+ function readI128LE(data, offset) {
558
+ let val = 0n;
559
+ for (let i = 0; i < 16; i++) {
560
+ val |= BigInt(data[offset + i]) << BigInt(8 * i);
561
+ }
562
+ if (val >= 1n << 127n)
563
+ val -= 1n << 128n;
564
+ return val;
565
+ }
566
+ /** Decode one Chainlink `Transmissions`/`Transmission` hop into `answer * 10^(15-decimals)`. */
567
+ function decodeChainlinkHop(account, currentSlot) {
568
+ if (!account.owner.equals(constants_1.CHAINLINK_STORE_PROGRAM_ID)) {
569
+ throw new Error("decodeChainlinkHop: unexpected owner");
570
+ }
571
+ const data = account.data;
572
+ if (data.length < CHAINLINK_MIN_LEN)
573
+ throw new Error("decodeChainlinkHop: account data too short");
574
+ if (!data.subarray(0, 8).equals(CHAINLINK_TRANSMISSIONS_DISCRIMINATOR)) {
575
+ throw new Error("decodeChainlinkHop: discriminator mismatch");
576
+ }
577
+ const decimals = data[CHAINLINK_OFFSET_DECIMALS];
578
+ const latestRoundId = data.readUInt32LE(CHAINLINK_OFFSET_LATEST_ROUND_ID);
579
+ const liveLength = data.readUInt32LE(CHAINLINK_OFFSET_LIVE_LENGTH);
580
+ const feedSlot = data.readBigUInt64LE(CHAINLINK_OFFSET_SLOT);
581
+ const answer = readI128LE(data, CHAINLINK_OFFSET_ANSWER);
582
+ if (liveLength !== 1)
583
+ throw new Error("decodeChainlinkHop: unexpected live_length");
584
+ if (latestRoundId === 0)
585
+ throw new Error("decodeChainlinkHop: latest_round_id is zero");
586
+ if (answer <= 0n)
587
+ throw new Error("decodeChainlinkHop: answer is non-positive");
588
+ if (decimals > ORACLE_RATE_OUTPUT_DECIMALS)
589
+ throw new Error("decodeChainlinkHop: decimals out of range");
590
+ if (currentSlot < feedSlot)
591
+ throw new Error("decodeChainlinkHop: feed slot is in the future");
592
+ const slotsElapsed = currentSlot - feedSlot;
593
+ const elapsedS = (slotsElapsed * AVG_SLOT_TIME_MS) / 1000n;
594
+ if (elapsedS > CHAINLINK_MAX_STALENESS_SECS)
595
+ throw new Error("decodeChainlinkHop: feed stale");
596
+ const scale = 10n ** BigInt(ORACLE_RATE_OUTPUT_DECIMALS - decimals);
597
+ return answer * scale;
598
+ }
599
+ /** Decode one Pyth `PriceUpdateV2` hop into `price * 10^(15-|exponent|)`. */
600
+ function decodePythHop(account, nowUnixSec) {
601
+ if (!account.owner.equals(constants_1.PYTH_RECEIVER_PROGRAM_ID)) {
602
+ throw new Error("decodePythHop: unexpected owner");
603
+ }
604
+ const data = account.data;
605
+ if (data.length < PYTH_MIN_LEN)
606
+ throw new Error("decodePythHop: account data too short");
607
+ if (!data.subarray(0, 8).equals(PYTH_PRICE_UPDATE_DISCRIMINATOR)) {
608
+ throw new Error("decodePythHop: discriminator mismatch");
609
+ }
610
+ const verificationLevel = data[PYTH_OFFSET_VERIFICATION_LEVEL];
611
+ const price = data.readBigInt64LE(PYTH_OFFSET_PRICE);
612
+ const conf = data.readBigUInt64LE(PYTH_OFFSET_CONF);
613
+ const exponent = data.readInt32LE(PYTH_OFFSET_EXPONENT);
614
+ const publishTime = data.readBigInt64LE(PYTH_OFFSET_PUBLISH_TIME);
615
+ if (verificationLevel !== PYTH_VERIFICATION_LEVEL_FULL) {
616
+ throw new Error("decodePythHop: verification level is not Full");
617
+ }
618
+ if (price <= 0n)
619
+ throw new Error("decodePythHop: price is non-positive");
620
+ if (exponent > 0 || exponent < -PYTH_MAX_ABS_EXPONENT) {
621
+ throw new Error("decodePythHop: exponent out of range");
622
+ }
623
+ if (conf * PYTH_CONFIDENCE_SCALE_FACTOR_LIQUIDATE > price) {
624
+ throw new Error("decodePythHop: confidence interval too wide");
625
+ }
626
+ if (nowUnixSec < publishTime)
627
+ throw new Error("decodePythHop: publish_time is in the future");
628
+ const elapsedS = nowUnixSec - publishTime;
629
+ if (elapsedS > PYTH_MAX_STALENESS_SECS)
630
+ throw new Error("decodePythHop: price stale");
631
+ const absExponent = -exponent;
632
+ const scale = 10n ** BigInt(ORACLE_RATE_OUTPUT_DECIMALS - absExponent);
633
+ return price * scale;
634
+ }
635
+ /**
636
+ * Decode hop `meta.sourcePk`'s feed (dispatching on `meta.sourceType`), apply this hop's own
637
+ * `invert`, and fold it into the running chained rate — mirrors `apply_hop` /
638
+ * `get_exchange_rate_for_hop` exactly: `rate = hop_value; if invert { rate = 1e30 / rate };
639
+ * running = running * rate / 1e15`.
640
+ */
641
+ function applyHop(meta, feedAccount, nowUnixSec, currentSlot, runningRate) {
642
+ let r;
643
+ if (meta.sourceType === ORACLE_SOURCE_TYPE_PYTH) {
644
+ r = decodePythHop(feedAccount, nowUnixSec);
645
+ }
646
+ else if (meta.sourceType === ORACLE_SOURCE_TYPE_CHAINLINK) {
647
+ r = decodeChainlinkHop(feedAccount, currentSlot);
648
+ }
649
+ else {
650
+ throw new Error(`applyHop: unsupported oracle source type ${meta.sourceType}`);
651
+ }
652
+ if (r > 0n && meta.invert) {
653
+ r = ORACLE_INVERT_NUMERATOR / r;
654
+ }
655
+ const combined = (runningRate * r) / ORACLE_RATE_SCALE;
656
+ if (combined === 0n)
657
+ throw new Error("applyHop: combined rate is zero");
658
+ return combined;
659
+ }
660
+ /**
661
+ * Read one Fluid vault's pair-native price (1e15 precision — collateral, always JLP, valued in
662
+ * that vault's OWN debt mint), off the SAME oracle chain Fluid itself liquidates on. Pure
663
+ * orchestrator over already-fetched accounts — mirrors `read_fluid_price_1e15` exactly.
664
+ * See `readFluidPrice1e15` for the async fetch-and-compute convenience wrapper.
665
+ */
666
+ function readFluidPrice1e15FromAccounts(args) {
667
+ const { vaultConfig, expectedVaultConfig, positionVaultId, vaultStateVaultId, oracleCfg, expectedOracle, feedAccounts, nowUnixSec, currentSlot } = args;
668
+ const vaultConfigInfo = decodeFluidVaultConfig(vaultConfig, expectedVaultConfig);
669
+ requireFluidVaultIdBind(positionVaultId, vaultStateVaultId, vaultConfigInfo.vaultId);
670
+ if (!vaultConfigInfo.oracle.equals(expectedOracle)) {
671
+ throw new Error("readFluidPrice1e15FromAccounts: VaultConfig.oracle does not match the pinned oracle");
672
+ }
673
+ if (!oracleCfg.pubkey.equals(expectedOracle)) {
674
+ throw new Error("readFluidPrice1e15FromAccounts: oracle config pubkey does not match expected pin");
675
+ }
676
+ if (!oracleCfg.owner.equals(constants_1.FLUID_ORACLE_PROGRAM_ID)) {
677
+ throw new Error("readFluidPrice1e15FromAccounts: unexpected oracle config owner");
678
+ }
679
+ const oracleData = oracleCfg.data;
680
+ if (oracleData.length < ORACLE_CFG_OFFSET_SOURCES_LEN + 4) {
681
+ throw new Error("readFluidPrice1e15FromAccounts: oracle config data too short");
682
+ }
683
+ if (!oracleData.subarray(0, 8).equals(ORACLE_CONFIG_DISCRIMINATOR)) {
684
+ throw new Error("readFluidPrice1e15FromAccounts: oracle config discriminator mismatch");
685
+ }
686
+ const sourcesLen = oracleData.readUInt32LE(ORACLE_CFG_OFFSET_SOURCES_LEN);
687
+ if (sourcesLen !== 1 && sourcesLen !== 2) {
688
+ throw new Error(`readFluidPrice1e15FromAccounts: unsupported hop count ${sourcesLen}`);
689
+ }
690
+ if (feedAccounts.length !== sourcesLen) {
691
+ throw new Error(`readFluidPrice1e15FromAccounts: feed account count mismatch (got ${feedAccounts.length}, expected ${sourcesLen})`);
692
+ }
693
+ let rate = ORACLE_RATE_SCALE;
694
+ for (let i = 0; i < sourcesLen; i++) {
695
+ const meta = readOracleSourceMeta(oracleData, i);
696
+ if (!feedAccounts[i].pubkey.equals(meta.sourcePk)) {
697
+ throw new Error(`readFluidPrice1e15FromAccounts: feed account ${i} does not match the oracle config's declared source`);
698
+ }
699
+ rate = applyHop(meta, feedAccounts[i], nowUnixSec, currentSlot, rate);
700
+ }
701
+ if (sourcesLen === 1 && (rate < MIN_JLP_PRICE_1E15 || rate > MAX_JLP_PRICE_1E15)) {
702
+ throw new Error("readFluidPrice1e15FromAccounts: single-hop price outside [$0.50, $50] clamp");
703
+ }
704
+ return rate;
705
+ }
706
+ /**
707
+ * Fetch + decode one `HedgePositionSlot`'s pair-native oracle price (1e15), then read that
708
+ * slot's Position/VaultState and compute its LTV — the async convenience wrapper matching
709
+ * `hedge_borrow`'s / `hedge_withdraw_collateral`'s guard basis exactly. `debtDecimals` must be
710
+ * the debt mint's own `Mint.decimals` (fetch separately; not re-derived here).
711
+ */
712
+ async function fetchFluidSlotLtv(connection, slot, debtDecimals) {
713
+ const [positionInfo, vaultStateInfo, vaultConfigInfo] = await connection.getMultipleAccountsInfo([
714
+ slot.borrowPosition,
715
+ slot.vaultState,
716
+ slot.vaultConfig,
717
+ ]);
718
+ if (!positionInfo)
719
+ throw new Error("fetchFluidSlotLtv: Fluid position account not found");
720
+ if (!vaultStateInfo)
721
+ throw new Error("fetchFluidSlotLtv: Fluid vault_state account not found");
722
+ if (!vaultConfigInfo)
723
+ throw new Error("fetchFluidSlotLtv: Fluid vault_config account not found");
724
+ const position = decodeFluidPosition({ pubkey: slot.borrowPosition, owner: positionInfo.owner, data: positionInfo.data }, slot.borrowPosition);
725
+ const vaultState = decodeFluidVaultState({ pubkey: slot.vaultState, owner: vaultStateInfo.owner, data: vaultStateInfo.data }, slot.vaultState);
726
+ const oracleHopCountInfo = await connection.getAccountInfo(slot.fluidOracle);
727
+ if (!oracleHopCountInfo)
728
+ throw new Error("fetchFluidSlotLtv: Fluid oracle config account not found");
729
+ const oracleCfg = {
730
+ pubkey: slot.fluidOracle,
731
+ owner: oracleHopCountInfo.owner,
732
+ data: oracleHopCountInfo.data,
733
+ };
734
+ const hopCount = readOracleHopCount(oracleCfg, slot.fluidOracle);
735
+ const oracleData = oracleCfg.data;
736
+ const feedPubkeys = [];
737
+ for (let i = 0; i < hopCount; i++) {
738
+ feedPubkeys.push(readOracleSourceMeta(oracleData, i).sourcePk);
739
+ }
740
+ const [slotNumber, feedInfos] = await Promise.all([
741
+ connection.getSlot(),
742
+ connection.getMultipleAccountsInfo(feedPubkeys),
743
+ ]);
744
+ const blockTime = await connection.getBlockTime(slotNumber);
745
+ if (blockTime == null)
746
+ throw new Error("fetchFluidSlotLtv: could not resolve current block time");
747
+ const feedAccounts = feedPubkeys.map((pk, i) => {
748
+ const info = feedInfos[i];
749
+ if (!info)
750
+ throw new Error(`fetchFluidSlotLtv: oracle feed account ${pk.toBase58()} not found`);
751
+ return { pubkey: pk, owner: info.owner, data: info.data };
752
+ });
753
+ const price1e15 = readFluidPrice1e15FromAccounts({
754
+ vaultConfig: {
755
+ pubkey: slot.vaultConfig,
756
+ owner: vaultConfigInfo.owner,
757
+ data: vaultConfigInfo.data,
758
+ },
759
+ expectedVaultConfig: slot.vaultConfig,
760
+ positionVaultId: position.vaultId,
761
+ vaultStateVaultId: vaultState.vaultId,
762
+ oracleCfg,
763
+ expectedOracle: slot.fluidOracle,
764
+ feedAccounts,
765
+ nowUnixSec: BigInt(blockTime),
766
+ currentSlot: BigInt(slotNumber),
767
+ });
768
+ // PROGRAM view (dust ADDED) deliberately: this LTV feeds operation sizing, and the deployed
769
+ // program enforces its own guards against the +dust reading until the sign fix ships — sizing
770
+ // from the true (lower) debt could build an op the program rejects.
771
+ const debtNative = debtNativeCeilProgramView(position, vaultState, debtDecimals);
772
+ const collateralJlpNative = collateralJlpNormal(position, vaultState);
773
+ const ltvBps = positionLtvBps(debtNative, debtDecimals, collateralJlpNative, price1e15);
774
+ return { ltvBps, debtNative, collateralJlpNative, price1e15, position, vaultState };
775
+ }
776
+ // ============================================================================
777
+ // Jup-Lend Earn — live fToken exchange rate
778
+ // ============================================================================
779
+ /**
780
+ * Read the Jupiter Lend Earn `lending` account's stored exchange rate (1e12 precision) for one
781
+ * fToken — mirrors `earn_exchange_rate` exactly (uses the STORED rate directly, no forward
782
+ * accrual; `lending`'s identity must be pinned by the caller).
783
+ */
784
+ function earnExchangeRate(account) {
785
+ if (!account.owner.equals(constants_1.JUP_LEND_EARN_PROGRAM_ID)) {
786
+ throw new Error("earnExchangeRate: unexpected owner");
787
+ }
788
+ const data = account.data;
789
+ if (data.length < LENDING_MIN_LEN)
790
+ throw new Error("earnExchangeRate: account data too short");
791
+ if (!data.subarray(0, 8).equals(LENDING_DISCRIMINATOR)) {
792
+ throw new Error("earnExchangeRate: discriminator mismatch");
793
+ }
794
+ const rate = data.readBigUInt64LE(LENDING_OFFSET_EXCHANGE_RATE);
795
+ if (rate < exports.JUP_LEND_EARN_MIN_SANE_RATE || rate > exports.JUP_LEND_EARN_MAX_SANE_RATE) {
796
+ throw new Error("earnExchangeRate: rate outside sane bounds");
797
+ }
798
+ return rate;
799
+ }