1- import type { Connection , PublicKey , Signer , TransactionError } from '@solana/web3.js' ;
2- import { Transaction } from '@solana/web3.js' ;
3- import { TOKEN_PROGRAM_ID } from '../constants.js' ;
1+ import type { Connection , Signer , TransactionError } from '@solana/web3.js' ;
2+ import { PublicKey , Transaction } from '@solana/web3.js' ;
3+ import { TOKEN_2022_PROGRAM_ID , TOKEN_PROGRAM_ID } from '../constants.js' ;
44import { createAmountToUiAmountInstruction } from '../instructions/amountToUiAmount.js' ;
5+ import { unpackMint } from '../state/mint.js' ;
6+ import { getInterestBearingMintConfigState } from '../extensions/interestBearingMint/state.js' ;
57
68/**
79 * Amount as a string using mint-prescribed decimals
@@ -28,3 +30,242 @@ export async function amountToUiAmount(
2830 }
2931 return err ;
3032}
33+
34+ /**
35+ * Calculates the exponent for the interest rate formula.
36+ * @param t1 - The start time in seconds.
37+ * @param t2 - The end time in seconds.
38+ * @param r - The interest rate in basis points.
39+ * @returns The calculated exponent.
40+ */
41+ function calculateExponentForTimesAndRate ( t1 : number , t2 : number , r : number ) {
42+ const ONE_IN_BASIS_POINTS = 10000 ;
43+ const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24 ;
44+ const timespan = t2 - t1 ;
45+ const numerator = r * timespan ;
46+ const exponent = numerator / ( SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS ) ;
47+ return Math . exp ( exponent ) ;
48+ }
49+
50+ /**
51+ * Retrieves the current timestamp from the Solana clock sysvar.
52+ * @param connection - The Solana connection object.
53+ * @returns A promise that resolves to the current timestamp in seconds.
54+ * @throws An error if the sysvar clock cannot be fetched or parsed.
55+ */
56+ async function getSysvarClockTimestamp ( connection : Connection ) : Promise < number > {
57+ const info = await connection . getParsedAccountInfo ( new PublicKey ( 'SysvarC1ock11111111111111111111111111111111' ) ) ;
58+ if ( ! info ) {
59+ throw new Error ( 'Failed to fetch sysvar clock' ) ;
60+ }
61+ if ( typeof info . value === 'object' && info . value && 'data' in info . value && 'parsed' in info . value . data ) {
62+ return info . value . data . parsed . info . unixTimestamp ;
63+ }
64+ throw new Error ( 'Failed to parse sysvar clock' ) ;
65+ }
66+
67+ /**
68+ * Convert amount to UiAmount for a mint with interest bearing extension without simulating a transaction
69+ * This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs
70+ * In general to calculate compounding interest over a period of time, the formula is:
71+ * A = P * e^(r * t) where
72+ * A = final amount after interest
73+ * P = principal amount (initial investment)
74+ * r = annual interest rate (as a decimal, e.g., 5% = 0.05)
75+ * t = time in years
76+ * e = mathematical constant (~2.718)
77+ *
78+ * In this case, we are calculating the total scale factor for the interest bearing extension which is the product of two exponential functions:
79+ * totalScale = e^(r1 * t1) * e^(r2 * t2)
80+ * where r1 and r2 are the interest rates before and after the last update, and t1 and t2 are the times in years between
81+ * the initialization timestamp and the last update timestamp, and between the last update timestamp and the current timestamp.
82+ *
83+ * @param amount Amount of tokens to be converted
84+ * @param decimals Number of decimals of the mint
85+ * @param currentTimestamp Current timestamp in seconds
86+ * @param lastUpdateTimestamp Last time the interest rate was updated in seconds
87+ * @param initializationTimestamp Time the interest bearing extension was initialized in seconds
88+ * @param preUpdateAverageRate Interest rate in basis points (1 basis point = 0.01%) before last update
89+ * @param currentRate Current interest rate in basis points
90+ *
91+ * @return Amount scaled by accrued interest as a string with appropriate decimal places
92+ */
93+ export function amountToUiAmountWithoutSimulation (
94+ amount : bigint ,
95+ decimals : number ,
96+ currentTimestamp : number , // in seconds
97+ lastUpdateTimestamp : number ,
98+ initializationTimestamp : number ,
99+ preUpdateAverageRate : number ,
100+ currentRate : number ,
101+ ) : string {
102+ // Calculate pre-update exponent
103+ // e^(preUpdateAverageRate * (lastUpdateTimestamp - initializationTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS))
104+ const preUpdateExp = calculateExponentForTimesAndRate (
105+ initializationTimestamp ,
106+ lastUpdateTimestamp ,
107+ preUpdateAverageRate ,
108+ ) ;
109+
110+ // Calculate post-update exponent
111+ // e^(currentRate * (currentTimestamp - lastUpdateTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS))
112+ const postUpdateExp = calculateExponentForTimesAndRate ( lastUpdateTimestamp , currentTimestamp , currentRate ) ;
113+
114+ // Calculate total scale
115+ const totalScale = preUpdateExp * postUpdateExp ;
116+ // Scale the amount by the total interest factor
117+ const scaledAmount = Number ( amount ) * totalScale ;
118+
119+ // Calculate the decimal factor (e.g. 100 for 2 decimals)
120+ const decimalFactor = Math . pow ( 10 , decimals ) ;
121+
122+ // Convert to UI amount by:
123+ // 1. Truncating to remove any remaining decimals
124+ // 2. Dividing by decimal factor to get final UI amount
125+ // 3. Converting to string
126+ return ( Math . trunc ( scaledAmount ) / decimalFactor ) . toString ( ) ;
127+ }
128+
129+ /**
130+ * Convert amount to UiAmount for a mint without simulating a transaction
131+ * This implements the same logic as `process_amount_to_ui_amount` in /token/program-2022/src/processor.rs
132+ * and `process_amount_to_ui_amount` in /token/program/src/processor.rs
133+ *
134+ * @param connection Connection to use
135+ * @param mint Mint to use for calculations
136+ * @param amount Amount of tokens to be converted to Ui Amount
137+ *
138+ * @return Ui Amount generated
139+ */
140+ export async function amountToUiAmountForMintWithoutSimulation (
141+ connection : Connection ,
142+ mint : PublicKey ,
143+ amount : bigint ,
144+ ) : Promise < string > {
145+ const accountInfo = await connection . getAccountInfo ( mint ) ;
146+ const programId = accountInfo ?. owner ;
147+ if ( programId !== TOKEN_PROGRAM_ID && programId !== TOKEN_2022_PROGRAM_ID ) {
148+ throw new Error ( 'Invalid program ID' ) ;
149+ }
150+
151+ const mintInfo = unpackMint ( mint , accountInfo , programId ) ;
152+
153+ const interestBearingMintConfigState = getInterestBearingMintConfigState ( mintInfo ) ;
154+ if ( ! interestBearingMintConfigState ) {
155+ const amountNumber = Number ( amount ) ;
156+ const decimalsFactor = Math . pow ( 10 , mintInfo . decimals ) ;
157+ return ( amountNumber / decimalsFactor ) . toString ( ) ;
158+ }
159+
160+ const timestamp = await getSysvarClockTimestamp ( connection ) ;
161+
162+ return amountToUiAmountWithoutSimulation (
163+ amount ,
164+ mintInfo . decimals ,
165+ timestamp ,
166+ Number ( interestBearingMintConfigState . lastUpdateTimestamp ) ,
167+ Number ( interestBearingMintConfigState . initializationTimestamp ) ,
168+ interestBearingMintConfigState . preUpdateAverageRate ,
169+ interestBearingMintConfigState . currentRate ,
170+ ) ;
171+ }
172+
173+ /**
174+ * Convert an amount with interest back to the original amount without interest
175+ * This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs
176+ *
177+ * @param uiAmount UI Amount (principal plus continuously compounding interest) to be converted back to original principal
178+ * @param decimals Number of decimals for the mint
179+ * @param currentTimestamp Current timestamp in seconds
180+ * @param lastUpdateTimestamp Last time the interest rate was updated in seconds
181+ * @param initializationTimestamp Time the interest bearing extension was initialized in seconds
182+ * @param preUpdateAverageRate Interest rate in basis points (hundredths of a percent) before the last update
183+ * @param currentRate Current interest rate in basis points
184+ *
185+ * In general to calculate the principal from the UI amount, the formula is:
186+ * P = A / (e^(r * t)) where
187+ * P = principal
188+ * A = UI amount
189+ * r = annual interest rate (as a decimal, e.g., 5% = 0.05)
190+ * t = time in years
191+ *
192+ * In this case, we are calculating the principal by dividing the UI amount by the total scale factor which is the product of two exponential functions:
193+ * totalScale = e^(r1 * t1) * e^(r2 * t2)
194+ * where r1 is the pre-update average rate, r2 is the current rate, t1 is the time in years between the initialization timestamp and the last update timestamp,
195+ * and t2 is the time in years between the last update timestamp and the current timestamp.
196+ * then to calculate the principal, we divide the UI amount by the total scale factor:
197+ * P = A / totalScale
198+ *
199+ * @return Original amount (principal) without interest
200+ */
201+ export function uiAmountToAmountWithoutSimulation (
202+ uiAmount : string ,
203+ decimals : number ,
204+ currentTimestamp : number , // in seconds
205+ lastUpdateTimestamp : number ,
206+ initializationTimestamp : number ,
207+ preUpdateAverageRate : number ,
208+ currentRate : number ,
209+ ) : bigint {
210+ const uiAmountNumber = parseFloat ( uiAmount ) ;
211+ const decimalsFactor = Math . pow ( 10 , decimals ) ;
212+ const uiAmountScaled = uiAmountNumber * decimalsFactor ;
213+
214+ // Calculate pre-update exponent
215+ const preUpdateExp = calculateExponentForTimesAndRate (
216+ initializationTimestamp ,
217+ lastUpdateTimestamp ,
218+ preUpdateAverageRate ,
219+ ) ;
220+
221+ // Calculate post-update exponent
222+ const postUpdateExp = calculateExponentForTimesAndRate ( lastUpdateTimestamp , currentTimestamp , currentRate ) ;
223+
224+ // Calculate total scale
225+ const totalScale = preUpdateExp * postUpdateExp ;
226+
227+ // Calculate original principal by dividing the UI amount (principal + interest) by the total scale
228+ const originalPrincipal = uiAmountScaled / totalScale ;
229+ return BigInt ( Math . trunc ( originalPrincipal ) ) ;
230+ }
231+
232+ /**
233+ * Convert a UI amount back to the raw amount
234+ *
235+ * @param connection Connection to use
236+ * @param mint Mint to use for calculations
237+ * @param uiAmount UI Amount to be converted back to raw amount
238+ *
239+ *
240+ * @return Raw amount
241+ */
242+ export async function uiAmountToAmountForMintWithoutSimulation (
243+ connection : Connection ,
244+ mint : PublicKey ,
245+ uiAmount : string ,
246+ ) : Promise < bigint > {
247+ const accountInfo = await connection . getAccountInfo ( mint ) ;
248+ const programId = accountInfo ?. owner ;
249+ if ( programId !== TOKEN_PROGRAM_ID && programId !== TOKEN_2022_PROGRAM_ID ) {
250+ throw new Error ( 'Invalid program ID' ) ;
251+ }
252+
253+ const mintInfo = unpackMint ( mint , accountInfo , programId ) ;
254+ const interestBearingMintConfigState = getInterestBearingMintConfigState ( mintInfo ) ;
255+ if ( ! interestBearingMintConfigState ) {
256+ const uiAmountScaled = parseFloat ( uiAmount ) * Math . pow ( 10 , mintInfo . decimals ) ;
257+ return BigInt ( Math . trunc ( uiAmountScaled ) ) ;
258+ }
259+
260+ const timestamp = await getSysvarClockTimestamp ( connection ) ;
261+
262+ return uiAmountToAmountWithoutSimulation (
263+ uiAmount ,
264+ mintInfo . decimals ,
265+ timestamp ,
266+ Number ( interestBearingMintConfigState . lastUpdateTimestamp ) ,
267+ Number ( interestBearingMintConfigState . initializationTimestamp ) ,
268+ interestBearingMintConfigState . preUpdateAverageRate ,
269+ interestBearingMintConfigState . currentRate ,
270+ ) ;
271+ }
0 commit comments