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' ;
4
4
import { createAmountToUiAmountInstruction } from '../instructions/amountToUiAmount.js' ;
5
+ import { unpackMint } from '../state/mint.js' ;
6
+ import { getInterestBearingMintConfigState } from '../extensions/interestBearingMint/state.js' ;
5
7
6
8
/**
7
9
* Amount as a string using mint-prescribed decimals
@@ -28,3 +30,242 @@ export async function amountToUiAmount(
28
30
}
29
31
return err ;
30
32
}
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