Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 96a1575

Browse files
gitterijoncinque
andauthored
token-js: add JS helpers for interest bearing tokens' UIAmounts (#7541)
* add JS helpers for interest bearing tokens' UIAmounts * reorder imports * update comments to provide formulas * working with sys clock * remove sinon * use number instead of decimal.js and remove deps * update pnpm * use upstream pnpm-lock * remove unnecessary math * prettier fix * restore timestamps as bigints * address comments * Update token/js/src/actions/amountToUiAmount.ts Co-authored-by: Jon C <[email protected]> * fix eslint import issues --------- Co-authored-by: Jon C <[email protected]>
1 parent 5ef1bd6 commit 96a1575

File tree

2 files changed

+571
-3
lines changed

2 files changed

+571
-3
lines changed

token/js/src/actions/amountToUiAmount.ts

Lines changed: 244 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
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';
44
import { 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

Comments
 (0)