@exponent-labs/exponent-fetcher 0.1.7 → 0.1.8

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 (50) hide show
  1. package/build/exponentFetcher.d.ts +303 -31
  2. package/build/exponentFetcher.js +666 -168
  3. package/build/exponentFetcher.js.map +1 -1
  4. package/build/index.d.ts +10 -0
  5. package/build/index.js +10 -0
  6. package/build/index.js.map +1 -1
  7. package/build/utils/adrena.d.ts +13 -0
  8. package/build/utils/adrena.js +29 -0
  9. package/build/utils/adrena.js.map +1 -0
  10. package/build/utils/fragmetric.d.ts +17 -0
  11. package/build/utils/fragmetric.js +23 -0
  12. package/build/utils/fragmetric.js.map +1 -0
  13. package/build/utils/jito.d.ts +7 -0
  14. package/build/utils/jito.js +29 -0
  15. package/build/utils/jito.js.map +1 -0
  16. package/build/utils/jupiter.d.ts +30 -0
  17. package/build/utils/jupiter.js +63 -0
  18. package/build/utils/jupiter.js.map +1 -0
  19. package/build/utils/kamino.d.ts +5 -0
  20. package/build/utils/kamino.js +10 -0
  21. package/build/utils/kamino.js.map +1 -0
  22. package/build/utils/meteora.d.ts +19 -0
  23. package/build/utils/meteora.js +36 -1
  24. package/build/utils/meteora.js.map +1 -1
  25. package/build/utils/ore.d.ts +74 -0
  26. package/build/utils/ore.js +217 -0
  27. package/build/utils/ore.js.map +1 -0
  28. package/build/utils/perena.d.ts +12 -0
  29. package/build/utils/perena.js +27 -0
  30. package/build/utils/perena.js.map +1 -0
  31. package/build/utils/sanctum.d.ts +6 -0
  32. package/build/utils/sanctum.js +26 -0
  33. package/build/utils/sanctum.js.map +1 -0
  34. package/build/utils/solstice.d.ts +12 -0
  35. package/build/utils/solstice.js +45 -0
  36. package/build/utils/solstice.js.map +1 -0
  37. package/package.json +20 -18
  38. package/src/exponentFetcher.ts +1077 -222
  39. package/src/index.ts +10 -0
  40. package/src/utils/adrena.ts +44 -0
  41. package/src/utils/fragmetric.ts +34 -0
  42. package/src/utils/jito.ts +30 -0
  43. package/src/utils/jupiter.ts +98 -0
  44. package/src/utils/kamino.ts +6 -0
  45. package/src/utils/meteora.ts +73 -1
  46. package/src/utils/ore.ts +322 -0
  47. package/src/utils/perena.ts +28 -0
  48. package/src/utils/sanctum.ts +24 -0
  49. package/src/utils/solstice.ts +51 -0
  50. package/tsconfig.json +2 -0
package/src/index.ts CHANGED
@@ -1,3 +1,13 @@
1
1
  // TODO - rename to "sol"
2
2
  export * from "./exponentFetcher"
3
3
  export { fetchJupiterPerpsIndex } from "./exponentFetcher"
4
+ export * from "./utils/meteora"
5
+ export * from "./utils/sanctum"
6
+ export * from "./utils/fragmetric"
7
+ export * from "./utils/adrena"
8
+ export * from "./utils/solstice"
9
+ export * from "./utils/jupiter"
10
+ export * from "./utils/kamino"
11
+ export * from "./utils/perena"
12
+ export * from "./utils/jito"
13
+ export * from "./utils/ore"
@@ -0,0 +1,44 @@
1
+ import BN from "bn.js"
2
+
3
+ import { calculateTotalFeesFromCustodies, decodeCustodyAccount, decodePoolAccount } from "@exponent-labs/adrena-idl"
4
+ import { AnchorizedPNum } from "@exponent-labs/exponent-types"
5
+ import { PreciseNumber } from "@exponent-labs/precise-number"
6
+
7
+ export function calculateAdrenaIndex(
8
+ accounts: {
9
+ pool: Buffer
10
+ custody1: Buffer
11
+ custody2: Buffer
12
+ custody3: Buffer
13
+ custody4: Buffer
14
+ },
15
+ previousTotalFees: BN,
16
+ currentIndex: AnchorizedPNum,
17
+ ) {
18
+ const ZERO_BN = new BN(0)
19
+
20
+ // Decode accounts using adrena-idl package functions
21
+ // @ts-ignore
22
+ const poolAccount = decodePoolAccount(accounts.pool)
23
+ const custodyAccounts = [accounts.custody1, accounts.custody2, accounts.custody3, accounts.custody4].map(
24
+ decodeCustodyAccount,
25
+ )
26
+
27
+ // @ts-ignore
28
+ const currentTotalFees = calculateTotalFeesFromCustodies(custodyAccounts)
29
+
30
+ let yieldIncrement: number
31
+ let aumValue = poolAccount.aumUsd.low
32
+
33
+ if (aumValue.eq(ZERO_BN)) {
34
+ yieldIncrement = 0
35
+ }
36
+
37
+ let feeDifference = currentTotalFees.gte(previousTotalFees) ? currentTotalFees.sub(previousTotalFees) : ZERO_BN
38
+
39
+ yieldIncrement = feeDifference.toNumber() / aumValue.toNumber()
40
+
41
+ let currentIndexNumber = parseFloat(PreciseNumber.fromRaw(currentIndex[0]).valueString)
42
+
43
+ return { index: currentIndexNumber + yieldIncrement }
44
+ }
@@ -0,0 +1,34 @@
1
+ import { web3 } from "@coral-xyz/anchor"
2
+
3
+ import { decodeFundAccount as decodeFragmetricFundAccount } from "@exponent-labs/fragmetric-idl"
4
+
5
+ export function calculateFragmetricIndex(accounts: { fragmetricFund: Buffer }) {
6
+ const fundAccount = decodeFragmetricFundAccount(accounts.fragmetricFund)
7
+ const index = Number(fundAccount.one_receipt_token_as_sol) / Number(10 ** fundAccount.receipt_token_decimals)
8
+
9
+ const receiptTokenMint = new web3.PublicKey(fundAccount.receipt_token_mint)
10
+ const wrappedTokenMint = new web3.PublicKey(fundAccount.wrapped_token.mint)
11
+
12
+ return { index, receiptTokenMint, wrappedTokenMint }
13
+ }
14
+
15
+ export const calculateFragmetricSupportedTokenIndex = (
16
+ accounts: {
17
+ fragmetricFund: Buffer
18
+ },
19
+ index = 0,
20
+ ): {
21
+ index: number
22
+ receiptTokenMint: web3.PublicKey
23
+ wrappedTokenMint: web3.PublicKey
24
+ } => {
25
+ const fundAccount = decodeFragmetricFundAccount(accounts.fragmetricFund)
26
+ const syIndex =
27
+ Number(10 ** fundAccount.supported_tokens[index].decimals) /
28
+ Number(fundAccount.supported_tokens[index].one_token_as_receipt_token)
29
+
30
+ const receiptTokenMint = new web3.PublicKey(fundAccount.receipt_token_mint)
31
+ const wrappedTokenMint = new web3.PublicKey(fundAccount.wrapped_token.mint)
32
+
33
+ return { index: syIndex, receiptTokenMint, wrappedTokenMint }
34
+ }
@@ -0,0 +1,30 @@
1
+ import { BN, web3 } from "@coral-xyz/anchor"
2
+ import Decimal from "decimal.js"
3
+
4
+ export function decodeJitoVaultData(vaultAccountData: Buffer) {
5
+ // the vault has an 8 byte discriminator at the beginning
6
+ const DISCRIMINATOR_OFFSET = 8
7
+ const VRT_MINT_OFFSET = 32 + DISCRIMINATOR_OFFSET
8
+ const VRT_SUPPLY_OFFSET = 96 + DISCRIMINATOR_OFFSET
9
+ const JITO_VAULT_TOTAL_DEPOSITS_OFFSET = 104 + DISCRIMINATOR_OFFSET
10
+ const mintBase = new web3.PublicKey(vaultAccountData.subarray(VRT_MINT_OFFSET, VRT_MINT_OFFSET + 32))
11
+
12
+ // For Borsh, numbers are serialized in little-endian format
13
+ const jitoVaultTotalSharesBuffer = vaultAccountData.subarray(VRT_SUPPLY_OFFSET, VRT_SUPPLY_OFFSET + 8)
14
+ const jitoVaultTotalShares = new BN(jitoVaultTotalSharesBuffer, "le")
15
+
16
+ const jitoVaultTotalDepositsBuffer = vaultAccountData.subarray(
17
+ JITO_VAULT_TOTAL_DEPOSITS_OFFSET,
18
+ JITO_VAULT_TOTAL_DEPOSITS_OFFSET + 8,
19
+ )
20
+ const jitoVaultTotalDeposits = new BN(jitoVaultTotalDepositsBuffer, "le")
21
+
22
+ const jitoVaultTotalSharesD = new Decimal(jitoVaultTotalShares.toString())
23
+ const jitoVaultTotalDepositsD = new Decimal(jitoVaultTotalDeposits.toString())
24
+
25
+ const exchangeRate = jitoVaultTotalDepositsD.isZero()
26
+ ? "1.0"
27
+ : jitoVaultTotalDepositsD.div(jitoVaultTotalSharesD).toString()
28
+
29
+ return { exchangeRate: parseFloat(exchangeRate), mintBase }
30
+ }
@@ -0,0 +1,98 @@
1
+ import { web3 } from "@coral-xyz/anchor"
2
+ import BN from "bn.js"
3
+ import Decimal from "decimal.js"
4
+
5
+ import { AnchorizedPNum } from "@exponent-labs/exponent-types"
6
+ import { decodeJupiterLendAccount } from "@exponent-labs/jupiter-lend-idl"
7
+ import { decodePoolAccount as decodeJupiterPerpsPoolAccount } from "@exponent-labs/jupiter-perps-idl"
8
+ import { PreciseNumber } from "@exponent-labs/precise-number"
9
+
10
+ export function calculateJupiterLendIndex(accounts: { jupiterLend: Buffer }): {
11
+ index: number
12
+ baseTokenMint: any
13
+ tokenReservesLiquidity: any
14
+ lendingSupplyPosition: any
15
+ rewardsRateModel: any
16
+ } {
17
+ const accountData = decodeJupiterLendAccount(accounts.jupiterLend)
18
+
19
+ const index = Number(accountData.token_exchange_price) / Number(10 ** 12)
20
+ const tokenReservesLiquidity = accountData.token_reserves_liquidity
21
+ const lendingSupplyPosition = accountData.supply_position_on_liquidity
22
+ const rewardsRateModel = accountData.rewards_rate_model
23
+
24
+ return {
25
+ index,
26
+ baseTokenMint: accountData.mint,
27
+ tokenReservesLiquidity,
28
+ lendingSupplyPosition,
29
+ rewardsRateModel,
30
+ }
31
+ }
32
+
33
+ export function calculateJupiterPerpsIndex(
34
+ accounts: {
35
+ pool: Buffer
36
+ },
37
+ params: {
38
+ lastFeeUsdResetUnixTimestamp: number
39
+ lastRealizedFeeUsd: BN
40
+ lastAumUsd: BN
41
+ currentIndex: AnchorizedPNum
42
+ lastRealizedFeeUsdUpdateUnixTimestamp: number
43
+ },
44
+ ): {
45
+ index: number
46
+ newState: {
47
+ lastAumUsd: BN
48
+ lastRealizedFeeUsd: BN
49
+ lastFeeUsdResetUnixTimestamp: number
50
+ lastRealizedFeeUsdUpdateUnixTimestamp: number
51
+ }
52
+ } {
53
+ const SECONDS_PER_YEAR = 365 * 24 * 60 * 60
54
+
55
+ const {
56
+ lastFeeUsdResetUnixTimestamp,
57
+ lastRealizedFeeUsd,
58
+ lastAumUsd,
59
+ currentIndex,
60
+ lastRealizedFeeUsdUpdateUnixTimestamp,
61
+ } = params
62
+
63
+ const account: any = decodeJupiterPerpsPoolAccount(accounts.pool)
64
+
65
+ let newFeesBn: BN
66
+
67
+ if (lastRealizedFeeUsdUpdateUnixTimestamp === 0) {
68
+ newFeesBn = new BN(account.poolApr.realizedFeeUsd.toString())
69
+ } else if (Number(account.poolApr.lastUpdated) > lastFeeUsdResetUnixTimestamp) {
70
+ const timeBetweenResets = Number(account.poolApr.lastUpdated) - lastFeeUsdResetUnixTimestamp
71
+ const feeAprBps = new BN(account.poolApr.feeAprBps)
72
+ const estTotalFees =
73
+ lastAumUsd.gt(new BN(0)) && timeBetweenResets > 0
74
+ ? feeAprBps.mul(lastAumUsd).mul(new BN(timeBetweenResets)).div(new BN(SECONDS_PER_YEAR)).div(new BN(10_000))
75
+ : new BN(0)
76
+
77
+ const missingFees = estTotalFees.sub(lastRealizedFeeUsd)
78
+ const feesSinceReset = new BN(account.poolApr.realizedFeeUsd.toString())
79
+ newFeesBn = missingFees.add(feesSinceReset)
80
+ } else {
81
+ newFeesBn = new BN(account.poolApr.realizedFeeUsd.toString()).sub(lastRealizedFeeUsd)
82
+ }
83
+
84
+ const aumUsd = new BN(account.aumUsd.toString())
85
+ const indexIncrease = new Decimal(newFeesBn.toString()).div(new Decimal(aumUsd.toString())).toNumber()
86
+ const currentIndexNum = parseFloat(PreciseNumber.fromRaw(currentIndex[0]).valueString)
87
+ const nextIndex = currentIndexNum + indexIncrease
88
+
89
+ return {
90
+ index: nextIndex,
91
+ newState: {
92
+ lastAumUsd: aumUsd,
93
+ lastRealizedFeeUsd: new BN(account.poolApr.realizedFeeUsd.toString()),
94
+ lastFeeUsdResetUnixTimestamp: Number(account.poolApr.lastUpdated),
95
+ lastRealizedFeeUsdUpdateUnixTimestamp: Math.floor(Date.now() / 1000),
96
+ },
97
+ }
98
+ }
@@ -0,0 +1,6 @@
1
+ import { Reserve } from "@exponent-labs/kamino-reserve-deserializer"
2
+
3
+ export function calculateKaminoIndex(accounts: { reserveAccountData: Buffer }) {
4
+ const reserve = Reserve.decode(accounts.reserveAccountData)
5
+ return reserve.getCollateralExchangeRate().toNumber()
6
+ }
@@ -1,6 +1,13 @@
1
- import { CurveType, VaultState } from "@exponent-labs/meteora-idl"
1
+ import {
2
+ CurveType,
3
+ VaultState,
4
+ decodePool as decodeMeteoraPool,
5
+ decodeVault as decodeMeteoraVault,
6
+ } from "@exponent-labs/meteora-idl"
7
+ import Decimal from "decimal.js"
2
8
  import { BN } from "@coral-xyz/anchor"
3
9
  import sqrt from "bn-sqrt"
10
+ import { AccountLayout, MintLayout } from "@solana/spl-token"
4
11
 
5
12
  /**
6
13
  * Calculates the unlocked (withdrawable) amount of tokens in a Meteora vault.
@@ -129,3 +136,68 @@ export function computeD(curve: CurveType, tokenAmountA: BN, tokenAmountB: BN):
129
136
  // Default case - should never reach here if CurveType is properly defined
130
137
  throw new Error("Invalid curve type")
131
138
  }
139
+
140
+ export function calculateMeteoraIndex(accounts: {
141
+ pool: Buffer
142
+ lpMint: Buffer
143
+ vaultA: Buffer
144
+ vaultB: Buffer
145
+ vaultALpMint: Buffer
146
+ vaultBLpMint: Buffer
147
+ poolVaultALpAccount: Buffer
148
+ poolVaultBLpAccount: Buffer
149
+ }) {
150
+ const VIRTUAL_PRICE_PRECISION = new BN(100_000_000)
151
+ const nowUnix = Math.floor(Date.now() / 1000)
152
+
153
+ const pool = decodeMeteoraPool(accounts.pool)
154
+
155
+ const poolMint = MintLayout.decode(accounts.lpMint.subarray(0, MintLayout.span))
156
+ const poolLpSupply = new BN(poolMint.supply.toString())
157
+ const poolLpDecimals = Number(poolMint.decimals.toString())
158
+
159
+ const vaultA = decodeMeteoraVault(accounts.vaultA)
160
+ const vaultB = decodeMeteoraVault(accounts.vaultB)
161
+
162
+ const vaultLpMintA = MintLayout.decode(accounts.vaultALpMint.subarray(0, MintLayout.span))
163
+ const vaultLpMintB = MintLayout.decode(accounts.vaultBLpMint.subarray(0, MintLayout.span))
164
+
165
+ const vaultALpSupply = vaultLpMintA.supply
166
+ const vaultBLpSupply = vaultLpMintB.supply
167
+
168
+ const decimalsA = vaultLpMintA.decimals
169
+ const decimalsB = vaultLpMintB.decimals
170
+
171
+ const poolVaultALpTokenAmount = AccountLayout.decode(
172
+ accounts.poolVaultALpAccount.subarray(0, AccountLayout.span),
173
+ ).amount
174
+ const poolVaultBLpTokenAmount = AccountLayout.decode(
175
+ accounts.poolVaultBLpAccount.subarray(0, AccountLayout.span),
176
+ ).amount
177
+
178
+ const tokenAAmount = getAmountByShare(
179
+ new BN(poolVaultALpTokenAmount.toString()),
180
+ new BN(vaultALpSupply.toString()),
181
+ vaultA,
182
+ nowUnix,
183
+ )
184
+ const tokenBAmount = getAmountByShare(
185
+ new BN(poolVaultBLpTokenAmount.toString()),
186
+ new BN(vaultBLpSupply.toString()),
187
+ vaultB,
188
+ nowUnix,
189
+ )
190
+
191
+ const d = computeD(pool.curveType, tokenAAmount, tokenBAmount)
192
+ const virtualPriceBigNum = poolLpSupply.isZero() ? new BN(0) : d.mul(VIRTUAL_PRICE_PRECISION).div(poolLpSupply)
193
+ const virtualPrice = new Decimal(virtualPriceBigNum.toString()).div(VIRTUAL_PRICE_PRECISION.toString()).toNumber()
194
+
195
+ return {
196
+ virtualPrice,
197
+ tokenAMint: pool.tokenAMint,
198
+ tokenBMint: pool.tokenBMint,
199
+ poolVaultALpTokenAmount: Number(poolVaultALpTokenAmount) / 10 ** decimalsA,
200
+ poolVaultBLpTokenAmount: Number(poolVaultBLpTokenAmount) / 10 ** decimalsB,
201
+ lpSupply: poolLpSupply.toNumber() / 10 ** poolLpDecimals,
202
+ }
203
+ }
@@ -0,0 +1,322 @@
1
+ import { BN } from "@coral-xyz/anchor"
2
+ import { web3 } from "@coral-xyz/anchor"
3
+ import { MintLayout } from "@solana/spl-token"
4
+ import Decimal from "decimal.js"
5
+
6
+ // Steel's Numeric type is a u128 with 12 decimal places of precision
7
+ const NUMERIC_DECIMAL_PLACES = 12
8
+
9
+ /**
10
+ * Represents a Numeric value from the steel crate
11
+ * Steel's Numeric is a u128 (16 bytes) with 1e12 scaling factor
12
+ */
13
+ export interface Numeric {
14
+ rawValue: BN // The raw u128 value
15
+ decimalValue: Decimal // The scaled decimal representation
16
+ }
17
+
18
+ /**
19
+ * Deserialize a steel Numeric type from buffer
20
+ * Numeric is stored as u128 (16 bytes = 2 x u64, little-endian)
21
+ */
22
+ function deserializeNumeric(data: Buffer, offset: number): Numeric {
23
+ const low = new BN(data.slice(offset, offset + 8), "le")
24
+ const high = new BN(data.slice(offset + 8, offset + 16), "le")
25
+ const rawValue = low.add(high.shln(64))
26
+ const decimalValue = new Decimal(rawValue.toString()).div(new Decimal(10).pow(NUMERIC_DECIMAL_PLACES))
27
+ return { rawValue, decimalValue }
28
+ }
29
+
30
+ /**
31
+ * TypeScript representation of the Stake struct from ore_cpi
32
+ * Matches the Rust struct in solana/libraries/ore_cpi/src/state/stake.rs
33
+ */
34
+ export interface Stake {
35
+ /// The authority of this miner account.
36
+ authority: web3.PublicKey
37
+ /// The balance of this stake account.
38
+ balance: BN
39
+ /// Buffer a (placeholder)
40
+ bufferA: BN
41
+ /// Buffer b (placeholder)
42
+ bufferB: BN
43
+ /// Buffer c (placeholder)
44
+ bufferC: BN
45
+ /// Buffer d (placeholder)
46
+ bufferD: BN
47
+ /// The lamport reserve to pay fees for auto-compounding bots.
48
+ compoundFeeReserve: BN
49
+ /// The timestamp of last claim.
50
+ lastClaimAt: BN
51
+ /// The timestamp the last time this staker deposited.
52
+ lastDepositAt: BN
53
+ /// The timestamp the last time this staker withdrew.
54
+ lastWithdrawAt: BN
55
+ /// The rewards factor last time rewards were updated on this stake account.
56
+ rewardsFactor: Numeric
57
+ /// The amount of ORE this staker can claim.
58
+ rewards: BN
59
+ /// The total amount of ORE this staker has earned over its lifetime.
60
+ lifetimeRewards: BN
61
+ /// Buffer f (placeholder)
62
+ bufferF: BN
63
+ }
64
+
65
+ /**
66
+ * TypeScript representation of the Treasury struct from ore_cpi
67
+ * Matches the Rust struct in solana/libraries/ore_cpi/src/state/treasury.rs
68
+ */
69
+ export interface Treasury {
70
+ /// The amount of SOL collected for buy-bury operations.
71
+ balance: BN
72
+ /// Buffer a (placeholder)
73
+ bufferA: BN
74
+ /// The amount of ORE in the motherlode rewards pool.
75
+ motherlode: BN
76
+ /// The cumulative ORE distributed to miners, divided by the total unclaimed ORE at the time of distribution.
77
+ minerRewardsFactor: Numeric
78
+ /// The cumulative ORE distributed to stakers, divided by the total stake at the time of distribution.
79
+ stakeRewardsFactor: Numeric
80
+ /// Buffer b (placeholder)
81
+ bufferB: BN
82
+ /// The current total amount of refined ORE mining rewards.
83
+ totalRefined: BN
84
+ /// The current total amount of ORE staking deposits.
85
+ totalStaked: BN
86
+ /// The current total amount of unclaimed ORE mining rewards.
87
+ totalUnclaimed: BN
88
+ }
89
+
90
+ /**
91
+ * Deserialize Stake account from Buffer
92
+ * Skips the 8-byte steel discriminator and deserializes the struct fields
93
+ *
94
+ * Account layout (152 bytes total):
95
+ * - discriminator: 8 bytes
96
+ * - authority: 32 bytes
97
+ * - balance: 8 bytes
98
+ * - buffer_a through buffer_d: 32 bytes (4 x u64)
99
+ * - compound_fee_reserve: 8 bytes
100
+ * - last_claim_at, last_deposit_at, last_withdraw_at: 24 bytes (3 x i64)
101
+ * - rewards_factor: 16 bytes (u128 Numeric)
102
+ * - rewards: 8 bytes
103
+ * - lifetime_rewards: 8 bytes
104
+ * - buffer_f: 8 bytes
105
+ */
106
+ function deserializeStakeAccount(stakeData: Buffer): Stake {
107
+ const DISCRIMINATOR_SIZE = 8
108
+ let offset = DISCRIMINATOR_SIZE
109
+
110
+ // authority: Pubkey (32 bytes)
111
+ const authority = new web3.PublicKey(stakeData.slice(offset, offset + 32))
112
+ offset += 32
113
+
114
+ // balance: u64 (8 bytes)
115
+ const balance = new BN(stakeData.slice(offset, offset + 8), "le")
116
+ offset += 8
117
+
118
+ // buffer_a: u64 (8 bytes)
119
+ const bufferA = new BN(stakeData.slice(offset, offset + 8), "le")
120
+ offset += 8
121
+
122
+ // buffer_b: u64 (8 bytes)
123
+ const bufferB = new BN(stakeData.slice(offset, offset + 8), "le")
124
+ offset += 8
125
+
126
+ // buffer_c: u64 (8 bytes)
127
+ const bufferC = new BN(stakeData.slice(offset, offset + 8), "le")
128
+ offset += 8
129
+
130
+ // buffer_d: u64 (8 bytes)
131
+ const bufferD = new BN(stakeData.slice(offset, offset + 8), "le")
132
+ offset += 8
133
+
134
+ // compound_fee_reserve: u64 (8 bytes)
135
+ const compoundFeeReserve = new BN(stakeData.slice(offset, offset + 8), "le")
136
+ offset += 8
137
+
138
+ // last_claim_at: i64 (8 bytes)
139
+ const lastClaimAt = new BN(stakeData.slice(offset, offset + 8), "le")
140
+ offset += 8
141
+
142
+ // last_deposit_at: i64 (8 bytes)
143
+ const lastDepositAt = new BN(stakeData.slice(offset, offset + 8), "le")
144
+ offset += 8
145
+
146
+ // last_withdraw_at: i64 (8 bytes)
147
+ const lastWithdrawAt = new BN(stakeData.slice(offset, offset + 8), "le")
148
+ offset += 8
149
+
150
+ // rewards_factor: Numeric (16 bytes = u128)
151
+ const rewardsFactor = deserializeNumeric(stakeData, offset)
152
+ offset += 16
153
+
154
+ // rewards: u64 (8 bytes)
155
+ const rewards = new BN(stakeData.slice(offset, offset + 8), "le")
156
+ offset += 8
157
+
158
+ // lifetime_rewards: u64 (8 bytes)
159
+ const lifetimeRewards = new BN(stakeData.slice(offset, offset + 8), "le")
160
+ offset += 8
161
+
162
+ // buffer_f: u64 (8 bytes)
163
+ const bufferF = new BN(stakeData.slice(offset, offset + 8), "le")
164
+
165
+ return {
166
+ authority,
167
+ balance,
168
+ bufferA,
169
+ bufferB,
170
+ bufferC,
171
+ bufferD,
172
+ compoundFeeReserve,
173
+ lastClaimAt,
174
+ lastDepositAt,
175
+ lastWithdrawAt,
176
+ rewardsFactor,
177
+ rewards,
178
+ lifetimeRewards,
179
+ bufferF,
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Deserialize Treasury account from Buffer
185
+ * Skips the 8-byte steel discriminator and deserializes the struct fields
186
+ *
187
+ * Account layout (96 bytes total):
188
+ * - discriminator: 8 bytes
189
+ * - balance: 8 bytes
190
+ * - buffer_a: 8 bytes
191
+ * - motherlode: 8 bytes
192
+ * - miner_rewards_factor: 16 bytes (u128 Numeric)
193
+ * - stake_rewards_factor: 16 bytes (u128 Numeric)
194
+ * - buffer_b: 8 bytes
195
+ * - total_refined: 8 bytes
196
+ * - total_staked: 8 bytes
197
+ * - total_unclaimed: 8 bytes
198
+ */
199
+ function deserializeTreasuryAccount(treasuryData: Buffer): Treasury {
200
+ const DISCRIMINATOR_SIZE = 8
201
+ let offset = DISCRIMINATOR_SIZE
202
+
203
+ // balance: u64 (8 bytes)
204
+ const balance = new BN(treasuryData.slice(offset, offset + 8), "le")
205
+ offset += 8
206
+
207
+ // buffer_a: u64 (8 bytes)
208
+ const bufferA = new BN(treasuryData.slice(offset, offset + 8), "le")
209
+ offset += 8
210
+
211
+ // motherlode: u64 (8 bytes)
212
+ const motherlode = new BN(treasuryData.slice(offset, offset + 8), "le")
213
+ offset += 8
214
+
215
+ // miner_rewards_factor: Numeric (16 bytes = u128)
216
+ const minerRewardsFactor = deserializeNumeric(treasuryData, offset)
217
+ offset += 16
218
+
219
+ // stake_rewards_factor: Numeric (16 bytes = u128)
220
+ const stakeRewardsFactor = deserializeNumeric(treasuryData, offset)
221
+ offset += 16
222
+
223
+ // buffer_b: u64 (8 bytes)
224
+ const bufferB = new BN(treasuryData.slice(offset, offset + 8), "le")
225
+ offset += 8
226
+
227
+ // total_refined: u64 (8 bytes)
228
+ const totalRefined = new BN(treasuryData.slice(offset, offset + 8), "le")
229
+ offset += 8
230
+
231
+ // total_staked: u64 (8 bytes)
232
+ const totalStaked = new BN(treasuryData.slice(offset, offset + 8), "le")
233
+ offset += 8
234
+
235
+ // total_unclaimed: u64 (8 bytes)
236
+ const totalUnclaimed = new BN(treasuryData.slice(offset, offset + 8), "le")
237
+
238
+ return {
239
+ balance,
240
+ bufferA,
241
+ motherlode,
242
+ minerRewardsFactor,
243
+ stakeRewardsFactor,
244
+ bufferB,
245
+ totalRefined,
246
+ totalStaked,
247
+ totalUnclaimed,
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Calculate ORE exchange rate from account data
253
+ * This mirrors the logic from the Rust implementation in generic_standard/src/utils.rs
254
+ *
255
+ * Exchange rate formula: (stake.balance + total_rewards) / stORE_supply
256
+ *
257
+ * Where total_rewards is calculated using ORE protocol's Stake::calculate_accrued_rewards:
258
+ * 1. accumulated_rewards = treasury.stake_rewards_factor - stake.rewards_factor
259
+ * 2. personal_rewards = accumulated_rewards * stake.balance
260
+ * 3. total_rewards = stake.rewards + personal_rewards
261
+ *
262
+ * Security note: We use stake.balance as the canonical staked amount, NOT
263
+ * stake_tokens.amount from the token account. This prevents manipulation
264
+ * where an attacker transfers ORE directly into the stake ATA to inflate
265
+ * the exchange rate. The stake.balance is only updated via ORE program
266
+ * deposit/withdraw instructions.
267
+ *
268
+ * Note: Numeric values are stored as fixed-point numbers with 1e12 precision.
269
+ * When multiplying two Numeric values, the result is divided by 1e12 to maintain scale.
270
+ */
271
+ export function calculateOreExchangeRate(accounts: {
272
+ stakeAccount: Buffer
273
+ treasuryAccount: Buffer
274
+ storeMint: Buffer
275
+ }): number {
276
+ const { stakeAccount, treasuryAccount, storeMint } = accounts
277
+
278
+ // Decode mint account to get stORE supply
279
+ const storeMintAccount = MintLayout.decode(storeMint.subarray(0, MintLayout.span))
280
+ const storeSupply = Number(storeMintAccount.supply.toString())
281
+
282
+ if (storeSupply === 0) {
283
+ return 1
284
+ }
285
+
286
+ // Deserialize accounts into structured types
287
+ const stake = deserializeStakeAccount(stakeAccount)
288
+ const treasury = deserializeTreasuryAccount(treasuryAccount)
289
+
290
+ // Use stake.balance as the canonical staked amount (not token account balance)
291
+ // This prevents exchange rate manipulation via direct token transfers
292
+ const stakeBalance = stake.balance.toNumber()
293
+
294
+ // Calculate accrued rewards using ORE protocol's calculate_accrued_rewards algorithm
295
+ // This mirrors the exact logic from Stake::calculate_accrued_rewards() in ore_cpi
296
+ const treasuryStakeRewardsFactorDecimal = treasury.stakeRewardsFactor.decimalValue
297
+ const stakeRewardsFactorDecimal = stake.rewardsFactor.decimalValue
298
+
299
+ let newlyAccruedRewards = 0
300
+ if (treasuryStakeRewardsFactorDecimal.gt(stakeRewardsFactorDecimal)) {
301
+ // accumulated_rewards = treasury.stake_rewards_factor - stake.rewards_factor
302
+ const accumulatedRewards = treasuryStakeRewardsFactorDecimal.minus(stakeRewardsFactorDecimal)
303
+
304
+ // personal_rewards = accumulated_rewards * stake.balance
305
+ // In Rust: accumulated_rewards (as Numeric) * Numeric::from_u64(stake.balance)
306
+ // The Numeric type uses 1e12 scaling internally. When multiplying two Numerics,
307
+ // the result is (a * b) / 1e12 to maintain scale. Then to_u64() divides by 1e12.
308
+ // Since decimalValue already gives us the decimal representation (divided by 1e12),
309
+ // we just multiply by balance and floor the result.
310
+ const personalRewards = accumulatedRewards.mul(new Decimal(stakeBalance))
311
+ newlyAccruedRewards = Math.floor(personalRewards.toNumber())
312
+ }
313
+
314
+ // Total rewards = existing rewards + newly accrued rewards
315
+ const totalRewards = stake.rewards.toNumber() + newlyAccruedRewards
316
+
317
+ // Calculate total balance: stake.balance + total_rewards
318
+ const totalBalanceWithRewards = stakeBalance + totalRewards
319
+
320
+ // Calculate exchange rate: (stake.balance + accrued_rewards) / stORE_supply
321
+ return totalBalanceWithRewards / storeSupply
322
+ }
@@ -0,0 +1,28 @@
1
+ import { web3 } from "@coral-xyz/anchor"
2
+ import { MintLayout } from "@solana/spl-token"
3
+ import Decimal from "decimal.js"
4
+
5
+ export function getPerenaLpMint(perenaStablePool: web3.PublicKey) {
6
+ const PERENA_NUMERAIRE_PROGRAM = new web3.PublicKey("NUMERUNsFCP3kuNmWZuXtm1AaQCPj9uw6Guv2Ekoi5P")
7
+
8
+ const [lpMint, _] = web3.PublicKey.findProgramAddressSync(
9
+ [perenaStablePool.toBuffer(), Buffer.from("liquidity")],
10
+ PERENA_NUMERAIRE_PROGRAM,
11
+ )
12
+ return lpMint
13
+ }
14
+
15
+ export function getPerenaStablePoolData(accounts: { perenaStablePoolData: Buffer; lpMintData: Buffer }) {
16
+ const { perenaStablePoolData, lpMintData } = accounts
17
+
18
+ const lpMintDeserialized = MintLayout.decode(lpMintData)
19
+
20
+ const discriminatorOffset = 8
21
+ const invTOffset = discriminatorOffset + 32 + 32 + 32 + 32 // 4 Pubkeys before invT
22
+ const invTBuffer = perenaStablePoolData.slice(invTOffset, invTOffset + 8)
23
+ const invT = Buffer.from(invTBuffer).readBigUInt64LE(0)
24
+
25
+ const exchangeRate = new Decimal(invT.toString()).div(new Decimal(lpMintDeserialized.supply.toString())).toString()
26
+
27
+ return { lpSupply: lpMintDeserialized.supply, invT, exchangeRate }
28
+ }
@@ -0,0 +1,24 @@
1
+ import { decodePoolState } from "@exponent-labs/sanctum-idl"
2
+ import { MintLayout } from "@solana/spl-token"
3
+ import Decimal from "decimal.js"
4
+ import { BN } from "@coral-xyz/anchor"
5
+
6
+ export function calculateSanctumIndex(accounts: { lpMintAccountData: Buffer; poolStateAccountData: Buffer }) {
7
+ const poolState = decodePoolState(accounts.poolStateAccountData)
8
+
9
+ const lpMint = MintLayout.decode(accounts.lpMintAccountData.subarray(0, MintLayout.span))
10
+
11
+ const zeroBn = new BN(0)
12
+ const precision = new BN(10 ** lpMint.decimals)
13
+ const lpTokenSupplyBn = new BN(lpMint.supply.toString())
14
+ const poolTotalSolValueBn = new BN(poolState.totalSolValue.toString())
15
+
16
+ if (lpTokenSupplyBn.eq(zeroBn) || poolTotalSolValueBn.eq(zeroBn)) {
17
+ return 1
18
+ }
19
+
20
+ const exchangeRateBn = lpTokenSupplyBn.mul(precision).div(poolTotalSolValueBn)
21
+ const exchangeRate = new Decimal(exchangeRateBn.toString()).div(precision.toString()).toNumber()
22
+
23
+ return 1 / exchangeRate // We return inverse exchange rate
24
+ }