1
1
import Decimal from 'decimal.js' ;
2
- import type { Connection , PublicKey , Signer , TransactionError } from '@solana/web3.js' ;
3
- import { Transaction } from '@solana/web3.js' ;
4
- import { TOKEN_PROGRAM_ID } from '../constants.js' ;
2
+ import type { Connection , Signer , TransactionError } from '@solana/web3.js' ;
3
+ import { PublicKey , Transaction } from '@solana/web3.js' ;
4
+ import { TOKEN_2022_PROGRAM_ID , TOKEN_PROGRAM_ID } from '../constants.js' ;
5
5
import { createAmountToUiAmountInstruction } from '../instructions/amountToUiAmount.js' ;
6
- import { getMint } from '../state/mint.js' ;
6
+ import { getMint , unpackMint } from '../state/mint.js' ;
7
7
import { getInterestBearingMintConfigState } from '../extensions/interestBearingMint/state.js' ;
8
8
9
9
/**
@@ -32,8 +32,14 @@ export async function amountToUiAmount(
32
32
return err ;
33
33
}
34
34
35
- const ONE_IN_BASIS_POINTS = 10000 ;
36
- const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24 ;
35
+ const calculateExponentForTimesAndRate = ( t1 : number , t2 : number , r : number ) => {
36
+ const ONE_IN_BASIS_POINTS = 10000 ;
37
+ const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24 ;
38
+ const timespan = new Decimal ( t2 ) . minus ( t1 ) ;
39
+ const numerator = new Decimal ( r ) . times ( timespan ) ;
40
+ const exponent = numerator . div ( new Decimal ( SECONDS_PER_YEAR ) . times ( ONE_IN_BASIS_POINTS ) ) ;
41
+ return exponent . exp ( ) ;
42
+ }
37
43
38
44
/**
39
45
* Convert amount to UiAmount for a mint with interest bearing extension without simulating a transaction
@@ -61,9 +67,8 @@ const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24;
61
67
*
62
68
* @return Amount scaled by accrued interest as a string with appropriate decimal places
63
69
*/
64
-
65
70
export function amountToUiAmountWithoutSimulation (
66
- amount : string ,
71
+ amount : bigint ,
67
72
decimals : number ,
68
73
currentTimestamp : number , // in seconds
69
74
lastUpdateTimestamp : number ,
@@ -75,31 +80,38 @@ export function amountToUiAmountWithoutSimulation(
75
80
76
81
// Calculate pre-update exponent
77
82
// e^(preUpdateAverageRate * (lastUpdateTimestamp - initializationTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS))
78
- const preUpdateTimespan = new Decimal ( lastUpdateTimestamp ) . minus ( initializationTimestamp ) ;
79
- const preUpdateNumerator = new Decimal ( preUpdateAverageRate ) . times ( preUpdateTimespan ) ;
80
- const preUpdateExponent = preUpdateNumerator . div ( new Decimal ( SECONDS_PER_YEAR ) . times ( ONE_IN_BASIS_POINTS ) ) ;
81
- const preUpdateExp = preUpdateExponent . exp ( ) ;
83
+ const preUpdateExp = calculateExponentForTimesAndRate ( initializationTimestamp , lastUpdateTimestamp , preUpdateAverageRate )
82
84
83
85
// Calculate post-update exponent
84
86
// e^(currentRate * (currentTimestamp - lastUpdateTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS))
85
- const postUpdateTimespan = new Decimal ( currentTimestamp ) . minus ( lastUpdateTimestamp ) ;
86
- const postUpdateNumerator = new Decimal ( currentRate ) . times ( postUpdateTimespan ) ;
87
- const postUpdateExponent = postUpdateNumerator . div ( new Decimal ( SECONDS_PER_YEAR ) . times ( ONE_IN_BASIS_POINTS ) ) ;
88
- const postUpdateExp = postUpdateExponent . exp ( ) ;
87
+ const postUpdateExp = calculateExponentForTimesAndRate ( lastUpdateTimestamp , Number ( currentTimestamp ) , currentRate )
89
88
90
89
// Calculate total scale
91
90
const totalScale = preUpdateExp . times ( postUpdateExp ) ;
92
91
93
92
// Calculate scaled amount with interest rounded down to the nearest unit
94
93
const decimalsFactor = new Decimal ( 10 ) . pow ( decimals ) ;
95
- const scaledAmountWithInterest = new Decimal ( amount ) . times ( totalScale ) . div ( decimalsFactor ) . toDecimalPlaces ( decimals , Decimal . ROUND_DOWN ) ;
94
+ const scaledAmountWithInterest = new Decimal ( amount . toString ( ) ) . times ( totalScale ) . div ( decimalsFactor ) . toDecimalPlaces ( decimals , Decimal . ROUND_DOWN ) ;
96
95
97
96
return scaledAmountWithInterest . toString ( ) ;
98
97
}
99
98
99
+ const getSysvarClockTimestamp = async ( connection : Connection ) : Promise < number > => {
100
+ const info = await connection . getParsedAccountInfo ( new PublicKey ( 'SysvarC1ock11111111111111111111111111111111' ) ) ;
101
+ if ( ! info ) {
102
+ throw new Error ( 'Failed to fetch sysvar clock' ) ;
103
+ }
104
+ if ( typeof info . value === 'object' && info . value && 'data' in info . value && 'parsed' in info . value . data ) {
105
+ return info . value . data . parsed . info . unixTimestamp ;
106
+ }
107
+ throw new Error ( 'Failed to parse sysvar clock' ) ;
108
+ }
109
+
110
+
100
111
/**
101
- * Convert amount to UiAmount for a mint with interest bearing extension without simulating a transaction
102
- * This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs
112
+ * Convert amount to UiAmount for a mint without simulating a transaction
113
+ * This implements the same logic as `process_amount_to_ui_amount` in /token/program-2022/src/processor.rs
114
+ * and `process_amount_to_ui_amount` in /token/program/src/processor.rs
103
115
*
104
116
* @param connection Connection to use
105
117
* @param mint Mint to use for calculations
@@ -111,29 +123,30 @@ export function amountToUiAmountWithoutSimulation(
111
123
export async function amountToUiAmountForMintWithoutSimulation (
112
124
connection : Connection ,
113
125
mint : PublicKey ,
114
- amount : string ,
115
- programId = TOKEN_PROGRAM_ID ,
126
+ amount : bigint ,
116
127
) : Promise < string > {
117
128
Decimal . set ( { toExpPos : 24 , toExpNeg : - 24 } )
118
- const mintInfo = await getMint ( connection , mint , 'confirmed' , programId ) ;
119
- const amountDecimal = new Decimal ( amount . toString ( ) ) ;
120
- const decimalsFactor = new Decimal ( 10 ) . pow ( mintInfo . decimals ) ;
121
-
122
- if ( programId . equals ( TOKEN_PROGRAM_ID ) ) {
123
- console . log ( 'amountDecimal' , amountDecimal . toString ( ) , 'mintInfo' , mintInfo ) ;
124
- return amountDecimal . div ( decimalsFactor ) . toString ( ) ;
129
+ const accountInfo = await connection . getAccountInfo ( mint ) ;
130
+ const programId = accountInfo ?. owner ;
131
+ if ( programId !== TOKEN_PROGRAM_ID && programId !== TOKEN_2022_PROGRAM_ID ) {
132
+ throw new Error ( 'Invalid program ID' ) ;
125
133
}
126
134
135
+ const mintInfo = unpackMint ( mint , accountInfo , programId ) ;
136
+
127
137
const interestBearingMintConfigState = getInterestBearingMintConfigState ( mintInfo ) ;
128
138
if ( ! interestBearingMintConfigState ) {
139
+ const amountDecimal = new Decimal ( amount . toString ( ) ) ;
140
+ const decimalsFactor = new Decimal ( 10 ) . pow ( mintInfo . decimals ) ;
129
141
return amountDecimal . div ( decimalsFactor ) . toString ( ) ;
130
142
}
131
143
132
- const currentTime = Math . floor ( Date . now ( ) / 1000 ) ; // Convert to seconds
144
+ const timestamp = await getSysvarClockTimestamp ( connection ) ;
145
+
133
146
return amountToUiAmountWithoutSimulation (
134
147
amount ,
135
148
mintInfo . decimals ,
136
- currentTime ,
149
+ timestamp ,
137
150
interestBearingMintConfigState . lastUpdateTimestamp ,
138
151
interestBearingMintConfigState . initializationTimestamp ,
139
152
interestBearingMintConfigState . preUpdateAverageRate ,
@@ -169,7 +182,6 @@ export async function amountToUiAmountForMintWithoutSimulation(
169
182
*
170
183
* @return Original amount (principle) without interest
171
184
*/
172
-
173
185
export function uiAmountToAmountWithoutSimulation (
174
186
uiAmount : string ,
175
187
decimals : number ,
@@ -185,16 +197,10 @@ export function uiAmountToAmountWithoutSimulation(
185
197
const uiAmountScaled = uiAmountDecimal . mul ( decimalsFactor ) ;
186
198
187
199
// Calculate pre-update exponent
188
- const preUpdateTimespan = new Decimal ( lastUpdateTimestamp ) . minus ( initializationTimestamp ) ;
189
- const preUpdateNumerator = new Decimal ( preUpdateAverageRate ) . times ( preUpdateTimespan ) ;
190
- const preUpdateExponent = preUpdateNumerator . div ( new Decimal ( SECONDS_PER_YEAR ) . times ( ONE_IN_BASIS_POINTS ) ) ;
191
- const preUpdateExp = preUpdateExponent . exp ( ) ;
200
+ const preUpdateExp = calculateExponentForTimesAndRate ( initializationTimestamp , lastUpdateTimestamp , preUpdateAverageRate ) ;
192
201
193
202
// Calculate post-update exponent
194
- const postUpdateTimespan = new Decimal ( currentTimestamp ) . minus ( lastUpdateTimestamp ) ;
195
- const postUpdateNumerator = new Decimal ( currentRate ) . times ( postUpdateTimespan ) ;
196
- const postUpdateExponent = postUpdateNumerator . div ( new Decimal ( SECONDS_PER_YEAR ) . times ( ONE_IN_BASIS_POINTS ) ) ;
197
- const postUpdateExp = postUpdateExponent . exp ( ) ;
203
+ const postUpdateExp = calculateExponentForTimesAndRate ( lastUpdateTimestamp , currentTimestamp , currentRate ) ;
198
204
199
205
// Calculate total scale
200
206
const totalScale = preUpdateExp . times ( postUpdateExp ) ;
@@ -205,40 +211,42 @@ export function uiAmountToAmountWithoutSimulation(
205
211
}
206
212
207
213
/**
208
- * Convert a UI amount with interest back to the original UI amount without interest
214
+ * Convert a UI amount back to the raw amount
209
215
*
210
216
* @param connection Connection to use
211
217
* @param mint Mint to use for calculations
212
- * @param uiAmount UI Amount (principle plus continuously compounding interest) to be converted back to original principle
218
+ * @param uiAmount UI Amount to be converted back to raw amount
213
219
* @param programId SPL Token program account (default: TOKEN_PROGRAM_ID)
214
220
*
215
221
*
216
- * @return Original UI Amount (principle) without interest
222
+ * @return Raw amount
217
223
*/
218
224
export async function uiAmountToAmountForMintWithoutSimulation (
219
225
connection : Connection ,
220
226
mint : PublicKey ,
221
227
uiAmount : string ,
222
- programId = TOKEN_PROGRAM_ID ,
223
228
) : Promise < bigint > {
224
229
Decimal . set ( { toExpPos : 24 , toExpNeg : - 24 } )
225
- const mintInfo = await getMint ( connection , mint , 'confirmed' , programId ) ;
226
- const uiAmountScaled = new Decimal ( uiAmount ) . mul ( new Decimal ( 10 ) . pow ( mintInfo . decimals ) ) ;
227
-
228
- if ( programId . equals ( TOKEN_PROGRAM_ID ) ) {
229
- return BigInt ( uiAmountScaled . trunc ( ) . toString ( ) ) ;
230
+ const accountInfo = await connection . getAccountInfo ( mint ) ;
231
+ const programId = accountInfo ?. owner ;
232
+ if ( programId !== TOKEN_PROGRAM_ID && programId !== TOKEN_2022_PROGRAM_ID ) {
233
+ throw new Error ( 'Invalid program ID' ) ;
230
234
}
231
235
236
+ const mintInfo = await getMint ( connection , mint , 'confirmed' , programId ) ;
237
+
232
238
const interestBearingMintConfigState = getInterestBearingMintConfigState ( mintInfo ) ;
233
239
if ( ! interestBearingMintConfigState ) {
240
+ const uiAmountScaled = new Decimal ( uiAmount ) . mul ( new Decimal ( 10 ) . pow ( mintInfo . decimals ) ) ;
234
241
return BigInt ( uiAmountScaled . trunc ( ) . toString ( ) ) ;
235
242
}
236
243
237
- const currentTime = Math . floor ( Date . now ( ) / 1000 ) ; // Convert to seconds
244
+ const timestamp = await getSysvarClockTimestamp ( connection ) ;
245
+
238
246
return uiAmountToAmountWithoutSimulation (
239
247
uiAmount ,
240
248
mintInfo . decimals ,
241
- currentTime ,
249
+ timestamp ,
242
250
interestBearingMintConfigState . lastUpdateTimestamp ,
243
251
interestBearingMintConfigState . initializationTimestamp ,
244
252
interestBearingMintConfigState . preUpdateAverageRate ,
0 commit comments