@@ -8,6 +8,10 @@ import {
8
8
import { fetchSysvarClock } from '@solana/sysvars' ;
9
9
import { fetchMint } from './generated' ;
10
10
11
+ // Constants
12
+ const ONE_IN_BASIS_POINTS = 10000 ;
13
+ const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24 ;
14
+
11
15
/**
12
16
* Calculates the exponent for the interest rate formula.
13
17
* @param t1 - The start time in seconds.
@@ -17,8 +21,6 @@ import { fetchMint } from './generated';
17
21
* @returns The calculated exponent.
18
22
*/
19
23
function calculateExponentForTimesAndRate ( t1 : number , t2 : number , r : number ) {
20
- const ONE_IN_BASIS_POINTS = 10000 ;
21
- const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24 ;
22
24
const timespan = t2 - t1 ;
23
25
if ( timespan < 0 ) {
24
26
throw new Error ( 'Invalid timespan: end time before start time' ) ;
@@ -56,15 +58,13 @@ function calculateTotalScale({
56
58
currentRate : number ;
57
59
} ) : number {
58
60
// Calculate pre-update exponent
59
- // e^(preUpdateAverageRate * (lastUpdateTimestamp - initializationTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS))
60
61
const preUpdateExp = calculateExponentForTimesAndRate (
61
62
initializationTimestamp ,
62
63
lastUpdateTimestamp ,
63
64
preUpdateAverageRate
64
65
) ;
65
66
66
67
// Calculate post-update exponent
67
- // e^(currentRate * (currentTimestamp - lastUpdateTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS))
68
68
const postUpdateExp = calculateExponentForTimesAndRate (
69
69
lastUpdateTimestamp ,
70
70
currentTimestamp ,
@@ -74,6 +74,15 @@ function calculateTotalScale({
74
74
return preUpdateExp * postUpdateExp ;
75
75
}
76
76
77
+ /**
78
+ * Calculates the decimal factor for a given number of decimals
79
+ * @param decimals - Number of decimals
80
+ * @returns The decimal factor (e.g., 100 for 2 decimals)
81
+ */
82
+ function getDecimalFactor ( decimals : number ) : number {
83
+ return Math . pow ( 10 , decimals ) ;
84
+ }
85
+
77
86
/**
78
87
* Retrieves the current timestamp from the Solana clock sysvar.
79
88
* @param rpc - The Solana rpc object.
@@ -90,6 +99,8 @@ async function getSysvarClockTimestamp(
90
99
return info . unixTimestamp ;
91
100
}
92
101
102
+ // ========== INTEREST BEARING MINT FUNCTIONS ==========
103
+
93
104
/**
94
105
* Convert amount to UiAmount for a mint with interest bearing extension without simulating a transaction
95
106
* This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs
@@ -119,7 +130,7 @@ async function getSysvarClockTimestamp(
119
130
export function amountToUiAmountForInterestBearingMintWithoutSimulation (
120
131
amount : bigint ,
121
132
decimals : number ,
122
- currentTimestamp : number , // in seconds
133
+ currentTimestamp : number ,
123
134
lastUpdateTimestamp : number ,
124
135
initializationTimestamp : number ,
125
136
preUpdateAverageRate : number ,
@@ -132,60 +143,14 @@ export function amountToUiAmountForInterestBearingMintWithoutSimulation(
132
143
preUpdateAverageRate,
133
144
currentRate,
134
145
} ) ;
146
+
135
147
// Scale the amount by the total interest factor
136
148
const scaledAmount = Number ( amount ) * totalScale ;
149
+ const decimalFactor = getDecimalFactor ( decimals ) ;
137
150
138
- // Calculate the decimal factor (e.g. 100 for 2 decimals)
139
- const decimalFactor = Math . pow ( 10 , decimals ) ;
140
-
141
- // Convert to UI amount by:
142
- // 1. Truncating to remove any remaining decimals
143
- // 2. Dividing by decimal factor to get final UI amount
144
- // 3. Converting to string
145
151
return ( Math . trunc ( scaledAmount ) / decimalFactor ) . toString ( ) ;
146
152
}
147
153
148
- /**
149
- * Convert amount to UiAmount for a mint without simulating a transaction
150
- * This implements the same logic as `process_amount_to_ui_amount` in
151
- * solana-labs/solana-program-library/token/program-2022/src/processor.rs
152
- * and `process_amount_to_ui_amount` in solana-labs/solana-program-library/token/program/src/processor.rs
153
- *
154
- * @param rpc Rpc to use
155
- * @param mint Mint to use for calculations
156
- * @param amount Amount of tokens to be converted to Ui Amount
157
- *
158
- * @return Ui Amount generated
159
- */
160
- export async function amountToUiAmountForMintWithoutSimulation (
161
- rpc : Rpc < GetAccountInfoApi > ,
162
- mint : Address ,
163
- amount : bigint
164
- ) : Promise < string > {
165
- const accountInfo = await fetchMint ( rpc , mint ) ;
166
- const extensions = unwrapOption ( accountInfo . data . extensions ) ;
167
- const interestBearingMintConfigState = extensions ?. find (
168
- ( ext ) => ext . __kind === 'InterestBearingConfig'
169
- ) ;
170
- if ( ! interestBearingMintConfigState ) {
171
- const amountNumber = Number ( amount ) ;
172
- const decimalsFactor = Math . pow ( 10 , accountInfo . data . decimals ) ;
173
- return ( amountNumber / decimalsFactor ) . toString ( ) ;
174
- }
175
-
176
- const timestamp = await getSysvarClockTimestamp ( rpc ) ;
177
-
178
- return amountToUiAmountForInterestBearingMintWithoutSimulation (
179
- amount ,
180
- accountInfo . data . decimals ,
181
- Number ( timestamp ) ,
182
- Number ( interestBearingMintConfigState . lastUpdateTimestamp ) ,
183
- Number ( interestBearingMintConfigState . initializationTimestamp ) ,
184
- interestBearingMintConfigState . preUpdateAverageRate ,
185
- interestBearingMintConfigState . currentRate
186
- ) ;
187
- }
188
-
189
154
/**
190
155
* Convert an amount with interest back to the original amount without interest
191
156
* This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs
@@ -217,14 +182,14 @@ export async function amountToUiAmountForMintWithoutSimulation(
217
182
export function uiAmountToAmountForInterestBearingMintWithoutSimulation (
218
183
uiAmount : string ,
219
184
decimals : number ,
220
- currentTimestamp : number , // in seconds
185
+ currentTimestamp : number ,
221
186
lastUpdateTimestamp : number ,
222
187
initializationTimestamp : number ,
223
188
preUpdateAverageRate : number ,
224
189
currentRate : number
225
190
) : bigint {
226
191
const uiAmountNumber = parseFloat ( uiAmount ) ;
227
- const decimalsFactor = Math . pow ( 10 , decimals ) ;
192
+ const decimalsFactor = getDecimalFactor ( decimals ) ;
228
193
const uiAmountScaled = uiAmountNumber * decimalsFactor ;
229
194
230
195
const totalScale = calculateTotalScale ( {
@@ -235,11 +200,123 @@ export function uiAmountToAmountForInterestBearingMintWithoutSimulation(
235
200
currentRate,
236
201
} ) ;
237
202
238
- // Calculate original principal by dividing the UI amount (principal + interest) by the total scale
203
+ // Calculate original principal by dividing the UI amount by the total scale
239
204
const originalPrincipal = uiAmountScaled / totalScale ;
240
205
return BigInt ( Math . trunc ( originalPrincipal ) ) ;
241
206
}
242
207
208
+ // ========== SCALED UI AMOUNT MINT FUNCTIONS ==========
209
+
210
+ /**
211
+ * Convert amount to UiAmount for a mint with scaled UI amount extension
212
+ * @param amount Amount of tokens to be converted
213
+ * @param decimals Number of decimals of the mint
214
+ * @param multiplier Multiplier to scale the amount
215
+ * @return Scaled UI amount as a string
216
+ */
217
+ export function amountToUiAmountForScaledUiAmountMintWithoutSimulation (
218
+ amount : bigint ,
219
+ decimals : number ,
220
+ multiplier : number
221
+ ) : string {
222
+ const scaledAmount = Number ( amount ) * multiplier ;
223
+ const decimalFactor = getDecimalFactor ( decimals ) ;
224
+ return ( Math . trunc ( scaledAmount ) / decimalFactor ) . toString ( ) ;
225
+ }
226
+
227
+ /**
228
+ * Convert a UI amount back to the raw amount for a mint with a scaled UI amount extension
229
+ * @param uiAmount UI Amount to be converted back to raw amount
230
+ * @param decimals Number of decimals for the mint
231
+ * @param multiplier Multiplier for the scaled UI amount
232
+ *
233
+ * @return Raw amount
234
+ */
235
+ export function uiAmountToAmountForScaledUiAmountMintWithoutSimulation (
236
+ uiAmount : string ,
237
+ decimals : number ,
238
+ multiplier : number
239
+ ) : bigint {
240
+ const uiAmountNumber = parseFloat ( uiAmount ) ;
241
+ const decimalsFactor = getDecimalFactor ( decimals ) ;
242
+ const uiAmountScaled = uiAmountNumber * decimalsFactor ;
243
+ const rawAmount = uiAmountScaled / multiplier ;
244
+ return BigInt ( Math . trunc ( rawAmount ) ) ;
245
+ }
246
+
247
+ // ========== MAIN ENTRY POINT FUNCTIONS ==========
248
+
249
+ /**
250
+ * Convert amount to UiAmount for a mint without simulating a transaction
251
+ * This implements the same logic as `process_amount_to_ui_amount` in
252
+ * solana-labs/solana-program-library/token/program-2022/src/processor.rs
253
+ * and `process_amount_to_ui_amount` in solana-labs/solana-program-library/token/program/src/processor.rs
254
+ *
255
+ * @param rpc Rpc to use
256
+ * @param mint Mint to use for calculations
257
+ * @param amount Amount of tokens to be converted to Ui Amount
258
+ *
259
+ * @return Ui Amount generated
260
+ */
261
+ export async function amountToUiAmountForMintWithoutSimulation (
262
+ rpc : Rpc < GetAccountInfoApi > ,
263
+ mint : Address ,
264
+ amount : bigint
265
+ ) : Promise < string > {
266
+ const accountInfo = await fetchMint ( rpc , mint ) ;
267
+ const extensions = unwrapOption ( accountInfo . data . extensions ) ;
268
+
269
+ // Check for interest bearing mint extension
270
+ const interestBearingMintConfigState = extensions ?. find (
271
+ ( ext ) => ext . __kind === 'InterestBearingConfig'
272
+ ) ;
273
+
274
+ // Check for scaled UI amount extension
275
+ const scaledUiAmountConfig = extensions ?. find (
276
+ ( ext ) => ext . __kind === 'ScaledUiAmountConfig'
277
+ ) ;
278
+
279
+ // If no special extension, do standard conversion
280
+ if ( ! interestBearingMintConfigState && ! scaledUiAmountConfig ) {
281
+ const amountNumber = Number ( amount ) ;
282
+ const decimalsFactor = getDecimalFactor ( accountInfo . data . decimals ) ;
283
+ return ( amountNumber / decimalsFactor ) . toString ( ) ;
284
+ }
285
+
286
+ // Get timestamp if needed for special mint types
287
+ const timestamp = await getSysvarClockTimestamp ( rpc ) ;
288
+
289
+ // Handle interest bearing mint
290
+ if ( interestBearingMintConfigState ) {
291
+ return amountToUiAmountForInterestBearingMintWithoutSimulation (
292
+ amount ,
293
+ accountInfo . data . decimals ,
294
+ Number ( timestamp ) ,
295
+ Number ( interestBearingMintConfigState . lastUpdateTimestamp ) ,
296
+ Number ( interestBearingMintConfigState . initializationTimestamp ) ,
297
+ interestBearingMintConfigState . preUpdateAverageRate ,
298
+ interestBearingMintConfigState . currentRate
299
+ ) ;
300
+ }
301
+
302
+ // At this point, we know it must be a scaled UI amount mint
303
+ if ( scaledUiAmountConfig ) {
304
+ let multiplier = scaledUiAmountConfig . multiplier ;
305
+ // Use new multiplier if it's effective
306
+ if ( timestamp >= scaledUiAmountConfig . newMultiplierEffectiveTimestamp ) {
307
+ multiplier = scaledUiAmountConfig . newMultiplier ;
308
+ }
309
+ return amountToUiAmountForScaledUiAmountMintWithoutSimulation (
310
+ amount ,
311
+ accountInfo . data . decimals ,
312
+ multiplier
313
+ ) ;
314
+ }
315
+
316
+ // This should never happen due to the conditions above
317
+ throw new Error ( 'Unknown mint extension type' ) ;
318
+ }
319
+
243
320
/**
244
321
* Convert a UI amount back to the raw amount
245
322
*
@@ -256,24 +333,54 @@ export async function uiAmountToAmountForMintWithoutSimulation(
256
333
) : Promise < bigint > {
257
334
const accountInfo = await fetchMint ( rpc , mint ) ;
258
335
const extensions = unwrapOption ( accountInfo . data . extensions ) ;
336
+
337
+ // Check for interest bearing mint extension
259
338
const interestBearingMintConfigState = extensions ?. find (
260
339
( ext ) => ext . __kind === 'InterestBearingConfig'
261
340
) ;
262
- if ( ! interestBearingMintConfigState ) {
341
+
342
+ // Check for scaled UI amount extension
343
+ const scaledUiAmountConfig = extensions ?. find (
344
+ ( ext ) => ext . __kind === 'ScaledUiAmountConfig'
345
+ ) ;
346
+
347
+ // If no special extension, do standard conversion
348
+ if ( ! interestBearingMintConfigState && ! scaledUiAmountConfig ) {
263
349
const uiAmountScaled =
264
- parseFloat ( uiAmount ) * Math . pow ( 10 , accountInfo . data . decimals ) ;
350
+ parseFloat ( uiAmount ) * getDecimalFactor ( accountInfo . data . decimals ) ;
265
351
return BigInt ( Math . trunc ( uiAmountScaled ) ) ;
266
352
}
267
353
354
+ // Get timestamp if needed for special mint types
268
355
const timestamp = await getSysvarClockTimestamp ( rpc ) ;
269
356
270
- return uiAmountToAmountForInterestBearingMintWithoutSimulation (
271
- uiAmount ,
272
- accountInfo . data . decimals ,
273
- Number ( timestamp ) ,
274
- Number ( interestBearingMintConfigState . lastUpdateTimestamp ) ,
275
- Number ( interestBearingMintConfigState . initializationTimestamp ) ,
276
- interestBearingMintConfigState . preUpdateAverageRate ,
277
- interestBearingMintConfigState . currentRate
278
- ) ;
357
+ // Handle interest bearing mint
358
+ if ( interestBearingMintConfigState ) {
359
+ return uiAmountToAmountForInterestBearingMintWithoutSimulation (
360
+ uiAmount ,
361
+ accountInfo . data . decimals ,
362
+ Number ( timestamp ) ,
363
+ Number ( interestBearingMintConfigState . lastUpdateTimestamp ) ,
364
+ Number ( interestBearingMintConfigState . initializationTimestamp ) ,
365
+ interestBearingMintConfigState . preUpdateAverageRate ,
366
+ interestBearingMintConfigState . currentRate
367
+ ) ;
368
+ }
369
+
370
+ // At this point, we know it must be a scaled UI amount mint
371
+ if ( scaledUiAmountConfig ) {
372
+ let multiplier = scaledUiAmountConfig . multiplier ;
373
+ // Use new multiplier if it's effective
374
+ if ( timestamp >= scaledUiAmountConfig . newMultiplierEffectiveTimestamp ) {
375
+ multiplier = scaledUiAmountConfig . newMultiplier ;
376
+ }
377
+ return uiAmountToAmountForScaledUiAmountMintWithoutSimulation (
378
+ uiAmount ,
379
+ accountInfo . data . decimals ,
380
+ multiplier
381
+ ) ;
382
+ }
383
+
384
+ // This should never happen due to the conditions above
385
+ throw new Error ( 'Unknown mint extension type' ) ;
279
386
}
0 commit comments